1 线程基础
1.1 线程的创建
线程的创建本质上只有一种就是构造 Thread 类,线程最终执行的都是 run 方法,区别在于运行内容的来源不同,实现 Runnable 接口就是将 run 方法作为参数传递给 Thread 类,而继承就是直接调用重写的 run 方法。
1.1.1 线程创建的 4 种方式
具体的实现可以有以下几种:
1. 继承 Thread 类
public class ExtendsThread extends Thread {
@Override
public void run() {
System.out.println('用Thread类实现线程');
}
}
run() 方法在 Thread 类中的具体实现如下:
/* What will be run. */
private Runnable target;
public void run() {
if (target != null) {
target.run();
}
}
因为重写了 run 方法所以直接执行 ExtendsThread 类的 run 方法。
2. 实现 Runnable 接口
public class RunnableThread implements Runnable {
@Override
public void run() {
System.out.println('用实现Runnable接口实现线程');
}
}
构造 Thread 类的时候会将 RunnableThread 实例传给 Thread 中,在调用 run 方法的时候,target 就不为 null,执行 RunnableThread 实例中的 run 方法。
3. 线程池创建线程
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
java.util.concurrent.Executors 类中可以看到线程池的默认实现方式还是 new Thread() 实现。
4. 有返回值的 Callable 创建线程
class CallableTask implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return new Random().nextInt();
}
}
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//提交任务,并用 Future提交返回结果
Future<Integer> future = service.submit(new CallableTask());
submit 最终还是将任务放在了线程池中,而线程的创建仍然是基于构造 Thread 类实现的。
1.1.2 Runnable 接口和继承 Thread
- Runnable 将线程创建管理和要执行的任务解耦,方便线程的管理
- 提升性能,节省了线程的开销,如果通过继承 Thread 的形式创建,任务完成后就需要重新释放创建新线程
- 继承限制了代码未来的拓展性
1.2 线程的中断
Java 中的线程中断是线程间的协作模式,通过设置线程的中断标志间接影响线程的执行,事实上interrupt仅仅用于==通知== 被停止线程,线程拥有完全的自主权决定自己是否停止。
1.2.1 线程不强制中断的原因
如果不了解对方正在做的工作,贸然强制停止线程就可能会造成一些安全的问题,为了避免造成问题就需要给对方一定的时间来整理收尾工作。
线程正在写入一个文件,如果收到终止信号,根据自身业务判断是否立即停止还是将整个文件都写入后再停止。如果立刻终止就会造成数据不完整,而这并不是我们希望的结果。
1.2.2 interrupt 停止线程
执行条件中增加 isInterrupted 方法控制线程的终止
while (!Thread.currentThread().isInterrupted() && more work to do) {
do more work
}
如果被中断的调用了wait/sleep/join 等方法然后被调用了 interrupt,就会抛出 InterruptedException 返回。当线程为了等待某些条件阻塞当前的线程,而在阻塞过程中条件被满足了就可以调用 interrupt 方法抛出异常而返回恢复到激活状态
| isInterrupted | interrputed |
|---|---|
| 不清除标记 | 清除标记 |
| thread.isinterrupted | Thread.interrupted( 当前线程 ) |
threadOne.interrupt();
System.out.println(threadOne.isInterrupted());
// true
System.out.println(threadOne.interrputed());
// false 虽然 threadOne 调用,但还是获取的当前线程也就是 main
System.out.println(Thread.interrputed());
// false 与 threadOne.interrputed() 保持一致
System.out.println(threadOne.isInterrupted());
// true threadOne 标记并灭有被清除
1.2.3 两种优雅停止线程的方法
throws 异常到顶层,由 run 方法捕获异常,并且 run 方法无法抛出 checked Exception 所以顶层必须处理此异常
catch 语句块中再次调用 interrupt 方法,因为如果线程在休眠期间被中断,那么会自动清除中断信号,所以手动添加中断信号,让后续的方法依然可以检测到中断,正常退出。
1.2.4 为什么volatile 标记位停止方法是错误的
while 循环体内不会产生阻塞的场景可以使用 volatile,但如果循环工程中产生了阻塞就不会执行到循环条件终止线程。但是 Interrupt 方法可以打断阻塞。
1.3 线程的生命周期
线程的生命周期一共 6 种状态:1.New,2.Runnable,3.Blocked,4.Waiting,5.Timed Waiting,6.Terminated
New
表示线程被创建但尚未启动的状态:当我们用 new Thread() 新建一个线程时,如果线程没有开始运行 start() 方法,所以也没有开始执行 run() 方法里面的代码,那么此时它的状态就是 New。而一旦线程调用了 start(),它的状态就会从 New 变成 Runnable。
Runnable
对应操作系统线程状态中的两种状态,分别是 Running 和 Ready,也就是说,Java 中处于 Runnable 状态的线程有可能正在执行,也有可能没有正在执行,正在等待被分配 CPU 资源。
阻塞状态
Blocked
| 进入 Blocked | 进入 Runnable |
|---|---|
| 进入 synchronized 保护的代码没有抢到 monitor 锁 | 抢到了 monitor锁 |
Waiting
| 进入 Waiting | 进入 Runnable | 进入 Blocked |
|---|---|---|
| Object.wait() | Object.notify/notifyAll | |
| Thread.join() | join 线程执行完毕/interrupt | |
| LockSupport.park() | LockSupport.unpark() |
Blocked 仅仅针对于 monitor 锁,而 Java 中还有很多其他锁,比如 ReentrantLock 如果线程没有抢到该锁就会进入 waiting 状态,因为本质上是调用的 LockSupport.park() 所以看成情况3。
notify/notifyAll 会进入 Blocked 是因为调用方法前必须首先持有该 monitor 锁,所以处于 Waiting 状态的线程被唤醒时拿不到该锁,就会进入 Blocked 状态。
Timed Waiting
| 进入 Timed Waiting | 进入 Runnable | 进入 Blocked |
|---|---|---|
| Object.wait(long timeout) | Object.notify/notifyAll | |
| Thread.sleep(long millis) | 时间到/interrupt | |
| Thread.join(long millis) | 时间到/join 线程执行完毕/interrupt | |
| LockSupport.parkNanos(long nanos) | LockSupport.unpark() | |
| LockSupport.parkUntil(long deadline) | LockSupport.unpark() |
Terminated
run() 方法结束或者出现未捕获的异常
1.4 wait/notify/notifyAll
- Object.wait():释放当前对象锁「必须首先持有对象锁」,并进入阻塞队列
- Object.notify():唤醒当前对象阻塞队列里的任一线程(并不保证唤醒哪一个)
- Object.notifyAll():唤醒当前对象阻塞队列里的所有线程
每个对象里都有一个 monitor ,而 monitor 里面有一个该对象的锁和一个等待队列和一个同步队列
1.4.1 wait
在使用 wait 方法时,必须把 wait 方法写在 synchronized 保护的 while 代码块中「必须持有 monitor 锁」,并始终判断执行条件是否满足。
“wait method should always be used in a loop: synchronized (obj) {
while (condition does not hold)
obj.wait();
... // Perform action appropriate to condition
}
This method should only be called by a thread that is the owner of this object's monitor.”
调度器执行到 wait 之前暂停,开始执行 notify 方法,造成 wait 在 notify 之后执行,线程无法唤醒的情况。增加 synchronized 同步代码块保证 notify 在执行前必须要取得 monitor 锁,保证了执行顺序。while 方法则是避免 wait 被虚假唤醒后没判断业务逻辑就执行后续处理。
使用 synchronized 保护的根本原因就在于将 while 和 wait 组成原子操作,避免在 isEmpty 和 wait 方法之间执行了 notify 方法。
public class MyBlockingQueue {
Queue<String> buffer = new LinkedList<String> ();
public void give(String data) {
synchronized(this) {
buffer.add(data);
notify();
}
}
public String take() throws InterruptedException {
synchronized(this) {
// 使用 while 循环避免出现虚假唤醒的问题
while (buffer.isEmpty()) {
wait();
}
return buffer.remove();
}
}
}
1.4.2 wait 和 sleep
wait/notify/notifyAll 方法被定义在 Object 类,而 sleep 方法定义在 Thread 中的原因:
- 每个对象都有 monitor 锁,并且由于所有的对象都可以上锁,所以作为所有对象父类的 Object 包含 wait/notify/notifyAll 就更加合理
- 一个线程可以持有多把锁,如果让 Thread 去管理会带来很大的局限性 | wait | sleep | | --- | --- | | 阻塞线程 | 阻塞线程 | | 可以响应 interrupt 中断 | 可以响应 interrupt 中断 | | 需要获取 monitor 锁 | 无需 | | 释放 monitor 锁 | 不释放锁 | | 只能被中断和唤醒恢复 | 时间到期自动恢复 | | Object | Thread |
wait/notify/notifyAll 方法调用前没有获取到 monitor 锁会抛出异常 IllegalMonitorStateException
虚假唤醒:一个线程可以从挂起状态变为可以运行状态( 就是被唤醒),即使该线程没有被其他线程调用 notify/notifyAll方法进行通知,或者被中断,或者等待超时。
虚假唤醒就是调用 wait 方法是必须放在循环中重复判断的根本原因。
notify 需要加锁的原因:当获取到该对象的锁之后,才能去该对象对应 monitor 的等待队列去唤醒一个线程。值得注意的是,只有当执行唤醒工作的线程离开同步块,即释放锁之后,被唤醒线程才能去竞争锁。
1.5 join/sleep/yield
1.5.1 join
join 方法是 Thread 直接提供的,等待线程终止的方法。主线程调用 join 方法后被阻塞,等待线程执行完毕后返回。
public void testJoin() {
Thread main = Thread.currentThread();
Thread threadA = new Thread(() -> {
System.out.println("thread a is running!");
// 死循环
while (true) {
}
});
Thread threadB = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread b is running!");
main.interrupt();
});
threadA.start();
threadB.start();
try {
threadA.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main is end");
}
主线程调用了 threadA.join 阻塞了自己,等待线程 A 执行完毕,threadB 在等待 1s 后会调用主线程的 interrupt 方法设置主线程的中断标志,所以异常是出现在 join 方法处。
1.5.2 sleep
sleep 方法也是由 Thread 直接提供。调用线程会让出指定时间的执行权,在这期间不参与 CPU 的调度,但是不会释放 monitor 锁,时间到了正常返回,线程处于就绪状态。
睡眠期间如果被 interrupt 中断,会在 sleep 方法处抛出 InterruptException。
1.5.3 yield
yield 方法由 Thread 提供,暗示线程调度器让出自己的 CPU 的使用,线程调度器可以忽略这个暗示。当让出 CPU 使用权之后并不是状态是处于就绪 Ready。实际开发中这个方法很少使用,一般用于测试复现并发问题。
用 sleep 方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程,yield 方法线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。
1.6. 实现生产者消费者
1.6.1. wait/notify
public class MyBlockQueue {
private int max_size;
private LinkedList storage;
public MyBlockQueue(int max_size) {
this.max_size = max_size;
storage = new LinkedList();
}
public synchronized void put() throws InterruptedException {
while (storage.size() == max_size) {
System.out.println("max_size");
wait();
}
storage.add(new Object());
System.out.println("add Object now size:" + storage.size());
notifyAll();
}
public synchronized void take() throws InterruptedException {
while (storage.size() == 0) {
System.out.println("0");
wait();
}
storage.remove();
System.out.println("remove Object now size:" + storage.size());
notifyAll();
}
public int size(){
return storage.size();
}
}
@Test
public void testMyBlockedQueue() throws InterruptedException {
MyBlockQueue myBlockQueue = new MyBlockQueue(10);
Runnable producer = () -> {
while (true) {
try {
myBlockQueue.put();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable consumer = () -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (true) {
try {
myBlockQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
new Thread(producer).start();
new Thread(consumer).start();
Thread thread = Thread.currentThread();
thread.sleep(10000);
}
核心部分 MyBlockedQueue 中的 put 和 take 方法,队列的空和满为条件唤醒生产者和消费者