我们先来看一个简单的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();
}
}
原理如下:
如下案例,我们先写出三个线程,分别启动线程:
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() 等待状态–阻塞状态(没有获取到锁的线程 队列)
—就绪状态→运行状态
我们先来进行一个简单的测试,下面是一个简单的死循环代码,如果在单线程环境中执行该代码,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中的线程分为两种类型:用户线程和守护线程。
1、守护线程是依赖于用户线程,用户线程退出了,守护线程也会退出,典型的守护线程如垃圾回收线程。
2、用户线程是独立存在的,不会因为其他用户线程的退出而退出。
Stop:中止线程,并且清除监控器锁的信息,但是可能导致 线程安全问题,JDK不建议用。 Destroy: JDK未实现该方法。
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;
}
}
这样我们就可以实现中断标志了。
在jdk1.5后新增的ReentrantLock类同样可达到此效果,且在使用上比synchronized更加灵活
相关API:
使用ReentrantLock实现同步
lock()方法:上锁
unlock()方法:释放锁
使用Condition实现等待/通知 类似于 wait()和notify()及notifyAll()
Lock锁底层基于AQS实现,需要自己封装实现自旋锁。
Synchronized —属于JDK 关键字 底层属于 C++虚拟机底层实现
Lock锁底层基于AQS实现-- 变为重量级锁
Synchronized 底层原理—锁的升级过程
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();
}
}
}
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();
}
}
主动释放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空闲时,设置优先级几乎没有任何作用。