一、线程的优雅关闭
1、为什么需要优雅关闭?运行一半的线程能否强制杀死?
- 杀死线程的方法:stop()、destory()函数
- 不建议使用,强制杀死线程,则线程中所使用的资源不能正常关闭(例如文件描述符、网络连接等)。
- 一个线程一旦运行起来,就不要去强行打断它,合理的关闭办法是让其运行完(也就是函数执行完毕),干净地释放掉所有资源,然后退出。
- 如果是一个不断循环运行的线程,就需要用到线程间的通信机制,让主线程通知其退出。
2、守护线程
- 介绍
a: 当在一个JVM进程里面开多个线程时,这些线程被分成两类:守护线程和非守护线程。默认开的都是非守护线程。在Java中有一个规定:当所有的非守护线程退出后,整个JVM进程就会退出。意思就是守护线程“不算作数”,守护线程不影响整个JVM进程的退出。
b: 例如,垃圾回收线程就是守护线程,它们在后台默默工作,当开发者的所有前台线程(非守护线程)都退出之后,整个JVM进程就退出了。
- 代码
package com.hao.demo.thread;
import lombok.extern.slf4j.Slf4j;
/**
* @date 2020-07-02
* 在main(..)函数中开了一个线程,不断循环打印。请问:main(..)函数退出之后,
* 该线程是否会被强制退出?整个进程是否会强制退出?
*/
@Slf4j
public class Thread01 {
public static void main(String[] args) {
log.info("main start ............");
Thread thread = new Thread(() -> {
while (true) {
try {
log.info("子线程。。。。。。。。start");
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
});
/**
* 加入下面的语句 当main(..)函数退出后,线程t1就会退出,整个进程也会退出。
*/
//thread.setDaemon(true);
thread.start();
log.info("main end ............");
}
}
3、设置关闭的标志位
package com.hao.demo.thread;
import lombok.extern.slf4j.Slf4j;
/**
* @author flame
*/
@Slf4j
public class MyThread extends Thread{
private boolean stoped = false;
@Override
public void run() {
while (!stoped) {
log.info("start..........");
}
}
public void stop1() {
this.stoped = true;
}
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
myThread.start();
myThread.stop1(); // 通知线程退出
myThread.join(); // 等待线程退出循环,自行退出
}
}
二、InterruptedException()函数与interrupt()函数
1、什么情况下会抛出Interrupted异常
- 声明了InterruptedException 的函数才会抛出异常
2、轻量级阻塞与重量级阻塞
- 能够被中断的阻塞称为轻量级阻塞,对应的线程状态是WAITING或者TIMED_WAITING
- 像synchronized 这种不能被中断的阻塞称为重量级阻塞,对应的状态是BLOCKED。
3、线程的状态迁移过程
- 初始线程处于NEW状态,调用start()之后开始执行,进入RUNNING或者READY状态。
- 如果没有调用任何的阻塞函数,线程只会在RUNNING和READY之间切换,也就是系统的时间片调度。这两种状态的切换是操作系统完成的,开发者基本没有机会介入,除了可以
- 调用yield()函数,放弃对CPU的占用。一旦调用了任何阻塞函数,线程就会进入WAITING(无限期阻塞)或者TIMED_WAITING(阻塞有限时间)状态。
- 使用了synchronized关键字或者synchronized块,则会进入BLOCKED状态。
- 阻塞/唤醒函数,LockSupport.park()/unpark()=>Concurrent包中Lock的实现即依赖这一对操作。还有常用的阻塞唤醒函数。
- t.interrupted()的精确含义是“唤醒轻量级阻塞”,而不是字面意思“中断一个线程”。
4、t.isInterrupted()与Thread.interrupted()的区别
- t.interrupted()相当于给线程发送了一个唤醒的信号,所以如果线程此时恰好处于WAITING或者TIMED_WAITING状态,就会抛出一个InterruptedException,并且线程被唤醒。如果线程此时并没有被阻塞,则什么都不会做。
- 区别:t.isInterrupted只是读取中断状态,不修改状态;Thread.interrupted不仅读取中断状态,还会重置中断标志位。
三、synchronized关键字
1、锁的对象是什么
- 对于非静态成员函数,锁其实是加在对象a上面的;对于静态成员函数,锁是加在A.class上面的。当然,class本身也是对象。
- 伪代码
class A{
public void synchronized f1() {...}
public static void synchronized f1() {...}
}
等价于
class A{
public void f1() {
synchronized(this) {...}
}
public static void f1() {
synchronized(A.class) {...}
}
}
A a = new A();
a.f1();
a.f2();
2、锁的本质是什么
- 多个线程要访问同一个资源。线程就是一段段运行的代码;资源就是一个变量、一个对象或一个文件等;而锁就是要实现线程对资源的访问控制,保证同一时间只能有一个线程去访问某一个资源
- 图片
- 锁完成的事情
(1)这个对象内部得有一个标志位(state变量),记录自己有没有被某个线程占用(也就是记录当前有没有游客已经进入了房子)。最简单的情况是这个state有0、1两个取值,0表示没有线程占用这个锁,1表示有某个线程占用了这个锁。
(2)如果这个对象被某个线程占用,它得记录这个线程的thread ID,知道自己是被哪个线程占用了(也就是记录现在是谁在房子里面)。
(3)这个对象还得维护一个thread id list,记录其他所有阻塞的、等待拿这个锁的线程(也就是记录所有在外边等待的)。在当前线程释放锁之后(也就是把state从1改回0),从这个thread id list里面取一个线程唤醒。
3、synchronized实现原理
在对象头里,有一块数据叫MarkWord。在64位机器上,Mark Word是8字节(64位)的,这64位中有2个重要字段:锁标志位和占用该锁的thread ID。
四、wait()与notify()
1、生产者-消费者模型
一个内存队列,多个生产者线程往内存队列中放数据;多个消费者线程从内存队列中取数据。
(1)内存队列本身要加锁,才能实现线程安全。
(2)阻塞。当内存队列满了,生产者放不进去时,会被阻塞;当内存队列是空的时候,消费者无事可做,会被阻塞。
(3)双向通知。消费者被阻塞之后,生产者放入新数据,要notify()消费者;反之,生产者被阻塞之后,消费者消费了数据,要notify()生产者。
2、如何阻塞?
- 线程自己阻塞自己,也就是生产者、消费者线程各自调用wait()和notify()。
- 用一个阻塞队列,当取不到或者放不进去数据的时候,入队/出队函数本身就是阻塞的=>BlockingQueue的实现
3、如何双向通知?
- wait()与notify()机制。
- Condition机制。
4、为什么必须和synchronized一起使用?
- 两个线程之间要通信,对于同一个对象来说,一个线程调用该对象的wait(),另一个线程调用该对象的notify(),该对象本身就需要同步!所以,在调用wait()、notify()之前,要先通过synchronized关键字同步给对象,给该对象加锁。
- 伪代码
class A{
private Object obj1 = new Object();
public void f1() {
synchronized(obj1) {
...
obj1.wait();
...
}
}
public void f2() {
synchronized(obj1) {
...
obj1.notify()
...
}
}
}
等于
class A{
private Object obj1 = new Object();
public synchronized void f1() {
...
obj1.wait();
...
}
public synchronized void f2() {
...
obj1.notify()
...
}
}
5、为什么wait()的时候必须释放锁
- 当线程A进入synchronized(obj1)中之后,也就是对obj1上了锁。此时,调用wait()进入阻塞状态,一直不能退出synchronized代码块;那么,线程B永远无法进入synchronized(obj1)同步块里,永远没有机会调用notify(),就会出现死锁了?
- 在wait()的内部,会先释放锁obj1,然后进入阻塞状态,之后,它被另外一个线程用notify()唤醒,去重新拿锁!其次,wait()调用完成后,执行后面的业务逻辑代码,然后退出synchronized同步块,再次释放锁。
- wait() 内部伪代码
wait() {
// 释放锁
// 阻塞,等待被其他线程notify
// 重新拿锁
}
6、wait()与notify()的问题
- 生产者本来只想通知消费者,但它把其他的生产者也通知了;
- 消费者本来只想通知生产者,但它被其他的消费者通知了。
- 原因就是wait()和notify()所作用的对象和synchronized所作用的对象是同一个,只能有一个对象,无法区分队列空和列队满两个条件。
- 伪代码
public void enqueue() {
synchronized(queue) {
while(queue.full())
queue.awit();
... // 入队列
queue.notify() // 通知消费者,队列中有数据了
}
}
public void dequeue() {
synchronized(queue) {
while(queue.empty())
queue.awit();
... //出队列
queue.notify() // 通知生产者,队列中有空位了,可以继续放数据
}
}