Java 5中引入了新的锁机制——java.util.concurrent.locks中的显式的互斥锁:Lock接口,它提供了比synchronized更加广泛的锁定操作。Lock接口有3个实现它的类:ReentrantLock、ReetrantReadWriteLock.ReadLock和ReetrantReadWriteLock.WriteLock,即重入锁、读锁和写锁。
基本语法
跟synchronized 不一样的是,synchronized 是关键字级别来保护临界区,而ReentrantLock是在对象级别来保护临界区;
lock必须被显式地创建、锁定和释放,为了可以使用更多的功能,一般用ReentrantLock为其实例化。
为了保证锁最终一定会被释放(可能会有异常发生),要把互斥区(临界区)放在try语句块内,并在finally语句块中释放锁,尤其当有return语句时,return语句必须放在try字句中,以确保unlock()不会过早发生(执行),从而将数据暴露给第二个任务。
所以要先去创建一个ReentrantLock对象,然后调用该对象的lock方法去获取锁。然后就是try跟finally块,其中try中的代码就是临界区中的代码。finally就是确保将来是否出现异常都将锁释放掉。
//获取ReentrantLock对象,默认使用非公平锁,如果要使用公平锁,需要传入参数trueprivate ReentrantLock lock = new ReentrantLock();//加锁lock.lock();try {//更新对象的状态//捕获异常,必要时恢复到原来的不变约束//如果有return语句,放在这里}finally {//释放锁,锁必须在finally块中释放lock.unlock();}
ReetrankLock与synchronized比较
性能方面
在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。
但是到了JDK1.6之后,就发生了变化,对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在JDK1.6之后的synchronize性能并不比Lock差。
浅析两种锁机制的底层的实现策略 (乐观锁与悲观锁的体现)
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因而这种同步又称为阻塞同步,它属于一种悲观的并发策略,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。synchronized采用的便是这种并发策略。
随着指令集的发展,我们有了另一种选择:基于冲突检测的乐观并发策略,通俗地讲就是先进性操作,如果没有其他线程争用共享数据,那操作就成功了,如果共享数据被争用,产生了冲突,那就再进行其他的补偿措施(最常见的补偿措施就是不断地重拾,直到试成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步被称为非阻塞同步。ReetrantLock采用的便是这种并发策略(ReetrantLock是独占式的悲观锁,但底层用到了AQS思想,AQS用到了CAS思想)。
在乐观的并发策略中,需要操作和冲突检测这两个步骤具备原子性,它靠硬件指令来保证,这里用的是CAS操作(Compare and Swap)。JDK1.5之后,Java程序才可以使用CAS操作。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState,这里其实就是调用的CPU提供的特殊指令。现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起。
用途比较
基本语法上,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对象绑定在一起的其他线程)。
ReentrantLock特点
1、支持锁重入
- 可重入锁是指
同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此 有权利再次获取这把锁 - 如果是不可重入锁,那么在尝试第二次获得锁时,自己也会被锁挡住
 
示例:
跟之前用synchronized 关键不一样,synchronized 关键字这种方式是将对象当成锁,当然真正的锁是对象关联的Monitor**;**
但是现在我们创建出来的ReentrantLock对象**本身即是锁**,将来线程执行 lock.lock();的时候,该线程就是这个锁对象的主人。若果获取不了锁,即成为主人失败,就进入到这个lock内部的等待队列去等待。
public class ReentrantLockMainTest {private static ReentrantLock lock = new ReentrantLock();public static void main(String[] args) {lock.lock();//尝试获取锁,获取成功则继续往后执行, 如果有竞争就进入`阻塞队列`, 一直等待着,不能被打断try {System.out.println("lock...");m1();}finally {lock.unlock();}}public static void m1(){System.out.println("是否可重入...");lock.lock(); //try {System.out.println("能进来,说明可重入...");System.out.println("是否支持多次重入...");m2();}finally {lock.unlock();}}public static void m2(){lock.lock(); //try {System.out.println("能进来,说明支持多次重入......");}finally {lock.unlock();}}}

如果是不可重入的话,在尝试第二次获取锁的时候就会被阻塞住了;
2、可中断
(针对于lockInterruptibly()方法获得的中断锁) 可中断意味着直接退出阻塞队列, 获取锁失败。
- 对于synchronized 和 reentrantlock.lock() 的锁, 两个都是不可被打断的; 也就是说别的线程已经获得了锁, 我的线程就需要一直等待下去, 不能中断,直到我们的线程获取到锁对象。
 - 可被中断的锁是指通过lock.lockInterruptibly()获取的锁对象, 可以通过调用阻塞线程的interrupt()方法去中断
 - 如果某个线程处于阻塞状态,可以调用其interrupt方法让其停止阻塞,获得锁失败 
- 处于阻塞状态的线程,被打断了就不用阻塞了,**直接停止运行**
 
 - 可中断的锁, 在一定程度上可以
被动的减少死锁的概率, 之所以被动, 是因为我们需要手动调用阻塞线程的interrupt方法; 
测试使用lock.lockInterruptibly()获得的锁可以从阻塞队列中打断
/*** Description: ReentrantLock, 演示RenntrantLock中的可打断锁方法 lock.lockInterruptibly();*/public class ReentrantLockMainTest {private static final ReentrantLock lock = new ReentrantLock();public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{//获取一个可打断的锁try {lock.lockInterruptibly();} catch (InterruptedException e) {e.printStackTrace();System.out.println("t1线程在等待锁的过程中被打断....");return; //打断获取锁后不应该继续往后执行}try {System.out.println("t1线程获取到锁.....");}finally {lock.unlock();}},"t1");lock.lock(); //让主线程获取到锁..t1.start();//启动t1线程//让主线程睡一会儿,期间主线程拿到锁不释放,t1只好等待,这个时候打断t1线程不让它继续等下去try {Thread.sleep(2000);t1.interrupt();//System.out.println("打断线程....");}finally {lock.unlock();}}}
3、锁超时
(lock.tryLock())方法 超时后直接退出阻塞队列, 获取锁失败。 防止无限制等待, 减少死锁的发生
- 使用 lock.tryLock() 方法会立刻返回获取锁是否成功。如果成功则返回true,反之则返回false。
 - 并且tryLock方法可以设置指定等待时间,参数为:tryLock(long timeout, TimeUnit unit) , 其中timeout为最长等待时间,TimeUnit为时间单位
 - 获取锁的过程中, 如果超过等待时间, 或者被打断, 就直接从阻塞队列移除, 此时获取锁就失败了, 不会一直阻塞着 ! (可以用来解决死锁问题)
 不设置等待时间, 立即失败 ```java /**
- Description: ReentrantLock, 演示RenntrantLock中的tryLock(), 获取锁立即失败
 
*/ @Slf4j(topic = “guizy.ReentrantTest”) public class ReentrantTest {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {log.debug("尝试获得锁");// 此时肯定获取失败, 因为主线程已经获得了锁对象if (!lock.tryLock()) {log.debug("获取立刻失败,返回");return;}try {log.debug("获得到锁");} finally {lock.unlock();}}, "t1");lock.lock();log.debug("获得到锁");t1.start();// 主线程2s之后才释放锁Sleeper.sleep(2);log.debug("释放了锁");lock.unlock();
} }
**设置等待时间, 超过等待时间还没有获得锁, 失败, 从阻塞队列移除该线程**```java/*** Description: ReentrantLock, 演示RenntrantLock中的tryLock(long mills), 超过锁设置的等待时间,就从阻塞队列移除*/@Slf4j(topic = "guizy.ReentrantTest")public class ReentrantTest {private static final ReentrantLock lock = new ReentrantLock();public static void main(String[] args) {Thread t1 = new Thread(() -> {log.debug("尝试获得锁");try {// 设置等待时间, 超过等待时间 / 被打断, 都会获取锁失败; 退出阻塞队列if (!lock.tryLock(1, TimeUnit.SECONDS)) {log.debug("获取锁超时,返回");return;}} catch (InterruptedException e) {log.debug("被打断了, 获取锁失败, 返回");e.printStackTrace();return;}try {log.debug("获得到锁");} finally {lock.unlock();}}, "t1");lock.lock();log.debug("获得到锁");t1.start();// t1.interrupt();// 主线程2s之后才释放锁Sleeper.sleep(2);log.debug("main线程释放了锁");lock.unlock();}}

4、通过lock.tryLock()来解决, 哲学家就餐问题 (重点)
/*** Description:使用了ReentrantLock锁, 该类中有一个tryLock()方法, 在指定时间内获取不到锁对象, 就从阻塞队列移除,不用一直等待。当获取了左手边的筷子之后, 尝试获取右手边的筷子,如果该筷子被其他哲学家占用, 获取失败, 此时就先把自己左手边的筷子,给释放掉.这样就避免了死锁问题*/@Slf4j(topic = "guizy.PhilosopherEat")public class PhilosopherEat {public static void main(String[] args) {Chopstick c1 = new Chopstick("1");Chopstick c2 = new Chopstick("2");Chopstick c3 = new Chopstick("3");Chopstick c4 = new Chopstick("4");Chopstick c5 = new Chopstick("5");new Philosopher("苏格拉底", c1, c2).start();new Philosopher("柏拉图", c2, c3).start();new Philosopher("亚里士多德", c3, c4).start();new Philosopher("赫拉克利特", c4, c5).start();new Philosopher("阿基米德", c5, c1).start();}}@Slf4j(topic = "guizy.Philosopher")class Philosopher extends Thread {final Chopstick left;final Chopstick right;public Philosopher(String name, Chopstick left, Chopstick right) {super(name);this.left = left;this.right = right;}@Overridepublic void run() {while (true) {// 获得了左手边筷子 (针对五个哲学家, 它们刚开始肯定都可获得左筷子)if (left.tryLock()) {try {// 此时发现它的right筷子被占用了, 使用tryLock(),// 尝试获取失败, 此时它就会将自己左筷子也释放掉// 临界区代码if (right.tryLock()) { //尝试获取右手边筷子, 如果获取失败, 则会释放左边的筷子try {eat();} finally {right.unlock();}}} finally {left.unlock();}}}}private void eat() {log.debug("eating...");Sleeper.sleep(0.5);}}// 继承ReentrantLock, 让筷子类称为锁class Chopstick extends ReentrantLock {String name;public Chopstick(String name) {this.name = name;}@Overridepublic String toString() {return "筷子{" + name + '}';}}
公平锁 new ReentrantLock(true)
ReentrantLock默认是非公平锁, 可以指定为公平锁。
//默认是不公平锁,需要在创建时指定为公平锁ReentrantLock lock = new ReentrantLock(true);
在线程获取锁失败,进入阻塞队列时,先进入的会在锁被释放后先获得锁。这样的获取方式就是公平的。一般不设置ReentrantLock为公平的, 会降低并发度。
- Synchronized底层的Monitor锁就是不公平的, 和谁先进入阻塞队列是没有关系的。
 
公平锁 (new ReentrantLock(true))
- 公平锁, 可以把竞争的线程放在一个先进先出的阻塞队列上。
 - 只要持有锁的线程执行完了, 唤醒阻塞队列中的下一个线程获取锁即可; 此时先进入阻塞队列的线程先获取到锁
 
非公平锁 (synchronized, new ReentrantLock())
- 非公平锁, 当阻塞队列中已经有等待的线程A了, 此时后到的线程B, 先去尝试看能否获得到锁对象. 如果获取成功, 此时就不需要进入阻塞队列了. 这样以来后来的线程B就先获得锁了
 
所以公平和非公平的区别 : 线程执行同步代码块时, 是否会去尝试获取锁, 如果会尝试获取锁, 那就是非公平的, 如果不会尝试获取锁, 直接进入阻塞队列, 再等待被唤醒, 那就是公平的
- 如果不进入队列呢? 线程一直尝试获取锁不就行了? 
- 一直尝试获取锁, 是在synchronized轻量级锁升级为重量级锁时, 做的一个优化, 叫做自旋锁, 一般很消耗资源, cpu一直空转, 最后获取锁也失败, 所以不推荐使用。在jdk6对于自旋锁有一个机制, 在重试获得锁指定次数就失败等等 。
 
 
条件变量 (可避免虚假唤醒)
lock.newCondition()创建条件变量对象; 通过条件变量对象调用**await/signal**方法, 等待/唤醒
- Synchronized 中也有条件变量,当条件不满足时进入Monitor监视器中的 waitSet等待集合
 - ReentrantLock 的条件变量比 synchronized 强大之处在于,它是 支持多个条件变量。这就好比synchronized 是那些不满足条件的线程都在一间休息室等通知; (此时会造成虚假唤醒), 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒; (可以避免虚假唤醒)
 
使用要点:
- await 前需要 获得锁
 - await 执行后,会释放锁,进入 conditionObject (条件变量)中等待
 - await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
 - 竞争 lock 锁成功后,从 await 后继续执行
 - signal 方法用来唤醒条件变量(等待室)汇总的某一个等待的线程
 - signalAll方法, 唤醒条件变量(休息室)中的所有线程
 
在之前的案例中,我们用synchronized实现互斥,并配合使用Object对象的wait()和notify()或notifyAll()方法来实现线程间协作。
Java 5之后,我们可以用Reentrantlock锁配合Condition对象上的await()和signal()或signalAll()方法来实现线程间协作。在ReentrantLock对象上newCondition()可以得到一个Condition对象,可以通过在Condition上调用await()方法来挂起一个任务(线程),通过在Condition上调用signal()来通知任务,从而唤醒一个任务,或者调用signalAll()来唤醒所有在这个Condition上被其自身挂起的任务。另外,如果使用了公平锁,signalAll()的与Condition关联的所有任务将以FIFO队列的形式获取锁,如果没有使用公平锁,则获取锁的任务是随机的,这样我们便可以更好地控制处在await状态的任务获取锁的顺序。与notifyAll()相比,signalAll()是更安全的方式。另外,它可以指定唤醒与自身Condition对象绑定在一起的任务。
/*** Description: ReentrantLock可以设置多个条件变量(多个休息室), 相对于synchronized底层monitor锁中waitSet*/public class ConditionVariable {private static boolean hasCigarette = false;private static boolean hasTakeout = false;private static final ReentrantLock lock = new ReentrantLock();static Condition waitCigaretteSet = lock.newCondition();static Condition waitTakeoutSet = lock.newCondition();public static void main(String[] args) throws InterruptedException {new Thread(()->{//尝试获取锁lock.lock();try {System.out.println("获取到锁,如果能获取到需要的资源--烟,就可以继续往下执行,没有获取到资源则一直循环尝试获取...");while (!hasCigarette){System.out.println("没获取到需要的资源..烟,此时进行等待..");try {waitCigaretteSet.await(); //进行等待} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("等到当前线程需要的资源--烟了,开始干活");}finally {lock.unlock();}},"小南").start();new Thread(()->{//尝试获取锁lock.lock();try {System.out.println("获取到锁,如果能获取到需要的资源--外卖,就可以继续往下执行,没有获取到资源则一直循环尝试获取...");while (!hasTakeout){System.out.println("没获取到需要的资源..外卖,此时进行等待..");try {waitTakeoutSet.await(); //进行等待} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("等到当前线程需要的资源--外卖l ,开始干活");}finally {lock.unlock();}},"小女").start();Thread.sleep(1000);new Thread(()->{lock.lock();try {System.out.println("我是来送烟的...");hasCigarette=true;waitCigaretteSet.signal();}finally {lock.unlock();}},"送烟的").start();Thread.sleep(1000);new Thread(()->{lock.lock();try {System.out.println("我是来送外卖的...");hasTakeout=true;waitTakeoutSet.signal();}finally {lock.unlock();}},"送外卖的").start();}}
同步模式之顺序控制
使用synchronized+锁对象的wait/ notify方法进行线程之间的协调
public class FirstMainTest {/*- 假如有两个线程, 线程A打印1, 线程B打印2.- 要求: 程序先打印2, 再打印1* */private static Object room=new Object();private static boolean P1=false;private static boolean P2=true;//用wait/ notify实现public static void main(String[] args) {new Thread(()->{synchronized (room){while (!P1){try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}}if (P1){System.out.println("A1");}}},"A").start();new Thread(()->{synchronized (room){while (!P2){try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}}if (P2){System.out.println("B2");room.notifyAll();P1=true;}}},"B").start();}}
使用await() / signal 方法实现
public class FirstMainTest {/*- 假如有两个线程, 线程A打印1, 线程B打印2.- 要求: 程序先打印2, 再打印1* */private static ReentrantLock lock=new ReentrantLock();private static boolean P1=false;private static boolean P2=true;static Condition waitP1Set = lock.newCondition();static Condition waitP2Set = lock.newCondition();public static void main(String[] args) {new Thread(()->{lock.lock();try {while (!P1){try {waitP1Set.await();} catch (InterruptedException e) {e.printStackTrace();}}if (P1){System.out.println("A1");}}finally {lock.unlock();}},"A").start();new Thread(()->{lock.lock();try {while (!P2){try {waitP2Set.await();} catch (InterruptedException e) {e.printStackTrace();}}if (P2){System.out.println("B2");P1=true;waitP1Set.signal();}}finally {lock.unlock();}},"B").start();}}
使用LockSupport中的part与unpart实现
public class FirstMainTest {/*- 假如有两个线程, 线程A打印1, 线程B打印2.- 要求: 程序先打印2, 再打印1* */private static boolean P1=false;private static boolean P2=true;public static void main(String[] args) {Thread t1 = new Thread(()->{while (!P1){LockSupport.park();}if (P1){System.out.println("A1");}},"A");t1.start();Thread t2 = new Thread(()->{while (!P2){LockSupport.park();}if (P2){System.out.println("B2");P1=true;LockSupport.unpark(t1);}},"B");t2.start();}}
练习
- 线程1 输出 a 5次, 线程2 输出 b 5次, 线程3 输出 c 5次。现在要求输出 abcabcabcabcabcabc
 
wait/notify版本
await/signal版本
LockSupport的park/unpark实现
```java
```java
```java

