共享模型之管程

共享模型之管程

共享带来的问题

两个线程对一个初始值是 0 的静态变量,一个做自增操作,一个做自减操作,结果还是 0 吗?

    static int num = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                num++;
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                num--;
            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("num: {}", num);
    }

我们的期望值是 0, 但结果:

21:43:16.824 [main] DEBUG com.byteframework.learn.sync.Test1 - num: -533

这就是线程切换带来的安全问题。

临界区 Critical Section

一段代码块内,如果存在对共享资源的多线程读写操作,称这段代码块为临界区

临界区代码举例:

    static int counter = 0;
    static void increment()
    // 临界区
    {
        counter++;
    }

    static void decrement()
    // 临界区
    {
        counter--;
    }

竞态条件 Race Condition

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

synchronized 解决方案

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

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

本次使用 synchronized 来解决上述问题,俗称对象锁,它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程想再获取这个对象锁时就会被阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。

注意

虽然 java 中互斥和同步都可以采用 sysnchronized 关键字来完成,但他们还是有区别的:

  • 互斥是保证临界区的竞态条件发生时,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同,需要一个线程等待其他线程运行到某个点

sysnchronized 语法:

synchronized (对象) {
    // 临界区
}

使用 synchronized 解决上述的线程安全问题,代码:

    static int num = 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) {
                    num++;
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (room) {
                    num--;
                }
            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("num: {}", num);
    }

反复执行均输出:

22:10:09.411 [main] DEBUG com.byteframework.learn.sync.Test2 - num: 0

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

使用面向对象改进锁对象

代码:

/**
 * 锁对象面向对象改进
 */
@Slf4j
public class Test3 {
    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                room.increment();
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                room.decrement();
            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("num: {}", 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;
        }
    }
}

反复执行均输出:

21:12:10.888 [main] DEBUG com.byteframework.learn.sync.Test3 - num: 0

方法上的 synchronized

  • 加在成员方法上

    public class Test {
        public synchronized void test(){
            
        }
    }
    // 等价于
    public class Test {
        public void test(){
            synchronized (this){
                
            }
        }
    }
    
  • 加在静态方法上

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

总结:
synchronized 只能锁对象,加在成员方法上表示锁的是this对象或对象的实例,加在静态方法上表示锁的是类对象。

变量的线程安全分析

  1. 成员变量和静态变量是否线程安全?
    • 如果它们没有共享,则线程安全
    • 如果他们被共享了,根据它们的状态是否能够改变,又分两种情况
      • 如果只有读操作,则线程安全
      • 如果有写操作,则这段代码是临界区,需要考虑线程安全
  2. 局部变量是否线程安全?
    • 局部变量是线程安全的
    • 但局部变量引用的对象则未必
      • 如果该对象没有逃离方法的作用范围,它是线程安全的
      • 如果该对象逃离方法的作用范围,需要考虑线程安全

举例1:(局部变量)

public static void test1(){
    // 局部变量
    int i = 10;
    i++;
}

问:此静态方法是否存在线程安全问题?

答案:不存在

分析:i++ 虽然不是原子操作,但 i 变量不会被多个线程共享,所有不存在线程安全问题。

举例2:(成员变量)

public class TestThreadSafe {

    static final int THREAD_NUM = 2;
    static final int LOOP_NUM = 200;

    public static void main(String[] args) {
        ThreadUnsafe unsafe = new ThreadUnsafe();
        for (int i = 0; i < THREAD_NUM; i++) {
            new Thread(() -> {
                unsafe.method1(LOOP_NUM);
            }, "thread_" + i).start();
        }
    }
}

class ThreadUnsafe {
    // 成员变量
    ArrayList<String> list = new ArrayList<>();

    public void method1(int loopNum) {
        for (int i = 0; i < loopNum; i++) {
            method2();
            method3();
        }
    }

    private void method2() {
        list.add("1");
    }

    private void method3() {
        list.remove(0);
    }
}

问:是否存在线程安全问题?

答案:存在

分析:多个线程操作的是同一个成员变量 list

运行输出:

Exception in thread "thread_0" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
	at java.util.ArrayList.rangeCheck(ArrayList.java:657)
	at java.util.ArrayList.remove(ArrayList.java:496)
	at com.byteframework.learn.sync.ThreadUnsafe.method3(TestThreadSafe.java:40)
	at com.byteframework.learn.sync.ThreadUnsafe.method1(TestThreadSafe.java:31)
	at com.byteframework.learn.sync.TestThreadSafe.lambda$main$0(TestThreadSafe.java:17)
	at java.lang.Thread.run(Thread.java:748)

对代码进行改进:

public class TestThreadSafe {

    static final int THREAD_NUM = 5;
    static final int LOOP_NUM = 200;

    public static void main(String[] args) {
        ThreadSafe safe = new ThreadSafe();
        for (int i = 0; i < THREAD_NUM; i++) {
            new Thread(() -> {
                safe.method1(LOOP_NUM);
            }, "thread_" + i).start();
        }
    }
}

class ThreadSafe {
    public void method1(int loopNum) {
        // 将成员变量修改为局部变量
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNum; i++) {
            method2(list);
            method3(list);
        }
    }

    private void method2(ArrayList<String> list) {
        list.add("1");
    }

    private void method3(ArrayList<String> list) {
        list.remove(0);
    }
}

反复执行再无错误输出。没有共享就没有线程安全问题

模拟售票例子

package com.byteframework.learn.sync;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;

/**
 * 售票实例
 */
@Slf4j
public class ExerciseSell {
    // 总票数20000张
    final static int TICKET_COUNT = 20000;

    public static void main(String[] args) {
        TicketWindow ticketWindow = new TicketWindow(TICKET_COUNT);

        // 用来存储线程对象
        List<Thread> threads = new ArrayList<>();

        // 用来存储卖出去多少张票
        List<Integer> amountList = new Vector<>();

        // 模拟 200个买票用户的线程
        for (int i = 0; i < 2000; i++) {
            Thread thread = new Thread(() -> {
                int count = ticketWindow.sell(randomAmount());
                amountList.add(count);
            }, "t1");
            threads.add(thread);
            thread.start();
        }

        // 等待所有线程结束
        threads.forEach(thread -> {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 统计卖出的票数和剩余的票数
        int sellCount = amountList.stream().mapToInt(i -> i).sum();
        log.debug("总票数:{}", TICKET_COUNT);
        log.debug("余票数:{}", ticketWindow.getCount());
        log.debug("卖出的票数:{}", sellCount);
        log.debug("余票数 + 卖出去的票数 = {}", ticketWindow.getCount() + sellCount);
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机数(1-5)
    public static int randomAmount() {
        return random.nextInt(5) + 1;
    }
}

/**
 * 售票窗口
 */
class TicketWindow {
    private int count;

    public TicketWindow(int count) {
        this.count = count;
    }

    public int getCount() {
        return count;
    }

    // 售票
    public int sell(int amount) {
        if (this.count >= amount) {
            this.count -= amount;
            return amount;
        } else {
            return 0;
        }
    }
}

多次执行后发现卖出去的票数加余票数不等于总票数:

22:14:13.222 [main] DEBUG com.byteframework.learn.sync.ExerciseSell - 总票数:20000
22:14:13.228 [main] DEBUG com.byteframework.learn.sync.ExerciseSell - 余票数:14074
22:14:13.228 [main] DEBUG com.byteframework.learn.sync.ExerciseSell - 卖出的票数:5930
22:14:13.228 [main] DEBUG com.byteframework.learn.sync.ExerciseSell - 余票数 + 卖出去的票数 = 20004

分析:售票方法中存在临界区

解决:给临界区加对象锁 synchronized, 锁实例对象。

调整后的代码:

package com.byteframework.learn.sync;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;

/**
 * 售票实例
 */
@Slf4j
public class ExerciseSell {
    final static int TICKET_COUNT = 20000;

    public static void main(String[] args) {
        // 总票数2000张
        TicketWindow ticketWindow = new TicketWindow(TICKET_COUNT);

        // 用来存储线程对象
        List<Thread> threads = new ArrayList<>();

        // 用来存储卖出去多少张票
        List<Integer> amountList = new Vector<>();

        // 模拟 200个买票用户的线程
        for (int i = 0; i < 2000; i++) {
            Thread thread = new Thread(() -> {
                int count = ticketWindow.sell(randomAmount());
                amountList.add(count);
            }, "t1");
            threads.add(thread);
            thread.start();
        }

        // 等待所有线程结束
        threads.forEach(thread -> {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 统计卖出的票数和剩余的票数
        int sellCount = amountList.stream().mapToInt(i -> i).sum();
        log.debug("总票数:{}", TICKET_COUNT);
        log.debug("余票数:{}", ticketWindow.getCount());
        log.debug("卖出的票数:{}", sellCount);
        log.debug("余票数 + 卖出去的票数 = {}", ticketWindow.getCount() + sellCount);
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机数(1-5)
    public static int randomAmount() {
        return random.nextInt(5) + 1;
    }
}

/**
 * 售票窗口
 */
class TicketWindow {
    private int count;

    public TicketWindow(int count) {
        this.count = count;
    }

    public int getCount() {
        return count;
    }

    // 售票 (临界区代码加对象锁)
    public synchronized int sell(int amount) {
        // 临界区
        if (this.count >= amount) {
            this.count -= amount;
            return amount;
        } else {
            return 0;
        }
    }
}

反复执行后发现卖出的票数加余票数等于总票数:

22:21:53.545 [main] DEBUG com.byteframework.learn.sync.ExerciseSell - 总票数:20000
22:21:53.549 [main] DEBUG com.byteframework.learn.sync.ExerciseSell - 余票数:14017
22:21:53.549 [main] DEBUG com.byteframework.learn.sync.ExerciseSell - 卖出的票数:5983
22:21:53.549 [main] DEBUG com.byteframework.learn.sync.ExerciseSell - 余票数 + 卖出去的票数 = 20000

模拟转账例子

package com.byteframework.learn.sync;

import lombok.extern.slf4j.Slf4j;

import java.util.Random;

/**
 * 模拟转账例子
 */
@Slf4j
public class ExerciseTransfer {

    // 账户A、B的金额
    final static int A_MONEY = 10000;
    final static int B_MONEY = 10000;

    public static void main(String[] args) throws InterruptedException {
        // 账户A、B
        Account a = new Account(A_MONEY);
        Account b = new Account(B_MONEY);

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 2000; i++) {
                a.transfer(b, randomAmount());
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 2000; i++) {
                b.transfer(a, randomAmount());
            }
        }, "t2");

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

        t1.join();
        t2.join();

        // 查看多次转账后的总金额
        log.debug("转账前, 两个账户的金额总和:{}", A_MONEY + B_MONEY);
        log.debug("转账后, 两个账户的金额总和:{}", a.getMoney() + b.getMoney());
    }


    // Random 为线程安全
    static Random random = new Random();

    // 随机数(1-5)
    public static int randomAmount() {
        return random.nextInt(5) + 1;
    }
}

/**
 * 账户
 */
class Account {
    private int money;

    public Account(int money) {
        this.money = money;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    /**
     * 转账
     *
     * @param target 目标账户
     * @param amount 转账金额
     */
    public void transfer(Account target, int amount) {
        if (this.money >= amount) {
            this.setMoney(this.getMoney() - amount);
            target.setMoney(target.getMoney() + amount);
        }
    }
}

多次执行后总金额不一致:

22:45:01.328 [main] DEBUG com.byteframework.learn.sync.ExerciseTransfer - 转账前, 两个账户的金额总和:20000
22:45:01.332 [main] DEBUG com.byteframework.learn.sync.ExerciseTransfer - 转账后, 两个账户的金额总和:20136

分析:转账方法中存在临界区

解决:给临界区加对象锁 synchronized, 锁类对象。

调整后的代码:

package com.byteframework.learn.sync;

import lombok.extern.slf4j.Slf4j;

import java.util.Random;

/**
 * 模拟转账例子
 */
@Slf4j
public class ExerciseTransfer {

    // 账户A、B的金额
    final static int A_MONEY = 10000;
    final static int B_MONEY = 10000;

    public static void main(String[] args) throws InterruptedException {
        // 账户A、B
        Account a = new Account(A_MONEY);
        Account b = new Account(B_MONEY);

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 2000; i++) {
                a.transfer(b, randomAmount());
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 2000; i++) {
                b.transfer(a, randomAmount());
            }
        }, "t2");

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

        t1.join();
        t2.join();

        // 查看多次转账后的总金额
        log.debug("转账前, 两个账户的金额总和:{}", A_MONEY + B_MONEY);
        log.debug("转账后, 两个账户的金额总和:{}", a.getMoney() + b.getMoney());
    }


    // Random 为线程安全
    static Random random = new Random();

    // 随机数(1-5)
    public static int randomAmount() {
        return random.nextInt(5) + 1;
    }
}

/**
 * 账户
 */
class Account {
    private int money;

    public Account(int money) {
        this.money = money;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    /**
     * 转账
     *
     * @param target 目标账户
     * @param amount 转账金额
     */
    public void transfer(Account target, int amount) {
        // 锁实例对象,总金额还是不一致。
        // synchronized(this){
        // 锁类对象
        synchronized (Account.class) {
            if (this.money >= amount) {
                this.setMoney(this.getMoney() - amount);
                target.setMoney(target.getMoney() + amount);
            }
        }
    }
}

反复执行后发现总金额一致:

22:50:58.301 [main] DEBUG com.byteframework.learn.sync.ExerciseTransfer - 转账前, 两个账户的金额总和:20000
22:50:58.307 [main] DEBUG com.byteframework.learn.sync.ExerciseTransfer - 转账后, 两个账户的金额总和:20000

wait notify

  • obj.wait() 让进入 object 监视器的线程到 waitSet 中等待
  • obj.notify() 在 objcet 上正在 waitSet 中等待的线程中挑一个唤醒
  • obj.notifyAll() 让 objcet 上正在 waitSet 中等待的线程全部唤醒

它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法

举例,不获得锁直接调用对象的 wait方法:

    static final Object lock = new Object();

    public static void main(String[] args) {
        try {
            lock.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

出现异常:

Exception in thread "main" java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at com.byteframework.learn.sync.TestWait.main(TestWait.java:15)

所以,必须要先获取到对象的锁后,才能调用对象的 wait、notify、notifyAll 方法。 代码调整为:

    static final Object lock = new Object();

    public static void main(String[] args) {
        synchronized(lock) {
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

测试 wait notify notifyAll, 例子:

/**
 * 测试 wait notify notifyAll
 */
@Slf4j
public class TestWaitNotify {

    static final Object lock = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lock) {
                log.debug("线程t1获取到锁后进入waitSet...");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("线程t1开始执行后续代码...");
            }
        }, "t1").start();

        new Thread(() -> {
            synchronized (lock) {
                log.debug("线程t2获取到锁后进入waitSet...");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("线程t2开始执行后续代码...");
            }
        }, "t2").start();

        synchronized (lock) {
            log.debug("主线程获取到锁后调用totify, 挑一个唤醒...");
            lock.notify();

            // log.debug("主线程获取到锁后调用totifyAll, 全部唤醒...");
            // lock.notifyAll();
        }
    }
}

执行notify时,输出:

22:31:10.935 [t1] DEBUG com.byteframework.learn.sync.TestWaitNotify - 线程t1获取到锁后进入waitSet...
22:31:10.938 [t2] DEBUG com.byteframework.learn.sync.TestWaitNotify - 线程t2获取到锁后进入waitSet...
22:31:10.938 [main] DEBUG com.byteframework.learn.sync.TestWaitNotify - 主线程获取到锁后调用totify, 挑一个唤醒...
22:31:10.938 [t1] DEBUG com.byteframework.learn.sync.TestWaitNotify - 线程t1开始执行后续代码...

执行 notifyAll 时, 输出:

22:33:16.187 [t1] DEBUG com.byteframework.learn.sync.TestWaitNotify - 线程t1获取到锁后进入waitSet...
22:33:16.192 [t2] DEBUG com.byteframework.learn.sync.TestWaitNotify - 线程t2获取到锁后进入waitSet...
22:33:16.192 [main] DEBUG com.byteframework.learn.sync.TestWaitNotify - 主线程获取到锁后调用totifyAll, 全部唤醒...
22:33:16.192 [t2] DEBUG com.byteframework.learn.sync.TestWaitNotify - 线程t2开始执行后续代码...
22:33:16.192 [t1] DEBUG com.byteframework.learn.sync.TestWaitNotify - 线程t1开始执行后续代码...

sleep 和 wait 的区别

不同点:

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

相同点:

  • 都会让出 CPU 的时间片
  • 他们的状态都是 TIMED_WAITING

举例:

    static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized(lock) {
                log.debug("线程t1获得锁...");
                try {
                    //log.debug("线程t1调用wait...");
                    //lock.wait(10000);

                    TimeUnit.SECONDS.sleep(10);
                    log.debug("sleep 10 秒后...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1").start();

        TimeUnit.SECONDS.sleep(1);
        synchronized(lock){
            log.debug("主线程获得锁...");
        }
    }

调用sleep 后的输出:

21:38:41.168 [t1] DEBUG com.byteframework.learn.sync.TestSleepWait - 线程t1获得锁...
21:38:51.185 [t1] DEBUG com.byteframework.learn.sync.TestSleepWait - sleep 10 秒后...
21:38:51.185 [main] DEBUG com.byteframework.learn.sync.TestSleepWait - 主线程获得锁...

可以看出 sleep 后线程没有释放对象锁, sleep 的时间到后主线程才获得了对象锁.

调用 wait 后的输出:

21:46:56.362 [t1] DEBUG com.byteframework.learn.sync.TestSleepWait - 线程t1获得锁...
21:46:56.367 [t1] DEBUG com.byteframework.learn.sync.TestSleepWait - 线程t1调用wait...
21:46:57.362 [main] DEBUG com.byteframework.learn.sync.TestSleepWait - 主线程获得锁...

可以看出 t1 线程获取到对象锁后, 调用 wait 方法又释放了对象锁, 1秒后主线程获得了对象锁.

wait notify 的正确姿势

step1

package com.byteframework.learn.sync;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j
public class TestStep1 {

    static final Object room = new Object();
    // 有烟没烟
    static boolean hasCigarette = false;
    static boolean hasTakeout;

    public static void main(String[] args) throws InterruptedException {
        // 小南没烟不干活
        new Thread(()-> {
            synchronized(room) {
                log.debug("有烟没? [{}]", hasCigarette);
                if(!hasCigarette) {
                    log.debug("没烟, 先休息一会...");
                    try {
                        TimeUnit.SECONDS.sleep(5);
                        log.debug("5秒后...");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没? [{}]", hasCigarette);
                if(hasCigarette) {
                    log.debug("有烟, 开始干活...");
                }
            }
        }, "小南").start();

        for(int i = 0; i< 5; i++){
            new Thread(() -> {
                synchronized(room) {
                    log.debug("开始干活...");
                }
            }, "其他人[" + i + "]").start();
        }

        TimeUnit.SECONDS.sleep(1);
        new Thread(()-> {
            hasCigarette = true;
            log.debug("1秒后, 烟到了...");
        }, "送烟的").start();
    }
}

输出:

22:18:23.314 [小南] DEBUG com.byteframework.learn.sync.TestStep1 - 有烟没? [false]
22:18:23.318 [小南] DEBUG com.byteframework.learn.sync.TestStep1 - 没烟, 先休息一会...
22:18:24.325 [送烟的] DEBUG com.byteframework.learn.sync.TestStep1 - 1秒后, 烟到了...
22:18:28.318 [小南] DEBUG com.byteframework.learn.sync.TestStep1 - 5秒后...
22:18:28.318 [小南] DEBUG com.byteframework.learn.sync.TestStep1 - 有烟没? [true]
22:18:28.318 [小南] DEBUG com.byteframework.learn.sync.TestStep1 - 有烟, 开始干活...
22:18:28.318 [其他人[4]] DEBUG com.byteframework.learn.sync.TestStep1 - 开始干活...
22:18:28.319 [其他人[3]] DEBUG com.byteframework.learn.sync.TestStep1 - 开始干活...
22:18:28.319 [其他人[2]] DEBUG com.byteframework.learn.sync.TestStep1 - 开始干活...
22:18:28.319 [其他人[0]] DEBUG com.byteframework.learn.sync.TestStep1 - 开始干活...
22:18:28.319 [其他人[1]] DEBUG com.byteframework.learn.sync.TestStep1 - 开始干活...

分析:

  • 其他干活的线程, 都要一直阻塞, 效率太低.

  • 小南线程必须睡足了5秒才会醒来, 就算烟提前到了, 也无法立即醒来.

解决方法: 使用 wait - notify 机制

step2

优化 step1代码:

package com.byteframework.learn.sync;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j
public class TestStep2 {

    static final Object room = new Object();
    // 有烟没烟
    static boolean hasCigarette = false;
    static boolean hasTakeout;

    public static void main(String[] args) throws InterruptedException {
        // 小南没烟不干活
        new Thread(()-> {
            synchronized(room) {
                log.debug("有烟没? [{}]", hasCigarette);
                if(!hasCigarette) {
                    log.debug("没烟, 先休息一会...");
                    try {
                        // 使用 wait 释放对象锁
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没? [{}]", hasCigarette);
                if(hasCigarette) {
                    log.debug("有烟, 开始干活...");
                }
            }
        }, "小南").start();

        for(int i = 0; i< 5; i++){
            new Thread(() -> {
                synchronized(room) {
                    log.debug("开始干活...");
                }
            }, "其他人[" + i + "]").start();
        }

        TimeUnit.SECONDS.sleep(1);
        new Thread(()-> {
            synchronized(room) {
                hasCigarette = true;
                log.debug("1秒后, 烟到了...");
                room.notify();
            }
        }, "送烟的").start();
    }
}

输出:

22:27:29.834 [小南] DEBUG com.byteframework.learn.sync.TestStep2 - 有烟没? [false]
22:27:29.839 [小南] DEBUG com.byteframework.learn.sync.TestStep2 - 没烟, 先休息一会...
22:27:29.839 [其他人[4]] DEBUG com.byteframework.learn.sync.TestStep2 - 开始干活...
22:27:29.839 [其他人[3]] DEBUG com.byteframework.learn.sync.TestStep2 - 开始干活...
22:27:29.839 [其他人[2]] DEBUG com.byteframework.learn.sync.TestStep2 - 开始干活...
22:27:29.840 [其他人[1]] DEBUG com.byteframework.learn.sync.TestStep2 - 开始干活...
22:27:29.840 [其他人[0]] DEBUG com.byteframework.learn.sync.TestStep2 - 开始干活...
22:27:30.840 [送烟的] DEBUG com.byteframework.learn.sync.TestStep2 - 1秒后, 烟到了...
22:27:30.840 [小南] DEBUG com.byteframework.learn.sync.TestStep2 - 有烟没? [true]
22:27:30.840 [小南] DEBUG com.byteframework.learn.sync.TestStep2 - 有烟, 开始干活...

分析:

可以看到, 小南线程在休息的期间释放了对象锁, 这是其他线程可以获取对象锁进行"干活". 烟送到后使用了 notify 通知了正在休息的小南, 小南立即起来干活.

思考: (虚假唤醒)

如果此时有两个线程都在等待 (比如: 小南没烟不干活, 小女没吃的不干活), notify 是否能准确唤醒小南或小女

step3

虚假唤醒:

step4

step5

posted on 2024-12-24 09:37  屋蓝  阅读(6)  评论(0)    收藏  举报