Java并发编程学习(5):死锁、可重入锁、控制线程顺序

死锁

要点

一个线程需要同时获取多把锁,这是就容易发生死锁,例如:

  • t1线程获得A对象的锁,接下来想获得B对象的锁
  • t2线程获得B对象的锁,接下来想获得A对象的锁

代码示例

public class DeathLockDemo {
    public static void main(String[] args) {
        Object a = new Object();
        Object b = new Object();

        Runnable task1= () -> {
            synchronized (a){
                log.info("lock a...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b){
                    log.info("lock b...");
                    log.info("do sth.");
                }
            }
        };

        Runnable task2= () -> {
            synchronized (b){
                log.info("lock b...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (a){
                    log.info("lock a...");
                    log.info("do sth.");
                }
            }
        };

        new Thread(task1,"t1").start();
        new Thread(task2,"t2").start();
    }
}

打印日志如下,线程t1与线程t2均处于卡死状态,无法往下运行。

[299 ms] [INFO][t2] i.k.e.c.e.DeathLockDemo : lock b...
[299 ms] [INFO][t1] i.k.e.c.e.DeathLockDemo : lock a...

用jps和定位死锁

具体步骤:

  1. 运行上述代码
  2. 打开控制台/终端,输入jps查看系统中的java进程号
    我电脑中的打印如下,其中死锁程序对应的进程号为16876
6692 Launcher
9252 Jps
4056 RemoteMavenServer
7864 KotlinCompileDaemon
16876 DeathLockDemo
4492 Launcher
  1. 在控制台/终端,输入jstack 进程号可以查看java进程的栈信息,如果程序中存在死锁,相关信息也会打印出来
    我电脑中,死锁相关信息打印如下,上半部分包含了死锁的线程与原因,下半部分则展示了死锁线程的信息与发生的位置
Found one Java-level deadlock:
=============================
"t1":
  waiting to lock monitor 0x000001c93f2aa500 (object 0x000000071156ec58, a java.lang.Object),
  which is held by "t2"
"t2":
  waiting to lock monitor 0x000001c93f2aad00 (object 0x000000071156ec48, a java.lang.Object),
  which is held by "t1"
  
Java stack information for the threads listed above:
===================================================
"t1":
        at info.kuangkuang.exercise.concurrent.ex.DeathLockDemo.lambda$main$0(DeathLockDemo.java:25)
        - waiting to lock <0x000000071156ec58> (a java.lang.Object)
        - locked <0x000000071156ec48> (a java.lang.Object)
        at info.kuangkuang.exercise.concurrent.ex.DeathLockDemo$$Lambda$35/0x000000080121c040.run(Unknown Source)
        at java.lang.Thread.run(java.base@12/Thread.java:835)
"t2":
        at info.kuangkuang.exercise.concurrent.ex.DeathLockDemo.lambda$main$1(DeathLockDemo.java:40)
        - waiting to lock <0x000000071156ec48> (a java.lang.Object)
        - locked <0x000000071156ec58> (a java.lang.Object)
        at info.kuangkuang.exercise.concurrent.ex.DeathLockDemo$$Lambda$36/0x000000080121c440.run(Unknown Source)
        at java.lang.Thread.run(java.base@12/Thread.java:835)

Found 1 deadlock.

哲学家就餐问题

有N位哲学家,围坐在圆桌旁。

  • 他们只做两件事情:思考与吃饭,思考一会吃一口饭,吃完后节则思考
  • 吃饭需要使用两根筷子,桌上一共有N根筷子,每位哲学家左手右手各有一根
  • 如果筷子被边上的人拿走,自己就得等待

注意 :此时如果每个哲学家都持有一根筷子,那么谁都吃不了饭,谁也不会放下筷子

下面用代码对该问题进行演示

筷子类

筷子类仅包含一个名字用以区分不同的筷子

class Chopstick{
    private String name;

    public Chopstick(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

哲学家类

哲学家类组合了两个筷子对象,分别用来表示左手筷子和右手筷子。
哲学家类继承了Thead类,并修改了其构造方法和run()方法,在run()方法,哲学家需要同时持有两个对象的锁才会执行吃饭函数eat()。吃饭函数eat()会在吃饭前后打印日志,

class Philosopher extends Thread{
    private final Chopstick left;
    private 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) {
            synchronized (left) {
                synchronized (right) {
                    eat();
                }
            }
        }
    }

    private void eat(){
        log.info("开始吃饭...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("饭吃完了");
    }
}

测试类

在测试类中,我实例化了N个筷子类和N个哲学家类,并讲哲学家对象与筷子对象进行了关联。
我将变量N取为3,方便快速看到死锁效果。

public class PhilosopherEating {
    public static void main(String[] args) {
        int N = 3;
        Chopstick[] chopsticks = new Chopstick[N];
        for (int i = 0; i < N; i++) {
            chopsticks[i] = new Chopstick("筷子-"+i);
        }
        Philosopher[] philosophers = new Philosopher[N];
        for (int i = 0; i < N; i++) {
            philosophers[i] = new Philosopher("哲学家-"+i,chopsticks[i],chopsticks[(i+1)%N]);
        }
        for (int i = 0; i < N; i++) {
            philosophers[i].start();
        }
    }
}

打印日志

测试类打印日志如下:当程序运行了约6秒后,不再有打印日志,初步判断程序陷入了死锁,具体可以通过jstack工具分析判断。

[173 ms] [INFO][哲学家-0] i.k.e.c.h.Philosopher : 开始吃饭...
[1187 ms] [INFO][哲学家-0] i.k.e.c.h.Philosopher : 饭吃完了
[1187 ms] [INFO][哲学家-2] i.k.e.c.h.Philosopher : 开始吃饭...
[2199 ms] [INFO][哲学家-2] i.k.e.c.h.Philosopher : 饭吃完了
[2199 ms] [INFO][哲学家-2] i.k.e.c.h.Philosopher : 开始吃饭...
[3214 ms] [INFO][哲学家-2] i.k.e.c.h.Philosopher : 饭吃完了
[3214 ms] [INFO][哲学家-2] i.k.e.c.h.Philosopher : 开始吃饭...
[4228 ms] [INFO][哲学家-2] i.k.e.c.h.Philosopher : 饭吃完了
[4228 ms] [INFO][哲学家-1] i.k.e.c.h.Philosopher : 开始吃饭...
[5242 ms] [INFO][哲学家-1] i.k.e.c.h.Philosopher : 饭吃完了
[5242 ms] [INFO][哲学家-1] i.k.e.c.h.Philosopher : 开始吃饭...
[6255 ms] [INFO][哲学家-1] i.k.e.c.h.Philosopher : 饭吃完了

jstack分析结果

jstack分析结果如下,哲学家-0,1,2相互之间持有了其他人需要的锁,导致程序无法往下进行,形成了死锁。

Found one Java-level deadlock:
=============================
"哲学家-0":
  waiting to lock monitor 0x000001c6c99cb200 (object 0x0000000711cf9010, a info.kuangkuang.exercise.concurrent.homework.Chopstick),
  which is held by "哲学家-1"
"哲学家-1":
  waiting to lock monitor 0x000001c6c99cb100 (object 0x0000000711cf90d0, a info.kuangkuang.exercise.concurrent.homework.Chopstick),
  which is held by "哲学家-2"
"哲学家-2":
  waiting to lock monitor 0x000001c6c99c9200 (object 0x0000000711cf8df8, a info.kuangkuang.exercise.concurrent.homework.Chopstick),
  which is held by "哲学家-0"

解决办法

  1. 按照固定的顺序加载锁,即不同类之间加载锁的先后顺序应该是一样的
  2. 引入可重入锁ReentrantLock,后续会介绍到

活锁

要点

活锁指两个线程互相改变对方的结束条件,使得最后谁也无法结束

代码示例

在示例代码中,变量i的初始值为10。subTask需要将变量i减到0才能结束任务,而addTask需要将变量i加到20才能结束任务。如果两个代码同时执行,则会让变量i不断地在10附近浮动,进而导致两个任务都难以结束。

public class LiveLockDemo {

    public static int i = 10;
    public static void main(String[] args) {
        Runnable subTask = () -> {
            while (i > 0){
                i --;
                log.info("i = {}",i);
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Runnable addTask = () -> {
            while (i < 20){
                i ++;
                log.info("i = {}",i);
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        new Thread(subTask,"sub").start();
        new Thread(addTask,"add").start();
    }
}

打印日志如下,变量i总是在10附近浮动,两个线程难以结束。

[163 ms] [INFO][add] i.k.e.c.e.LiveLockDemo : i = 10
[163 ms] [INFO][sub] i.k.e.c.e.LiveLockDemo : i = 9
[380 ms] [INFO][sub] i.k.e.c.e.LiveLockDemo : i = 9
[380 ms] [INFO][add] i.k.e.c.e.LiveLockDemo : i = 10
[590 ms] [INFO][add] i.k.e.c.e.LiveLockDemo : i = 11
[590 ms] [INFO][sub] i.k.e.c.e.LiveLockDemo : i = 10
[799 ms] [INFO][add] i.k.e.c.e.LiveLockDemo : i = 11
[799 ms] [INFO][sub] i.k.e.c.e.LiveLockDemo : i = 10
[1001 ms] [INFO][add] i.k.e.c.e.LiveLockDemo : i = 11
[1001 ms] [INFO][sub] i.k.e.c.e.LiveLockDemo : i = 10
[1205 ms] [INFO][sub] i.k.e.c.e.LiveLockDemo : i = 9
[1205 ms] [INFO][add] i.k.e.c.e.LiveLockDemo : i = 10
[1409 ms] [INFO][add] i.k.e.c.e.LiveLockDemo : i = 10

ReentrantLock

可重入锁

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

相比synchronized它具备一下特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁(防止线程饥饿)
  • 支持多个条件变量(可以对等待的线程进行细分)

synchronizedReentrantLock都是可重入锁。

基本语法与使用规范

  1. 创建一个ReentrantLock对象
  2. 调用ReentrantLock对象的lock()方法
  3. 将临界区的代码写在try代码块中
  4. ReentrantLock对象的unlock()方法写在finally代码块中

注意:lock.lock();与try代码块之间不要有空行或者其它逻辑,且lock.unlock();要写在finally代码块的第一行

public static void main(String[] args) {
    ReentrantLock lock = new ReentrantLock();

    lock.lock();
    try {

    }finally {
        lock.unlock();
    }
}

特点1:可重入

public class ReentrantLockDemo {
    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        new Thread(() -> method1(),"t").start();
        method1();
    }

    public static void method1(){
        lock.lock();
        try {
            log.info("method 1...");
            method2();
        }finally {
            lock.unlock();
        }
    }

    private static void method2() {
        lock.lock();
        try {
            log.info("method 2...");
        }finally {
            lock.unlock();
        }
    }
}

打印日志如下,主函数和线程t都成功执行了方法1和方法2

[160 ms] [INFO][main] i.k.e.c.e.ReentrantLockDemo : method 1...
[162 ms] [INFO][main] i.k.e.c.e.ReentrantLockDemo : method 2...
[163 ms] [INFO][t] i.k.e.c.e.ReentrantLockDemo : method 1...
[163 ms] [INFO][t] i.k.e.c.e.ReentrantLockDemo : method 2...

这里需要注意:加锁与解锁是必须匹配的,只有当解锁次数==加锁次数时,锁才会被正确释放。

特点2:可打断

  • lock.lock()等待锁是不可以被打断的
  • lock.lockInterruptibly()的等待过程是可以被打断的

代码示例:主线程首先获得锁,因此被创建出的线程t1启动后无法获得锁,之后,主线程打断线程t1,使得线程t1结束等待。

public class ReentrantLockDemo2 {
    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        lock.lock();
        try {
            Thread t1 = new Thread(() -> method1(), "t1");
            t1.start();
            t1.interrupt();
        }finally {
            lock.unlock();
        }
    }

    public static void method1(){
        try {
            lock.lockInterruptibly();
            try {
                log.info("method 1...");
            }finally {
                lock.unlock();
            }
        } catch (InterruptedException e) {
            log.info("被打断,没有获得锁");
        }
    }
}

打印日志如下:

[162 ms] [INFO][t1] i.k.e.c.e.ReentrantLockDemo2 : 被打断,没有获得锁

特点3:锁超时

当等待锁的时间超过给定时间时,会触发锁超时,不再等待。

  • 可打断属于被动避免一直等待
  • 可打断属于主动避免一直等待

锁超时需要使用lock.trylock()方法,相关API有两个:

  1. 如果无参数传入,则会立刻返回尝试获取锁的结果;
  2. 也可以使用tryLock(long timeout, TimeUnit unit)传入最大等待时间,如果在等待时间内获取到锁则立刻返回true,如果等待超时则返回false
@Slf4j(topic = "kzz")
public class ReentrantLockDemo3 {
    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        lock.lock();
        try {
            Thread t1 = new Thread(() -> method1(), "t1");
            t1.start();
            Thread.sleep(200);
        }finally {
            lock.unlock();
        }


    }

    public static void method1(){
        log.info("开始等待锁");
        try {
            if (!lock.tryLock(100, TimeUnit.MILLISECONDS)){
                log.info("等待超时,没有获得锁");
                return;
            }
        } catch (InterruptedException e) {
            log.info("被打断,没有获得锁");
            e.printStackTrace();
            return;
        }
        try {
            log.info("获得到锁");
        }finally {
            lock.unlock();
        }
    }
}

打印日志如下,可以看到线程t1大约等待100ms后出现超时日志

[165 ms] [INFO][t1] i.k.e.c.e.ReentrantLockDemo3 : 开始等待锁
[277 ms] [INFO][t1] i.k.e.c.e.ReentrantLockDemo3 : 等待超时,没有获得锁

如果把主线程中的睡眠时间改为50ms,打印日志如下,可以看到线程t1大约等待50ms后就获得了锁

[173 ms] [INFO][t1] i.k.e.c.e.ReentrantLockDemo3 : 开始等待锁
[235 ms] [INFO][t1] i.k.e.c.e.ReentrantLockDemo3 : 获得到锁

特点4:公平锁

公平锁是指:按照阻塞队列的顺序,先到先得的获得锁。
ReentrantLock默认为非公平锁
可以通过new ReentrantLock(true)方式将其设置为公平锁。
公平锁一般没有必要,会降低并发度。

条件变量

关键字synchronized中也有条件变量,就是waitSet,可以理解为条件不满足时进入waitSet等待,一个synchronized只能对应一个waitSet

ReentrantLock可以支持多个条件变量,因此可以将不同条件的线程放入等待集合中,以便于后续进行专门的唤醒。
ReentrantLock中使用条件变量需要使用await()方法

使用流程

  1. 使用ReentrantLock对象创建条件变量condition;
  2. 执行condition.await()前需要先获取锁;
  3. 执行condition.await()后,线程会释放锁,并进入conditionObject中等待;
  4. 其它线程执行````condition.signal()或者condition.signalAll()唤醒conditionObject```中等待的线程;
  5. 被唤醒后会重新竞争锁
  6. 竞争锁成功后,会从await()后的代码处开始执行

函数await()包含多种调用方式,具体如下:

函数签名用法返回值含义
void await()进入等待,直到被其它线程唤醒无返回值
boolean await(long time, TimeUnit unit)进入等待,直到被其它线程唤醒或超时如果等待时间耗尽依然没有被唤醒,则会返回false
void awaitUninterruptibly()该次等待不可以被打断,其余都是可以被打断的无返回值
boolean awaitUntil(Date deadline)进入等待,直到被其它线程唤醒或到达某一时间节点如果到达指定时间依然没有被唤醒,则会返回false
long awaitNanos(long nanosTimeout)进入等待,并返回剩余等待的时间剩余等待的时间,如果为正值则可以继续等待,为负值则说明等待已经超时

代码示例

有四名学生竞争同一个自习室的锁,他们分别是小王、小李、小美和小红。其中小王是一个大胃王,他在自习室学习一段时间后会感到饥饿,感到饥饿后他就会停止自习,当小王吃饱后才可以继续学习。而小李在学习时会发现工具缺失,无法继续学习,直到工具送到。
Java并发编程学习(3):Monitor、wait-notify与保护性暂停中我们使用了wait-notify去解决这一问题。
在使用条件变量时,我们可以为工具和食物分别设置条件变量,并在达到要求时分别唤醒对应的线程。

public static final ReentrantLock room = new ReentrantLock();
public static final Condition waitFood = room.newCondition();
public static final Condition waitTool = room.newCondition();
public static boolean food = false;
public static boolean tool = false;

public static void main(String[] args) throws InterruptedException {
    scheme4();
}

public static void scheme4() throws InterruptedException {
    Thread wang = new Thread(() -> {
        room.lock();
        try {
            log.info("学习一段时间");
            while (!food){
                try {
                    waitFood.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.info("吃饱了,继续学习");
        }finally {
            room.unlock();
        }
    }, "小王");
    Thread li = new Thread(() -> {
        room.lock();
        try {
            log.info("学习一段时间");
            while (!tool){
                try {
                    waitTool.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.info("工具到了,继续学习");
        }finally {
            room.unlock();
        }
    }, "小李");
    Runnable task = () -> {
        room.lock();
        try {
            log.info("学习");
        }finally {
            room.unlock();
        }
    };
    Thread mei = new Thread(task, "小美");
    Thread hong = new Thread(task, "小红");
    wang.start();
    TimeUnit.MILLISECONDS.sleep(100);
    li.start();
    mei.start();
    hong.start();
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        tool = true;
        log.info("工具送到了");
        room.lock();
        try {
            waitTool.signalAll();
        }finally {
            room.unlock();
        }
    }
    try {
        TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        food = true;
        log.info("食物送到了");
        room.lock();
        try {
            waitFood.signalAll();
        }finally {
            room.unlock();
        }
    }
}

使用ReentrantLock解决哲学家就餐问题

哲学家就餐问题的死锁原因:每个人都只抢到一只筷子
思路:如果哲学家等待一段时间后能够放弃手中的筷子,让其他人先就餐就好了,而ReentrantLock中存在trylock()方法可以在得不到说的情况下执行其它指令,借助此方法,我们可以完成放弃的过程。

筷子类

为了让筷子具备锁的特性,需要让筷子类继承于ReentrantLock

class ChopstickWithReentrantLock extends ReentrantLock{
    private String name;

    public ChopstickWithReentrantLock(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

}

哲学家类

在哲学家类中,原来使用synchronized的地方,现在需要使用ReentrantLock进行改写。

class PhilosopherWithReentrantLock extends Thread{
    private final ChopstickWithReentrantLock left;
    private final ChopstickWithReentrantLock right;

    public PhilosopherWithReentrantLock(String name, ChopstickWithReentrantLock left, ChopstickWithReentrantLock right) {
        super(name);
        this.left = left;
        this.right = right;
    }

    @Override
        while (true) {
            if (left.tryLock()){
                try {
                    if (right.tryLock()){
                        try {
                            eat();
                        }finally {
                            right.unlock();
                        }
                    }
                }finally {
                    left.unlock();
                }
            }
        }
    }

    private void eat(){
        log.info("开始吃饭...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("饭吃完了");
    }
}

测试类

public class PhilosopherEatingWithReentrantLock {
    public static void main(String[] args) {
        int N = 3;
        ChopstickWithReentrantLock[] chopsticks = new ChopstickWithReentrantLock[N];
        for (int i = 0; i < N; i++) {
            chopsticks[i] = new ChopstickWithReentrantLock("筷子-"+i);
        }
        PhilosopherWithReentrantLock[] philosophers = new PhilosopherWithReentrantLock[N];
        for (int i = 0; i < N; i++) {
            philosophers[i] = new PhilosopherWithReentrantLock("哲学家-"+i,chopsticks[i],chopsticks[(i+1)%N]);
        }
        for (int i = 0; i < N; i++) {
            philosophers[i].start();
        }
    }
}

在打印日志中,没有出现死锁现象。

设计模式:控制线程执行顺序

固定顺序

线程1打印数字t1,线程t2打印数字2,要求:先打印2,再打印1。

wait-notify实现

我们可以设置一个变量用来保存t2是否已经执行完成打印,如果没有,则线程t1进入等待状态,直到线程t2将它唤醒。

static final Object lock = new Object();
static boolean t2runned = false;

public static void main(String[] args) {
    scheme1();
}


public static void scheme1(){

    Runnable task1 = () -> {
        synchronized (lock){
            while (!t2runned){
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.info("1");
        }
    };

    Runnable task2 = () -> {
        synchronized (lock){
            log.info("2");
            t2runned = true;
            lock.notifyAll();
        }
    };

    new Thread(task1,"t1").start();
    new Thread(task2,"t2").start();
}

await-singal实现

static final ReentrantLock lock = new ReentrantLock();
static final Condition t2runCondtion = lock.newCondition();
static boolean t2runned = false;

public static void main(String[] args) {
    scheme2();
}



public static void scheme2(){
    Runnable task1 = () -> {
        lock.lock();
        try {
            while (!t2runned){
                try {
                    t2runCondtion.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.info("1");
        }finally {
            lock.unlock();
        }
    };

    Runnable task2 = () -> {
        lock.lock();
        try {
            log.info("2");
            t2runned = true;
            t2runCondtion.signalAll();
        }finally {
            lock.unlock();
        }
    };

    new Thread(task1,"t1").start();
    new Thread(task2,"t2").start();
}

park-unpark实现

此实现方法不需要使用锁对象,但是在执行unpark()时需要获得其它线程对象。

    public static void main(String[] args) {
        scheme3();
    }

    public static void scheme3(){
        Runnable task1 = () -> {
            LockSupport.park();
            log.info("1");
        };

        Thread t1 = new Thread(task1, "t1");

        Runnable task2 = () -> {
            log.info("2");
            LockSupport.unpark(t1);
        };

        Thread t2 = new Thread(task2, "t2");

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

交替输出

有三个线程,分别输出5次a,5次b,5次c,要求最终的输出为abcabcabcabcabc的次序。

wait-notify实现

这里设置了一个等待标记flag,并将打印方法抽象为void print(String str, int waitFlag, int nextFlag)只有当flagwaitFlag相等时才会执行打印操作,并将flag修改为nextFlag同时唤醒其它线程。
根据题意,打印字符strwaitFlagnextFlag对应关系如下:

strwaitFlagnextFlag
a12
b23
c31
static int flag = 1;
static int loopNum = 5;

public static void main(String[] args) {
    scheme1();
}

static void scheme1(){
    Runnable printa = () -> {
        for (int i = 0; i < loopNum; i++) {
            print("a",1,2);
        }
    };
    Runnable printb = () -> {
        for (int i = 0; i < loopNum; i++) {
            print("b",2,3);
        }
    };
    Runnable printc = () -> {
        for (int i = 0; i < loopNum; i++) {
            print("c",3,1);
        }
    };

    new Thread(printa,"printer-a").start();
    new Thread(printb,"printer-b").start();
    new Thread(printc,"printer-c").start();

}

static synchronized void print(String str, int waitFlag, int nextFlag){
    while (waitFlag != flag){
        try {
            CrossExecution.class.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    System.out.print(str);
    flag = nextFlag;
    CrossExecution.class.notifyAll();
}

await-singal实现

利用ReentrantLock的特性,我们可以创建三个条件变量分别给三个线程使用,然后让他们相互唤醒。
具体思路为:

  1. 打印前,每个线程先执行await()方法等待;
  2. 线程被唤醒后,打印内容,并使用single()唤醒下一个线程;
  3. 主线程作为发起者,首先唤醒打印字符a的线程。
static int flag = 1;
static int loopNum = 5;
static ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) throws InterruptedException {
    scheme2();
}

static void scheme2() throws InterruptedException {
    Condition aCond = lock.newCondition();
    Condition bCond = lock.newCondition();
    Condition cCond = lock.newCondition();


    Runnable printa = () -> {
        for (int i = 0; i < loopNum; i++) {
            print("a",aCond,bCond);
        }
    };
    Runnable printb = () -> {
        for (int i = 0; i < loopNum; i++) {
            print("b",bCond,cCond);
        }
    };
    Runnable printc = () -> {
        for (int i = 0; i < loopNum; i++) {
            print("c",cCond,aCond);
        }
    };

    new Thread(printa,"printer-a").start();
    new Thread(printb,"printer-b").start();
    new Thread(printc,"printer-c").start();

    // Thread.sleep(100);
    lock.lock();
    try {
        aCond.signalAll();
    }finally {
        lock.unlock();
    }
}

static void print(String str, Condition current, Condition next){
    lock.lock();
    try {
        current.await();
        System.out.print(str);
        next.signalAll();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        lock.unlock();
    }
}

注意:以上的写法可能会使得主线程比printer-a先抢到锁,此时printer-a还没有执行到current.await();而主线程却已经调用了aCond.signalAll();。如果之后printer-a抢到锁并执行了current.await();无人能够将它唤醒,这将导致程序卡死,没有输出。
解决方案:在主线程lock.lock();前先睡眠一会。

park-unpark实现

在park-unpark实现中,考虑到线程t1的实现需要拿到线程t2,线程t2的实现需要拿到线程t3,线程t3的实现需要拿到线程t1,因此,需将它们设置为全局变量以避免产生循环定义。

public static void main(String[] args) throws InterruptedException {
    scheme3();
}

static Thread t1;
static Thread t2;
static Thread t3;

static void scheme3(){
    Runnable printa = () -> {
        for (int i = 0; i < loopNum; i++) {
            print("a",t2);
        }
    };
    Runnable printb = () -> {
        for (int i = 0; i < loopNum; i++) {
            print("b",t3);
        }
    };
    Runnable printc = () -> {
        for (int i = 0; i < loopNum; i++) {
            print("c",t1);
        }
    };

    t1 = new Thread(printa);
    t2 = new Thread(printb);
    t3 = new Thread(printc);

    t1.start();
    t2.start();
    t3.start();

    LockSupport.unpark(t1);
}

static void print(String str, Thread next){
    LockSupport.park();
    System.out.print(str);
    LockSupport.unpark(next);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值