• 多线程核心API和Lock锁的使用


    一、分析wait和notify的实现原理

    我们先来看一个简单的wait()和notify()的应用,如下:

    @Slf4j
    public class Test {
        final Object object = new Object();
    
        public static void main(String[] args) throws InterruptedException {
            Test test = new Test();
            test.waitThread();
            // 主线程3s之后唤醒子线程
            Thread.sleep(3000);
            test.notifyThread();
        }
    
        public void notifyThread() {
            synchronized (object) {
                // 主线程唤醒子线程,结束阻塞
                object.notify();
            }
        }
    
        public void waitThread() {
            new Thread(() -> {
                synchronized (object) {
                    try {
                        log.info(Thread.currentThread().getName() + "1");
                        // 子线程调用了wait方法之后会阻塞
                        object.wait();
                        log.info(Thread.currentThread().getName() + "2");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).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

    原理如下:
    在这里插入图片描述

    二、join方法底层的设计原理

    如下案例,我们先写出三个线程,分别启动线程:

    	 Thread t1 = new Thread(() -> System.out.println(Thread.currentThread().getName() + "线程执行"), "t1");
    	 Thread t2 = new Thread(() -> System.out.println(Thread.currentThread().getName() + "线程执行"), "t2");
    	 Thread t3 = new Thread(() -> System.out.println(Thread.currentThread().getName() + "线程执行"), "t3");
    	  t1.start();
    	  t2.start();
    	  t3.start();       
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    然后我们执行,发现每次执行的打印顺序都不一样,某一次打印顺序如下:

    	t1线程执行
    	t3线程执行
    	t2线程执行
    
    • 1
    • 2
    • 3

    这是为什么呢?因为这三个线程并不是单线程执行的,而是多线程执行的,在多线程环境下,每个线程都会去抢夺CPU的时间片,谁抢到了谁就先执行,所以每次的打印顺序都是不一样的。

    那么,我们想让这三个线程顺序执行,该如何实现呢?
    我们可以使用join方法。

    		Thread t1 = new Thread(() -> System.out.println(Thread.currentThread().getName() + "线程执行"), "t1");
            Thread t2 = new Thread(() -> {
                try {
                    t1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "线程执行");
            }, "t2");
            Thread t3 = new Thread(() -> {
                try {
                    t2.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "线程执行");
            }, "t3");
            t1.start();
            t2.start();
            t3.start();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    这次无论执行多少次,打印结果都是:

    	t1线程执行
    	t2线程执行
    	t3线程执行
    
    • 1
    • 2
    • 3

    我们查看join()方法的源码,如下:

    // synchronized 使用的是this锁
    // 说明 哪个线程调用join()方法,哪个线程就会进入阻塞状态
    // 在上面的demo中,t2中调用t1.join(),相当于t1.wait(),t1就是this锁
    // t1主动释放了this锁,同时t2线程就变为阻塞状态
    public final synchronized void join(long millis)
        throws InterruptedException {
            long base = System.currentTimeMillis();
            long now = 0;
            if (millis < 0) {
                throw new IllegalArgumentException("timeout value is negative");
            }
            // 该方法中只有wait()阻塞方法,没有唤醒方法
            // 其实这里是因为 当线程的run方法结束之后,在JVM中会主动唤醒阻塞的线程
            // 这里涉及较为深入,请自行研究
            if (millis == 0) {
                while (isAlive()) {
                    wait(0);
                }
            } else {
                while (isAlive()) {
                    long delay = millis - now;
                    if (delay <= 0) {
                        break;
                    }
                    wait(delay);
                    now = System.currentTimeMillis() - base;
                }
            }
        }
    
    • 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

    join()方法的底层其实就是封装的wait()方法,默认使用的是this锁。

    三、多线程的七种状态

    在这里插入图片描述
    初始化状态
    就绪状态
    运行状态
    死亡状态
    阻塞状态
    超时等待
    等待状态

    start():调用start()方法会使得该线程开始执行,正确启动线程的方式。
    wait():调用wait()方法,进入等待状态,释放资源,让出CPU。需要在同步快中调用。
    sleep():调用sleep()方法,进入超时等待,不释放资源,让出CPU
    stop():调用sleep()方法,线程停止,线程不安全,不释放锁导致死锁,过时。
    join():调用sleep()方法,线程是同步,它可以使得线程之间的并行执行变为串行执行。
    yield():暂停当前正在执行的线程对象,并执行其他线程,让出CPU资源可能立刻获得资源执行。yield()的目的是让相同优先级的线程之间能适当的轮转执行
    notify():在锁池随机唤醒一个线程。需要在同步快中调用。

    nnotifyAll():唤醒锁池里所有的线程。需要在同步快中调用。
    Sleep 主动释放cpu执行权 休眠一段时间
    运行状态→限时等待状态
    限时等待状态→就绪状态→运行状态

    Synchronized 没有获取到锁 当前线程变为阻塞状态
    如果有线程释放了锁,唤醒正在阻塞没有获取到锁的线程
    从新进入到获取锁的状态

    wait() 运行—等待状态

    notify() 等待状态–阻塞状态(没有获取到锁的线程 队列)
    —就绪状态→运行状态

    四、sleep()方法防止CPU占用100%

    我们先来进行一个简单的测试,下面是一个简单的死循环代码,如果在单线程环境中执行该代码,CPU会瞬间飙高到100%,下面来进行一个测试,由于我的本地电脑是多核CPU,所以CPU不会达到100%, 但是占用CPU会变得很高。

    public class Main {
        public static void main(String[] args) {
            new Thread(() -> {
                while (true) {
    
                }
            }).start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    没有执行demo前,IDEA占用CPU的情况如下:
    在这里插入图片描述

    我们启动死循环线程,再次查看任务管理器的CPU占比,如下:
    在这里插入图片描述
    发现IDEA占用CPU的内容变得非常高,这也就是为什么不推荐我们在生产环境中出现死循环代码的原因。

    我们在代码中进行优化,加上sleep()方法。如下:

    	public static void main(String[] args) {
            new Thread(() -> {
                while (true) {
                    try {
                        Thread.sleep(30);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    再次进行执行代码,虽然刚开始运行时CPU占比很高,但很快就降了下来,如下图:
    在这里插入图片描述

    五、守护与用户线程的区别

    Java中的线程分为两种类型:用户线程和守护线程

    • 通过Thread.setDaemon(false)设置为用户线程
    • 通过Thread.setDaemon(true)设置为守护线程
      如果不设置次属性,默认为用户线程。

    1、守护线程是依赖于用户线程,用户线程退出了,守护线程也会退出,典型的守护线程如垃圾回收线程。
    2、用户线程是独立存在的,不会因为其他用户线程的退出而退出。

    六、如何安全的停止一个线程

    1、调用stop方法

    Stop:中止线程,并且清除监控器锁的信息,但是可能导致 线程安全问题,JDK不建议用。 Destroy: JDK未实现该方法。

    2、Interrupt(线程中止)

    Interrupt 打断正在运行或者正在阻塞的线程。

    (1)如果目标线程在调用Object class的wait()、wait(long)或wait(long, int)方法、join()、join(long, int)或sleep(long, int)方法时被阻塞,那么Interrupt会生效,该线程的中断状态将被清除,抛出InterruptedException异常。

    (2)如果目标线程是被I/O或者NIO中的Channel所阻塞,同样,I/O操作会被中断或者返回特殊异常值。达到终止线程的目的。

    如果以上条件都不满足,则会设置此线程的中断状态。

    我们先看如下代码:

    public class Main extends Thread{
    
        @Override
        public void run() {
            while (true) {
                try {
                    System.out.println(Thread.currentThread().getName() + " - 1");
                    Thread.sleep(10000000);
                    System.out.println(Thread.currentThread().getName() + " - 2");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public static void main(String[] args) {
            Main main = new Main();
            // Main线程会阻塞很长时间
            main.start();
            try {
                // 三秒后中断子线程
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("<中断子线程>");
            // 打断阻塞状态
            main.interrupt();
        }
    }
    
    • 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

    控制台发现产生了报错信息,我们发现,线程run方法内的循环还没有完成一次就被中断了,这是一种很不好的中断,最起码让该线程的一次循环流程执行完毕再进行中断。

    Thread-0 - 1
    <中断子线程>
    Thread-0 - 1
    java.lang.InterruptedException: sleep interrupted
    	at java.base/java.lang.Thread.sleep(Native Method)
    	at com.example.springbootdemo.service.Main.run(Main.java:11)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如果线程在运行状态,那么Thread.interrupt()是如何作用的呢?
    我们将上述代码run()方法中的sleep去掉,然后再次执行,发现,线程仍在运行,并没有被中断。这是为什么呢?

    这是因为Thread.interrupt()这个api,在执行的过程中会将线程的一个属性作为控制判断,对该属性进行了修改,并没有中断我们运行中的线程,只是对这个属性进行了修改。
    在这里插入图片描述
    那么我们可以改进代码如下:

    @Override
        public void run() {
            while (true) {
                if (this.isInterrupted()) {
                    break;
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这样就可以中断运行中的线程了。

    了解了interrupt()方法之后,我们自己是不是也可以实现这样一个中断的标志呢?

    public class Main extends Thread {
    	// 为什么要加volatile?
    	// 要保证线程可见性
        private volatile boolean isStart = true;
    
        @Override
        public void run() {
            while (true) {
                if (isStart) {
                    break;
                }
            }
        }
    
        public static void main(String[] args) {
            Main main = new Main();
            main.start();
            try {
                // 三秒后中断子线程
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("<中断子线程>");
            main.isStart = false;
        }
    }
    
    
    • 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

    这样我们就可以实现中断标志了。

    七、Lock锁与synchronized锁的区别

    在jdk1.5后新增的ReentrantLock类同样可达到此效果,且在使用上比synchronized更加灵活
    相关API:

    使用ReentrantLock实现同步
    lock()方法:上锁
    unlock()方法:释放锁
    使用Condition实现等待/通知 类似于 wait()和notify()及notifyAll()
    Lock锁底层基于AQS实现,需要自己封装实现自旋锁。

    Synchronized —属于JDK 关键字 底层属于 C++虚拟机底层实现
    Lock锁底层基于AQS实现-- 变为重量级锁
    Synchronized 底层原理—锁的升级过程
    Lock 过程中 注意 获取锁 释放锁

    1、Lock锁的使用示例

    public class Main {
        private Lock lock = new ReentrantLock();
        public static void main(String[] args) {
            
        }
    
        public void count() {
            try {
                // 获取锁
                lock.lock();
            } catch (Exception e) {
    
            }finally {
                // 释放锁,如果不释放锁,线程会一直保持阻塞状态
                lock.unlock();
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    2、Lock锁的condition用法

    public class Main {
        private Lock lock = new ReentrantLock();
        private Condition condition = lock.newCondition();
    
        public static void main(String[] args) {
            Main main = new Main();
            main.cal();
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            main.signalThread();
        }
    
        /**
         * 唤醒线程的方法
         */
        public void signalThread() {
            try {
                lock.lock();
                // 唤醒线程的api
                // 需要在获取锁之后的逻辑中使用
                condition.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        public void cal() {
            new Thread(() -> {
                try {
                    lock.lock();
                    System.out.println(1);
                    // 主动释放锁,同时当前线程变为阻塞状态
                    condition.await();
                    System.out.println(2);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }).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
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    八、多线程的yield的方法

    主动释放cpu执行权
    1、多线程yield 会让线程从运行状态进入到就绪状态,让后调度执行其他线程。
    2、具体的实现依赖于底层操作系统的任务调度器

    九、多线程的优先级

    1、在java语言中,每个线程都有一个优先级,当线程调控器有机会选择新的线程时,线程的优先级越高越有可能先被选择执行,线程的优先级可以设置1-10,数字越大代表优先级越高
    注意:Oracle为Linux提供的java虚拟机中,线程的优先级将被忽略,即所有线程具有相同的优先级。
    所以,不要过度依赖优先级。
    2、线程的优先级用数字来表示,默认范围是1到10,即Thread.MIN_PRIORITY到Thread.MAX_PRIORTY.一个线程的默认优先级是5,即Thread.NORM_PRIORTY。
    3、如果cpu非常繁忙时,优先级越高的线程获得更多的时间片,但是cpu空闲时,设置优先级几乎没有任何作用。

  • 相关阅读:
    【面试必刷TOP101】二分查找-I & 二维数组中的查找
    Vue的侦听器
    集成学习
    ASP.NET6 + Mongo + OData
    Centos7的yum使用国内源阿里源163源等提高下载速度
    Pandas数据分析:快速图表可视化各类操作详解+实例代码(三)
    xv6源码阅读——虚拟内存
    安卓性能优化
    JUnit5单元测试提示“Not tests were found”错误
    OLTP和OLAP有什么区别和不同?
  • 原文地址:https://blog.csdn.net/z318913/article/details/127779713