• 【Java笔记】多线程2: 加锁小练习(卖票+交替打印+哲学家就餐)



    练习下Thread,Runnable,synchronized, Lock,稍微记录下

    0. 稍微回顾点基础

    0.1 如何起多线程

    一般就是三种方式:

    1. Thread的类,
    2. Runnable接口
    3. Callable+FutureTask接口

    0.1.1 继承Thread

    主要就是重写run方法,实例化后调用Thread.start()开启线程

    public class MyThread extends Thread{
        @Override
        public void run() {
            super.run();
            for (int i = 0; i < 1000; i++) {
                System.out.println(getName()+": Hello MyThread!");
            }
        }
    }
    // ----------------------------------------
    public class Main {
        public static void main(String[] args) {
            MyThread t1 = new MyThread();
            t1.setName("T1");
            MyThread t2 = new MyThread();
            t2.setName("T2");
            t1.start();
            t2.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    0.1.2 实现Runnable接口

    实现run方法,并实例化作为参数传入Thread实例
    最后也是通过调用Thread.start()开启线程

    public class MyRun implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                Thread t = Thread.currentThread(); // 获取当前线程对象
                System.out.println(t.getName()+"Hello Runnable");
                // System.out.println(Thread.currentThread().getName()+"Hello Runnable");
            }
        }
    }
    // ---------------------------
    public class Main {
        public static void main(String[] args) {
            // 创建MyRun对象,表示多线程要执行的任务
            MyRun myRun = new MyRun();
            // 创建线程对象
            Thread t1 = new Thread(myRun);
    	    Thread t2 = new Thread(myRun);
            // 启动线程
            t1.start();
            t2.start();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    0.1.3 Callable接口+Future接口

    最特别的就是可以获取多线程的运行结果,也就是run有返回值

    public class MyCallable implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            // 求1-100和并返回
            int sum = 0;
            for (int i = 0; i < 100; i++) {
                sum += i;
            }
            return sum;
        }
    }
    // ---------------------------
    public class Main {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            MyCallable mc = new MyCallable();
            FutureTask<Integer> ft = new FutureTask<>(mc);
            Thread t1 = new Thread(ft);
            t1.start();
            int sum = ft.get();
            System.out.println(sum);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    0.2 如何加锁

    主要就是synchronized修饰符或者Lock接口的实现类

    0.2.1 synchronized

    大概有需要注意几点:

    • synchronized是基于悲观锁的,当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
    • 锁对象:锁对象一定要唯一。
      • 可以设为本类的class对象,因为一个类的class对象是唯一的,哪怕有很多实例;
      • 也可以在类内增加一个static final的Object实例,也不一定是Object实例,只要是static保证所有类实例都相同,final保证不可更改,也就是一起确保了锁对象的唯一性。
      • 锁膨胀:无锁->偏向锁->轻量级锁->重量级锁

    有两种用法:

    1. synchronized 同步代码块
    synchronized(锁对象){
    	...
    }
    
    • 1
    • 2
    • 3
    1. synchronized 同步方法
    修饰符 synchronized 返回值类型 方法名(方法参数) {...}
    
    • 1

    注意一下,同步方法会锁住方法里的所有代码,并且锁对象不能自己指定:

    • 非静态方法:this,即当前方法的调用者。此时也叫方法锁
    • 静态方法:当前类的字节码文件。此时也叫类锁

    0.2.2 Lock的实现类

    比较常用的就是ReentranLock
    需要自己手动上锁,一般会用try{要上锁的代码}finally{解锁}来保证所以定会被释放

    Lock lock=new ReentrantLock();
    lock.lock();
    try{
    	...
    }finally{
    	lock.unlock();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    下面来看点案例练练手

    1. 案例:卖电影票

    1000张电影票,在两个窗口领取,每次领取一张,假设被刺领取的时间为3000ms
    用多线程模拟卖票过程,并打印剩余电影票的数量

    1.1 案例实现:Runnable接口

    1.1.1 synchronized 同步代码块

    Runnable实现类

    // 实现Runnable接口+synchronized同步代码块
    public class TicketRunnable1 implements Runnable {
        static int tickets = 1000;
    //    定义一个锁对象,也可以用当前类的class对象
        private static Object lock = new Object();
    
        @Override
        public void run() {
            while (tickets > 0) {
                synchronized (lock) {
                    if (tickets > 0) {
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                        System.out.println(Thread.currentThread().getName() + " 卖出1张票,剩余电影票:" + --tickets);
                    }
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    1.1.2 synchronized 同步方法

    就是把上面同步代码块抽出一个函数,并且不用自己制定锁对象

    // 实现Runnable接口+synchronized同步方法
    public class TicketRunnable2 implements Runnable{
        static int tickets = 1000;
        // 静态
        @Override
        public void run() {
            while(tickets > 0){
                sell();
            }
        }
    
        private synchronized void sell() {
            if (tickets > 0){
                try{
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + " 卖出1张票,剩余电影票:" + --tickets);
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    开两个线程测试

    public class TestRunnable {
        // 1000张电影票,在两个窗口领取,假设被领取的时间为3000ms
        // 时间可以调小点,比如50ms,给cpu上点强度,可以更好体现出线程交替互斥的过程
        // 用多线程模拟卖票过程,并打印剩余电影票的数量
        public static void main(String[] args) {
    //        TicketRunnable1 = new TicketRunnable1();
    //        Thread window1 = new Thread(ticketRunnable1, "Window1");
    //        Thread window2 = new Thread(ticketRunnable1, "Window2");
            TicketRunnable2 ticketRunnable2 = new TicketRunnable2();
            Thread window1 = new Thread(ticketRunnable2, "Window1");
            Thread window2 = new Thread(ticketRunnable2, "Window2");
            window1.start();
            window2.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    运行结果
    在这里插入图片描述
    没啥问题。
    如果你想实例化两个runnable实现类也没问题,因为锁对象是唯一,比如下面这样

    public class TestRunnable {
        // 1000张电影票,在两个窗口领取,假设被领取的时间为3000ms
        // 时间可以调小点,比如50ms,给cpu上点强度,可以更好体现出线程交替互斥的过程
        // 用多线程模拟卖票过程,并打印剩余电影票的数量
        public static void main(String[] args) {
            TicketRunnable1 ticketRunnable1 = new TicketRunnable1();
            TicketRunnable1 ticketRunnable11 = new TicketRunnable1();
            Thread window1 = new Thread(ticketRunnable1, "Window1");
            Thread window2 = new Thread(ticketRunnable11, "Window2");
            window1.start();
            window2.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    1.1.3 Lock手动上锁

    // 实现Runnable接口+Lock
    public class TicketRunnable3 implements Runnable{
        static int tickets = 1000;
    
        @Override
        public void run() {
            ReentrantLock lock = new ReentrantLock();
            while(tickets > 0){
                lock.lock();
                try {
                    Thread.sleep(50);
                    if (tickets > 0){
                        System.out.println(Thread.currentThread().getName() + " 卖出1张票,剩余电影票:" + --tickets);
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    lock.unlock();
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    测试结果
    在这里插入图片描述

    1.2 案例实现:Thread

    先写个Thread的继承类,这里就用同步方法写了,其他锁的办法也差不多,就不多讲咯

    public class MyThread1 extends Thread{
        static int tickets = 1000;
        @Override
        public void run() {
            super.run();
            while (tickets > 0){
                sell();
            }
        }
    
        private static synchronized void sell() {
            if (tickets > 0){
                try{
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + " 卖出1张票,剩余电影票:" + --tickets);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    需要注意的是,这里synchronized的方法sell()必须是静态的,
    因为静态的同步方法的锁对象是当前类的字节码文件对象,
    而非静态的同步方法的锁对象是this,也就是调用类的实例,这里我们需要实例化两个对象window1和window2,他们的锁对象是不同的,因此会出现不同步的问题,如下图
    在这里插入图片描述
    票卖的只剩-1了,明显线程间没有同步

    2. 案例:两个线程交替打印0-100

    这是之前牛客上看到个面试手撕题,用wait()notifyAll()让线程等待与唤醒就行
    先给段代码,大家可以思考一下对不对

    public class Printer implements Runnable {
        private static int number = 0;
        int printId;
    
        public Printer(int printId) {
            this.printId = printId;
        }
    
        @Override
        public void run() {
            while (number <= 100) {
                printNum();
            }
        }
    
        private synchronized void printNum() {
            if (number % 2 != printId) {
                // 不是当前线程就等待
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            // 被唤醒后
            if (number <= 100) {
                System.out.println(Thread.currentThread().getName() + ": " + number++);
                this.notifyAll();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    测试一下

    public class Test {
        public static void main(String[] args) {
            Printer printer1 = new Printer(0);
            Printer printer2 = new Printer(1);
            Thread thread1 = new Thread(printer1);
            Thread thread2 = new Thread(printer2);
            thread1.start();;
            thread2.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述
    很明显,寄了。主要问题就出在代码里用的synchronized同步方法,因为我们是给两个线程一个printId(0或1)以便通过number的奇偶来判断该哪个线程打印,显然,我们需要实例化两个Runnable实现类的对象,前面也说了synchronized非静态同步方法的锁对象是this,也就是实例本身,因此这里其实是没有同步了。
    那,是不是直接把这个要同步的printNum方法给static了就行了呢?
    很遗憾,也不行,因为printNum方法需要用printId来判断是不是当前线程的回合,而printId是需要通过构造函数初始化的,不可以是static,所以这样直接用同步方法行不通。
    换成同步代码块,加一个唯一的锁对象就行了

    public class Printer implements Runnable {
        private static int number = 0;
        int printId;
        private static final Object lock = new Object();
    
        public Printer(int printId) {
            this.printId = printId;
        }
    
        @Override
        public void run() {
            while (number <= 100) {
                synchronized (lock){
                    if (number % 2 != printId) {
                        // 不是当前线程就等待
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    // 被唤醒后
                    if (number <= 100) {
                        System.out.println(Thread.currentThread().getName() + ": " + number++);
                        lock.notifyAll();
                    }
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    当然这个锁对象用当前类的字节码文件也可以

    synchronized (Printer.class){
    	...
    	Printer.class.wait();
    	...
    	Printer.class.notifyAll();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    结果没毛病
    在这里插入图片描述
    当然,lock手动加锁就没这些麻烦,记得unlock就好

    3. 面试官:你来写个死锁吧(哲学家就餐问题)

    这也是今天在牛客上看到的(第一次听到这种请求)
    死锁,简单来说就是两个线程互相等对方释放锁资源,然后你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你…一直等下去

    public class Test {
        static Object lock1 = new Object();
        static Object lock2 = new Object();
        public static void main(String[] args) {
            Thread thread1 = new Thread(() -> {
                synchronized (lock1){
                    System.out.println(Thread.currentThread().getName() + " get lock1!");
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + " wait lock2...");
                    synchronized (lock2){
                        System.out.println(Thread.currentThread().getName() + " get lock2!");
                    }
                }
            });
    
            Thread thread2 = new Thread(() -> {
                synchronized (lock2){
                    System.out.println(Thread.currentThread().getName() + " get lock2!");
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + " wait lock1...");
                    synchronized (lock1){
                        System.out.println(Thread.currentThread().getName() + " get lock1!");
                    }
                }
            });
    
            thread1.start();
            thread2.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    启动一下
    在这里插入图片描述
    就一直在wait力。哲学家就餐问题来说,这里两个线程就是两个哲学家,两个锁就是两支筷子,只有凑够两支筷子才能吃上饭。

    3.* 有关死锁

    预防死锁

    上面这个问题出现的很大一部分原因就是锁的嵌套,实际写代码的时候也要尽量避免这种情况来预防死锁。此外也有银行家算法之类的来评估预防死锁,但今天就先不展开了。

    解除死锁

    一般就是资源剥夺法,撤销进程法,进程回退法

    • 撤销进程:强制结束一个或多个进程并回收它们的资源,以打破死锁(比如直接kill掉);
    • 进程回退:将一个或多个进程回退到某一安全状态,这些状态之前未涉及死锁。通过回退并重新计算资源分配,系统尝试解决死锁问题;
    • 资源剥夺:挂起/激活机制。挂起一些进程,剥夺它们的资源以解除死锁,待条件满足时,再激活进程。
      这里【进程回退】时其实也涉及到了【资源剥夺】
  • 相关阅读:
    RHCE(逻辑卷LVM,NFS服务)
    数学计算式转为表达式树
    51单片机入门——I2C总线与EEPROM
    4.【opencv打开美女热舞视频以及摄像头】
    SOC设计:关于reset的细节
    Java框架 SpringMVC--拦截器与异常处理器
    Milvus以及Web UI 安装
    【Linux】进程控制 —— 进程创建/终止/等待
    MySQL如何避免全表扫描?
    Adobe Illustrator 2022 for Mac/Win:设计的新篇章
  • 原文地址:https://blog.csdn.net/Hansdas/article/details/137976094