多线程核心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();
    }
}

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

二、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();       

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

	t1线程执行
	t3线程执行
	t2线程执行

这是为什么呢?因为这三个线程并不是单线程执行的,而是多线程执行的,在多线程环境下,每个线程都会去抢夺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();

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

	t1线程执行
	t2线程执行
	t3线程执行

我们查看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;
            }
        }
    }

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();
    }
}

没有执行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();
    }

再次进行执行代码,虽然刚开始运行时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();
    }
}

控制台发现产生了报错信息,我们发现,线程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)

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

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

@Override
    public void run() {
    
    
        while (true) {
    
    
            if (this.isInterrupted()) {
    
    
                break;
            }
        }
    }

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

了解了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;
    }
}

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

七、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();
        }
    }
}

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();
    }
}

八、多线程的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空闲时,设置优先级几乎没有任何作用。

猜你喜欢

转载自blog.csdn.net/z318913/article/details/127779713