多线程锁及其相关策略介绍
多线程笔记(二)
1. Synchronized 和 Lock 的区别
-
synchronized是Java的关键字,是 JVM 层面的内置功能和实现。
Lock是一个接口,是代码层面的实现
-
synchronized可以隐式的获取,释放锁
lock是显式的获取,释放锁
-
synchronized在发生异常的时候会自动释放锁
lock在发生异常的时候,不会自动释放锁,必须要调用unlock方法才会释放锁,否则容易引起死锁
-
Lock可以尝试非阻塞获取锁,可中断获取锁,超时获取锁。
synchronized并没有这些功能
2. LockSupport
LockSupport是一个编程工具类,主要是为了阻塞线程(park)和唤醒线程(unpark)时使用
设计原理的核心:许可
park:挂起当前线程,等待一个许可
unpark:为某个线程提供一个许可,唤醒某个指定的线程
park/unpark和wait/notify很类似,但其具有以下的优点
- park/unpark是以thread为操作对象,语义更加直观
- 操作更为精准和灵活,可以准确的去唤醒某一个线程
park/unpark和wait/notify的区别
wait/notify和synchronized联系在一起的,wait过后,线程是进入Blocked状态
park方法使当前线程挂起,进入到waiting状态
3. CAS
CAS(Compare And Swap, 比较并替换)中有3个基本的操作数:V:内存地址的值; A:旧的预期的值;B:要修改的新的值
基本实现方式:
使用CAS去更新一个变量的时候,只有变量的旧的预期的值A 和内存地址的值V 相同的时候,才会将V 修改为新的值B。如果修改失败,会自旋等待,直到修改成功。
CAS实现的基石:Unsafe类
CAS想要保证操作时线程安全的,一个实现的关键在于如何保证 比较并替换 是一个原子操作
在Java中,用Unsafe类来实现CAS的原子操作,Unsafe类 ==> JNI(Java本地接口) ==>本地实现的C++库 ==>操作内存空间
CAS在Java中的应用和缺点
应用:
-
Atomic包, Lock包下系列的类
-
在JDK1.6以后,sychronized升级为重量级锁之前也采用的CAS机制
缺点
-
CAS采用自旋的方式,会浪费CPU的资源
-
不能保证代码块的原子性,保证的是对一个变量的 比较和替换 的操作是原子的
-
ABA问题:CAS操作内存值,由A改成了B,但是又改回了A,从而导致后续本不应该成功的操作,最后成功执行
自己举个栗子:由于网络延时,线程一线程二都想对内存值A操作,目的就是将内存值A改成B(只修改一次),按理说一个线程操作成功,那另一个线程就要操作失败。线程一和线程二取到的旧的值都是A,假定线程一操作成功,将A改成了B。按理说接下来线程二拿到的内存的值是B,和取到的旧的值A比较,B不等于A,就会提交失败,但是捏,好巧不巧,在线程二修改之前,线程三过来执行它自己的任务,将B改成了A。这个时候线程二拿到的内存值是A,之前取到的旧的值也是A,A等于A,线程二就会对A进行修改。这个时候就对内存值修改了两次,而我们只想让它修改一次,就出错了。可以把内存值想成自己的工资,谁都不想自己的工资被莫名其妙的多改几次把,改多了当我没说,哈哈哈。
ABA的解决方案:给数据加上版本号,每次不仅要比较内存的值,还要比较版本号
4. AQS
AQS是什么?
AQS(AbstractQueuedSynchronizer,抽象队列同步器)是构建锁和其他同步组件的基础框架
AQS能干什么?
- 同步队列的管理和维护
- 同步状态的光临
- 线程的阻塞,唤醒的管理
基本设计思路
- 把竞争的线程和等待状态,封装成为Node对象
- AQS把这些Node,放到一个同步队列中去,这个同步队列是一个FIFO(先进先出)的一个双向队列,是基于CLH(贡献者名字缩写首字母,不用纠结这个)队列实现的
- AQS使用int类型的成员变量来表示同步状态,比如:是否有线程获取锁,锁的重入次数等,具体的含义由具体的子类来定义
- AQS使用LockSupport来实现对线程的唤醒和阻塞,线程的唤醒和阻塞便随着同步队列的维护。
AQS如何把基础功能提供出去?
AQS使用模板方法模式,大概的意思是规定了整体的流程,自己可以具体实现子流程,整体的流程是不能变的。后续把设计模式学了再做补充
非阻塞的获取独占锁的流程
自己画的简化版流程,没有涉及到里面的中断
AQS中获取和释放独占锁和共享锁区别
独占锁:正常情况下,只有持有锁的线程运行结束了,释放锁了,该节点才会出队。
共享锁:当前节点唤醒了下一个节点并且将下一个节点设置尾Head之后,该节点出队。
独占锁:只有在释放锁的时候,才会去看看要不要唤醒下一个节点。
共享锁:在获取锁的过程中会在两个地方看看要不要去唤醒下一个节点。一个是在获取锁的流程中调用setHeadAndPropagate()
方法的时候,一个是在释放锁的时候。
5. ReentrantLock
ReentrantLock是Lock接口的实现,主要实现了可重入的独占锁的功能,与synchronized关键字功能类型
ReentrantLock与synchronized对比
ReentrantLock功能更加强大和灵活
- 可非中断的获取锁
- 可中断式的获取锁
- 可超时获取锁
- 提供了公平锁和非公平锁
公平锁和非公平锁的却别主要体现在获取锁的方式上
公平锁:多个线程按照申请获取锁的先后顺序来获取锁
非公平锁:多个线程按照不是按照申请获取锁的先后顺序来获取锁。比如抢占式获取锁。高并发的情况可能会造成饥饿现象
在ReentrantLock的源码中,公平锁主要是通过判断当前的AQS队列是否有节点来控制当前节点是否获得锁。队列中如果有节点那么tryAcquire()
方法直接返回false表示获取锁失败,再将节点其排到队列末尾。
ReentrantReadWriteLock
在实际的业务中,往往读数据比写数据更加频繁,如果我们对读数据使用共享锁,对写数据使用独占锁,那么整个读写的性能就会提高。
读锁:用在读取临界资源的地方
写锁:用在更新临界资源的地方
读锁和写锁的互斥规则:
- 一个线程读,另一个线程读:共享
- 一个线程读,另一个线程写:互斥
- 一个线程写,另一个线程读:互斥
- 一个线程写,另一个线程写:互斥
ReadWriteLock是一个接口,该接口中只有两个方法,分别为Lock readLock();
和Lock writeLock();
ReentrantReadWriteLock:可重入式读写锁,是读写锁(ReadWriteLock)的实现类。
- 支持读锁和写锁
- 支持公平锁和非公平锁
- 支持可重入锁
- 支持锁降级(如果一个线程持有写锁,在不释放写锁的情况下,它还可以继续持有读锁,这种情况就是锁降级)
读写锁的状态存储机制
AQS里的state是一个int值。在读写锁中,需要同时保存两种锁的状态。其同样使用int类型的变量表示state,总共32位,前16位表示读锁的同步状态,后面16位表示写锁的同步状态。获取读锁状态就将state无符号右移16位。获取写锁状态就将state与掩码相与,保留后16位。
6. StampedLock类
ReentrantReadWriteLock中存在着一些问题,写线程可能会出现“饥饿”问题;如果有线程在读,那么写线程是无法获取写锁的。
优点:
在Java8中引入了StampedLock,其对ReentrantReadWriteLock进行了增强,优化了读锁和写锁的访问,使读写锁之间可以相互转换,因此可以更细粒度地控制并发。
缺点:
其设计初衷使作为一个内部工具类来使用,用于辅助开发其他的线程安全组件。用不好的花会产生死锁,产生莫名其妙的问题。不支持可重入也是一个问题。
特点:
- 所有获取锁的方法,都会返回一个stamp
- 所有释放锁的方法,都需要一个stamp
- 是不可重入的
- 有三种访问方式,分别为读模式,写模式,乐观读模式
- 支持读锁和写锁的相互转换
- 不支持Condition
7. Condition
该接口对原生的wait, notify/notifyAll这些方法进行增强,从Java语言层面,实现类似的功能。
AQS是使用同步队列来控制节点获取锁,在Condition中使用条件队列来控制节点什么时候await(),什么时候signal()。与多个节点共用一个同步队列不同的是,一个Conditon对象就对应一个条件队列。
总体流程为,调用await()时,将节点加入等待队列,然后将线程挂起,等待其他线程对其调用signal()方法。其他线程对其调用signal()方法后,将该节点从条件队列中出队,将其添加到同步队列的末尾,然后将其唤醒。然后就走同步队列的那一套流程。
8. ThreadLocal
ThreadLocal是用来存放线程自身相关数据的一个容器。提供线程本地变量,访问这个变量的每个线程都会有这个变量的一个副本。线程操作数据的时候就会操作线程本地的数据,从而避免了线程安全性问题。
threadLocals其实是一个ThreadLocalMap类型的,在Thread类中的一个属性,伴随的线程的存在而存在。当我们设置ThreadLocal变量的时候,ThreadLocalMap中的key就是ThreadLocal,value就是ThreadLocal变量的值。
-
由于threadLocals是Thread的一个属性,会跟着线程一直存在,为了避免内存溢出,在确定ThreadLocal数据以后不再使用后,要及时remove掉。
-
由于ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部关联的强引用,在垃圾回收的时候,JVM会回收掉ThreadLocal,就会出现ThreadLocalMap中key为空,但是value值还在。造成内存泄漏,所以在确定ThreadLocal数据以后不再使用后,要及时remove掉。