Java并发核心知识体系

问题:

  1. 实现多线程的方法有多少种?
  2. 启动线程的方法
  3. 停止线程的方法
  4. 线程的6个状态(线程的生命周期)
  5. Thread和Object类中和线程相关的重要方法
  6. 线程的属性
  7. 线程的未捕获异常UncaughtException应该如何处理
  8. 线程双刃剑
  9. 常见面试题

实现多线程的方法

  1. 方法一:实现Runnable接口,重写run()函数,运行start()方法

  2. 方法二:继承Thread类,重写run()函数,运行start()方法

方法一比方法二好,灵活、新建线程消耗、Java不支持双继承

错误认识

  1. 线程池创建线程也算一种新建线程的方法✖️(底层基于 new Thread())

  2. 通过Callable和TutureTask创建线程,也是一种新的创建线程的方法✖️(底层基于Runnable和Thread)


  3. 无返回值是实现runnable:接口,有返回值是实现callable接口,所以callable是新的实现线程的方式。✖️

  4. 定时器 ✖️

  5. 匿名内部类 ✖️

  6. Lambda表达式 ✖️

启动线程的方法

  1. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

    这是另一个非常经典的 Java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!

​ new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。

start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

线程的一生

线程的6个状态/生命周期

New
Runnable
Blocked
Waiting
Timed Waiting
Terminated

/**
* 展示线程New、Runnable、Terminated状态。即使正在运行,也是Runnable状态,而不是Running
*/
public static void main(String[] args) {
Thread thread = new Thread(new NewRunnableTerminated());
// 打印New状态
System.out.println(thread.getState());
//
thread.start();
System.out.println(thread.getState());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 即使正在运行,也是Runnable状态,而不是Running
System.out.println(thread.getState());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getState());
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println(i);
}
}
NEW
RUNNABLE
0
1
……
789
RUNNABLE(正在运行中)
……
9999
TERMINATED

阻塞状态

一般习惯而言,把Blocked(被阻塞)、Waiting(等待)、Timed_waiting(计时等待)都称为阻塞状态不仅仅是Blocked

  • 有关方法概览

  • wait、notify、notifyAll方法及sleep方法详解

作用阶段

一、阻塞阶段

要执行wait方法,必须要持有monitor锁

直到以下4种情况之一发生时,才会被唤醒:

  1. 另一个线程调用这个对象的notify方法且刚好被唤醒的是本线程;
  2. 另一个线程调用这个对象的notifyAll方法;
  3. 过了wait(long timeout)规定的超时时间,如果传入0就是永久等待;
  4. 线程自身调用了interrupt()(中断)

二、唤醒阶段(notify、notifyAll起作用)

区别在于后者会把所有在等待的线程都唤醒

三、遇到中断

interrupt

代码

package threadObject;

/**
* 描述:
* 展示wait和notify的基本用法
* 1. 研究代码执行顺序
* 2. 证明wait释放锁
*/
public class Wait {
public static Object object = new Object();

static class Thread1 extends Thread {
@Override
public void run() {
synchronized (object) {
System.out.println("Thread start: " + Thread.currentThread().getName());
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread: " + Thread.currentThread().getName() + " get lock.");
}
}
}

static class Thread2 extends Thread {
@Override
public void run() {
synchronized (object) {
object.notify();
System.out.println("Thread: " + Thread.currentThread().getName() + " calls notify()");
}

}
}

public static void main(String[] args) {
Thread thread1 = new Thread1();
Thread thread2 = new Thread2();
thread1.start();
try {
Thread.sleep(200); // 不休眠无法保证执行顺序
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
}
}
Thread start: Thread-0
Thread: Thread-1 calls notify()
Thread: Thread-0 get lock.
package threadObject;

/**
* 描述: 3个线程,线程1和线程2首先被阻塞,线程3唤醒它们。
* notify, notifyAll
* start先执行不代表线程先启动
*/
public class WaitNotifyAll implements Runnable {

private static final Object resourceA = new Object();

public static void main(String[] args) throws InterruptedException {
Runnable r = new WaitNotifyAll();
Thread threadA = new Thread(r);
Thread threadB = new Thread(r);
Thread threadC = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA) {
resourceA.notifyAll();
// resourceA.notify();
System.out.println("ThreadC notified.");
}
}
});
threadA.start();
threadB.start();
Thread.sleep(200); // 不休眠无法保证执行顺序
threadC.start();
}

@Override
public void run() {
synchronized (resourceA) {
System.out.println(Thread.currentThread().getName() + " got resourceA lock.");
try {
System.out.println(Thread.currentThread().getName() + " waits to start.");
resourceA.wait();
System.out.println(Thread.currentThread().getName() + "'s waiting to end.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Thread-0 got resourceA lock.
Thread-0 waits to start.
Thread-1 got resourceA lock.
Thread-1 waits to start.
ThreadC notified.
Thread-1's waiting to end.
Thread-0's waiting to end.

原理

  1. 注意点

基于wait、notify的生产者消费者设计模式

代码实现

package threadObject;

import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

/**
* 用wait/notify来实现生产者消费者模式
*/
public class ProducerConsumer {
public static void main(String[] args) {
EventStorage eventStorage = new EventStorage();
Producer producer = new Producer(eventStorage);
Consumer consumer = new Consumer(eventStorage);
new Thread(producer).start();
new Thread(consumer).start();
}
}

class Producer implements Runnable {

private EventStorage storage;

public Producer(EventStorage storage) {
this.storage = storage;
}

@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.put();
}
}
}

class Consumer implements Runnable {

private EventStorage storage;

public Consumer(EventStorage storage) {
this.storage = storage;
}

@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.take();
}
}
}

class EventStorage {

private int maxSize;
private LinkedList<Date> storage;

public EventStorage() {
maxSize = 10;
storage = new LinkedList<>();
}

public synchronized void put() {
while (storage.size() == maxSize) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.add(new Date());
System.out.println("仓库里有了" + storage.size() + "个产品。");
notify();
}

public synchronized void take() {
while (storage.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("拿到了" + storage.poll() + ",现在仓库还剩下" + storage.size());
notify();
}
}
仓库里有了1个产品。
仓库里有了2个产品。
……
仓库里有了10个产品。
拿到了Mon Jun 20 22:25:58 CST 2022,现在仓库还剩下9
拿到了Mon Jun 20 22:25:58 CST 2022,现在仓库还剩下8
……
拿到了Mon Jun 20 22:25:58 CST 2022,现在仓库还剩下1
拿到了Mon Jun 20 22:25:58 CST 2022,现在仓库还剩下0

sleep()方法详解

作用:只让线程在预期的时间执行,其他时候不占用CPU资源

特点: 不释放锁

sleep方法可以让线程进入Vaiting状态,并且不占用CPU资源,但是不释放锁,直到规定时间后再执行,休眠期间如果被中断,会抛出异常并清除中断状态

wait/notify、sleep异同(方法属于哪个对象?线程状态怎么切换?)

  • 相同
    ◆ 阻塞
    ◆ 都可以响应中断
  • 不同
    ◆ wait/notify必须在同步方法中执行,而sleep不需要
    ◆ wait释放锁,而sleep不释放锁
    ◆ sleep要指定时间
    ◆ 所属类不同,wait/notify属于Object,sleep属于Thread

Join()详解 ——用得少

作用:因为新的线程加入了我们,所以我们要等他执行完再出发(大伙拍了拍你:“快来,出发了”

用法:main等待thread1执行完毕,注意谁等谁

注意:join期间,线程处于waiting状态

Yield方法

作用:释放自己的时间片,但不释放锁也不阻塞,保持Running状态(一般不使用)

什么时候我们需要设置守护线程?
我们应该如何应用线程优先级来帮助程序运行?有哪些禁忌?
不同的操作系统如何处理优先级问题?

线程各属性纵览

  1. 线程ID问题:主线程ID从n开始,JVM运行起来后,我们自己创建的线程的ID必然递增,但不会是N+1。

    因为在主线程启动后,JVM创建了很多线程:

    Signal Dispatcher:把操作系统的信号发给适当的程序

    Reference Handler:GC引用相关的线程

    Finalizer:执行Finalizer相关的方法

守护线程

  • 守护用户线程的线程
  • 作用:给用户线程提供服务
  • 跟普通线程的区别:是否会影响JVM的退出

线程优先级

  • 总共有10个
  • 默认是5

未捕获的线程异常处理

  • UncaughtExceptionandler

为什么需要UncaughtExceptionandler?

  • 祝线程可以轻松发现异常,子线程却不行

  • 子线程异常无法用传统方法(Try-Catch)捕获

如何捕获子线程异常

  • 方法一(不推荐): 在run()中捕获异常
  • 使用UncaughtExceptionandler

如何实现?

  • 给程序统一设置
  • 给每个线程单独设置
  • 给线程池设置

线程安全

简单点说就是,不管业务中遇到怎样的多个线程访问某对象或某方法的情况而在编程这个业务逻辑的时候,都不需要额外做任何额外的处理(也就是可以像单线程编程一样),程序也可以正常运行(不会因为多线程而出错),就可以称为线程安全。

  1. 线程不安全的情况:get()的同时set()、额外同步
  2. 全都线程安全可以吗?:会影响运行速度、要考虑设计成本
  3. 完全不用于多线程:不过度设计

什么情况下会出现线程安全问题,怎么避免?

  1. 数据争用
    • 运行结果错误:a++多线程下出现消失的请求现象
    • 活跃性问题:死锁、活锁、饥饿
    • 对象发布初始化的时候的安全问题