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对象,默认使用非公平锁,如果要使用公平锁,需要传入参数true
private 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;
}
@Override
public 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;
}
@Override
public 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