ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

多线程笔记(二)

2022-05-10 16:04:19  阅读:178  来源: 互联网

标签:AQS 队列 笔记 获取 线程 一个 多线程 节点


多线程笔记(二)

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

标签:AQS,队列,笔记,获取,线程,一个,多线程,节点
来源: https://www.cnblogs.com/xuzhuo123/p/16253912.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有