公平和非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁
非公平锁是指不按照申请锁的顺序来获取锁。有可能会造成优先级反转或饥饿现象。非公平锁的优点在于吞吐量明显比公平锁大
java中提供的重入锁的实现ReentrantLock通过构造器可以指定是否使用公平锁。
public ReentrantLock(boolean fair) { //布尔类型的参数用于指定是否使用公平锁
sync = fair ? new FairSync() : new NonfairSync();
}
public ReentrantLock() { //默认情况下使用的是非公平锁
sync = new NonfairSync();
}
对于synchronized使用的是非公平锁,没有什么方法可以使其转换为公平锁
可重入锁
可重入锁又可以称为递归锁,是指在同一个线程获取到某个锁之后还可以继续申请获取到该锁,自己不会阻塞自己
可以看到ReentrantLock类和synchronized都是可重入锁,可重入锁可以在一定程度上避免死锁
synchronized void aaa() throws Exception{
Thread.sleep(300);//对应的业务处理
bbb(); //调用另外一个同步方法。如果synchronized不是重入锁,则调用该方法就会死锁
}
synchronized void bbb() throws Exception{
Thread.sleep(200);//对应的业务处理
}
Java提供了一个实现ReentrantReadWriteLock类,其中包含两个锁,一个是读锁(共享锁),一个是写锁(排他锁)
一个线程持有读锁获取写锁时互斥,持有写锁获取读锁则没有问题
共享锁和排他锁
共享锁指该锁可以被多个线程同时持有;排他锁也称为独享锁,是指锁一次只能被一个线程所持有 synchronized都是独享锁
ReentrantReadWriteLock类包含2个锁,其中读锁是共享的,写锁是排他的;可以保证并发读操作的高效性,多线程中读写、写读、写写的过程是互斥的。
独享锁
读锁属于共享锁,写锁属于独享读
读锁可以保证并发读的高效性,多线程中只有读读是允许的,但是读写、写读、写写都是不允许的
在一个线程中读读、写写都不互斥【可重入性】,读不能再申请写,但是写可以申请读
synchronized和ReentrantLock属于独享锁
乐观锁
针对同步处理的方案
AQS是悲观锁;CAS是乐观锁
悲观锁就是利用各种锁机制;乐观锁就是无锁编程,常见的样例就是CAS
偏向锁、轻量级锁和重量级锁
是针对synchronized锁的三种状态。这三种锁状态是通过对象在对象头中的特定字段来表示的,实际上就是使用Object Minitor控制多线程的工作过程并进行协调处理
只有一个线程进入临界区使用偏向锁
多个线程交替进入临界区使用轻量级锁
多个线程同时进入临界区使用重量
锁
状
态
| 优点 | 缺点 | 使用场景 |
偏向锁 |
加锁和解锁无需额外的消耗, 和非同步方法相差很小
|
如果竞争临界资源的线程多,那么会带来额外的锁撤销的消耗
|
基本用于没有线程竞争的同步场景下
|
轻量级锁 |
竞争的线程不会阻塞,而是适用自旋的方式,可以提高程序的相应速度
|
如果一直不能获取锁,则长时间运行自旋处理会造 成CPU
的消耗
|
一般用于少量线程竞争锁对象,而且线程持有锁的时间较短
|
重量级锁 |
线程竞争中不适用
CPU
自旋,不会导致CPU
空转消耗
|
线程阻塞,响应时间变长
|
一般用于多线程竞争锁,且锁持有的时间较长
|
CAS模型
CAS就是比较并交换,用于解决多线程并发情况下使用锁造成性能耗损的一种机制,采用无锁的方式实现线程同步的效果。CAS操作包括了3个操作数:内存位置、预期原始数据和新计算的数据。如果内存位置的值与预期值相匹配,则自动将该位置上的数据更新为新值,否则处理器不作任何处理。同时都会在CAS执行之前返回该位置上的值。比较和交换是用于实现多线程同步的原子指令,是作为单个原子操作完成的,原子性保证新值基于最新信息进行计算。
特性:
1.通过调用JNI的代码实现
2.非阻塞算法
3.非独占锁
存在问题:
1.ABA问题
2.循环时间开销大
3.只能保证一个共享变量的原子操作
死锁
使用多进程和多线程改善了系统资源的利用率并提高了系统的处理能力(吞吐量)。并发执行也带来了死锁问题。死锁就是指多个线程因为竞争资源而造成的互相等待的一种僵局状态,如果没有外力作用,这些程序都无法向前推进。
特殊的概念
饥饿指的是无法获取所需要的资源而不能继续执行。在程序中使用Thread类中定义的方法 yield和sleep仍其它线程有一定的执行机会
活锁指的是线程不断重复执行相同的操作,但是每次操作的结果都是失败的。在每次重复执行的时 候引入随机机制
活锁和死锁的区别
处于活锁状态的进程或者线程的状态是不断改变的,活锁可以理解为一种特殊的饥饿。死锁和活锁最大的区别是前者的状态不可能改变,但是后者的状态可以改变,但是都是不能继续执行
死锁的原因
1、系统中不可剥夺资源的竞争。
2、不可剥夺条件
3、进程或者线程的推进顺序非法
死锁的4大必要条件
死锁的产生需要4个必要条件,也就是说满足4个条件不一定死锁,但是死锁一定满足4个条件
1、互斥条件。竞争的资源具有排他性
2、不可剥夺性条件。只能由获取资源的进程或线程主动释放,不能剥夺
3、请求和保持条件。可以请求新资源,同时永远不释放已经获取到的资源
4、循环等待条件。出现了循环等待链
如何避免死锁
指导思想:避免死锁就是破坏4个必要条件。
强制定义加锁顺序。线程按照一定的顺序加锁,避免出现嵌套封锁,如果一定出现嵌套问题,则定义一个加锁顺序
加锁时限。线程尝试获取锁时加上一个时间限制,超过时间限制则放弃对该锁的申请,同时释放资源占用的资源
让程序每次至多只能获取一个锁,这种方式在多线程环境下不现实。一般在设计时要求必须考虑清楚锁的顺序,尽量减少嵌套加锁。
死锁检测
当出现死循环、死锁、阻塞等问题时,获取线程dump文件是最好的解决问题的途径。所谓线程的dump文件就是线程堆栈
获取dump文件的步骤:
1、可以使用命令jps获取线程的pid,在linux中还可以使用 ps -ef | grep java
2、可以使用 jstack pid 命令打印线程堆栈
Thread类中提供了一个getStackTrace()方法获取线程的堆栈
AQS模型
AQS抽象队列同步器,AQS时JDK下提供的一套用于实现基于FIFO等待队列的阻塞锁和相同的同步器的一个同步框架。AQS中维护了一个volatile int state(共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时进入此队列)
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态;如果被请求的共享资源已经被占用,则需要通过线程阻塞等待已经唤醒时锁分配的 机制,这个机制是由AQS通过CLH队列锁实现的,就是将暂时获取不到锁的线程加入到队列中。
信号量Semaphone
semaphone实际上就是一个功能完备的计数器,主要用于控制对有限资源的访问数量,能监控由多少个线程等待获取资源,并且通过信号量可以得知可用资源的数目。底层实现AQS,支持公平和非公平
应用场景:对有限资源的使用限制
障碍器CyclicBarrier
CyclicBarrier作用就是会让所有线程等待完成后才会执行下一个动作。一般用于大型任务划分为多个小子任务去执行,当子任务参收后才执行主任务。
工作原理:通过构造函数创建一个执行屏障数的CyclicBarrier对象,在各个子线程中可以调用await,调用后当前子线程会进入阻塞态,直到调用的次数达到屏障数,则在最后一个到达屏障的子线程中执行主任务,然后所有阻塞的子线程恢复继续执行。