线程同步
多个线程操作同一个资源,并发问题
- 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。这时候就需要线程同步
- 线程同步是一种等待机制,多个需要同时访问此对象的线程进入线程等待池形成队列,等待前面线程使用完毕,下一个线程再使用
- 形成条件:队列 + 锁
锁
线程同步:
- 由于同一个进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制
synchronized
,当一个线程获得对象的排它锁,独占资源,其他资源必须等待,使用后释放锁即可,存在以下问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题
线程不安全案例
1. 买票
可以观察到问题,第三张和第二张都被两个人买走了
2. 不安全取钱
设定银行里一共只有100块钱,但是我和我女朋友一人取了50,一人取了100,给取出来了,显然不对
3. 不安全集合
一些线程可能被覆盖
同步方法与同步块
- 由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是
synchronized
关键字,它包括两种用法:
- synchronized方法
- synchronized块
同步方法:
public synchronized void method(int args){}
- synchronized方法控制对"对象"的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞
- 方法一旦执行,旧独占该锁,知道该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行
缺陷:若将一个大的方法声明为synchronized会影响效率
同步方法的弊端
- 方法里需要修改的内容才需要锁,锁太多,浪费资源
同步块
- 同步块
synchronized(Object){}
- Object称为同步监视器
- Object可以使任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是对象本身,或者是class
- 同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
将买票案例修改为线程安全的
在
buy()
方法上加synchronized
将取钱案例修改为线程安全的
用同步块,锁
account
。 synchronized写在方法run()
方法上的话,锁的是this,就是GettingMoney的对象。所以没有用
将线程不安全集合改为线程安全集合
死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源释放才能运行,而导致两个或多个线程都在等待对方释放资源,全部停止执行的情形。某一个同步块同时拥有「两个以上对象的锁」时,就可能发生
死锁
问题
互相持有对方锁,程序卡住。
死锁的避免方法:
- 产生死锁的四个必要条件:
- 互斥条件: 一个资源每次只能被一个线程使用
- 请求保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:线程已获得的资源,在未用完之前,不能强行剥夺
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
上面列出了死锁的四个必要条件,只需要想办法破除其中任意一个或多个条件,就能避免死锁的发生
Lock锁
- 从JDK 5开始,Java提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步。同步锁使用
Lock
对象充当- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前先获得
Lock
对象ReentrantLock
类实现了Lock
接口,它拥有与synchronized相同的并发行和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock
,可以显式加锁、释放锁ReentrantLock
——可重入锁
在 Lock 接口的注释中写到:Lock 的意义在于提供了区别于 synchronized 的另一种具有更多广泛操作的同步方式,它能支持更多灵活的结构,并且可以关联多个
Condition
对象Lock 接口中的六个方法
void lock()
—— 获取锁,获取不到就等待void lockInterruptibly()
—— 获取锁,加入当前线程在等待锁的过程中被中断,则退出等待并抛出中断异常boolean tryLock()
—— 尝试获取锁,立即返回结果boolean tryLock(long time, @NotNull TimeUnit unit)
—— 在指定时间内尝试获取锁,假设中间被中断,则退出并抛出中断异常void unlock()
Condition newCondition()
—— 新建一个绑定在当前 Lock 上的 Condition 对象Condition 表示一个等待状态,可以做精准唤醒
ReentrantLock
ReentrantLock 基于 AQS,在并发编程中它可以实现公平锁和非公平锁来对共享资源进行同步,同时,和 synchronzied 一样,ReentrantLock 支持可重入,除此之外 ReentrantLock 在调度上更灵活,支持更丰富的功能
public class ReentrantLock implements Lock, java.io.Serializable {private static final long serialVersionUID = 7373984872572414699L;/** Synchronizer providing all implementation mechanics */private final Sync sync;...// 三个内部类abstract static class Sync extends AbstractQueuedSynchronizer {...}static final class NonfairSync extends Sync {...}static final class FairSync extends Sync {...}}
- ReentrantLock 只有一个属性
sync
并且是 final;三个内部类,其中第一个就是成员属性 sync 对应的类 Sync,它继承了 AQS
- Sync 可以使用 AQS 的机制
- Sync 是抽象类,而
NonfairSync
与FairSync
是它唯二的实现子类Sync 类 nonfairTryAcquire 方法/*** Performs non-fair tryLock. tryAcquire is implemented in* subclasses, but both need nonfair try for trylock method.*/final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) { // 0表示 AQS 状态为空闲,可以通过CAS原子操作修改stateif (compareAndSetState(0, acquires)) { // 如果修改成功则获取了锁,将点前线程设置为独占线程setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) { // 当 state 不为0,首先判断当前线程是否已经是独占线程// 这里就是对可重入特性的实现int nextc = c + acquires; // 累加记录重入次数,之后释放时也需要释放相应的次数if (nextc < 0) // overflow 将重入次数限制为 int 类型能表示的最大正数throw new Error("Maximum lock count exceeded");setState(nextc);return true;}// 来到这里表示获取锁失败return false;}Sync 类 tryRelease 方法// 返回是否完全被释放,而不是是否成功释放protected final boolean tryRelease(int releases) {int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}
公平锁与非公平锁
- 公平锁:按照请求锁的顺序分配拥有稳定获取锁的机会,但是性能可能比非公平锁低
- 非公平锁:不按照请求锁的顺序分配,不一定拥有获得锁的机会,但是性能可能比公平锁高
非公平锁实现/*** Sync object for non-fair locks*/static final class NonfairSync extends Sync {private static final long serialVersionUID = 7316153563782823691L;/*** Performs lock. Try immediate barge, backing up to normal* acquire on failure.*/final void lock() {// 直接CAS尝试拿锁,不管前面的人,体现非公平if (compareAndSetState(0, 1)) // 只尝试一次setExclusiveOwnerThread(Thread.currentThread());else // 一次没拿到就直接进入 AQS FIFO 排队acquire(1); // 内部会调用 tryAcquire}protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}}公平锁实现/*** Sync object for fair locks*/static final class FairSync extends Sync {private static final long serialVersionUID = -3000897897090466540L;final void lock() {acquire(1); // AQS FIFO 队列,会调用tryAcquire()}/*** Fair version of tryAcquire. Don't grant access unless* recursive call or no waiters or is first.*/protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) { // 如果锁空闲if (!hasQueuedPredecessors() && // 没有排在当前线程之前的线程compareAndSetState(0, acquires)) { // 就 CAS,如果成功setExclusiveOwnerThread(current); // 将当前线程设置为独占线程return true; // 返回获取锁成功}}else if (current == getExclusiveOwnerThread()) { // AQS 状态不是0,判断当前线程是不是独占线程int nextc = c + acquires; // 记录重入次数if (nextc < 0) // 限制重入次数throw new Error("Maximum lock count exceeded");setState(nextc);return true;}// 不是独占线程,获取锁失败return false;}}总结:ReentrantLock 如何通过 AQS 实现可重入锁
- 公平锁:只有当 AQS FIFO 队列上,当前线程节点 与 headNode 之间没有其他线程节点时,才能尝试直接通过 CAS 获取锁;否则都要正常通过 AQS 排队(体现公平)
- 非公平锁:上来就是一个 CAS 直接抢,但是只会尝试一次,失败就进 AQS 队列
ReetrantLock 实现 Lock 接口的方法,调用 sync 的子类实现公平锁与非公平锁,通过构造方法区分公平与非公平
通过抽象类 Sync 的引用 sync 来切换公平与非公平实现;构造方法区分公平与非公平声明/*** Creates an instance of {@code ReentrantLock}.* This is equivalent to using {@code ReentrantLock(false)}.*/public ReentrantLock() {sync = new NonfairSync();}/*** Creates an instance of {@code ReentrantLock} with the* given fairness policy.** @param fair {@code true} if this lock should use a fair ordering policy*/public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}
synchronized与Lock对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能个更好,并且具有更好的扩展性(提供更多的子类)
- synchronized是java内置关键字,Lock是一个Java类
- synchronized是可重入锁,不可以中断,非公平 Lock,可重入锁,非公平(可以自己设置)
- synchronized适合少量的代码同步问题,Lock适合大量的同步代码
- 优先使用顺序
- Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)
synchronized 锁升级过程
https://blog.csdn.net/zwx900102/article/details/106305107
对象头——持有一把锁
- markword 32/64 bit
- 锁标志位
- ClassPoint : 指向方法区中的数据
synchronized
关键字在 javac 后会生成两条字节码指令
:monitorenter
、monitorexit
Monitor —— 管程/监视器
- 可以想象成一个只能容纳一个人的房间
- 将想要进入 Monitor 这个房间的线程想象成人
- 底层依赖操作系统的
mutex
指令- Java 线程模型采用
一对一模型
,一个 Java 线程对应一个操作系统内核线程,频繁的线程阻塞挂起会带来频繁的内核态用户态切换,耗费资源进入队列
:一堆想要访问临界区资源的线程,等待系统调度,尝试用acquire()
方法进去房间:临界区
:里面有共享资源,同一时刻只允许一个线程持有,wait()
方法会让临界区的资源释放锁,进入等待队列
;notify
会唤醒一个等待的线程重新进去进入队列
,参与时间片竞争,notifyAll
则是唤醒一堆release()
等方法就是直接退出整个管程
无锁、偏向锁、轻量级锁、重量级锁以及升级过程
- 无锁:
- 资源无竞争
- 资源存在竞争,但是希望通过无锁的方式,实现同步
- CAS——比较与交换,自旋锁,原语
- 偏向锁:资源会被加锁,但在实际工程中只有一个线程会使用这个资源,那么希望:
- 不使用
mutex lock
获取锁- 不使用
CAS
获取锁- 而是,对象认识这个唯一的线程
- mark word 的倒数 2bit 为所标志位,
01
表示无锁或偏向锁- mark word 的倒数第3个 bit 表示是否为偏向锁,
1
表示偏向锁,0
表示无锁- 如果是偏向锁,1 ~ 23bit 为
偏向的线程ID
- 如果
偏向线程ID
与当前线程ID 不一致,说明有多个线程在竞争资源,则 升级为轻量级锁- 轻量级锁:将 mark word 的前 30bit 拿来指向栈中锁记录
Lock Record
的指针
- 检查倒数 2bit 为
00
说明是轻量级锁- 在 JVM 栈中开辟一块称为
Lock Record
的空间,存放mark word
的副本,以及owner
指针- 线程通过 CAS 尝试获取对象的锁,一旦成功获得锁,就将当前 mark word 复制到 Lock Record,并让 Lock Record 中的 owner 指针指向当前对象,这样就实现了
对象 ↔ Lock Record
双射- 此时获得锁的线程执行自己的代码,其他想要获取资源的线程 自旋等待,自旋消耗cpu算力,默认自旋10次,可以通过指令开启自适应自旋
- 一旦自旋等待的线程数目超过 1 个,触发 轻量级锁升级到重量级锁
- 重量级锁:完全锁定资源,通过阻塞机制,操作系统调度实现