java锁的底层原理

知识整理

  1. Synchronized 内置锁,JVM级别
    1. 使用
    2. 底层 锁升级过程、CAS操作的缺点【替换线程和copy mw】
    3. 优化
      1. 代码优化:同步代码块、减少锁粒度、读锁并发
      2. JDK自带 偏置锁、轻量级锁(CAS操作)、自适应自旋、锁粗化、锁消除
  2. Volatile
      1. 概念:非阻塞可见性、禁止指令重排序*
      2. 与syn区别: 无法实现原子操作、使用场景--单线程、不依赖当前值
  3. Reentrantlock 显示锁:基于AQS实现,API级别
    1. AQS原理:
      1. 数据结构:state、waitstate【signal-1、传播-3】、
      2. 独占、共享 tryAcquireShared
    2. 非公平锁
    3. 特性锁 可重入、轮询、定时、可中断
    4. 优点、使用场景
    5. 与Syn区别、Syn优点
  4. 死锁
    1. 概念:多个线程因竞争资源而互相等待的僵局;4个必要条件:资源互斥、不可剥夺、保持与请求、循环等待
    2. 死锁避免:锁顺序、锁时限、死锁检测与恢复
    3. 死锁检测与恢复:分配资源时不加条件;检测时机:进程等待、定时、利用率下降
      1. 检测算法:资源分配表、遍历锁关系图
      2. 撤销进程、设置线程随机优先级
  5. 锁模式
    1. 读锁、写锁
    2. 乐观锁:用户解决---数据版本id、时间戳;CAS;适合写操作少的场景;MVCC实现
    3. 悲观锁:数据库行锁、页锁...

 

synchronized的4种应用方式 jvm内部实现 称为:内置锁

synchronized关键字最主要有以下3种应用方式,都是作用在对象上

  1. 修饰类,作用范围:synchronized括号内, 作用对象:类的所有对象;synchronized(Service.class){ }
  2. 修改静态方法,作用范围:整个静态方法, 作用对象:类的所有对象;
  3. 修饰方法,被修饰的同步方法,作用范围:整个方法, 作用对象:调用这个方法的对象;
    1. 缺点:A线程执行一个长时间任务,B线程必须等待
  4. 修饰代码块,被修饰的代码块同步语句块,作用范围:大括号内的代码, 作用对象:调用这个代码块的对象;
    1. 优点:减少锁范围,耗时的代码放外面,可以异步调用

 

notify 方法实现只唤醒一个线程,由操作

lock.notify()方法最终通过ObjectMonitor的void notify(TRAPS)实现:

1、如果当前_WaitSet【线程等待的集合】为空,即没有正在等待的线程,则直接返回;

2、通过ObjectMonitor::DequeueWaiter 出队方法,获取_WaitSet列表中的第一个ObjectWaiter节点,实现也很简单【选择哪个线程取决于操作系统对多线程管理的实现】

3、根据不同的策略,将取出来的ObjectWaiter节点,加入到Contention List,或自旋操作,CAS改变第一个节点的的指针为新增节点

 

notifyAll方法实现

lock.notifyAll()方法最终通过ObjectMonitor的void notifyAll(TRAPS)实现:

通过for循环取出_WaitSet的ObjectWaiter节点,并根据不同策略,加入到_EntryList或则进行自旋操作。

从JVM的方法实现中,可以发现:notify和notifyAll并不会释放所占有的ObjectMonitor对象,其实真正释放ObjectMonitor对象的时间点是在执行monitorexit指令,

一旦释放ObjectMonitor对象了,Entryset中ObjectWaiter节点所保存的线程,就可以开始竞争ObjectMonitor对象进行加锁操作了,和ready线程竞争?

 

2.锁的的实现:内存机制 copy到工作内存->   修改->   刷新主存   的过程,才会释放它得到的锁,达到线程安全。

  1. 锁住(lock)
  2. 主->从 read load 将需要的数据从主内存拷贝到自己的工作内存(read and load)
  3. 修改 use assign 根据程序流程读取或者修改相应变量值(use and assign)
  4. 从->主 store write将自己工作内存中修改了值的变量拷贝回主内存(store and write)
  5. 释放对象锁(unlock)

线程安全:

  1. 当多个线程访问某个类,其始终能表现出正确的行为
  2. 采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,限制其他线程访问,直到锁释放

 

Java中的锁优化 代码方式、JDK自带方式

1.代码 锁优化

  1. 减少锁持有时间 
    1. 使用同步代码块,而非同步方法;
  2. 减小锁粒度
    1. JDK1.6中 ConcurrentHashMap采取对segment加锁而不是整个map加锁,提高并发性;
  3. 锁分离  读锁之间不互斥;读写分离
    1. 根据同步操作的性质,把锁划分为的读锁和写锁,读锁之间不互斥,提高了并发性

2.JDK1.6 锁优化 synchronized底层

1.引入偏向锁、轻量级锁

  1. 锁主要存在四中状态,依次是:无锁状态01、偏向锁状态01、轻量级锁状态00、重量级锁状态10,
  2. 会随着竞争的激烈而逐渐升级,锁可以升级不可降级,提高 获得锁和释放锁 效率
  3. “轻量级锁”和“偏向锁”作用:减少 获得锁和释放锁 的性能消耗

优点

缺点

适用场景

偏向锁

记录线程iD,若该线程,则不加锁;锁状态01

如果线程间存在锁竞争,会带来额外的锁撤销的消耗。

适用于只有一个线程访问同步块场景。

轻量级锁

Mark Word复制到锁记录,CAS更新指针及标志位00

自旋方式竞争,竞争的线程不会阻塞,提高了程序的响应速度

如果始终得不到锁竞争的线程使用,自旋会消耗CPU。

追求响应时间。

同步块执行速度非常快。

重量级锁

CAS失败时,升级。锁状态:10

线程阻塞,响应时间缓慢。

追求吞吐量。

同步块执行速度较长。

偏向锁获取过程

  1. 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。
  2. 如果为可偏向状态,则判断偏向线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。
  3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;
    1. 如果竞争失败,执行4,偏向锁升级为轻量级锁。
  4. 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)
  5. 执行同步代码

轻量级锁

  1. 虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝【因为栈帧为线程私有,对象大家都有】
  2. 拷贝对象头中的Mark Word复制到锁记录(Lock Record)中;
  3. 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word中的,更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。
  4. 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示。

 

2.锁粗化 

  1. 如果一系列的连续操作都对同一个对象反复加锁和解锁,如循环体内,很耗性能
  2. 加锁同步的范围扩展到整个操作序列的外部:第一个append到最后一个append;不对每个append加锁

3.锁消除 逃逸分析的数据的支持

        编译器判断到一段代码中,堆上的数据不会逃逸出当前线程,可以认为是线程安全的,不必加锁

4.自旋与自适应自旋:想要获取锁的线程做几个空循环 10 CAS实现

.为什么引入:

轻量级锁失败后,线程会在操作系统层面挂起

操作系统实现线程之间的切换时,需要从用户态转换到核心态,状态转换耗时

.解决方法:

当线程在获取轻量级锁时CAS操作失败时,通过自旋让线程等待,避免线程切换的开销

假设不久当前的线程可以获得锁,虚拟机会让当前想要获取锁的线程做几个空循环,可能是50个循环或100循环

结果:

如果得到锁,就顺利进入临界区;如果不能,就将线程在操作系统层面挂起,升级为重量级锁

自旋锁的优化:自适应自旋

自旋是需要消耗CPU的,如果一直获取不到锁,线程一直自旋,浪费CPU资源

线程如果自旋成功了,下次自旋的次数会更多,自旋失败了,自旋的次数就会减少。

 

CAS底层实现原理    用于更新数据

CAS:Compare and Swap, 翻译成比较并交换 

CAS需要在:操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,最终都会返回内存地址,且是原子操作

需要3个操作数:内存地址V,旧预期值A、新值B

当且仅当V符合预期值A时(即V存储的值无变化),用B更新A,否则不执行更新,最终都返回内存地址V

适用场景:

  1. 适合资源竞争较少的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;
  2. CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,可以获得更高的性能
  3. synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS
  4. 原子类中都使用到了CAS

 

volatile详解

非可见性:

编译器为了加快程序运行的速度,对一些变量的写操作会先在(工作内存)寄存器或者是CPU缓存上进行,最后才写入内存,这个过程中,变量的新值对其他线程是不可见的

1.实现对所有线程的可见性 SMP:对称多处理器架构通过总线BUS进行 Cache一致性流量 通信

  1. volatile保证新值立即同步到主存,
  2. 线程对变量读取的时候,要从主内存中读,而不是缓存
  3. 变量的赋值一旦变化就会通知到其他线程,如果其他线程的工作内存中存在这个同一个变量拷贝副本,那么其他线程会放弃这个副本中变量的值,重新去主内存中获取

适用场景:

确保只有单一的线程修改变量的值 或 运算结果不依赖当前变量值(i++时,运算结果依赖当前变量,且是多个线程改变)

变量不需要与其他的状态变量共同参与不变约束

2.实现 禁止指令重排序优化 应用:双边检查单例

  1. 为了减少CPU空闲时间,java不能保证程序执行的顺序与代码中一致,
  2. volatile修饰的变量相当于生成内存屏障,重排序时不能把后面的指令排到屏障之前;指令屏障
  3. 作用:为了保证happen-before原则:写、锁lock、传递性、线程启动、中断、终结、对象创建的先后关系
  1. 定义了线程、锁、volatile变量、对象创建的先后关系
  2. 若满足,则保证一个操作执行的结果需要对另一个操作可见
  3. 判断数据是否存在竞争、线程是否安全的依据

内存屏障实现方式;volatile的内存语义

编译器在生成字节码时,会在指令序列中,插入内存屏障来禁止特定类型的处理器重排序;

写前后、读后后;

写写【写上下】写读、【读后面】读读、读写

 

1.volatile写操作的前面插入一个StoreStore屏障:禁止上面的写

2.volatile写操作的后面插入一个SotreLoad屏障:禁止下面的读

3.volatile读操作的后面插入一个LoadLoad屏障:禁止下面的读

4.volatile读操作的后面插入一个LoadStore屏障:禁止下面的写

 

 

3.无法实现 i++ 原子操作

A读取 i 后,B也读取 i ,此时A进行 +1,B的 i 就变了

原子类如何解决:CAS,B在进行+1时,检查此时的 i 跟主存的 i 是否一致,一致才+1;原子类

synchronized和volatile区别 锁的目标:关注互斥性和可见性

1.锁提供了两种主要特性:互斥性(mutual exclusion) 和可见性(visibility)。

  互斥即一次只允许一个线程持有某个锁,使用该共享数据。

  可见性确保新值立即同步到主存,每次使用前立即从主内存刷新

2.概念

synchronized同步阻塞:释放锁之前会将对变量的修改刷新到主存当中;

volatile关键字非阻塞:确保新值立即同步到主存,其他线程每次使用前立即从主内存刷新;

3.区别

1)volatile非阻塞,synchronized只有当前线程可以访问修饰的变量,其他线程阻塞

2)volatile仅能修饰变量,synchronized则可以使用在变量,方法.

3)volatile仅能实现变量的修改可见性,而synchronized则可以保证变量的修改可见性和原子性(操作不可分割)

 

可重入锁 Re entrantlock

显示锁:基于JDK API、AQS、乐观锁实现、需要显式的加锁以及释放锁

  1. 无阻塞的同步机制(非公平锁实现)
  2. 可实现轮询锁、定时锁、可中断锁特性;
  3. 提供了一个Condition(条件)类,对锁进行更精确的控制
  4. 默认使用非公平锁,可插队跳过对线程队列的处理(因此被称为可重入)
    1. ReentrantLock的内部类Sync继承了AQS,分为公平锁FairSync和非公平锁NonfairSync。
    2. 公平锁:线程获取锁的顺序和调用lock的顺序一样,FIFO;唤醒锁的时间CPU浪费;是否是AQS队列中的头结点
    3. 非公平锁:线程获取锁的顺序和调用lock的顺序无关,先执行lock方法的锁不一定先获得锁
  5. 加锁和解锁都需要显式写出,实现了Lock接口,注意一定要在适当时候unlock
  6. 总结:公平锁与非公平锁对比
  • FairSync:lock()少了插队部分(即少了CAS尝试将state从0设为1,进而获得锁的过程)
  • FairSync:tryAcquire(int acquires)多了需要判断当前线程是否在等待队列首部的逻辑(实际上就是少了再次插队的过程,但是CAS获取还是有的)。

公平锁的核心

  1. 获取一次锁数量,state值
    1. 如果锁数量为0,如果当前线程是等待队列中的头节点,基于CAS尝试将state(锁数量)从0设置为1一次,如果设置成功,设置当前线程为独占锁的线程;
    2. 如果锁数量不为0或者当前线程不是等待队列中的头节点或者上边的尝试又失败了,查看当前线程是不是已经是独占锁的线程了,如果是,则将当前的锁数量+1;如果不是,则将该线程封装在一个Node内,并加入到等待队列中去。等待被其前一个线程节点唤醒。

非公平锁 两者都是非公平锁

  1. 非公平锁,可以直接插队获取锁,跳过了对队列的处理,速度会更快
    1. 公平锁为了保证线程规规矩矩地排队,需要增加阻塞和唤醒的时间开销;
  2. AQS底层原理:在lock获取锁时首先判断当前锁是否可以用(AQS的state状态值是否为0),如果是 直接“插队”获取锁,否则进入排队队列,并阻塞当前线程; 充分利用了唤醒线程的时间【Singel标志唤醒,需要前驱节点唤醒】

 

非公平锁加锁的简单步骤

基于CAS尝试将state(锁数量)从0设置为1 ---第一次插队

  1. 如果设置成功,设置当前线程为独占锁的线程;
  2. 如果设置失败,还会再获取一次锁数量,---第二次插队
    1. 如果锁数量为0,再基于CAS尝试将state(锁数量)从0设置为1一次,如果设置成功,设置当前线程为独占锁的线程;
    2. 如果锁数量不为0或者上边的尝试又失败了,查看当前线程是不是已经是独占锁的线程了,如果是,则将当前的锁数量+1;如果不是,则将该线程封装在一个Node内,并加入到等待队列中去。等待被其前一个线程节点唤醒
  3. 入队后,无限循环tryAcquire(1)方法 ---第三次插队

 

非公平锁源码

1.基于CAS将state(锁数量)从0设置为1,如果设置成功,设置当前线程为独占锁的线程;-->第一次插队

若失败,调用acquire(1)->tryAcquire(1),acquireQueued(addWaiter(Node.EXCLUSIVE)

public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();//请求锁成功,中断自己 }

tryAcquire(arg)会调用nonfairTryAcquire(1)调用,作用:第二次插队请求锁

final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread();//获取当前线程 int c = getState();//获取锁数量 if (c == 0) {//如果锁数量为0,证明该独占锁已被释放,当下没有线程在使用 if (compareAndSetState(0, acquires)) {//继续通过CAS将state由0变为1,注意这里传入的acquires为1 setExclusiveOwnerThread(current);//将当前线程设置为独占锁的线程 return true; } } else if (current == getExclusiveOwnerThread()) {//查看当前线程是不是就是独占锁的线程 int nextc = c + acquires;//如果是,锁状态的数量为当前的锁数量+1 if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc);//设置当前的锁数量 return true; } return false; }

若请求锁失败,将当前线程链入队尾并挂起,之后等待被唤醒 【快速、正常】

  1. 首先会使用addWaiter(Node.EXCLUSIVE)将当前线程封装进Node节点node,然后将该节点加入等待队列
  2. 先快速入队【存在尾节点,将使用CAS尝试将尾节点设置为node】
  3. 如果快速入队不成功【尾节点为空】,使用正常入队方法enq,无限循环=第一次阻塞,直到Node节点入队为止【创建一个dummy节点,并将该节点通过CAS设置到头节点,若头结点不为null,cas继续快速入队】

入队成功后返回node节点,继续第三次插队

无限循环调用:acquireQueued(final Node node, int arg)获取node的前驱节点p

p==head&&tryAcquire(1) 是唯一跳出循环的方法:p成为头结点并且获取锁成功:如果p是头节点,就继续使用tryAcquire(1)方法插队,若成功,不用中断,第三次插队成功

  1. 如果p不是头节点,或者tryAcquire(1)请求不成功,执行shouldParkAfterFailedAcquire(Node pred, Node node)来检测当前节点是不是可以安全的被挂起:判断p的等待状态waitStatus
    1. SIGNAL(即可以唤醒下一个节点的线程),则node节点的线程可以安全挂起,返回true
    2. CANCELLED,则p的线程被取消了,我们会将p之前的连续几个被取消的前驱节点从队列中剔除
    3. 等待状态是除了上述两种的其他状态,CAS尝试将前驱节点的等待状态设为SIGNAL【p与node竞争】

挂起后后 跳出循环,需要中断自身

LockSupport.park(this);//挂起当前的线程,后等待前去节点unpark唤醒该线程;方法为public

 

DelayQueue中的使用示例:

  1. take()和offer()都是lock了重入锁,按照synchronized的公平锁,两个方法是互斥
  2. take()方法需要等待1个小时才能返回,offer()需要马上提交一个10秒后运行的任务,此时offer()可以插队获取锁
  3. 原理:A执行时,B lock()锁,并休眠;当锁被A释放处于可用状态时,B线程却还处于被唤醒的过程中,此时C线程请求锁,可以优先C得到锁

 

Reentrantlock优点

  1. 显示锁可中断,防止死锁,内置锁不可中断,会产生死锁
  2. 实现其他特性的锁
  3. 对锁更精细的控制

 

synchronized优点

  1. 显示锁易忘记 finally 块释放锁,对程序有害
  2. 显示锁只能用在代码块,强制更细粒度的加锁;syn可以用在方法上
  3. synchronized 管理锁定和释放时,能标识死锁或者其他异常行为的来源,利于调试
  4. Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多

使用场景

  1. Condition类对锁进行更精确的控制,指定唤醒、分组唤醒
  2. 防止死锁
  3. 轮询锁:用tryLock(long timeout, TimeUnit unit)和tryLock() 这两个方法实现,即没有获取到锁,可以使用while循环 隔一段时间再次获取,直到获取到为止
  4. 定时锁:指的是在指定时间内没有获取到锁,就取消阻塞并返回获取锁失败;tryLock(long timeout, TimeUnit unit)
  5. 可中断锁:lockInterruptibly,防止死锁

 

区别

synchronied是JVM级别的,而ReentrantLock是api级别的

JVM会对synchronied做出相应的优化

锁消除:JVM判断堆上的数据不会逃逸出当前线程,不加锁;

自旋锁

自适应锁

 

提供的lock()方法:

重入锁实现 直到state为0,其他锁才可以用

  1. 如果该锁没有被另一个线程持有,则获取该锁并立即返回,将锁计数设置为 1;对应AQS中的state
  2. 如果当前线程已经持有该锁,将锁计数加 1,并立即返回方法---重入锁
  3. 如果该锁被另一个线程持有,则禁用当前线程,在获得锁之前,一直休眠,此时锁保持计数设置为 1

排他锁实现:Lock类有读锁和写锁,读读共享,写写互斥,读写互斥:每次获取锁时都是首先判断state是否为0,并且只有1个线程能获取到锁

tryLock和lock和lockInterruptibly的区别

  1. tryLock能获得锁就返回true,不能就立即返回false,可以增加时间限制,如果超过该时间段还没获得锁,返回false;tryLock(long timeout,TimeUnit unit),
  2. lock能获得锁就返回true,不能的话一直等待获得锁
  3. lockInterruptibly,中断会抛出异常

 

锁的Condition类

Lock类可以创建Condition对象,Condition对象用来是线程等待和唤醒线程;Condition condition=lock.newCondition();

对锁进行更精确的控制

  1. Condition中的await()方法相当于Object的wait()方法
  2. Condition中的signal()方法相当于Object的notify()方法
  3. Condition中的signalAll()相当于Object的notifyAll()方法
  4. ReentrantLock类可以唤醒指定条件的线程,而object的唤醒是随机的

 

Condition函数列表

  1. 造成当前线程在接到信号或被中断之前一直处于等待状态 void await()
  2. 唤醒一个等待线程 void signal()
  3. 唤醒所有等待线程 void signalAll()
  4. 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态 boolean await(long time, TimeUnit unit)
  5. 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态 long awaitNanos(long nanosTimeout)
  6. 造成当前线程在接到信号之前一直处于等待状态 void awaitUninterruptibly()
  7. 造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态 boolean awaitUntil(Date deadline)

 

 

共享锁实现 并发读 ReentrantReadWriteLock、计数器

共享锁的AQS实现

实现tryAcquireShared方法【检查下一个节点是共享节点】,获取共享锁,锁在所有调用await方法的线程间共享

底层:

  1. 在AQS队列中,将线程包装为Node.SHARED节点,即标志为共享锁
  2. 当头节点获得共享锁后,唤醒下一个共享类型结点的操作
    1. 头节点node1调用unparkSuccessor()方法唤醒了Node2,并且调用tryAcquireShared方法检查下一个节点是共享节点
    2. 如果是,更改头结点,重复以上步骤,以实现节点自身获取共享锁成功后,唤醒下一个共享类型结点的操作

应用:

1.ReentrantReadWriteLock

2.CountDownLatch为java.util.concurrent包下的计数器工具类

可被多线程并发的实现减1操作,并在计数器为0后,调用await方法的线程被唤醒,从而实现多线程间的协作

new CountDownLatch(3).countDown();

用来实现等所有共享锁线程都唤醒后一起协作

 

死锁

多个线程因竞争资源而造成僵局(互相等待),无法向前推进

产生的原因

1) 系统资源的竞争

系统不可剥夺资源,数量不足以满足多个进程运行,使得进程在运行过程中,因竞争资源而陷入僵局

2) 进程推进顺序非法

请求和释放资源的顺序不当,也同样会导致死锁。如,互相申请各占有的资源。

信号量使用不当也会造成死锁。进程间彼此相互等待消息,结果也会使得这 些进程间无法继续向前推进。

3) 死锁产生的4个必要条件,只要其中任一条件不成立,死锁就不会发生

  1. 资源互斥条件:资源互斥,即某资源仅为一个进程占有
  2. 资源不可剥夺条件:进程所获得的资源在未使用完毕之前,只能是主动释放,不能被其他进程强行夺走
  3. 保持和请求条件:进程已经保持了一个资源,又提出了新的资源请求,而该资源已被其他进程占有
  4. 循环等待条件:进程资源循环等待

 

如何避免死锁

  1. 加锁顺序(线程按照一定的顺序加锁)
    1. 按照顺序加锁是一种死锁预防机制,需要事先知道所有会用到的锁
  2. 加锁时限(超时则放弃)
    1. 获取锁时加上时限,超过时限则放弃请求,并释放锁,等待一段随机的时间再重试
  3. 死锁检测与恢复
    1. 操作系统中:系统为进程分配资源,不采取任何限制性措施,提供检测和恢复的手段

死锁检测:当一个线程请求锁失败时,遍历锁的关系图检测死锁

死锁恢复

  1. 撤消进程,剥夺资源
  2. 线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁
  3. 死锁发生的时候设置随机的优先级;如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。

 

锁模式包括: 

  1. 共享锁:(读取)用户可以并发读取数据,但不能获取写锁,直到释放所有读锁。
  2. 排他锁(写锁):加上写锁后,其他线程无法加任何锁;写锁可以读和写
  3. 更新锁: 防止死锁而设立,转换读锁为写锁之前的准备,仅一个线程可获得更新锁

乐观锁:认为数据一般情况下不会造成冲突,在数据提交更新时,才进行数据的冲突检测;

如果冲突,返回信息让用户决定如何去做。实现方式:记录数据版本。

悲观锁:操作数据时上锁保护,限制其他线程访问,直到该锁释放。关系型数据库锁机制,行锁、页锁、表锁,都是在做操作之前先上锁。

锁的粒度: 都是悲观锁

  1. 行锁: 粒度最小,并发性最高
  2. 页锁:锁定一页。25个行锁可升级为一个页锁。
  3. 表锁:粒度大,并发性低
  4. 数据库锁:控制整个数据库操作

 

Happen-Before原则 八大原则:

  1. 定义了线程、锁、volatile变量、对象创建的先后关系
  2. 若满足,则保证一个操作执行的结果需要对另一个操作可见
  3. 判断数据是否存在竞争、线程是否安全的依据
  • 单线程:在同一个线程中,书写在前面的操作happen-before后面的操作。
  • 锁:解锁先于锁定;同一个锁的unlock操作happen-before此锁的lock操作。
  • volatile:先写;对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。
  • 传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
  • 线程启动:start方法优先;同一个线程的start方法happen-before此线程的其它方法。
  • 线程中断:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
  • 线程终结:线程中的所有操作都happen-before线程的终止检测。
  • 对象创建:先初始化,后finalize;一个对象的初始化完成先于他的finalize方法调用。

 

你可能感兴趣的:(java基础)