背景:最近在准备java基础知识,对于可重入锁一直没有个清晰的认识,有必要对这块知识进行总结。
1 . 什么是可重入锁
锁的概念就不用多解释了,当某个线程A已经持有了一个锁,当线程B尝试进入被这个锁保护的代码段的时候.就会被阻塞.
而锁的操作粒度是”线程”,而不是调用(至于为什么要这样,下面解释).同一个线程再次进入同步代码的时候.可以使用自己已经获取到的锁,这就是可重入锁
java里面内置锁(synchronize)和Lock(ReentrantLock)都是可重入的
2 . 为什么要可重入
如果线程A继续再次获得这个锁呢?比如一个方法是synchronized,递归调用自己,那么第一
次已经获得了锁,第二次调用的时候还能进入吗? 直观上当然需要能进入.这就要求必须是可重入的.可重入锁又叫做递归锁,再举个例子.
public class Widget { public synchronized void doSomething() { ... } } public class LoggingWidget extends Widget { public synchronized void doSomething() { System.out.println(toString() + ": calling doSomething"); super.doSomething();//若内置锁是不可重入的,则发生死锁 } }
这个例子是java并发编程实战中的例 子.synchronized 是父类Widget的内置锁,当执行子 类的方法的时候,先获取了一次Widget的锁,然后在执行super的时候,就要获取一次,如果不可重入,那么就跪了.
3 . 如何实现可重入锁
为每个锁关联一个获取计数器和一个所有者线程,当计数值为0的时候,这个所就没有被任何线程持有.
当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1,如果同一个线程再次获取这个锁,计数值将递增,退出一次同步代码块,计算值递减,当计数值为0时,这个锁就被释放.
ReentrantLock源码里面有实现
ps:可重入是指对同一线程而言。
4 . 有不可重入锁吗
这个还真有.Linux下的pthread_mutex_t锁是默认是非递归的。可以通过设置PTHREAD_MUTEX_RECURSIVE属性,将pthread_mutex_t锁设置为递归锁。如果要自己实现不可重入锁,同可重入锁,这个计数器只能为1.或者0,再次进入的时候,发现已经是1了,就进行阻塞.jdk里面没有默认的实现类.
5 . demo代码展示
5.1 内置锁的可重入
public class Reentrant { public void method1() { synchronized (Reentrant.class) { System.out.println("method1 run"); method2(); } } public void method2() { synchronized (Reentrant.class) { System.out.println("method2 run in method1"); } } public static void main(String[] args) { new Reentrant().method1(); } }
结果:
method1 run
method2 run in method1
Lock对象可重入
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Reentrant2 { private Lock lock = new ReentrantLock(); public void method1() { lock.lock(); try { System.out.println("method1 run"); method2(); } finally { lock.unlock(); } } public void method2() { lock.lock(); try { System.out.println("method2 run in method1"); } finally { lock.unlock(); } } public static void main(String[] args) { new Reentrant2().method1(); } }
结果:
method1 run
method2 run in method1
在同一线程里,method1调用持同样锁的method2,不会等锁。这就是锁的”重入”。
不同线程里锁不可重入
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Reentrant3 { private static Lock lock = new ReentrantLock(); private static class T1 extends Thread { @Override public void run() { System.out.println("Thread 1 start"); lock.lock(); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } System.out.println("Thread 1 end"); } } private static class T2 extends Thread { @Override public void run() { System.out.println("Thread 2 start"); lock.lock(); lock.unlock(); System.out.println("Thread 2 end"); } } public static void main(String[] args) { new T1().start(); Thread.sleep(100); new T2().start(); } }
结果:
Thread 1 start Thread 2 start Thread 1 end Thread 2 end
因为不可重入,所以不同线程可以看到T2一定会等到T1释放锁之后。
Synchronized和ReentrantLock关系与区别汇总
(1)synchronized是JVM层面的实现的,JVM会确保释放锁,而且synchronized使用简单;而Lock是个普通类,需要在代码中finally中显式释放锁lock.unlock(),但是使用灵活。
(2)synchronized采用的是悲观锁机制,线程获得独占锁,而其他线程只能阻塞来等待释放锁。当竞争激烈时,CPU频繁的上下文切换会降低效率。而Lock是乐观锁机制,每次假设不存在竞争而不上锁,若存在竞争就重试。当竞争激烈时JVM可以花更少的时间来调度线程,把更多时间用在执行线程上,因此性能最佳。
(3)ReentrantLock可以实现定时锁、轮询锁,可以选择放弃等待或者轮询请求。有效防止了死锁。
lock();//用来获取锁,如果锁已被其他线程获取,则进行等待 tryLock(); //尝试获取锁,若成功返回true,失败(即锁已被其他线程获取)则返回false tryLock(long timeout, TimeUnit unit); //在拿不到锁时会等待一定的时间 //两个线程同时通过lock.lockInterruptibly()想获取某个锁时 //若线程A获取到了锁,而线程B在等待 //线程B调用threadB.interrupt()方法能够中断线程B的等待过程 lockInterruptibly();
(4)synchronized是非公平锁。而ReentrantLock可以通过构造函数传入true实现公平锁,即按照申请锁顺序获得锁。
(5)ReentrantLock类有一个重要的函数newCondition(),用于获取Lock上的一个条件,Condition可用于线程间通信。
参考:Java并发——Synchronized和ReentrantLock的联系与区别
ps:Java技术——ReentrantLock的Condition的作用以及使用
详解synchronized与Lock的区别与使用
ps:两篇博客都做了详细的总结,好好读读
两者的区别:
存在层次
锁的获取
锁的释放
锁的状态
锁类型
锁性能
两者底层实现
其实synchronized映射成字节码指令就是增加来两个指令:monitorenter和monitorexit。
在java语言中存在两种内建的synchronized语法:1、synchronized语句;2、synchronized方法。
对于synchronized语句当Java源代码被javac编译成bytecode的时候,会在同步块的入口位置和退出位置分别插入monitorenter和monitorexit字节码指令。
而synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象。
尽可能去使用synchronized而不要去使用LOCK
synchronized优化
在jdk1.6~jdk1.7的时候,也就是synchronized 6、7岁的时候,你作为爸爸,你给他优化了,具体优化在哪里呢:
1 线程自旋和适应性自旋
java’线程其实是映射在内核之上的,线程的挂起和恢复会极大的影响开销。并且jdk官方人员发现,很多线程在等待锁的时候,在很短的一段时间就获得了锁,所以它们在线程等待的时候,并不需要把线程挂起,而是让他无目的的循环,一般设置10次。这样就避免了线程切换的开销,极大的提升了性能。
而适应性自旋,是赋予了自旋一种学习能力,它并不固定自旋10次一下。他可以根据它前面线程的自旋情况,从而调整它的自旋,甚至是不经过自旋而直接挂起。
2、锁消除
什么叫锁消除呢?就是把不必要的同步在编译阶段进行移除。
3、锁粗化
在用synchronized的时候,我们都讲究为了避免大开销,尽量同步代码块要小。那么为什么还要加粗呢?
我们继续以上面的字符串拼接为例,我们知道在这一段代码中,每一个append都需要同步一次,那么我可以把锁粗化到第一个append和最后一个append(这里不要去纠结前面的锁消除,我只是打个比方)
4、轻量级锁
这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。当存在锁竞争的情况下,执行CAS指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒。
5、偏向锁
是为了在无锁竞争的情况下避免在锁获取过程中执行不必要的CAS原子指令,因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟。
可以参考Cyc2018的总结
https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/Java%20%E5%B9%B6%E5%8F%91.md
ps:涉及对象头的内存布局,这些数据被称为 Mark Word
JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。rd,虚拟机栈中的Lock record和锁对象对应。
轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。
当尝试获取一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。
如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁
偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。
当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。
当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。
五.下面浅析以下两种锁机制的底层的实现策略:
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因而这种同步又称为阻塞同步,它属于一种悲观的并发策略,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。synchronized采用的便是这种并发策略。
随着指令集的发展,我们有了另一种选择:基于冲突检测的乐观并发策略,通俗地讲就是先进性操作,如果没有其他线程争用共享数据,那操作就成功了,如果共享数据被争用,产生了冲突,那就再进行其他的补偿措施(最常见的补偿措施就是不断地重拾,直到试成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步被称为非阻塞同步。ReetrantLock采用的便是这种并发策略。
在乐观的并发策略中,需要操作和冲突检测这两个步骤具备原子性,它靠硬件指令来保证,这里用的是CAS操作(Compare and Swap)。JDK1.5之后,Java程序才可以使用CAS操作。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState,这里其实就是调用的CPU提供的特殊指令。现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起。
Java 5中引入了注入AotomicInteger、AotomicLong、AotomicReference等特殊的原子性变量类,它们提供的如:compareAndSet()、incrementAndSet()和getAndIncrement()等方法都使用了CAS操作。因此,它们都是由硬件指令来保证的原子方法。
六.用途比较:
基本语法上,ReentrantLock与synchronized很相似,它们都具备一样的线程重入特性,只是代码写法上有点区别而已,一个表现为API层面的互斥锁(Lock),一个表现为原生语法层面的互斥锁(synchronized)。ReentrantLock相对synchronized而言还是增加了一些高级功能,主要有以下三项:
1、等待可中断:当持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待,改为处理其他事情,它对处理执行时间非常上的同步块很有帮助。而在等待由synchronized产生的互斥锁时,会一直阻塞,是不能被中断的。
2、可实现公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序排队等待,而非公平锁则不保证这点,在锁释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁时非公平锁,ReentrantLock默认情况下也是非公平锁,但可以通过构造方法ReentrantLock(ture)来要求使用公平锁。
3、锁可以绑定多个条件:ReentrantLock对象可以同时绑定多个Condition对象(名曰:条件变量或条件队列),而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含条件,但如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无需这么做,只需要多次调用newCondition()方法即可。而且我们还可以通过绑定Condition对象来判断当前线程通知的是哪些线程(即与Condition对象绑定在一起的其他线程)。
待整理
使用场景
- CAS 适合简单对象的操作,比如布尔值、整型值等;
- CAS 适合冲突较少的情况,如果太多线程在同时自旋,那么长时间循环会导致 CPU 开销很大;
ReentrantLock实现原理及源码分析
ps:还没有认真的分析总结
ReentrantLock是基于AQS的,AQS是Java并发包中众多同步组件的构建基础,它通过一个int类型的状态变量state和一个FIFO队列来完成共享资源的获取,线程的排队等待等。AQS是个底层框架,采用模板方法模式,它定义了通用的较为复杂的逻辑骨架,比如线程的排队,阻塞,唤醒等,将这些复杂但实质通用的部分抽取出来,这些都是需要构建同步组件的使用者无需关心的,使用者仅需重写一些简单的指定的方法即可(其实就是对于共享变量state的一些简单的获取释放的操作)。
AQS是基于CAS+CHL实现