Java并发编程(三)——管程:使用悲观锁的思想解决多线程并发中的共享问题

本文深入探讨了线程安全问题及其解决方案,包括互斥锁synchronized的使用、线程间通信的wait/notify机制、ReentrantLock特性,以及如何避免死锁、活锁和饥饿现象。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本节学会后的主要应用:

  • 互斥:使用synchronizedLock达到共享资源互斥效果
  • 同步:使用wait/notifyLock的条件变量达到线程间的通信效果

管程:Monitor(本节原理)

一、共享带来的问题

\quad 当多个线程访问一个共同的资源时,会发生线程安全问题。假设有共享变量i=0,有两个任务,任务1对该变量进行+1操作,任务2对该变量进行-1操作。假设线程t1对i进行+1操作,但还未来得及保存,此时i=0,时间片就切换给线程2,此时线程2又对i进行-1操作并保存,此时i=-1,t2线程结束,时间片给t1,t1将上次未保存的结果1给i,最终输出i=1。按理说i最后等于0,但是由于多个线程操作共享资源,导致了线程安全问题。例如下面程序,返回结果就可能是:[-10000,10000]中任意一个值

static int i = 0;
@Test
public void test3() throws InterruptedException {
    Thread t1 = new Thread(()->{
        for(int j = 0; j < 10000; j ++ ){
            i ++ ;
        }
    }, "t1");
    Thread t2 = new Thread(()->{
        for(int j = 0; j < 10000; j ++ ){
            i -- ;
        }
    }, "t2");
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    log.debug("{}", i);
}

临界区

  • 多个线程访问共享资源,如果只涉及到读操作,不会有安全问题;如果对共享资源进行修改时发生指令交错,就会出现问题
  • 临界区:一段代码块内如果存在对共享内存的多线程读写操作,称这段代码块为临界区

竞态条件

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

如何避免临界区竞态条件

  • 阻塞式:synchronized,Lock
  • 非阻塞式:原子变量

二、synchronized对象锁

\quad synchronized俗称对象锁,采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程想要获得这个对象锁时就会被阻塞住。这样就能保证拥有锁的线程可以安全地执行临界区中代码,不用担心上下文问题。语法如下:

synchronized(任意对象)
{
	临界区
}

\quad 对第一节的示例代码中临界区(也就是i++,i–)处加锁,加锁需要随便创建一个对象,我们使用private Object lock = new Object();作为锁对象,用synchronized把临界区代码包起来,这样就不会发生线程安全问题了,如下:

private int i = 0;
private Object lock = new Object();
@Test
public void test3() throws InterruptedException {
    Thread t1 = new Thread(()->{
        for(int j = 0; j < 10000; j ++ ){
            synchronized (lock) {
                i++;
            }
        }
    }, "t1");
    Thread t2 = new Thread(()->{
        for(int j = 0; j < 10000; j ++ ){
            synchronized (lock) {
                i--;
            }
        }
    }, "t2");
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    log.debug("{}", i);
}

\quad 可以将临界区的代码封装在一个类中,这样封装性更好,例如上述例子,可以改造为如下:

@Test
public void test3() throws InterruptedException {
    Room room = new Room();
    Thread t1 = new Thread(()->{
        for(int j = 0; j < 10000; j ++ ){
            room.increment();
        }
    }, "t1");
    Thread t2 = new Thread(()->{
        for(int j = 0; j < 10000; j ++ ){
            room.decrement();
        }
    }, "t2");
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    log.debug("{}", room.getCounter());
}
}

class Room{
private int counter = 0;
public void increment(){
    synchronized (this){
        counter ++ ;
    }
}
public void decrement(){
    synchronized (this){
        counter -- ;
    }
}
public int getCounter(){
    synchronized (this){
        return counter;
    }
}

可以将synchronized锁在方法上,一般可以把临界区代码剥离成为一个函数,直接锁在该方法上

synchronized method()
{
	临界区
}
等价于
method(){
	synchronized(this){
		临界区
	}
}

上面例子中给出的Room也可以这样写:

class Room{
    private int counter = 0;
    public synchronized void increment(){
        counter ++ ;
    }
    public synchronized void decrement(){
        counter -- ;
    }
    public synchronized int getCounter(){
            return counter;
    }
}

如果加在静态方法上等于锁住了类对象。

class Test{
	public synchronized static void method(){
	}
}
等价于
class Test{
	public static void method(){
		synchronized(Test.class){
		}
	}
}

总结:

  • synchronized实际上是用对象锁保证了临界区内代码的原子性,临界区内代码不可分割,不会被线程切换所打断

三、常见的线程安全类

  • String
  • Integer等包装类
  • StringBuffer
  • Vector
  • Hashtable:与HashMap底层实现一样,只是在方法上加上了锁,如下:
    在这里插入图片描述
  • java.util.concurrent,简称juc

\quad 注:只能保证上锁的方法是线程安全的,如果是多个方法的组合,就不是线程安全的了,毕竟上锁只是给其中方法上了锁,方法内的代码具有原子性。如下示例:
在这里插入图片描述

三、synchronized优化

\quad synchronized代码块是由一对 monitorenter/moniterexit 字节码指令实现, monitor 是其同步实现的基础, Java SE1.6 为了改善性能, 使得 JVM 会根据竞争情况, 使用如下 3 种不同的锁机制:

  • 偏向锁
  • 轻量级锁
  • 重量级锁

\quad 上述这三种机制的切换是根据竞争激烈程度进行的, 在几乎无竞争的条件下, 会使用偏向锁, 在轻度竞争的条件下, 会由偏向锁升级为轻量级锁, 在重度竞争的情况下, 会升级到重量级锁。为了更好的理解这些锁,我们先学习几个前置知识:

前置知识1:java对象头

每一个 Java 对象都至少占用 2 个字宽的内存(数组类型占用3个字宽)。

  • 对象头包含了多种不同的信息, 其中就包含对象锁相关的信息。
  • 无锁时对应标志位为01、轻量级锁位00,重量级锁位10

前置知识2:Monitor

  • Monitor也称为监视器或者管程
  • Monitor对象就相当于锁,每个Java对象都可以关联一个Monitor对象,不同对象关联不同的Monitor对象。如果使用synchronized给对象上锁后,该对象头的mark word就被设置为指向Monitor对象的指针
    在这里插入图片描述
  • 刚开始Monitor中owner为null
  • 当Thread-2执行synchronized(obj)时就会把Monitor的所有者owner置为Thread-2,Monitor只能有一个owner
  • 在Thread-2上锁过程中,如果Thread-3等也来执行synchronized(obj),就会进入EntryList
  • Thread-2执行完同步代码块中的内容,然后再唤醒EntryList中的线程来竞争锁
  • synchronized必须进入同一个对象的monitor才有上述效果,不加synchronized的对象不会关联监视器

前置知识3:cas操作

  • CAS (Compare And Swap) 指令是一个CPU层级的原子性操作指令。 在 Intel 处理器中, 其汇编指令为 cmpxchg。
  • 该指令概念上存在 3 个参数, 第一个参数【目标地址】, 第二个参数【值1】, 第三个参数【值2】, 指令会比较【目标地址存储的内容】和 【值1】 是否一致, 如果一致, 则将【值 2】 填写到【目标地址】, 其语义可以用如下的伪代码表示。
  • 注意: 该指令是是原子性的, 也就是说 CPU 执行该指令时, 是不会被中断执行其他指令的

1、轻量级锁

\quad 如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(没有竞争),可以使用轻量级锁来优化。轻量级锁对使用者透明,语法仍然是synchronized,没有新的语法,由JVM根据竞争激烈成都来进行决定。假设有两个方法同步块,利用同一个对象加锁:

static final Obejct obj = new Object()
public static void method1(){
	synchronized(obj){
		// 同步块A
		method2()
	}
}
public static void method2(){
	synchronized(obj){
		// 同步块B
	}
}

\quad 以上代码块的加锁原理如下:

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

2、锁膨胀

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

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

3、自旋优化

\quad 重量级锁竞争的时候,可以使用自旋来进行优化,如果当前线程自旋成功,即这时持锁线程退出了同步块,释放了锁,这时当前线程就可以避免阻塞。因为阻塞需要线程上下文切换,耗费资源。
在这里插入图片描述

  • Java6以后自旋锁是自适应的,自旋次数智能绝顶
  • 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势
  • 对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

4、偏向锁

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

四、wait/notify

在这里插入图片描述

  • 当owner对应的线程不满足条件,无法运行,也不能一直占着资源,这时候调用wait方法进入WaitSet
  • Waiting状态是已经获得了锁,但是又放弃了锁进入wait的状态
  • waiting状态线程会在owner对应线程调用notify或notifyAll时唤醒,唤醒后并不意味着立即获得锁,需要进入EntryList重新竞争
  • obj.wait():让进入Object锁的线程到WaitSet等待
  • obj.wait(long n):会等待最多n毫秒,如果这个时间没有线程唤醒,则会自己唤醒
  • obj.notify():让Object上正在WaitSet等待的线程中挑一个唤醒
  • obj.notifyAll():让Object上正在WaitSet等待的全部线程唤醒
  • NOTE:这些方法都需要线程称为owner后才能调用
@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
    final Object obj = new Object();

    @Test
    public void test() {
        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t1").start();

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t2").start();

        // 主线程两秒后执行
        sleep(0.5);
        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
//            obj.notify(); // 唤醒obj上一个线程
            obj.notifyAll(); // 唤醒obj上所有等待线程
        }
    }
}
16:05:09.512 c.TestWaitNotify [t1] - 执行....
16:05:09.513 c.TestWaitNotify [t2] - 执行....
16:05:10.012 c.TestWaitNotify [main] - 唤醒 obj 上其它线程
16:05:10.012 c.TestWaitNotify [t1] - 其它代码....
16:05:10.012 c.TestWaitNotify [t2] - 其它代码....

sleep和wait的取别:

  • sleep是Thread方法,而wait是Object的方法
  • sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起使用
  • sleep在睡眠时不会释放锁,wait在等待时需要释放锁

Park &Unpark

\quad 用于暂停当前线程和恢复某个线程的运行。

LockSupport.park();
LockSupport.unpark(需要恢复运行的线程)
public class TestParkUnpark {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            log.debug("park...");
            LockSupport.park();
            log.debug("resume...");
        }, "t1");
        t1.start();

        sleep(1);
        log.debug("unpark...");
        LockSupport.unpark(t1);
    }
}

对于上述例子,线程t1先park,1s后主线程对其进行unpark,t1恢复运行,可以打印出resume…。假设我们在t1 park前sleep 2s,这样,就是主线程先对t1进行unpark,1s后t1 park,相当于先喝解药再喝毒药,这样能解毒吗?

public class TestParkUnpark {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            log.debug("park...");
            sleep(2);
            LockSupport.park();
            log.debug("resume...");
        }, "t1");
        t1.start();

        sleep(1);
        log.debug("unpark...");
        LockSupport.unpark(t1);
    }
}

\quad 答案是可以的。相当于先喝解药再喝毒药,也是可以解毒的。这是怎么实现的呢?

wait/notify 和 park/unpark 区别
  • wait&notify 必须要 Object Monitor 一起使用,而 park 与 unpark 不需要
  • park&unpark是以确定的单个线程【堵塞】和【唤醒】线程的,而notify是随机唤醒同一把锁对象的线程的
  • park不会释放锁资源,wait会释放锁资源
  • park&unpark可以先unpark(park后直接就唤醒了),而wait&notify不能提前notify

五、线程状态切换

在这里插入图片描述

  • NEW:新建线程
  • RUNNABLE:运行状态、可运行状态和操作系统层面的阻塞状态都称为RUNNABLE,因此调用t1.start()后线程就进入运行状态
  • BLOCKED:java中三种阻塞状态之一,后续讲完锁后会详解
  • WATING:java中三种阻塞状态之一,后续讲完锁后会详解
  • TIMED_WAITING:java中三种阻塞状态之一,t1.sleep()会让线程进入该状态
  • RERMINATED
1、RUNNABLE <—> WAITING

\quad 线程t用synchronized(obj)获取了对象锁后,

  • 调用obj.wait()方法,t线程从 RUNNABLE --> WAITING
  • 调用obj.notify(), obj.notifyAll(), t.interrupt()时:
    • 竞争锁成功,t线程从WAITING --> RUNNABLE
    • 竞争锁失败,t线程从WAITING --> BLOCKED

\quad 当前线程调用t.join()方法,当前线程从 RUNNABLE --> WAITING。当t线程运行结束或者被interrput时,当前线程从WAITING --> RUNNABLE
\quad 当前线程从LockSupport.park()方法会让当前线程从RUNNABLE --> WAITING,当调用LockSupport.unpark(该线程)或者被interrput时,当前线程从WAITING --> RUNNABLE

2、RUNNABLE <—>TIME_WAITING

\quad 线程t用synchronized(obj)获取了对象锁后,

  • 调用obj.wait(long n)方法,t线程从 RUNNABLE --> TIMED_WAITING
  • t线程等待了n毫秒,或者调用obj.notify(), obj.notifyAll(), t.interrupt()时:
    • 竞争锁成功,t线程从WAITING --> RUNNABLE
    • 竞争锁失败,t线程从WAITING --> BLOCKED

\quad 当前线程调用t.join(long n)方法,当前线程从 RUNNABLE -->TIMED_WAITING。当前线程等待事件超过了n毫秒,或当t线程运行结束或者被interrput时,当前线程从TIMED_WAITING --> RUNNABLE
\quad 当前线程从LockSupport.parkNanos(long nanos) 或者 LockSupport.parkUtil(long millis)方法会让当前线程从RUNNABLE --> TIMED_WAITING,当等待超时,或调用LockSupport.unpark(该线程)或者被interrput时,当前线程从TIMED_WAITING--> RUNNABLE
\quad 当前线程调用Thread.sleep(long n),当前线程从RUNNABLE -->TIMED_WAITING;当前线程等待事件超过n毫秒,则当前线程从TIMED_WAITING--> RUNNABLE

3、RUNNABLE <—> BLOCKED

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

4、RUNNABLE --> TERMINATED

六、多把锁

public class TestMultiLock {
    public static void main(String[] args) {
        BigRoom bigRoom = new BigRoom();
        new Thread(() -> {
            bigRoom.study();
        },"t1").start();
        new Thread(() -> {
            bigRoom.sleep();
        },"t2").start();
    }
}
class BigRoom {
    public void sleep() {
        synchronized (this) {
            log.debug("sleeping 2 h");
            Sleeper.sleep(2);
        }
    }
    public void study() {
        synchronized (this) {
            log.debug("study 1 h");
            Sleeper.sleep(1);
        }
    }
}
19:36:07.027 c.BigRoom [t1] - study 1 h
19:36:08.031 c.BigRoom [t2] - sleeping 2 h

sleep和study都只有一个对象锁,两个任务不能同时进行,因此并发低。解决方法是准备多个对象锁,对Room类做如下改进:

class BigRoom {
    private final Object studyRoom = new Object();
    private final Object sleepRoom = new Object();
    public void sleep() {
        synchronized (sleepRoom) {
            log.debug("sleeping 2 h");
            Sleeper.sleep(2);
        }
    }
    public void study() {
        synchronized (studyRoom) {
            log.debug("study 1 h");
            Sleeper.sleep(1);
        }
    }
}
19:39:19.206 c.BigRoom [t2] - sleeping 2 h
19:39:19.206 c.BigRoom [t1] - study 1 h

\quad 使用多把锁将锁的粒度细分,好处是可以增强并发度;坏处时如果一个线程需要同时获得多把锁,容易发生死锁。

1、死锁

\quad 多把锁的情况下可能会出现死锁,如下示例:

private static void test1() {
    Object A = new Object();
    Object B = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (A) {
            log.debug("lock A");
            sleep(1);
            synchronized (B) {
                log.debug("lock B");
                log.debug("操作...");
            }
        }
    }, "t1");

    Thread t2 = new Thread(() -> {
        synchronized (B) {
            log.debug("lock B");
            sleep(0.5);
            synchronized (A) {
                log.debug("lock A");
                log.debug("操作...");
            }
        }
    }, "t2");
    t1.start();
    t2.start();
}
19:44:47.059 c.TestDeadLock [t1] - lock A
19:44:47.077 c.TestDeadLock [t2] - lock B
2、活锁

\quad 两个线程互相改变对方结束条件,导致最后都无法结束。

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();
}
3、饥饿

\quad 一个线程由于优先级太低,始终得不到cpu的调度执行,也不能结束。

七、ReentrantLock

\quad reentrant表示可重入,ReentrantLock表示可重入锁,该锁与synchronized有相似之处,但也有如下不同的特点:

  • 可中断,意思是这个锁可以被打断,防止某个线程无限等待,是解决死锁的一种方式
  • 可以设置超时时间,这样避免了一个线程永久霸占一个锁,可以避免死锁问题
  • 可以设置为公平锁,防止线程饥饿的情况
  • 支持多个条件变量,可以让正在阻塞中等待获取该锁的线程按照不同条件进入不同的entryList,可以按照不同条件唤醒不同entryList中的线程
  • synchronized一样,都支持可重入
1、可重入

\quad 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。意思是某个线程可以对一个对象反复加锁。

2、基本语法
reentrantLock.lock();
try{
	// 临界区
}finally{
	// 释放锁
	reentrantLock.unlock();	
}

fially保证即使出现异常也能释放锁。

public class TestReentrant {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args){
        lock.lock();
        try {
            log.debug("enter main");
            m1();  // 测试是否可重入
        }finally {
            lock.unlock();
        }
    }

    public static void m1(){
        lock.lock();
        try {
            log.debug("enter m1");
        }finally {
            lock.unlock();
        }
    }
}
20:21:22.070 c.TestReentrant [main] - enter main
20:21:22.072 c.TestReentrant [main] - enter m1
3、可打断

\quad 在等待锁的过程中,其他线程可以用interrupt终止该线程的等待。

    public static void test1() {
        ReentrantLock lock = new ReentrantLock();

        Thread t1 = new Thread(() -> {
            try {
                // 如果没有竞争则获得 lock 对象锁
                // 如果有竞争则进入阻塞队列,可以被其他线程用 interrupt 打断
                log.debug("try to get lock");
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("do not get lock");
                return;
            }
            try {
                log.debug("get lock");
            } finally {
                lock.unlock();
            }
        }, "t1");
        t1.start();
        lock.lock();  // 主线程获得了锁,t1线程无法得到锁
    }

主线程打断线程t1等待锁的过程,t1无法获得锁,直接return:

    public static void test1() {
        ReentrantLock lock = new ReentrantLock();

        Thread t1 = new Thread(() -> {
            try {
                // 如果没有竞争则获得 lock 对象锁
                // 如果有竞争则进入阻塞队列,可以被其他线程用 interrupt 打断
                log.debug("try to get lock");
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("do not get lock");
                return;
            }
            try {
                log.debug("get lock");
            } finally {
                lock.unlock();
            }
        }, "t1");
        t1.start();
        lock.lock();  // 主线程获得了锁,t1线程无法得到锁
        log.debug("interrupt lock");
        t1.interrupt();
    }
4、锁超时

\quad 与可打断不同,锁超时指某个线程在等待获取锁的过程中最多等待指定时间,超时则自动放弃等待,也可以解决死锁问题。可打断是由其他线程来完成对某个线程终止无限等待的操作,是被动的。锁超时是自己超时主动放弃锁,是主动的。使用lock.tryLock()来实现,相当于自旋:

    private static void test1() {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            log.debug("启动...");
            try {
                if (!lock.tryLock(1, TimeUnit.SECONDS)) {
                    log.debug("获取等待 1s 后失败,返回");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                log.debug("获得了锁");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        log.debug("获得了锁");
        t1.start();
    }
5、公平锁

其实使用tryLock的方式放弃锁也相当于实现了公平。公平锁其实会降低并发度,一般不用。

6、条件变量

在这里插入图片描述

八、控制线程执行顺序

\quad 如果需要线程2先执行,线程1后执行,可以使用wait/notify实现:

    static final Object lock = new Object();
    // 表示 t2 是否运行过
    static boolean t2runned = false;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                while (!t2runned) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("1");
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                log.debug("2");
                t2runned = true;
                lock.notify();
            }
        }, "t2");

        t1.start();
        t2.start();
    }

这样写对cpu压力很大,t1会一直自旋。也可以使用park/unpark实现:

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            LockSupport.park();
            log.debug("1");
        }, "t1");
        t1.start();

        new Thread(() -> {
            log.debug("2");
            LockSupport.unpark(t1);
        },"t2").start();
    }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值