Loading

多线程锁及其相关策略介绍

多线程笔记(二)

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掉。

posted @ 2022-05-10 15:50  KeepGoing4everZxz  阅读(101)  评论(0编辑  收藏  举报