多线程总结
文章目录
进程和线程的区别
- 资源分配的角度来看:进程是资源分配的最小单位,线程的切换仅仅保存了较少的寄存器,线程不是资源分配的最小单位
- 调度的角度:没有引入线程的时候,进程是调度的最小单位,有了线程之后操作系统调度的就是线程了。
- 切换开销来看:线程的切换开销较小,只用保存较少的寄存器,进程切换的开销较大
- 并发的角度来看:同一个进程的线程可以并发,不同进程的线程之间也可以并发
Java的线程和操作系统的线程有什么区别
在jdk1.2以前,java的线程是用户级线程,也就是一对多模型,向下操作系统申请一个线程,这个线程来轮询java的操作,在用户层面看上去是有多线程,但这样效率并没有提升
在此之后java的线程可以直接用内核级线程,也就是java创建的线程就是操作系统认可的一个线程。
创建线程的方法
网上都说有四五种方法,但是其实只用一种就是thread.start()方法,但是为了完整性还是说一下这四种方法:
- 继承Thread
- 实现Runnable接口:run方法不允许有返回值,也不能抛出异常
- 实现Callable接口:call可以有返回值,可以抛出异常
- 线程池
什么时候实现runnable接口什么时候继承thread:由于java是单继承的,如果有继承别的类就不能继承Thread了,此时就要使用Runnable接口
runnable和callable只是两个接口而已,直接运行对应的run和call是不能成为一个线程的,start是Thread类才有的方法,所以要真正运行起来线程需要new Thread(),这里面传入的是一个Runnable参数,Callable不行,需要用futureTask包装
callable怎么用?
- callable和futureTask:由于new Thread要传一个Runnable参数,Callable不是;所以要用futureTask包装
- callable和线程池:submit给线程池返回一个future对象
Future和FutureTask
future是异步编程的一个体现,当前线程正在执行,我需要取到程序运行结束的结果,也就是未来的结果。具有取消任务,查看任务执行情况,获取任务执行结果等功能。而futureTask是其的一个实现类,同时实现了future和runnable接口,由于new Thread只能传Runnable接口,futureTask正式为了搭配Callable接口,使用的时候new Thread(new FutureTask(new Callable))
线程池的submit和execute什么区别
- submit返回值为Future,可以执行Callable和Runnable任务,但是如果是runnable那future.get返回就是null,也可以抛出异常
- execute返回值为空,只能执行Runnable任务
线程的状态
类似原来408学的线程状态,但是在java中这些状态略有不同,相比于底层操作系统,这些状态更加高层次,因为不涉及到io操作阻塞进程:
- NEW:初始状态,一般是new Thread(),创建出来但是还没有执行start方法
- RUNNABLE:运行状态,执行了start方法
- BLOCKED:被synchronized方法阻塞了,没有抢到锁就进入这个状态,等到锁释放就变为RUNNABLE
- WAITING:一般是线程之间通讯,调用了wait方法变成waiting状态,而等到notify或者notifyAll变为运行态
- TIME_WAITING:具有具体时间的等待状态
- TERMINATED:线程终止
BLOCKED和WAITING的区别
- 触发条件:线程没有获取到监视器锁(synchronized)就BLOCKING,WAITING是获取到了synchronized锁但是锁对象wait了,也就是释放了锁,主要用于线程通讯
- 唤醒条件:获取到锁了之后会解除BLOCKING,而别的线程对锁对象.notify或者notifyAll之后会唤醒waiting线程,但是要继续执行还是要获取锁
notify和notifyAll的区别
- notify随机唤醒一个(这个是hotspot的机制,可能别的虚拟机是指定的)
- notifyAll,当前锁对象下所有的WAITING线程,共同争抢。唤醒的线程在被 notify() 或 notifyAll() 唤醒后,仍然需要重新争抢对象的锁。它会从 WAITING 状态转换到 BLOCKED 状态,等待对象的锁释放。
wait和sleep方法的区别
- wait是Object的方法,sleep是Thread的方法
- wait释放锁,sleep不释放锁
- wait需要notify唤醒而sleep到时间就唤醒了
- wait主要用于进程同步,sleep延时
为什么wait定义在object里面而sleep定义在Thread里面:
与他们使用的场合有关,wait需要在synchronized方法中运用,而代码块的锁正是object,操作的并不是线程
而sleep的作用是让线程睡眠一段时间,这显然跟锁没什么关系,就定义在线程中
死锁的四个条件,通过什么方法解决
- 互斥条件:一个资源同时刻只能有一个线程访问
- 不剥夺条件:一个线程的资源不可以被别的线程剥夺
- 占有且等待条件:一个线程获取不到更多的资源,原来持有的资源也不会释放
- 循环等待条件:资源的请求变成了一个环,大家都拿不到对应的资源
破坏这四个条件是死锁预防要做的事情。此外还有死锁避免(银行家算法),死锁检测和恢复(有向图检测方法)。这三个大的方法性能由低到高,对死锁的控制由严格到松。
- 破坏互斥条件:多增加临界资源的数量(虚拟化)
- 破坏不剥夺条件:抢占式调度(还有一种是协同式调度,是等到当前线程结束才进行线程调度),得不到资源就释放
- 破坏占有等待条件:可以先申请,检查当前系统剩余资源是否能够让线程运行,一次性全拿而不是一次拿一点
- 循环等待条件:编号,类似TCP的同步号
volatile关键字
- 在多线程环境中,每个线程都有自己的 CPU 缓存,线程在读写变量时可能会先将变量的值缓存在自己的工作内存中,而不是直接从主内存中读取或写入。使用volatile关键字就告诉程序这个变量是不稳定的,禁用cpu缓存,每次找这个变量都需要去主存取,
- 在jvm的执行优化中可能出现指令重排序,也就是123条指令,可能实际变成132,volatile关键字能够在其中加上内存屏障,禁止指令重排序(主要用在双校验单例模式)
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Singleton{
private static volatile Singleton singleton;
public Singleton(){}
public Singleton getSingleton(){
if (singleton == null){
synchronized (this){
if (singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
} - 能保证可见性,但不能保证原子性,例如再出现i++这种操作的时候,即使volatile i,还是会有线程安全问题。因为i++其实是三个操作,从内存取出i,对这个副本+1,再把其写回内存。此时两个线程来操作就会少一次+1。(可以加锁synchronized,reentrantlock或者用AtomicInteger的cas操作)
内存屏障:
- 写 volatile 变量时: 当一个线程写入 volatile 变量时,JVM 会在写操作后插入一个写内存屏障(Store Memory Barrier),确保所有在 volatile 变量写操作之前的内存操作都已经完成,并且这些操作的结果对其他线程可见。 在 Java 内存模型中,写 volatile 变量时会确保该写入操作之前的所有变量修改都被刷新到主内存中,然后将 volatile 变量的值写入主内存。
- 读 volatile 变量时:当一个线程读取 volatile 变量时,JVM 会在读操作前插入一个读内存屏障(Load Memory Barrier),确保所有在 volatile 变量读操作之后的内存操作都不会被重排序到它之前。 在 Java 内存模型中,读 volatile 变量时,会确保在读取这个 volatile 变量之后,线程能够看到所有其他线程在写入该 volatile 变量之前对其他变量的修改。
synchronized
- 可以修饰实例方法,锁住当前对象
- 修饰静态方法,锁住当前类。这里就会有个有意思的事情,修饰在静态方法上的synchronized和实例方法上的不互斥,因为静态方法锁住的是当前类,实例方法锁住的是对象
- 修饰代码块,这个比较灵活
注意这里不能修饰构造函数,构造函数本身就是线程安全的
原理
基于jvm的monitor,在代码块进入前加上monitorenter,退出的时候加上monitorexit,也是可重入的,底层是监视器维护了一个state字段,有锁进来就+1,重入多次就+n,退出-1,同时也会有维护一个队列存放未获取锁的对象,等到释放了就会唤醒(BLOCKED)
两个队列entryList和waitList:
- entryList是进入锁代码块的所有线程,当锁释放了之后其中的一个线程会给monitor上锁,同时+1(队列里的状态为BLOCKED),注意这里是队列里所有线程都进行竞争,所以synchronized是非公平锁
- waitList是调用了wait方法的线程,等待notify或者notifyAll进行唤醒,唤醒之后还需要重新获得锁,也就是要进入entryList里(状态为WAITING)
锁升级
- 不加锁:
- 偏向锁:当第一个线程进入之后会给其加上偏向锁,使得这个资源偏向。
- 轻度锁:使用cas操作
- 重度锁:
为什么会有偏向锁这个过程:
对于竞争不激烈但是还有加了synchronized关键字的,如果是单线程就没有必要进行锁和上下文切换,偏向锁的初衷正是为了减小上下文切换的开销。如果一个线程反复进入和退出同步块,而没有其他线程争用这把锁,那么这些加锁解锁的开销是完全多余的。
AQS
抽象队列同步器(AbstractQueuedSynchronizer)提供了一种同步机制,被应用于Reentrantlock,semaphore,cyclicBarrier等排他和共享锁结构中。
在说AQS之前先要讨论一下CHL锁,因为AQS是基于CHL的。CHL锁是一个队列结构,头节点是一个空节点,当线程进入后会对前驱节点的资源释放状态做cas,例如第一个线程进入之后就对表头节点的锁资源cas,获取到了就改变自己的值为false,后续另外一个线程进入之后就会轮询上一个节点的状态,拉成一个队列。
这种结构的好处是资源状态分布在每个节点上,但同时也增大了cpu的开销,因为每一个节点都需要轮询。
AQS正式基于这种思想,首先他把CHL的虚拟队列改造成了显式队列,保存了前驱和后继(为了满足一些对于公平锁和非公平锁的要求,使用了双向链表),全局维护一个state,当获取到锁之后state+1,可以实现锁重入。
reentrantLock
reentrantLock基于aqs,实现可重入,可中断,可以设置公平锁,可以有多个条件设置,可以设置超时时间。