共享模型之管程

一、共享问题

问题引入

考虑这样一个问题:
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

static int counter = 0;
public static void main(String[] args) throws InterruptedException {
	Thread t1 = new Thread(() -> {
		for (int i = 0; i < 5000; i++) {
			counter++;
		}
	}, "t1");
	Thread t2 = new Thread(() -> {
		for (int i = 0; i < 5000; i++) {
			counter--;
		}
	}, "t2");
	t1.start();
	t2.start();
	t1.join();
	t2.join();
	log.debug("{}",counter);
}

答案分析

以上的结果可能是正数、负数、零。为什么呢?因为程序对于静态变量不是原子操作,要彻底理
解,必须从字节码来进行分析
例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i

而对应 i-- 也是类似:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i

再看看Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
在这里插入图片描述
对于这8行字节码,如果我们是在单线程执行(顺序执行)是不会出现错误的
在这里插入图片描述

但是多线程(交错执行)下就会出现错误,下面分别分析出现正数和负数的每个情况。
1、出现负数的情况:
在这里插入图片描述

2、出现正数
在这里插入图片描述

临界区 Critical Section

  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    • 多个线程共享资源其实也没有问题
    • 在多个线程对共享资源读操作时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

二、java对象头

由于下面部分地方需要用到对象头的知识,所以这里简单讲一下
以 32 位虚拟机为例

  • 对于普通对象
    在这里插入图片描述

  • 数组对象
    在这里插入图片描述

  • 其中 Mark Word 结构

    在这里插入图片描述

  • 64 位虚拟机 Mark Word

    在这里插入图片描述

三、synchronized

为了避免临界区的竞态条件发生,可以使用阻塞式的解决方案:synchronized 来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

注意

虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
 - 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
 - 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

那么对于开头给出的问题就可以用下面的代码解决问题

static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
	Thread t1 = new Thread(() -> {
		for (int i = 0; i < 5000; i++) {
			synchronized (room) {
				counter++;
			}
		}
	}, "t1");
	Thread t2 = new Thread(() -> {
		for (int i = 0; i < 5000; i++) {
			synchronized (room) {
				counter--;
			}
		}
	}, "t2");
	t1.start();
	t2.start();
	t1.join();
	t2.join();
	log.debug("{}",counter);
}

方法上的Synchronized

class Test{
	public synchronized void test() {
	}
}
//等价于
class Test{
	public void test() {
		synchronized(this) {
		}
	}
}

特殊情况分析

/* 先输出2,1s后再输出1
 * a()方法是静态方法,锁住的是 Nummber 类的类对象
 * 而b()方法锁住的是 n1 对象
 * 所以b()先执行,再执行a()方法
 */
class Number{
	public static synchronized void a() {
		sleep(1);
		log.debug("1");
	}
	public synchronized void b() {
		log.debug("2");
	}
}
public static void main(String[] args) {
	Number n1 = new Number();
	new Thread(()->{ n1.a(); }).start();
	new Thread(()->{ n1.b(); }).start();
}

synchronized 原理进阶

轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。轻量级锁对使用者是透明的,即语法仍然是 synchronized

  • 创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
    在这里插入图片描述
  • 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark
    Word 的值存入锁记录
    在这里插入图片描述
  • 如果 cas 替换成功,对象头中存储了锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下
    在这里插入图片描述
  • 如果 cas 失败,有两种情况
    • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
    • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
      在这里插入图片描述
  • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
    在这里插入图片描述
  • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
    • 成功,则解锁成功
    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
    在这里插入图片描述
  • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
    • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
    • 然后自己进入 Monitor 的 EntryList BLOCKED
      在这里插入图片描述
  • 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

  • 自旋重试成功的情况
    在这里插入图片描述
  • 自旋重试失败的情况
    在这里插入图片描述
1、自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
2、在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
3、Java 7 之后不能控制是否开启自旋功能

偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

  • 如果是轻量级锁:
    在这里插入图片描述

  • 但是改为偏向锁后:
    在这里插入图片描述

偏向状态

看偏向状态我们先回忆一下对象头格式在这里插入图片描述
一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值
  • 在上面测试代码运行时在添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁
  • 处于偏向锁的对象解锁后,线程 id 仍存储于对象头中

撤销偏向锁
1、调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销

  • 轻量级锁会在锁记录中记录 hashCode
  • 重量级锁会在 Monitor 中记录 hashCode

2、当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
3、调用 wait/notify

批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程

批量撤销
当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

四、线程安全分析

/* 
 * 示例代码
 * list 是局部变量,每个线程调用时会创建其不同实例,没有共享
 * 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
 * method3 的参数分析与 method2 相同
 */
class ThreadSafe {
	public final void method1(int loopNumber) {
	ArrayList<String> list = new ArrayList<>();
		for (int i = 0; i < loopNumber; i++) {
			method2(list);
			method3(list);
		}
	}
	private void method2(ArrayList<String> list) {
		list.add("1");
	}
	private void method3(ArrayList<String> list) {
		list.remove(0);
	}
}

class ThreadSafeSubClass extends ThreadSafe{
	@Override
	public void method3(ArrayList<String> list) {
		new Thread(() -> {
			list.remove(0);
		}).start();
	}
}

成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

  • 局部变量是线程安全的,每个线程调用时会创建其不同实例,没有共享
  • 但局部变量引用的对象则未必
    • 如果该对象没有逃离方法的作用访问,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全

局部变量线程安全分析

方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public是否线程安全?

  • 在有其它线程调用时,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,此时会出现问题
/*
 * ThreadSafeSubClass类中覆写了 method3 
 * 在覆写方法中调用了另一个线程,使得 list 对象被多线程共享
 * 会出现线程安全问题
 * 从这个例子可以看出 private 或 final 提供【安全】的意义所在,体会开闭原则中的【闭】
 */
class ThreadSafe {
	public final void method1(int loopNumber) {
	ArrayList<String> list = new ArrayList<>();
		for (int i = 0; i < loopNumber; i++) {
			method2(list);
			method3(list);
		}
	}
	public void method2(ArrayList<String> list) {
		list.add("1");
	}
	public void method3(ArrayList<String> list) {
		list.remove(0);
	}
}

class ThreadSafeSubClass extends ThreadSafe{
	@Override
	public void method3(ArrayList<String> list) {
		new Thread(() -> {
			list.remove(0);
		}).start();
	}
}

常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable java.util.concurrent 包下的类

这里说它们是线程安全的是指:
多个线程调用它们同一个实例的某个方法时,是线程安全的。即它们的每个方法是原子的
但注意它们多个方法的组合不是原子的,例如以下代码

/* 
 * 当线程1和线程2同时对 table 进行了null判断
 * 这就导致了线程1和线程2会同时对 table 进行写入操作
 */
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
	table.put("key", value);
}

图示:
在这里插入图片描述

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
那么String 有 replace,substring 等方法是怎么改变值的呢,这些方法又是如何保证线程安全的呢?我们来看看jdk中的String类的部分源码

/*
 * String 类中 substring(int beginIndex, int endIndex) 方法
 * 如果传入的参数有效,并且需要改变字符串
 * 那么会执行new String(value, beginIndex, subLen);
 * 而String(value, beginIndex, subLen)方法中又创建了新的字符串类
 * 并没有对原有的字符串进行改动
 * 所以不可变类即使被线程共享也没有线程安全问题
 */
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    //~~省略N行~~ 
	public String substring(int beginIndex, int endIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        if (endIndex > value.length) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        int subLen = endIndex - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }
	public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }
}

五、Monitor

在这里插入图片描述

  • 刚开始 Monitor 中的 Owner 为 null
  • 当 Thread-2 执行 synchronized(object) 上锁,就会将 Monitor 的所有者 Owner 置为 Thread-2, 同时对象 object 中的 Mark Word 的 state 也会发生相应改变。
  • 在 Thread-2上锁的过程中,如果 Thread-3 , Thread-4 , Thread-5 也来执行 synchronized(obj) ,就会进入EntryList BLOCKED
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争为下一个不是先来先成为下一个 Owner ,是jdk底层实现决定的
  • 图中的 WaitSet 中的Thread-0 ,Thread-1 是之前获得过锁,但是条件不满足而进入 WAITING 状态的线程

注意:

  • synchronized 必须是进入同一个对象的 monitor 才有上述的效果
  • 不加 synchronized 的对象不会关联监视器,不遵从以上的规则

六、wait/notify

原理之 wait / notify

在这里插入图片描述

  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒
  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争

wait/notify的正确姿势

  1. sleep(long n) 和 wait(long n) 的区别

     1) sleep 是 Thread 方法,而 wait 是 Object 的方法
     2) sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用 
     3) sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁 
     4) 它们状态 TIMED_WAITING
    
  2. notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为【虚假唤醒】,解决方法,改为 notifyAll

  3. 用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了解决方法,用 while + wait,当条件不成立,再次 wait

    //if + wait
    if (条件) {
    	try {
    		wait();
    	} catch (InterruptedException e) {
    		e.printStackTrace();
    	}
    }
    //改为while + wait
    while (条件) {
    	try {
    		wait();
    	} catch (InterruptedException e) {
    		e.printStackTrace();
    	}
    }
    

七、park & Unpark

基本使用

它们是 LockSupport 类中的方法

// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)

特点

与 Object 的 wait & notify 相比

  • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
  • park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify

park unpark 原理

每个线程都有自己的一个 Parker 对象,由三部分组成 _counter , _cond 和 _mutex
打个比喻

  • 线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0 为耗尽,1为充足)
  • 调用 park 就是要看需不需要停下来歇息
    • 如果备用干粮耗尽,那么钻进帐篷歇息
    • 如果备用干粮充足,那么不需停留,继续前进
  • 调用 unpark,就好比令干粮充足
    • 如果这时线程还在帐篷,就唤醒让他继续前进
    • 如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进
      • 因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮

图示:

  1. 调用 park 方法:

    检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁
    线程进入 _cond 条件变量阻塞
    设置 _counter = 0
    

    在这里插入图片描述

  2. 调用 unpark 方法

    调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
    唤醒 _cond 条件变量中的 Thread_0
    hread_0 恢复运行
    设置 _counter 为 0
    

    在这里插入图片描述

  3. 先调用了 unpark 再调用 park

    调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
    当前线程调用 Unsafe.park() 方法
    检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
    设置 _counter 为 0
    

    在这里插入图片描述

八、线程状态转换

在这里插入图片描述
假设有线程 【Thread t】

情况 1 NEW --> RUNNABLE

  • 当调用 t.start() 方法时,由 NEW --> RUNNABLE

情况 2 RUNNABLE <–> WAITING

  • t 线程用 synchronized(obj) 获取了对象锁后
    • 调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING
    • 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
      • 竞争锁成功,t 线程从 WAITING --> RUNNABLE
      • 竞争锁失败,t 线程从 WAITING --> BLOCKED

情况 3 RUNNABLE <–> WAITING

  • 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING
    • 注意是当前线程在t 线程对象的监视器上等待
  • t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE

情况 4 RUNNABLE <–> WAITING

  • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING -->
    RUNNABLE

情况 5 RUNNABLE <–> TIMED_WAITING

  • t 线程用 synchronized(obj) 获取了对象锁后
    • 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING
  • t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
    • 竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
    • 竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED

情况 6 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING
    • 注意是当前线程在t 线程对象的监视器上等待
  • 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从TIMED_WAITING --> RUNNABLE

情况 7 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
  • 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE

情况 8 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从TIMED_WAITING–> RUNNABLE

情况 9 RUNNABLE <–> BLOCKED

  • t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
  • 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED

情况 10 RUNNABLE <–> TERMINATED

  • 当前线程所有代码运行完毕,进入TERMINATED

九、活跃性

死锁

一个线程需要同时获取多把锁,这时就容易发生死锁
例如:t1 线程获得 A 对象锁,接下来想获取 B 对象的锁, t2 线程获得 B 对象锁,接下来想获取 A 对象的锁,这时就发生了死锁。

//两个线程相互等待
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
	//拥有 A 锁想获得 B 锁
	synchronized (A) {
		sleep(1);
		synchronized (B) {
			log.debug("lock B");
		}
	}
}, "t1");
Thread t2 = new Thread(() -> {
	//拥有 B 锁想获得 A 锁
	synchronized (B) {
		log.debug("lock B");
		sleep(0.5);
		synchronized (A) {
			log.debug("lock A");
			log.debug("操作...");
		}
	}
}, "t2");
t1.start();
t2.start();

活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如

public class TestLiveLock {
	static volatile int count = 10;
	static final Object lock = new Object();
	public static void main(String[] args) {
		new Thread(() -> {
		// 期望减到 0 退出循环
			while (count > 0) {
				sleep(0.2);
				count--;
				log.debug("count: {}", count);
			}
		}, "t1").start();
		new Thread(() -> {
		// 期望超过 20 退出循环
			while (count < 20) {
				sleep(0.2);
				count++;
				log.debug("count: {}", count);
			}
		}, "t2").start();
	}
}

饥饿

很多教程把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束。
下面的线程产生了死锁:
在这里插入图片描述

如果使用顺序加锁的方式可以解决上面的死锁问题
在这里插入图片描述

十、ReentrantLock

相对于 synchronized 它具备如下特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量

与 synchronized 一样,都支持可重入

基本语法

// 获取锁
reentrantLock.lock();
try {
	// 临界区
} finally {
	// 释放锁
	reentrantLock.unlock();
}

可重入

  • 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
  • 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

可打断

ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
	try {
		//如果是 lock.lock(); 为不可中断模式,那么即使使用了 interrupt 也不会让等待中断
		lock.lockInterruptibly();
	} catch (InterruptedException e) {
		e.printStackTrace();
		return;
	}
	try {
	} finally {
		lock.unlock();
	}
}, "t1");
//主线程加上锁,使 t1 线程处于阻塞状态
lock.lock();
t1.start();
try {
	sleep(1);
	//打断 t1 线程
	t1.interrupt();
} finally {
	lock.unlock();
}

锁超时

  • tryLock() 方法:尝试获得锁,如果获得锁返回 true,否则返回 false
  • tryLock(long, TimeUnit.SECONDS) 方法:是带参数的 tryLock() 方法,两个参数分别是等待的时间、和时间的单位
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
	//演示调用 tryLock() 方法,避免锁超时
	if (!lock.tryLock()) {
		log.debug("获取立刻失败,返回");
		return;
	}
	/* 
	 * 演示调用 tryLock(long, TimeUnit.SECONDS) 方法,避免锁超时
	 * if (!lock.tryLock(1, TimeUnit.SECONDS)) {
	 * 		log.debug("获取等待 1s 后失败,返回");
	 * 		return;
	 * }
	*/
	try {
		log.debug("获得了锁");
	} finally {
		lock.unlock();
	}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
	sleep(2);
} finally {
	lock.unlock();
}

公平锁

ReentrantLock 默认是不公平的,公平锁一般没有必要,会降低并发度

ReentrantLock lock = new ReentrantLock(false);
lock.lock();
for (int i = 0; i < 500; i++) {
	new Thread(() -> {
		lock.lock();
		try {
			System.out.println(Thread.currentThread().getName() + " running...");
		} finally {
			lock.unlock();
		}
	}, "t" + i).start();
}
// 1s 之后去争抢锁
Thread.sleep(1000);
new Thread(() -> {
	System.out.println(Thread.currentThread().getName() + " start...");
	lock.lock();
	try {
		System.out.println(Thread.currentThread().getName() + " running...");
	} finally {
		lock.unlock();
	}
}, "强行插入").start();
lock.unlock();

/* 
 * 截取部分运行结果,可以发现 1s 后启动的线程争抢到了锁,而不是在 t500 之后
 * ...
 * t42 running...
 * t43 running...
 * 强行插入 start...
 * 强行插入 running...
 * t44 running...
 * t45 running...
 * ...
 */

可以通过ReentrantLock lock = new ReentrantLock(true);改为公平锁
再看看部分运行结果就会发现公平锁起到了作用,强行插入线程总是在最后输出

...
t465 running...
t464 running...
t477 running...
t442 running...
t468 running...
t493 running...
t482 running...
t485 running...
t481 running...
强行插入 running...
强行插入 running...

条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比

  • synchronized 是那些不满足条件的线程都在一间休息室等消息
  • 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

使用要点:

  • await 前需要获得锁
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await的线程被唤醒(或打断、或超时)后重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行

例子:

static ReentrantLock lock = new ReentrantLock();
static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitbreakfastQueue = lock.newCondition();
static volatile boolean hasCigrette = false;
static volatile boolean hasBreakfast = false;
public static void main(String[] args) {
	//线程没烟,进入等烟的房间中等待
	new Thread(() -> {
		try {
			lock.lock();
			while (!hasCigrette) {
				try {
					waitCigaretteQueue.await();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			log.debug("等到了它的烟");
		} finally {
			lock.unlock();
		}
	}).start();
	//线程没早餐,进入等早餐的房间等待
	new Thread(() -> {
		try {
			lock.lock();
			while (!hasBreakfast) {
				try {
					waitbreakfastQueue.await();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			log.debug("等到了它的早餐");
		} finally {
			lock.unlock();
		}
	}).start();
	sleep(1);
	sendBreakfast();
	sleep(1);
	sendCigarette();
}

//买烟方法
private static void sendCigarette() {
	lock.lock();
	try {
		log.debug("送烟来了");
		hasCigrette = true;
		waitCigaretteQueue.signal();
	} finally {
		lock.unlock();
	}
}

//买早餐方法
private static void sendBreakfast() {
	lock.lock();
	try {
		log.debug("送早餐来了");
		hasBreakfast = true;
		waitbreakfastQueue.signal();
	} finally {
		lock.unlock();
	}
}
输出
18:52:27.680 [main] c.TestCondition - 送早餐来了
18:52:27.682 [Thread-1] c.TestCondition - 等到了它的早餐
18:52:28.683 [main] c.TestCondition - 送烟来了
18:52:28.683 [Thread-0] c.TestCondition - 等到了它的烟
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值