多线程
多线程
-
程序 :用某种语言编写的一组指令的集合
-
进程 :正在运行的程序
-
线程 :一个进程可以细分为多个线程
多核CPU:在同一时间单元内,可以执行多个线程
单核CPU:在同一时间单元内,只能执行一个线程
并发:一个CPU执行不同的任务
多线程基础
-
新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
-
就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
-
运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
-
阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
-
等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
-
同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
-
其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
-
-
死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
线程包括五个转态:新建,就绪,运行,阻塞,死亡
线程的优先级
每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
创建一个线程
Java 提供了三种创建线程的方法:
-
通过实现 Runnable 接口;
-
通过继承 Thread 类本身;
-
通过 Callable 和 Future 创建线程。
串行 并行 并发
串行:一个任务/指令 执行完,下一个任务才执行
并行:在同一时刻,同时执行多个任务/指令
并发:在微观上,在同一时刻,只能执行一个任务/指令,在宏观上,在一段时间内可执行多个任务/指令,只不 过,把时间分成若干份,使得多个任务交换执行。
并发三大特点:有序性,可见性,原子性
有序性:程序的执行顺序由代码的先后顺序执行
可见性:多个线程同时执行,当一个线程修改了共享变量的值,其他线程能够看见
原子性:一个或多个操作,要么执行完毕中途不被外界干扰所打断,要么不执行,
-
理解synchronized
就是一个同步监视器,俗称锁。调用它是,当有多个线程在同时调用一个方法时,当有一个线程在执行,与他同步的线程,要等该线程执行完才执行。
-
死锁
不同的线程分别占用对方需要的同步资源不放,都在等待对方放弃自己所需要的资源,就形成了死锁。
不会发生异常,也不会给出任何提示,所有线程处于阻塞状态,无法继续。
-
Lock锁
就是 new 一个 ReentrantLock ,在需要使用加锁的代码块,加锁;
ReentrantLock lock = new ReentrantLock();
lock.lock();
/*
代码块
*/
lock.unlock();
-
谈谈对线程安全的理解
线程安全可以理解为内存安全,堆是内存共享,可以被所有的内存访问。
当多个线程访问同一个对象时,如果不进行额外的同步控制或者其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象时线程安全的。
堆是进程和线程共有的空间,分全局堆和局部堆,全局堆就是所有没有分配的空间,局部堆就是用户分配的空间,堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给系统,不然就是内存泄漏。
在java中,堆是java虚拟机所管理的内存中最大的一块,是所有线程共享的一款线程区域,在虚拟机启动时创建,堆所存在的内存区域唯一目的就是存放对象实例,几乎多有的对象实例和数组都放在堆中。
栈是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,栈是线程安全的。操作系统在切换线程的时候回自动切换栈,栈空间不需要再高级语言里显式的分配和释放。
在每个进程的内存空间都有一块特殊的公共区域,通常称为堆内存,进程内多有的线程都可以访问该区域,这就是造成问题的潜在原因。
-
Runnable和Thread有什么区别
其实Thread也就是实现了Runnable接口,提供了更多的方法而已。所以说Thread与Runnable并没有什么区别。如果硬要说有什么区别的话,那就是类与接口的区别,继承与实现的区别。
-
ThreadLocal
ThreadLocal 定义了4个方法:
get():返回此线程局部变量的当前线程副本中的值。 initialValue():返回此线程局部变量的当前线程的“初始值”。 remove():移除此线程局部变量当前线程的值。 set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。
ThreadLocal的核心是通过静态内部类ThreadLocalMap来处理数据的,我们先看下ThreadLocalMap的源码。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry是ThreadLocalMap核心存储key-value的。,继承WeakReference,Entry所对应key(ThreadLocal实例)的引用为一个弱引用 。
-
ThreadLocal使用场景
数据库链接
session管理
ThreadLocal并不是为了解决多线程的数据共享,只是从另外一个方向来解决并发的问题,让变量线程局部化,就不存在并发。
-
理解volatile
底层原理就是
-
threadloacal内存泄漏问题如何避免
由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果都没有 手动删除对应key,都会导致内存泄漏。
但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap 调用set(),get(),remove()的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除 对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal正确的使用方法:
每次使用完ThreadLocal都调用它的remove()方法清除数据
将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能 通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
-
线程池中线程的复用原理
线程池最大的作用就是复用线程。在线程池中,经过同一个线程去执行不一样的任务,减少反复地创建线程带来的系统开销,就是线程的复用。
-
为什么要使用线程池
1.减少资源消耗 Thread线程,是操作系统的资源,创建和销毁是要有资源消耗的,如果有线程池事先准备好一批线程,创建线程和销毁线程的资源就没有了
2.使用线程池缩短任务的执行时间。 有一个新的任务就new 一个线程,那么时间消耗为:New Thread() T1:线程的创建时间,T2:任务的执行时间 ,T3:线程的销毁时间 准备好一堆的线程就不需要T1 T3
3.线程是稀缺而昂贵的资源,因为线程创建出来消耗CPU,消耗内存(一定消耗内存),线程执行太多,多操作系统是一种负担,使用某种机制把线程统一管理
-
线程池中源码的参数解释
corePoolSize:线程池核心线程数量
maximumPoolSize:线程池最大线程数量
keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间
unit:存活时间的单位
workQueue:存放任务的队列
handler:超出线程范围和队列容量的任务的处理程序
-
线程池的实现原理
提交一个任务到线程池中,线程池的处理流程如下:
1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
-
说说对守护线程的理解
作用:
守护线程拥有自动结束自己生命周期的特性,而非守护线程不具备这个特点。
场景:
JVM 中的垃圾回收线程就是典型的守护线程,如果说不具备该特性,会发生什么呢?
当 JVM 要退出时,由于垃圾回收线程还在运行着,导致程序无法退出,这就很尴尬了!!!由此可见,守护线程的重要性了。
通常来说,守护线程经常被用来执行一些后台任务,但是呢,你又希望在程序退出时,或者说 JVM 退出时,线程能够自动关闭,此时,守护线程是你的首选。
问题
-
如何开启线程?怎么保证线程安全?
线程和进程的区别:进程是操作系统进行资源分配的最小单元;线程是操作系统进行任务分配的最小单位;线程隶属于进程。
如何开启线程?
-
继承Thread类,重写run方法;
-
实现Runnable接口,实验run方法;
-
实现Callable接口,实现call方法,通过FutureTask创建一个线程,获取到线程执行的返回值
-
通过线程池来开启线程
怎么保证线程安全?加锁
-
JVM提供的锁,也就是Synchronized关键字
-
JDK提供的各种锁Lock
-
-
volatile 和synchronized 有什么区别?volatile是否线程安全呢?
dcl单例为什么要加volatile?
synchronized关键字。用来加锁。volatile只是保持变量的线程可见性,通常适用于一个线程写,多个线程读的场景。
不能保证线程安全。volatile关键字只能保证线程的可见性,不能保证原子性。
DCL:double check lock双重检查锁,如下面的代码
public class Singleton {
private static volatile Singleton INSTANCE = null;
private Singleton(){}
public static Singleton getInstance(){
if(INSTANCE==null){//第一次检查
synchronized (Singleton.class){
if(INSTANCE==null){//第二次检查
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
为什么要加两次判空,第一次判空能不能不加? 效率问题:假设第一次判空不加,那么每次进入这个方法,INSTANCE不论是不是null,都会执行下面的synchronized代码块,多线程下会出现锁的竞争,而除了第一次初始化,后面的都不会为null,判空的效率比加锁高。
为什么要进行第二次判空? 防止多次初始化:多线程下,有可能会出现两个线程都经过了前面第一次检查,来到了下面的synchronized这里,如果不判空,就会出现一个线程new了一个Singleton出来,然后释放锁,第二个线程进来又会new一个Singleton出来。
volatile能不能不加? volatile作用:
保持内存可见性 防止指令重排序 volatile这里的作用就是防止指令重排,INSTANCE = new Singleton();这一行主要做了下面几件事: 在内存中开辟空间 执行构造方法初始化对象 将对象的引用赋值给INSTANCE变量 在不加volatile的情况下,第2和第3步是有可能发生指令重排的,即执行顺序变成了1、3、2,假如我刚好执行到第3步,还没执行第2步,这时候另外一个线程调了这个方法,获取到的是还没执行初始化函数的对象,在上面的代码中,初始化函数什么都没做,所以没什么影响。但是如果初始化函数中需要做一些操作,那就有影响了。
-
用Callable创建线程
package com.ljx.test01.ThreadTest;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @author 李捷禧
* Date: 2022/11/6
* ClassName: ThreadTest
* email: 2465594828@qq.com
*/
public class ThreadTest implements Callable<Integer> {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadTest threadTest = new ThreadTest();
FutureTask<Integer> integerFutureTask = new FutureTask<>(threadTest);
new Thread(integerFutureTask).start();
new Thread(integerFutureTask).start();
//子线程返回的过程及结果时间
Integer result = integerFutureTask.get();
//子线程返回结果
System.out.println("子线程的返回结果:"+result);
System.out.println(Thread.currentThread().getName()+"->我是主线程!");
}
public class TestNumber{
public static void main(String[] args) {
ThreadTest threadTest = new ThreadTest();
ThreadTest02 threadTest02 = new ThreadTest02();
Thread thread = new Thread(threadTest02);
threadTest.start();
thread.start();
new Thread(new Runnable() {