分享

05 Java的ReentrantLock与线程的顺序控制

 小仙女本仙人 2022-11-26 发布于北京

1 JAVA中多把锁的使用基本常识

1-1 多把锁的简单例子

package chapter4;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.test3")
class BigRoom {
    public void sleep() throws InterruptedException {
        synchronized (this) {
            log.warn("sleeping 2 小时");
            Thread.sleep(2000);
        }
    }
    public void study() throws InterruptedException {
        synchronized (this) {
            log.warn("study 1 小时");
            Thread.sleep(1000);
        }
    }
}

public class test4 {
    public static void main(String[] args) {
        BigRoom bigRoom = new BigRoom();
        new Thread(() -> {
            try {
                bigRoom.study();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"小南").start();
        new Thread(() -> {
            try {
                bigRoom.sleep();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"小女").start();
    }
}

多把锁的优势与劣势

优势:

  • 可以增强线程的并发度 ,如上面的例子,2个线程不需要再去竞争同一把锁。

劣势

  • 线程需要同时获得多把锁,就容易发生死锁

1-2 死锁(阻塞状态的锁)的基本知识点

1-2-1 何时发生?

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

示例:t1 线程获得A对象锁,接下来想获取B对象的锁t2线程获得B对象锁接下来想获取 A对象的锁。(拥有对方想要的锁的同时,还想要对方的锁,最后都因为无法获得对方的锁而放生阻塞

1-2-2 Java中如何检测死锁(二种方式)?

死锁代码示例:

package chapter4;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.test5")
public class test5 {
    public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                log.warn("lock A");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (B) {
                    log.warn("lock B");
                    log.warn("操作...");
                }
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            synchronized (B) {
                log.debug("lock B");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (A) {
                    log.warn("lock A");
                    log.warn("操作...");
                }
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}
  • 上面的代码中可以通过规定加锁的顺序来避免死锁问题,但指定加锁顺序可能会引发饥饿问题。

方式1:使用jps定位可能死锁的线程id,再用jstack定位死锁。

注意:如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查

step1:执行上述死锁程序在cmd终端使用jps命令:(Java Virtual Machine Process Status Tool,是原生java包自带的工具)

C:\Users\Administrator>jps
12368 test5
16112 Launcher
16928 RemoteMavenServer36
6276
13608 Jps
21144 KotlinCompileDaemon

step2: 定位到我们执行程序的进程id为12368,执行jstack命令

输出的一部分信息如下系统会提示存在java级别的deadlock并且2个线程都是blocked状态):

"t2" #12 prio=5 os_prio=0 tid=0x000000001979e000 nid=0x2e14 waiting for monitor entry [0x000000001a99f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at chapter4.test5.lambda$main$1(test5.java:33)
        - waiting to lock <0x00000000d611b818> (a java.lang.Object)
        - locked <0x00000000d611b828> (a java.lang.Object)
        at chapter4.test5$$Lambda$2/159413332.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

"t1" #11 prio=5 os_prio=0 tid=0x000000001979d800 nid=0x4b2c waiting for monitor entry [0x000000001a89f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at chapter4.test5.lambda$main$0(test5.java:18)
        - waiting to lock <0x00000000d611b828> (a java.lang.Object)
        - locked <0x00000000d611b818> (a java.lang.Object)
        at chapter4.test5$$Lambda$1/1349393271.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
        
  
Found one Java-level deadlock:
=============================
"t2":
  waiting to lock monitor 0x000000000346a2b8 (object 0x00000000d611b818, a java.lang.Object),
  which is held by "t1"
"t1":
  waiting to lock monitor 0x000000000346cd58 (object 0x00000000d611b828, a java.lang.Object),
  which is held by "t2"

Java stack information for the threads listed above:
===================================================
"t2":
        at chapter4.test5.lambda$main$1(test5.java:33)
        - waiting to lock <0x00000000d611b818> (a java.lang.Object)
        - locked <0x00000000d611b828> (a java.lang.Object)
        at chapter4.test5$$Lambda$2/159413332.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
"t1":
        at chapter4.test5.lambda$main$0(test5.java:18)
        - waiting to lock <0x00000000d611b828> (a java.lang.Object)
        - locked <0x00000000d611b818> (a java.lang.Object)
        at chapter4.test5$$Lambda$1/1349393271.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

方式2:使用jconsole工具(有检测死锁的选项)。


1-3 活锁以及饥饿

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

解决方式:有意识的改变2个线程的执行次序。

饥饿定义:部分教程定义一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束 。(可以简单理解为部分线程始终得不到CPU的调度,产生饥饿现象。)

1-4 哲学家就餐问题(死锁的案例)

描述:有五位哲学家,围坐在圆桌旁。他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。如果筷子被身边的人拿着,自己就得等待 。

代码实现
package chapter4;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

class Chopstick {
    String name;
    public Chopstick(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}

@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
    // 每个哲学家都有2个筷子,左手的筷子与右手的筷子
    Chopstick left;
    Chopstick right;
    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }
    private void eat(){
        log.warn("eating...");
        try{
            Thread.sleep(1);
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
    @Override
    public void run() {
        while (true) {
            synchronized (left) {         // 获得左手筷子
                synchronized (right) {    // 获得右手筷子
                    eat();
                }  // 放下右手筷子
            }      // 放下左手筷子
        }
    }
}

@Slf4j(topic = "c.LockProblem")
public class LockProblem {
    public static void main(String[] args) {
        // 定义5根筷子
        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");
        // 定义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();
    }
}

执行结果程序由于死锁无法继续执行

[赫拉克力特] WARN c.Philosopher - eating...
[苏格拉底] WARN c.Philosopher - eating...
[赫拉克力特] WARN c.Philosopher - eating...
[赫拉克力特] WARN c.Philosopher - eating...
[赫拉克力特] WARN c.Philosopher - eating...
[赫拉克力特] WARN c.Philosopher - eating...

使用jps结合jstack分析程序

Found one Java-level deadlock:
=============================
"阿基米德":
  waiting to lock monitor 0x000000001924db58 (object 0x00000000d611cd90, a chapter4.Chopstick),
  which is held by "苏格拉底"
"苏格拉底":
  waiting to lock monitor 0x00000000031a87e8 (object 0x00000000d611cdd0, a chapter4.Chopstick),
  which is held by "柏拉图"
"柏拉图":
  waiting to lock monitor 0x00000000031a9e98 (object 0x00000000d611ce10, a chapter4.Chopstick),
  which is held by "亚力士多??"
"亚力士多德":
  waiting to lock monitor 0x00000000031ac938 (object 0x00000000d611ce50, a chapter4.Chopstick),
  which is held by "赫拉克力特"
"赫拉克力特":
  waiting to lock monitor 0x000000001924dc08 (object 0x00000000d611ce90, a chapter4.Chopstick),
  which is held by "阿基米德"

Java stack information for the threads listed above:
===================================================
"阿基米德":
        at chapter4.Philosopher.run(LockProblem.java:41)
        - waiting to lock <0x00000000d611cd90> (a chapter4.Chopstick)
        - locked <0x00000000d611ce90> (a chapter4.Chopstick)
"苏格拉底":
        at chapter4.Philosopher.run(LockProblem.java:41)
        - waiting to lock <0x00000000d611cdd0> (a chapter4.Chopstick)
        - locked <0x00000000d611cd90> (a chapter4.Chopstick)
"柏拉图":
        at chapter4.Philosopher.run(LockProblem.java:41)
        - waiting to lock <0x00000000d611ce10> (a chapter4.Chopstick)
        - locked <0x00000000d611cdd0> (a chapter4.Chopstick)
"亚力士多德":
        at chapter4.Philosopher.run(LockProblem.java:41)
        - waiting to lock <0x00000000d611ce50> (a chapter4.Chopstick)
        - locked <0x00000000d611ce10> (a chapter4.Chopstick)
"赫拉克力特":
        at chapter4.Philosopher.run(LockProblem.java:41)
        - waiting to lock <0x00000000d611ce90> (a chapter4.Chopstick)
        - locked <0x00000000d611ce50> (a chapter4.Chopstick)

Found 1 deadlock.

2 ReentrantLock(重进入锁)的基本知识点

2-1 概述

ReentrantLock与普通的synchronized区别

不同点

  • 可中断
    • 等待锁的线程可以被中断
  • 可以设置超时时间
    • 规定时间获取不到锁就会放弃锁
  • 可以设置为公平锁
    • 主要用于防止线程饥饿的情况(先进先出)
  • 支持多个条件变量
    • 多个waitset提供给线程去进行等待,不同线程根据其条件不同进入到不同的waitset(详见wait/notify原理)。

共同点

  • 二者都支持可重入
    • 重入:一个线程对重一对象反复加锁。

使用模板

注意:相比较使用synchronized关键字去定义,自动关联monitor对象,这里直接显式的去定义加锁对象去取代之前的monitor.

ReentrantLock relock1 = new ReentrantLock();// 获取锁
relock1.lock();
    try {
    // 临界区
    } finally {
    // 释放锁
    relock1.unlock();
}

2-2 可重入性的演示

package chapter4;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j(topic = "c.test6")
public class test6 {
    private static ReentrantLock relock1 = new ReentrantLock();
    public static void main(String[] args) {
        new Thread(()->{
            relock1.lock();
            try{
                log.warn("进入线程t1");
                m1();
            }finally {
                relock1.unlock();
            }
        },"t1").start();
    }
    public static void m1(){
        relock1.lock();
        try{
            log.warn("第二次");
            m2();
        }finally {
            relock1.unlock();
        }
    }
    public static void m2(){
        relock1.lock();
        try{
            log.warn("第三次");
        }finally {
            relock1.unlock();
        }
    }
}

执行结果

[t1] WARN c.test6 - 进入线程t1
[t1] WARN c.test6 - 第二次
[t1] WARN c.test6 - 第三次

2-3 可打断

package chapter4;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j(topic = "c.test7")
public class test7 {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            log.debug("启动...");
            try {
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.warn("线程t1等锁的过程中被打断");
                return;
            }
            /*测试lock()不会被打断
            try{
                lock.lock();
                log.warn("线程t1获得了锁");
            }finally {
                lock.unlock();
            }*/
        }, "t1");
        lock.lock();
        log.warn("主线程获得了锁");
        t1.start();
        try {
            Thread.sleep(1000);
            t1.interrupt();
            log.debug("主线程调用线程t1的interrupt执行打断");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();           //主线程释放了锁
        }
    }
}

运行结果(主线程获得了锁,然后打断了等待这把锁的线程t1):

注意:可打断锁是lockInterruptibly(),将lockInterruptibly()换成lock(),即便进行中断也不会被打断。

使用lockInterruptibly()

[main] WARN c.test7 - 主线程获得了锁
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
	at chapter4.test7.lambda$main$0(test7.java:12)
	at java.lang.Thread.run(Thread.java:748)
[t1] WARN c.test7 - 线程t1等锁的过程中被打断

使用lock()

[main] WARN c.test7 - 主线程获得了锁
[t1] WARN c.test7 - 线程t1获得了锁

2-4 锁超时tryLock()

实例
package chapter4;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j(topic = "c.test8")
public class test8 {
    public static void main(String[] args) {
        func1();
    }
    public static void func1(){
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            log.warn("t1启动...");
            if (!lock.tryLock()) {
                log.warn("线程t1获取锁失败,返回");
                return;
            }
            //测试可以等待的锁
//            try {
//                if (!lock.tryLock(2, TimeUnit.SECONDS)) {
//                    log.warn("线程t1获取锁失败");
//                    return;
//                }
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }

            try {
                log.warn("线程t1获得了锁");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        log.warn("主线程获得锁");
        t1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
            log.warn("主线程释放锁");
        }
    }
}

获取不到立刻返回

[main] WARN c.test8 - 主线程获得锁
[t1] WARN c.test8 - t1启动...
[t1] WARN c.test8 - 线程t1获取锁失败,返回
[main] WARN c.test8 - 主线程释放锁

获取不到等待2s

[main] WARN c.test8 - 主线程获得锁
[t1] WARN c.test8 - t1启动...
[main] WARN c.test8 - 主线程释放锁
[t1] WARN c.test8 - 线程t1获得了锁

2-5 使用tryLock()解决哲学家问题

package chapter5;
import java.util.concurrent.locks.ReentrantLock;
import lombok.extern.slf4j.Slf4j;

class Chopstick extends ReentrantLock {
    // 通过继承锁对象能够对业务类进行加锁
    String name;
    public Chopstick(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}

@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
    // 每个哲学家都有2个筷子,左手的筷子与右手的筷子
    Chopstick left;
    Chopstick right;
    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }
    private void eat(){
        log.warn("eating...");
        try{
            Thread.sleep(1);
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
    @Override
    public void run() {
        while (true) {
           if(left.tryLock()){
               try{
                   if(right.tryLock()){
                       try {
                           eat();        // 获得2只筷子
                       }finally {
                           right.unlock();
                       }
                   }
               }finally {
                   left.unlock();
               }
           }
        }
    }
}


public class test9 {
    public static void main(String[] args) {
        // 定义5根筷子
        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");
        // 定义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();
    }
}

总结:

  • 让资源类继承ReentrantLock,然后使用trylock来判断是否可以获得资源,从而避免占有一定资源而又申请其他资源的状况出现,从而避免死锁。

2-5 公平锁

公平锁定开启与关闭

ReentrantLock lock = new ReentrantLock(false);      // 默认是false,即锁是不公平的
ReentrantLock lock = new ReentrantLock(true);       // 开启公平锁

实际应用:公平锁一般没有必要,会降低并发度

2-6 条件变量

复习: synchronized内部代码执行时当线程执行条件不满足,则主动调用wait()方法进入到waitset进行等待

基本使用方法

Condition condition1 = reentrantLock.newCondition();    
Condition condition2 = reentrantLock.newCondition();
reentrantLock.lock();
// 通过调用不同的实例的await的方法,让不满足条件的线程进入不同的waitset
condition1.await();
condition2.await();
// 通过调用signal/signalAll唤醒在waitset等待的线程
condition1.signal();
condition1.signalAll();

使用注意点

  • step1:await执行前必须获得锁,类似于只有在synchronized方法内部才能调用wait()方法
  • step2:await 执行后,会释放锁,进入 conditionObject 等待
  • step3:await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁 (signal/interrupt/设定时间等待)
  • step4:竞争 lock 锁成功后,从 await 后继续执行

使用实例

package chapter5;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j(topic = "c.test10")
public class test10 {
    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) {
        // 线程1: 模拟人必须有烟才能干活
        new Thread(() -> {
            try {
                lock.lock();
                log.warn("获得锁后,有香烟吗?");
                while (!hasCigrette) {
                    try {
                        log.warn("没有烟,干活条件不满足");
                        waitCigaretteQueue.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.warn("等到了它的烟");
            } finally {
                lock.unlock();
            }
        }).start();
        // 线程2: 模拟人必须有早餐才能干活
        new Thread(() -> {
            try {
                lock.lock();
                log.warn("获得锁后,有早餐吗?");
                while (!hasBreakfast) {
                    try {
                        log.warn("没有早餐,干活条件不满足");
                        waitbreakfastQueue.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.warn("等到了它的早餐");
            } finally {
                lock.unlock();
            }
        }).start();

        // 让早餐与外卖晚点送到
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        sendBreakfast();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        sendCigarette();
    }

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

    private static void sendBreakfast() {
        lock.lock();
        try {
            log.warn("送早餐来了");
            hasBreakfast = true;
            waitbreakfastQueue.signal();
        } finally {
            lock.unlock();
        }
    }
}

执行结果

[Thread-0] WARN c.test10 - 获得锁后,有香烟吗?
[Thread-0] WARN c.test10 - 没有烟,干活条件不满足
[Thread-1] WARN c.test10 - 获得锁后,有早餐吗?
[Thread-1] WARN c.test10 - 没有早餐,干活条件不满足
[main] WARN c.test10 - 送早餐来了
[Thread-1] WARN c.test10 - 等到了它的早餐
[main] WARN c.test10 - 送烟来了
[Thread-0] WARN c.test10 - 等到了它的烟

总结:上面的代码通过条件变量让需要烟的线程与需要早餐的线程进入到不同的waitset进行等待

3 编程题:多线程的顺序执行实现

3-1 2个线程按照次序执行

要求:线程t2先打印2然后线程t1再打印1。

方式1:synchoronized中wait/notify实现
package chapter5;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.test11")
public class Test11 {
    static boolean t2Runed = false;
    public static void main(String[] args) {
        Object object = new Object();
        new Thread(()->{
            synchronized (object){
                while(!t2Runed){
                    try {
                        object.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.warn("1");
            }
        },"t1").start();

        new Thread(()->{
            synchronized (object){
                log.warn("2");
                t2Runed = true;
                object.notifyAll();
            }
        },"t2").start();
    }
}
方式2:ReentrantLock中的await与signal实现
package chapter5;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j(topic = "c.test12")
public class Test12 {
    static boolean t2Runed = false;
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        Condition tmp = reentrantLock.newCondition();
        new Thread(()->{
                reentrantLock.lock();
                try {
                    while(!t2Runed){
                        try {
                            tmp.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }finally {
                    reentrantLock.unlock();
                }
                log.warn("1");
        },"t1").start();
        new Thread(()->{
                reentrantLock.lock();
                try {
                    log.warn("2");
                    t2Runed = true;
                    tmp.signalAll();
                } finally {
                    reentrantLock.unlock();
                }
        },"t2").start();
    }
}
方式3:park与unpark实现
package chapter5;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
@Slf4j(topic = "c.test13")
public class Test13 {
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            LockSupport.park();
            log.warn("1");

        },"t1");
        t1.start();
        new Thread(()->{
            log.warn("2");
            LockSupport.unpark(t1);
        },"t2").start();
    }
}

执行结果

[t2] WARN c.test13 - 2
[t1] WARN c.test13 - 1

总结:t1线程必须等待t2线程unpark。

3-2 多个线程交替输出

要求:线程 t1 输出 a 5 次,线程 t2 输出 b 5 次,线程t 3 输出 c 5 次。现在要求按照顺序输出 abcabcabcabcabc 怎么实现 ?

方式1:synchoronized中wait/notify实现
package chapter5;

public class Test14 {
    public static void main(String[] args) {
        SyncWaitNotify syncWaitNotify = new SyncWaitNotify(1, 5);
        new Thread(() -> {
            syncWaitNotify.print(1, 2, "a");
        }).start();
        new Thread(() -> {
            syncWaitNotify.print(2, 3, "b");
        }).start();
        new Thread(() -> {
            syncWaitNotify.print(3, 1, "c");
        }).start();
    }
}

// 下面这个类被多个线程所使用,从而同步线程之间的打印顺序
// 基本思想:保证多个线程每次只有一个线程利用syncwaitnotify打印
class SyncWaitNotify {
    private int flag;
    private int loopNumber;
    // constructor function
    public SyncWaitNotify(int flag, int loopNumber) {
        this.flag = flag;                      // 当前打印的线程记号
        this.loopNumber = loopNumber;
    }
    // print function
    /*
        waitFlag:当前打印的线程标记
        nextFlag:下一个打印的线程标记
        str:线程打印的字符
     */
    public void print(int waitFlag, int nextFlag, String str) {

        for (int i = 0; i < loopNumber; i++) {
            synchronized (this) {
                while (this.flag != waitFlag) {   
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print(str);
                flag = nextFlag;
                this.notifyAll();
            }
        }
    }
}

基本思想:

  • 对线程进行编号,同时设置一个类内部的变量作为线程之间共享的变量。通过这个共享变量判断当前线程是否能够打印。
  • 打印完成后,修改共享变量让下一个线程打印。

执行结果

[Thread-0] WARN c.test14 - a
[Thread-1] WARN c.test14 - b
[Thread-2] WARN c.test14 - c
[Thread-0] WARN c.test14 - a
[Thread-1] WARN c.test14 - b
[Thread-2] WARN c.test14 - c
[Thread-0] WARN c.test14 - a
[Thread-1] WARN c.test14 - b
[Thread-2] WARN c.test14 - c
[Thread-0] WARN c.test14 - a
[Thread-1] WARN c.test14 - b
[Thread-2] WARN c.test14 - c
[Thread-0] WARN c.test14 - a
[Thread-1] WARN c.test14 - b
[Thread-2] WARN c.test14 - c
方式2:ReentrantLock中的await与signal实现
package chapter5;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
class Test15{
    public static void main(String[] args) {
        AwaitSignal as = new AwaitSignal(5);
        Condition aWaitSet = as.newCondition();
        Condition bWaitSet = as.newCondition();
        Condition cWaitSet = as.newCondition();
        new Thread(() -> {
            as.print("a", aWaitSet, bWaitSet);
        }).start();
        new Thread(() -> {
            as.print("b", bWaitSet, cWaitSet);
        }).start();
        new Thread(() -> {
            as.print("c", cWaitSet, aWaitSet);
        }).start();
        try {
            Thread.sleep(1000);    // 等待三个进程就绪
            as.start(aWaitSet);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

// 这里对锁对象进行了继承
@Slf4j(topic = "c.awaitsignal")
class AwaitSignal extends ReentrantLock {
    public void start(Condition first) {
        this.lock();
        try {
            log.warn("start");
            first.signal();
        } finally {
            this.unlock();
        }
    }
    /*
        将分别打印a,b,c的三个线程分别放入到不同的waitset中等待。
        current:当前打印字母 对应的 waitset
        next:   下一个打印的字母 对应的 waitset
        这里的实现逻辑:
        step1: 让所有的线程都进入waitset等待。
        step2: 使用条件变量唤醒指定的线程运行。
        step3: 打印输出后,指定更新条件变量
        循环step2,step3
     */
    public void print(String str, Condition current, Condition next) {
        for (int i = 0; i < loopNumber; i++) {
            this.lock();
            try {
                current.await();
                log.warn(str);
                next.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                this.unlock();
            }
        }
    }
    // 循环次数
    private int loopNumber;
    public AwaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }
}

执行结果(这里面每个waitset都只有一个线程,所以不需要考虑单个waitset的虚假唤醒)

[main] WARN c.awaitsignal - start
[Thread-0] WARN c.awaitsignal - a
[Thread-1] WARN c.awaitsignal - b
[Thread-2] WARN c.awaitsignal - c
[Thread-0] WARN c.awaitsignal - a
[Thread-1] WARN c.awaitsignal - b
[Thread-2] WARN c.awaitsignal - c
[Thread-0] WARN c.awaitsignal - a
[Thread-1] WARN c.awaitsignal - b
[Thread-2] WARN c.awaitsignal - c
[Thread-0] WARN c.awaitsignal - a
[Thread-1] WARN c.awaitsignal - b
[Thread-2] WARN c.awaitsignal - c
[Thread-0] WARN c.awaitsignal - a
[Thread-1] WARN c.awaitsignal - b
[Thread-2] WARN c.awaitsignal - c
方式3:park与unpark实现
package chapter5;
import java.util.concurrent.locks.LockSupport;
public class Test16 {
    /*
        基本思想:
        step1: 让所有线程都park
        step2: 通过id去unpark一个线程,打印出字符,
        step3: 设置下一个unpark线程的id
        重复step2和step3
     */
    public static void main(String[] args) {
        SyncPark syncPark = new SyncPark(5);
        Thread t1 = new Thread(() -> {
            syncPark.print("a");
        });
        Thread t2 = new Thread(() -> {
            syncPark.print("b");
        });
        Thread t3 = new Thread(() -> {
            syncPark.print("c\n");
        });
        syncPark.setThreads(t1, t2, t3);
        syncPark.start();
    }
}

class SyncPark {
    private int loopNumber;
    private Thread[] threads;
    public SyncPark(int loopNumber) {
        this.loopNumber = loopNumber;
    }
    public void setThreads(Thread... threads) {
        this.threads = threads;
    }
    public void print(String str) {
        for (int i = 0; i < loopNumber; i++) {
            LockSupport.park();
            System.out.print(str);
            LockSupport.unpark(nextThread());
        }
    }
    private Thread nextThread() {
        Thread current = Thread.currentThread();
        int index = 0;
        // 获取当前的线程id
        for (int i = 0; i < threads.length; i++) {
            if(threads[i] == current) {
                index = i;
                break;
            }
        }
        // 将线程id的自增循环
        if(index < threads.length - 1) {
            return threads[index+1];
        } else {
            return threads[0];
        }
    }
    public void start() {
        for (Thread thread : threads) {
            thread.start();
        }
        LockSupport.unpark(threads[0]);
    }
}

执行结果

[Thread-0] WARN c.syncpark - a
[Thread-1] WARN c.syncpark - b
[Thread-2] WARN c.syncpark - c
[Thread-0] WARN c.syncpark - a
[Thread-1] WARN c.syncpark - b
[Thread-2] WARN c.syncpark - c
[Thread-0] WARN c.syncpark - a
[Thread-1] WARN c.syncpark - b
[Thread-2] WARN c.syncpark - c
[Thread-0] WARN c.syncpark - a
[Thread-1] WARN c.syncpark - b
[Thread-2] WARN c.syncpark - c
[Thread-0] WARN c.syncpark - a
[Thread-1] WARN c.syncpark - b
[Thread-2] WARN c.syncpark - c

基本思想

step1: 让所有线程都park
step2: 打印出字符,通过this获取当前id,并自增获取下一个线程对象,然后去unpark一个线程。
step3: 设置下一个unpark线程的id。
重复step2和step3  
step2,step3循环前需要手动unpark一个线程

4 知识点总结(自我对照复习)

基本常识

  • 分析多线程访问共享资源时,哪些代码片段属于临界区

  • 使用 synchronized 互斥解决临界区的线程安全问题

  • 掌握 synchronized 锁对象语法

  • 掌握 synchronized 加载成员方法和静态方法语法(一个锁的是class,一个锁的是this

  • 掌握 wait/notify 同步方法

  • 使用 lock 互斥解决临界区的线程安全问题

  • 掌握reentrant lock 的使用细节:可打断、锁超时、公平锁、条件变量

  • 学会分析变量的线程安全性、掌握常见线程安全类的使用

  • 了解线程活跃性问题:死锁、活锁、饥饿(死锁和饥饿可以利用reentrant lock中的超时机制解决)

  • 互斥:使用 synchronized 或 Lock 达到共享资源互斥效果

  • 同步:使用 wait/notify 或 lock 的条件变量来达到线程间通信效果

原理方面xianchneg

  • monitor、synchronized 、wait/notify 原理

  • synchronized 进阶原理

  • park & unpark 原理

模式方面

  • 同步模式之保护性暂停(一对一)
  • 异步模式之生产者消费者
  • 同步模式之顺序控制

synchroized实现了JVM层面的lock,而reentrant lock实现了Java级别的锁。

参考资料

并发编程课程


20210308

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多