Java中的线程进阶篇:锁的详解

Java中的线程共享模型之管程Monitor

  • 前言
    • 1. 线程运行的原理
    • 2. 程序的运行过程
  • 一、共享带来的问题
    • 1. 问题的引入
    • 2. 问题的分析
    • 3. 新概念的引入
    • 4. 解决方案
  • 二、synchronized关键字
    • 1. 语法
      • 1.1 同步代码块
      • 1.2 同步方法
      • 1.3 同步静态方法
      • 1.4 解决方式图解
      • 1.5 附加:同步与互斥
    • 2. 线程安全性分析
      • 2.1 成员变量与静态变量
      • 2.2 局部变量
    • 3. 常见的线程安全类
      • 3.1 同步方法保证线程安全类
      • 3.2 不可变性保证线程安全类
    • 4. synchronized的底层原理
      • 4.1 Java对象头
      • 4.2 Monitor原理
      • 4.3 重量级锁的加锁过程
      • 4.4 重量级锁进一步解读:字节码分析
      • 4.4 轻量级锁的加锁过程
      • 4.5 锁的膨胀
      • 4.6 自旋优化
      • 4.7 偏向锁
      • 4.8 注意
  • 三、线程间的通信方法
    • 1. wait()和notify()
      • 1.1 原理介绍
      • 1.2 方法解析
      • 1.3 补充:wait()和sleep()的区别
    • 2. park()和unpark()
      • 2.1 原理解析
        • 2.1.1 park()
        • 2.1.2 unpark()
        • 2.1.3 先调用unpark()后调用park()的情况
      • 2.2 方法解析
  • 四、线程的活跃性
    • 1. 死锁
      • 1.1 基本概念
      • 1.2 死锁的本质
      • 1.3 死锁的条件
      • 1.4 死锁的解决
    • 2. 活锁
      • 2.1 基本概念
      • 2.3 活锁的演示
      • 2.3 解决方法
    • 3. 饥饿
      • 3.1 基本概念
      • 3.2 解决方法
  • 五、可重入锁ReentrantLcok简述
    • 1. ReentrantLcok与synchronized的对比
    • 2. ReentrantLock的基本语法
    • 3. 可重入性
      • 3.1 基本概念
    • 4. 可打断性
      • 4.1 基本概念
      • 4.2 使用细节
    • 5. 锁的超时
      • 5.1 基本概念
      • 5.2 使用细节
    • 6. 公平锁
      • 6.1 基本概念
      • 6.2 使用细节
    • 7. 条件变量
      • 7.1 概述
      • 7.2 使用

前言

1. 线程运行的原理

Java中的线程进阶篇:锁的详解_第1张图片

  • JVM内存模型中的栈实际上就是给线程使用的。
  • 每个线程启动以后,虚拟机就会为其分配一块栈内存。
  • 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存。
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的方法。

2. 程序的运行过程

Java中的线程进阶篇:锁的详解_第2张图片

public class Process{
	public static void main(String[] args){
		method1();
	}
	private static void method1(){
		int y = x + 1;
		Object m = method2();
		System.out.println(m);
	}
	private static Object method2(){
		Object n = new Object();
		return n;
	}
}

一、共享带来的问题

1. 问题的引入

  • 假设有一个变量count,两个线程t1和t2,t1线程对变量count执行5000次加一操作,t2线程对变量count执行5000次减一操作,结果却出现了count为正数、负数、零三种状况。
	public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 1;i < 5000; i++){
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 1;i < 5000; i++){
                count--;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("count的值是{}",count);
    }

2. 问题的分析

1.count++操作的字节码

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

2.count–操作的字节码

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

3.单线程情况不会出现问题

Java中的线程进阶篇:锁的详解_第3张图片

4.多线程情况下问题的产生(以负数为例)

Java中的线程进阶篇:锁的详解_第4张图片

5.总结

  • 问题出现在多个线程访问共享资源时,读写操作指令发生了交错。

3. 新概念的引入

  • 临界区:一段代码块内如果存在对共享资源的多线程读写操作,我们称该段代码块为临界区。
  • 竞态条件:多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,就称之为发生了竞态条件。

4. 解决方案

  • 阻塞式的解决方案:synchronized、Lock
  • 非阻塞式的解决方案:原子变量

二、synchronized关键字

1. 语法

1.1 同步代码块

synchronized{
	//临界区
}

1.2 同步方法

public class Test {
	// 在方法上加上synchronized关键字
	public synchronized void test() {
	
	}
	// 等价于
	public void test() {
		synchronized(this) { // 锁住的是对象
		
		}
	}
}

1.3 同步静态方法

public class Test {
	// 在静态方法上加上 synchronized 关键字
	public synchronized static void test() {
	
	}
	//等价于
	public void test() {
		synchronized(Test.class) { // 锁住的是类,是java.lang.Class类的对象
		
		}
	}
}

1.4 解决方式图解

synchronized实际上使用对象锁保证了临界区内代码的原子性,临界区内的代码是不可分割的,不会被线程切换所打断。
Java中的线程进阶篇:锁的详解_第5张图片
Java中的线程进阶篇:锁的详解_第6张图片

1.5 附加:同步与互斥

  • 同步:当条件不满足时,当前线程进入等待状态,等待条件满足后,当前线程恢复运行。wait()和notify()就是同步。
  • 互斥:让临界区的代码块具有原子性,避免不同的线程交替执行。synchronized和ReentrantLock就是互斥。

2. 线程安全性分析

2.1 成员变量与静态变量

  • 如果成员变量和静态变量没有被共享,则线程安全。
  • 如果成员变量和静态变量被共享且只有读操作,则线程安全。
  • 如果成员变量和静态变量被共享且存在写操作,则线程不安全。

2.2 局部变量

  • 局部变量如果始终是基本数据类型,则线程安全。
  • 局部变量如果是引用数据类型,且该数据类型不被其他的线程所操作,则线程安全。
  • 局部变量如果是引用数据类型,且该数据类型被其他的线程所操作,则线程不安全,比如定义了局部变量的方法中调用的方法新建了一个线程同时操作这个成员变量,此时,线程就是不安全的。

3. 常见的线程安全类

3.1 同步方法保证线程安全类

1.线程安全类

  • StringBuffer
  • Random
  • Vector(List的线程安全实现类)
  • Hashtable(Hash的线程安全实现类)
  • java.util.concurrent 包下的类

2.解释

  • 这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的,因为他们的方法都是用sychronized修饰。
  • 线程安全类的方法组合以后,方法不再是原子的,即是非线程安全的。
    //组合后线程不安全
    Hashtable table = new Hashtable();
    if(table.get(key) == null){
    	table.put(key, value);
    }
    
    Java中的线程进阶篇:锁的详解_第7张图片

3.2 不可变性保证线程安全类

1.线程安全类

  • String
  • Integer

2.解释

  • String和Integer类都是不可变的类,因为其类内部状态是不可改变的,因此它们的方法都是线程安全的,尽管String有replace,substring 等方法可以改变值,但其实这些方法实现的原理是在底层新创建了一个对象,对新的对象进行操作并返回,而并非直接对原对象进行操作。

4. synchronized的底层原理

4.1 Java对象头

1.普通对象

  • Mark Word:通常包括该对象的hashcode、分代年龄、是否偏向锁、加锁状态。在不同的加锁状态下有所区别。
  • Klass Word:指针,指向对应的java.lang.Class实例,表明该对象的类别。
    Java中的线程进阶篇:锁的详解_第8张图片

2.数组对象

  • Mark Word:通常包括该对象的hashcode、分代年龄、是否偏向锁、加锁状态。在不同的加锁状态下有所区别。
  • Klass Word:指针,指向对应的java.lang.Class实例,表明该对象的类别。
  • array length:数组长度。
    Java中的线程进阶篇:锁的详解_第9张图片

3.Mark Word结构

-Java中的线程进阶篇:锁的详解_第10张图片

4.2 Monitor原理

1.基本概念

  • Monitor即监视器,也被称为管程。每个Java对象都可以关联一个Monitor,如果使用synchronized给对象上锁(重量级),该对象头的Mark Word中就被设置为指向Monitor对象的指针。

2.结构解析

Java中的线程进阶篇:锁的详解_第11张图片

  • WaitSet中存放的是持有过该锁但通过wait()方法进入了WAITING状态的线程。
  • EntryList中存放的是被阻塞进入BLOCKED状态的线程。
  • Owner中存放的是当前正在持有该锁的线程。

4.3 重量级锁的加锁过程

Java中的线程进阶篇:锁的详解_第12张图片

  1. 起始时刻,Monitor中的Owner为null。
  2. 当Thread-2执行synchronized(obj){} 代码加上重量级锁时,首先将obj对象头的Mark Word改为Heavyweight Locked状态(即利用10表示重量级锁,前为30bit为指向某个Monitor的指针),随后将Monitor的所有者Owner设置为Thread-2,上锁成功,Monitor中同一时刻只能有一个Owner。
  3. 当Thread-2占据锁时,如果线程Thread-1也来执行synchronized(obj){} 代码,就会找到obj指向的Monitor对象,并进入EntryList(阻塞队列)中变成BLOCKED(阻塞)状态。
  4. Thread-2执行完同步代码块的内容,将Owner置为null,然后唤醒EntryList中等待的线程来竞争锁,竞争时是非公平的。
  5. 如果线程Thread-2之前存在一个线程先持有了该锁并执行了wait(long),则会被放入WaitSet中进入WAITING(等待)状态。

注意:synchronized必须是进入同一个对象的monitor才有上述的效果。如果不是同一个synchronized的对象,不遵循以上规则;如果不加synchronized的对象,不遵从以上规则。

4.4 重量级锁进一步解读:字节码分析

static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args){
	synchronized(lock){
		counter++;
	}
}

Java中的线程进阶篇:锁的详解_第13张图片
Exceprion table中的意思是从from到to如果发生任何异常,都会转到target来继续执行。

  1. monitor对象的指针存在于java对象的对象头之中,synchronized锁就是通过这个方式获取锁的,这也是为什么java中的任意对象可以作为锁的原因。
  2. synchronized同步代码块的实现是通过monitorentermonitorexit指令,其中,monitorenter指令指向同步代码块开始的位置,monitorexit指向同步代码块的结束位置;当执行monitorenter指令时,判断monitor中的计数器是否为0,如果为0,则会将monitor中的计数器加1表示加锁成功,如果不为0,则判断当前线程是否为monitor中Owner的线程,是则加1(可重入锁)表示加锁成功,否则阻塞;当执行monitorexit指令时,会将monitor中的计数器值减1,如果当前计数器值减1后为0,表示释放锁。
  3. synchronized修饰的方法并没有monitorentermonitorexit指令,取而代之的是ACC_SYNCHRONIZED标识,该标识指明了该方法是否是一个同步方法,JVM通过访问该标识来辨别一个方法是否为同步方法,从而执行相应的同步调用。

4.4 轻量级锁的加锁过程

1.轻量级锁的使用场景

  • 对于一个对象,如果有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是线程间不存在竞争关系),那么可以使用轻量级锁来进行优化。
  • 轻量级锁对使用者是透明的,其语法仍然是synchronized,轻量级锁的选择实际上是JVM进行的。
  • 如果轻量级锁加锁不成功,即出现了竞争的情况,则轻量级锁会自动升级为重量级锁。

2.轻量级锁的加锁过程

//以下代码,method1()和method()2加锁的时间就是错开的,即不会竞争锁
static final Object obj = new Object();
public static void method1() {
     synchronized( obj ) {
         // 同步块 A
         method2();
     }
}
public static void method2() {
     synchronized( obj ) {
         // 同步块 B
     }
}
  1. 每当代码执行到synchronized代码块时,都会创建一个锁记录(Lock Record)对象,线程的每个栈帧中都存放着一个锁记录对象,锁记录内部包含两个部分,起始时一部分存放了锁记录对象的地址和00标志位(00标志代表轻量级锁),另一部分为空(全为0)。
    Java中的线程进阶篇:锁的详解_第14张图片

  2. 让锁记录中的Object reference指向要加锁的Java对象的存储地址,并尝试用CAS方式来交换lock record地址和00标志位要加锁的Java对象的Mark Word
    Java中的线程进阶篇:锁的详解_第15张图片

  3. 如果交换成功,则表示加上了轻量级锁。此时Java对象的对象头中存储了锁记录对象的地址和00标志位

在这里插入图片描述
Java中的线程进阶篇:锁的详解_第16张图片

  1. 如果交换失败,则存在两种情况:
    情况一:自己执行了synchronized锁的重入,此时仍然需要添加一个锁记录对象,然后进行CAS交换并指向Java对象的引用,但此时CAS交换失败,新增加的锁记录对象中锁记录对象的地址和00标志位的位置为null且Object reference部分指向Java对象的锁记录。此时的锁记录对象作为重入的计数。
    Java中的线程进阶篇:锁的详解_第17张图片
    情况二:其他线程已经持有了该Java对象的轻量级锁,表明存在竞争,进入锁的膨胀过程。

  2. 当解锁时,分为三种情况考虑
    情况一:正常情况,直接使用CAS将Mark Word的值恢复给Java对象头时,取出的值不为null,且交换成功即表明解锁成功。
    情况二:存在重入锁,如果进行CAS交换时,发现取值为null,则表示有重入锁,此时删除该锁记录对象,表示重入计数减一。
    情况三:锁已膨胀,如果进行CAS交换时,发现取值不null,且交换失败,此时说明轻量级锁已膨胀为重量级锁,则进入重量级锁解锁流程。

4.5 锁的膨胀

1.基本概念

  • 如果在尝试加轻量级锁的过程中,CAS操作无法成功,则说明其它线程已经为这个对象加上了轻量级锁,就要进行锁膨胀,将轻量级锁变成重量级锁。

2.流程

  1. 当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁,这时Thread-1加轻量级锁失败,进入锁膨胀流程。
    Java中的线程进阶篇:锁的详解_第18张图片

  2. 首先,为对象申请Monitor锁,让Object指向重量级锁地址,随后,然后自己进入Monitor的EntryList变成BLOCKED状态。
    Java中的线程进阶篇:锁的详解_第19张图片

  3. 当Thread-0退出synchronized同步块时,使用CAS将Mark Word的值恢复给对象头,对象的对象头指向Monitor,那么会进入重量级锁的解锁过程,即按照Monitor的地址找到Monitor对象,将Owner设置为null ,唤醒EntryList中的Thread-1线程。

4.6 自旋优化

  • 重量级锁竞争的时候,还可以使用自旋来进行优化。

  • 如果当前线程自旋成功(即在自旋的时候持锁的线程释放了锁),那么当前线程就可以不用进行上下文切换就获得了锁。
    Java中的线程进阶篇:锁的详解_第20张图片

  • 如果自旋重试失败(即自旋了一定次数还是没有等到持锁的线程释放锁),则依然要进入阻塞状态。
    Java中的线程进阶篇:锁的详解_第21张图片

  • 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。Java7之后不能控制是否开启自旋功能。

4.7 偏向锁

1.基本概念

  • 在轻量级的锁中,如果同一个线程对同一个对象进行重入锁时,也需要执行CAS操作,这是也会产生时间消耗,因此Java6中,开始引入了偏向锁来进行优化。
  • 偏向锁即只有第一次使用CAS时将对象的Mark Word头设置为偏向线程ID,之后,该线程再次进行获取锁时,如果发现线程ID是自己,则表示没有竞争,那么就不用再进行CAS交换。

2.轻量级锁和偏向锁的对比

static final Object obj = new Object();
public static void m1() {
	synchronized(obj) {
		// 同步块 A
		m2();
	}
}
public static void m2() {
	synchronized(obj) {
		// 同步块 B
		m3();
	}
}
public static void m3() {
	synchronized(obj) {
		// 同步块 C
	}
}

Java中的线程进阶篇:锁的详解_第22张图片
Java中的线程进阶篇:锁的详解_第23张图片

3.偏向状态

在这里插入图片描述

  • 如果开启了偏向锁(默认开启),那么对象刚创建之后,Mark Word最后三位的值101,其余位都是0。
  • 偏向锁默认是延迟的,不会在程序启动的时候立刻生效,如果想避免延迟,可以添加虚拟机参数来禁用延迟:
    -XX:BiasedLockingStartupDelay=0来禁用延迟。
  • 如果不开启偏向锁,此时对象创建后,Mark Word最后三位的值位001,其余位都是0。该对象的hashcode、age等都为0,hashcode在第一次被调用时,才会在对象头中赋值。
  • 当我们对一个Java对象第一次进行加锁时,实际上加的就是偏向锁,此时访问该锁的线程的id会被记录在该对象的Mark Word中。注意,此线程id和Java中的getId()得到的id并不一样,此时的id时操作系统赋予的。
  • 可以通过添加VM参数-xx:-UseBiasedLocking来禁用偏向锁,UseBiasedLocking前的-就代表禁用,如果是+代表启用。

4.偏向锁的撤销

  • 如果在对Java对象加锁之前,调用了该Java对象hashcode,则会禁用该Java对象的偏向锁,因为已经将hashcode存放在对象头中了,无法存放线程的id了。
  • 当有其他线程想对该Java对象加锁时,会撤销该Java对象的偏向锁,将其升级为轻量级锁(时间错开,没有竞争,否则会升级成重量级锁)。
  • 调用wait-notify必然会撤销Java对象的偏向锁,因为此时,不光多个线程访问,而且存在竞争,直接升级为重量级锁。

5.批量重偏向

  • 如果起始时,对大量的Java对象都设置了偏向于A线程的锁。此时,再不断用B线程,重新获取这些锁,则被B线程访问的锁会不断地被撤销偏向锁,升级为轻量级锁。当撤销次数超过20次后,所有设置了偏向于A线程的Java对象的且未撤销的锁,都会设置偏向线程B的锁。

6.批量撤销

  • 接着上述的情况,如果此时还有线程C对这些Java对象再次进行加锁,升级成轻量级锁的锁不变,设置了对B线程的偏向锁的锁会被撤销并升级为轻量级锁,当总撤销次数超过40次时(包括之前对A偏向锁的20次撤销),所有偏向锁都会被撤销,全部升级成轻量级锁,此时,哪怕对新的Java对象加锁,也不会加偏向锁,而是加轻量级锁。

7.锁消除

  • 如果加锁的Java对象不能被共享,则JIT编译器在进行编译时会消除锁,来提升运行效率。
  • 可以通过设置VM参数-XX:-Eliminatelocks来禁止锁的消除。

4.8 注意

  • 加锁的顺序:当为Java对象加锁时,优先加偏向锁。如果其他线程用了该Java对象,则撤销偏向锁,变为轻量级锁。如果发生了竞争,则轻量锁会膨胀为重量级锁。
  • 上述轻量级锁和偏向锁都以重入锁为示例进行说明,但是实际并不要求是重入锁,任何情况下都是这个加载顺序,以重入锁为例只是为了演示不产生竞争,方便解释说明。

三、线程间的通信方法

1. wait()和notify()

1.1 原理介绍

前面,在介绍Moinitor和synchronized时,曾对wait()和notify()方法有所提及,接下来,我们进行详细的解析。
Java中的线程进阶篇:锁的详解_第24张图片

  • 当占用了Owner的线程发现条件不满足后,会调用wait()方法,进入WaitSet变为WAITING状态。
  • 如果调用wait()方法,等待条件满足后,由其他的获取了该锁的线程调用notify()或notifyAll()方法,来唤醒陷入WAITING状态的线程。
  • 醒来后的线程不一定会立刻获取锁,仍需进入EntryList重新开始竞争。

1.2 方法解析

方法 作用
wait() 会使当前线程陷入等待状态
wait(long timeout) 在wait()的基础上,额外设置了等待时间,单位为ms,如果超过等待时间还未被唤醒,则当前线程会自动苏醒,并开始竞争CPU资源
wait(long timeout, int nanos) 在wait(long timeout)的基础上,企图进行ns级的时间控制,但实际上也只能在ms级别进行设置,后面的nanos只要设置了大于0,就会增加1ms,并不能将时间精度控制在ns级
notify() 随机唤醒一个在该锁的WaitSet中的线程
notifyAll() 唤醒所有在该锁的WaitSet中的线程

注意:调用了wait()和notify()、notifyAll()方法的锁,一定会升级为重量级锁。

1.3 补充:wait()和sleep()的区别

sleep() wait()
Thread的方法 Object的方法
可以在任何地方使用 必须和synchronized配合使用
不会释放对象锁 会释放对象锁

2. park()和unpark()

2.1 原理解析

每个线程都有一个属于自己的Parker对象,该Parker对象是用C语言实现的,由_counter、_cond、_mutex三个部分组成。

2.1.1 park()

Java中的线程进阶篇:锁的详解_第25张图片

  • 在当前线程中调用了park()方法后,首先会判断_counter是否为0。
  • 如果为0,则获得_mutex互斥锁,该线程会被放入_cond条件变量,进入WATIING状态阻塞,随后将_counter重新设置为0。
  • 如果不为0,则会将_counter中的内容设置为0(最小就是0),且线程会继续运行。

2.1.2 unpark()

Java中的线程进阶篇:锁的详解_第26张图片

  • 在当前线程中调用了unpark()方法后,首先会将_counter设置为1(最大就是1)。
  • 如果此时_cond中有线程,则会将_cond中的线程唤醒,并将_counter设置为0。
  • 如果此时_cond中没有线程,没有任何其他操作。

2.1.3 先调用unpark()后调用park()的情况

Java中的线程进阶篇:锁的详解_第27张图片

  • 调用unpark()方法后,设置_counter为1。_cond中没有阻塞线程,因此没有其他操作。
  • 调用park()方法后,发现_counter为1,将其设置为0,线程继续运行。

2.2 方法解析

方法 作用
park() 阻塞当前线程
unpark() 解锁当前线程

注意:如果先调用了unpark(),再调用park()则,线程不会被阻塞。

四、线程的活跃性

1. 死锁

1.1 基本概念

Java中的线程进阶篇:锁的详解_第28张图片

  • 多个线程分别占有对方所需要的锁不放弃,就称为发生了死锁。

1.2 死锁的本质

  • 多线程获取锁的先后顺序不一致。

1.3 死锁的条件

  • 互斥条件:资源不允许同时被多个进程访问。
  • 占有且等待条件:线程占有已经分配给它们的资源(如锁)并且等待其他的资源(也就是说不会主动释放)。
  • 不可抢占条件:不会被动释放。
  • 环路等待条件:每个进程都在等待下一个进程占有的资源

1.4 死锁的解决

Java中的线程进阶篇:锁的详解_第29张图片

  • 破坏环路等待条件:对所有资源统一编号,所有进程对资源的请求必须按照序号递增的顺序提出,即只有占有了编号较小的资源才能申请编号较大的资源。这样避免了占有大号资源的进程去申请小号资源,各个进程申请资源的顺序都是从小到大,就不会有环了,也就不会产生死锁。

2. 活锁

2.1 基本概念

  • 两个线程互相改变对方的结束条件,导致双方线程都无法结束,这种现象称之为活锁。

2.3 活锁的演示

public class TestLiveLock{
	static volatile int count = 10;
	static final Object lock = new Object();
	public static void main(String[] args){
		//期望减到0退出循环
		new Thread(() -> {
			while(count > 0){
				sleep(1);
				count--;
			}
		}).start();
		//期望加到20退出循环
		new Thread(() -> {
			while(count < 20){
				sleep(1);
				count++;
			}
		}).start();
	}
}

2.3 解决方法

  • 让两个线程的间隔时间交错,不要一起执行。
  • 让两个线程的睡眠时间不同,比如上述案例,一个sleep(1),一个sleep(2)即可,不过真实场景中,一般设置睡眠时间为随机数,来解决此问题。

3. 饥饿

3.1 基本概念

  • 一个线程由于优先级太低,始终得不到CPU调度执行,也无法结束。

3.2 解决方法

  • 可以用ReentrantLcok解决。

五、可重入锁ReentrantLcok简述

1. ReentrantLcok与synchronized的对比

ReentrantLcok synchronized
可打断,获取不到锁可被其他线程打断等待 不可打断,获取不到锁会一直等待
可设置超时时间,指定时间内未获取锁则放弃 不可设置超时时间
默认为非公平锁,但可以设置为公平锁 一定为非公平锁
支持多个条件变量,WAITING状态线程可根据条件在不同的WaitSet内等待 仅支持单个条件变量,所有WAITING状态线程都在一个WaitSet内等待

注意:两者都是可重入锁。

2. ReentrantLock的基本语法

//reentrantLock是ReetrantLock的实例
reentrantLock.lock();//放在try的代码块内外都可以
try{
	//临界区
}finally{
	//临界区
	retrantLock.unLock();
}

3. 可重入性

3.1 基本概念

  • 可重入指的是,同一个线程如果首次获得了这把锁,则该线程就是此锁的拥有者,因此有权利再次获取这把锁。

4. 可打断性

4.1 基本概念

  • 可打断指的是,当前线程如果始终无法获取锁,其他线程可以终止当前线程的等待。

4.2 使用细节

  • 此时当前线程获取锁不能用lock()方法,需要用lockInterruptibly()方法。此时如果没有竞争,当前线程就会获取锁。如果存在竞争,当前线程就会进入阻塞队列,此时,当前线程可以被其他线程用interrput()方法打断阻塞。
    public static void main(String[] args) {
    		ReentrantLock lock = new ReentrantLock();
    		Thread t1 = new Thread(() -> {
    			try {
    				//没有竞争,则获取锁
    				//有竞争,则进入队列阻塞,可被其他线程打断
    				lock.lockInterruptibly();
    			} catch (InterruptedException e) {
    				//被打断
    				e.printStackTrace();
                    //返回,不再向下执行
    				return;
    			}finally {
    				// 释放锁
    				lock.unlock();
    			}
    		});
    		
    		//主线程获取锁
    		lock.lock();
    		try {
    			//t1线程企图获取锁
    			t1.start();
    			Thread.sleep(1000);
    			//主线程打断t1线程
    			t1.interrupt();
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		} finally {
    			lock.unlock();
    		}
    	}
    

5. 锁的超时

5.1 基本概念

  • 当前线程在指定时间内未获取锁则放弃。

5.2 使用细节

  • 使用方法为tryLock(),表明尝试获得锁,该方法会返回一个布尔值,如果获取成功,则返回true,如果获取失败,则不等待直接返回,并返回false。
    public static void main(String[] args) {
    	ReentrantLock lock = new ReentrantLock();
    	Thread t1 = new Thread(() -> {
               // 未设置等待时间,一旦获取失败,直接返回false
    		if(!lock.tryLock()) {
                   // 获取失败,不再向下执行,返回
    			return;
    		}
    		lock.unlock();
    	});
    
    
    	lock.lock();
    	try{
    		t1.start();
    	} catch (InterruptedException e) {
    		e.printStackTrace();
    	} finally {
    		lock.unlock();
    	}
    }	
    
  • 使用方法tryLock(long timeout, TimeUint),该方法的两个参数分别为等待时间和时间单位,如果当前线程在指定时间内未能成功获取锁,则不再等待,直接返回。注意,此时的tryLock的等待状态,也可以被其他线程中的interrupt()方法打断。
    public static void main(String[] args) {
       ReentrantLock lock = new ReentrantLock();
       Thread t1 = new Thread(() -> {
       	try {
       		// 判断获取锁是否成功,最多等待1秒
       		if(!lock.tryLock(1, TimeUnit.SECONDS)) {
       			// 获取失败,不再向下执行,直接返回
       			return;
       		}
       	} catch (InterruptedException e) {
       		// 被打断
       		e.printStackTrace();
       		//不再向下执行,直接返回
       		return;
       	}
       	// 释放锁
       	lock.unlock();
       });
    
       lock.lock();
       try{
       	t1.start();
       	// 打断等待
       	t1.interrupt();
       } catch (InterruptedException e) {
       	e.printStackTrace();
       } finally {
       	lock.unlock();
       }
    }
    

6. 公平锁

6.1 基本概念

  • 公平锁指的是,当一个线程释放了持有的锁以后,其他阻塞中的线程,安全先后顺序,依次获得锁。非公平锁指的就是不按照先后顺序获取锁。

6.2 使用细节

  • 可以在创建ReentrantLock时,设置参数fair为true来将该锁设置为公平锁。
  • 可以通过使用公平锁来解决饥饿的问题,但是没必要,因为会降低并发度。

7. 条件变量

7.1 概述

  • synchronized中也有条件变量,当条件不满足时进入waitSet 等待。
  • ReentrantLock的条件变量比synchronized强大之处在于它支持多个条件变量。

7.2 使用

  • 需要创建ReentrantLock的锁对象lock和Condition的条件变量condition(根据需求可以设置多个条件变量)。
  • 先获得锁。
  • 当前线程执行condition.await(),condition.await(long time)等方法,会释放锁,进入conditionObject等待。
  • 如果是因为await()方法陷入等待,其他线程可以通过执行condition.signal()或condition.signAll()方法来唤醒处于该conditionObject中等待的线程。
  • 如果是因为await(参数)方法陷入等待,除了被唤醒,还会在到达指定事件后自动醒来。
  • 醒来后哦的线程会重新竞争lock锁。
  • 竞争lock锁成功后,从conditon.await()后继续执行。

你可能感兴趣的:(JUC,java,开发语言)