Java并发编程基础篇2-线程八大核心2

欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

5. 核心5:Thread和Object类中线程相关方法(wait/notify、sleep、join、yield)

5.1 方法概览

方法名 简介
Thread sleep相关 本表格的“相关”,指的是重载方法,如sleep有多个重载方法,但实际作用大同小异
join 主线程等待ThreaA执行完毕(ThreadA.join())
yield相关 放弃已经获取到的CPU资源
currentThread 获取当前执行线程的引用
start,run相关 启动线程相关
interrupt相关 中断线程
stop(),suspend(),resuem()相关 已废弃
Object wait/notify/notifyAll相关 让线程暂时休息和唤醒

5.2 wait,notify,notifyAll方法详解

5.2.1 用法:阻塞阶段、唤醒阶段、遇到中断

1. 阻塞阶段

线程调用wait()方法,则该线程进入到阻塞状态,直到以下4种情况之一发生时,才会被唤醒:

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

2. 唤醒阶段

  • notify会唤起单个在等待某对象monitor的线程,如果有多个线程在等待,则只会唤起其中随机的一个
  • notifyAll会将所有等待的线程都唤起,而唤起后具体哪个线程会获得monitor,则看操作系统的调度
  • notify必须在synchronized中调用,否则会抛出异常
    java.lang.IllegalMonitorStateException
            at java.lang.Object.notify(Native Method)
            at BlockedWaitingTimedWaiting.run(BlockedWaitingTimedWaiting.java:37)
            at java.lang.Thread.run(Thread.java:748)
    复制代码

3. 遇到中断

假设线程执行了wait(),在此期间被中断,则会抛出interruptException,同时释放已经获取到的monitor

5.2.2 代码演示:4种情况

1. 普通用法

/**
 * 展示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.currentThread().getName() + "开始执行了");
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程" + Thread.currentThread().getName() + "获取到了锁");
 
            }
        }
    }
 
    static class Thread2 extends Thread {
        @Override
        public void run() {
            synchronized (object) {
                object.notify();
                System.out.println("线程" + Thread.currentThread().getName() + "调用了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-0开始执行了
线程Thread-1调用了notify()
线程Thread-0获取到了锁
复制代码

步骤解析:

  • Thread-0进入Thread1类synchronized代码块,获得锁,输出“Thread-0开始执行”
  • 然后Thread-0执行object.wait()释放了锁
  • Thread-1获得锁,进入Thread2类synchronized,执行object.notify(),输出“线程Thread-1调用了notify()”,同时Thread-0也被唤醒了
  • Thread-0回到object.wait()的位置,执行下面的代码逻辑,输出“线程Thread-0获取到了锁”

2. notify和notifyAll展示

/**
 * 3个线程,线程1和线程2首先被阻塞,线程3唤醒它们。notify,notifyAll
 * start先执行不代表线程先启动
 */
public class WaitNotifyAll implements Runnable{
    private static final Object resourceA = new Object();
    @Override
    public void run() {
        synchronized(resourceA) {
            System.out.println(Thread.currentThread().getName() + " get resourceA lock");
            try {
                System.out.println(Thread.currentThread().getName() + " wait to start");
                resourceA.wait();
                System.out.println(Thread.currentThread().getName() + "'s waiting end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
 
    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 notifyed.");
                }
            }
        });
        threadA.start();
        threadB.start();
        Thread.sleep(200);
        threadC.start();
    }
}
//输出结果
Thread-0 get resourceA lock
Thread-0 wait to start
Thread-1 get resourceA lock
Thread-1 wait to start
ThreadC notifyed.
Thread-1's waiting end
Thread-0's waiting end
复制代码

3. 只释放当前monitor展示

/**
 * 证明wait只释放当前的那把锁
 */
public class WaitNotifyReleaseOwnMonitor {
    private static volatile Object resourceA = new Object();
    private static volatile Object resourceB = new Object();
 
    public static void main(String[] args) {
        Thread thread1  = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceA) {
                    System.out.println("ThreadA got resourceA lock.");
                    synchronized (resourceB) {
                        System.out.println("ThreadA got resourceB lock.");
                        try {
                            System.out.println("ThreadA releases resourceA lock.");
                            resourceA.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (resourceA) {
                    System.out.println("ThreadB got resourceA lock.");
                    System.out.println("ThreadB tries to resourceB lock.");
                    synchronized (resourceB) {
                        System.out.println("ThreadB got resourceB lock.");
                    }
                }
            }
        });
        thread1.start();
        thread2.start();
    }
}
//输出结果
ThreadA got resourceA lock.
ThreadA got resourceB lock.
ThreadA releases resourceA lock.
ThreadB got resourceA lock.
ThreadB tries to resourceB lock.
复制代码

没有打印ThreadB got resourceB lock.(因为只调用了A.wait(),只释放了lockA,B还没占用着

5.2.3 特点、性质

  • 使用的时候必须先拥有monitor(synchronized锁)
  • notify只能唤醒其中一个
  • 属于Object
  • 类似Condition
  • 同时持有多个锁的情况

5.2.4、原理

1. wait原理

  • 入口集 Entry Set
  • 等待集 Wait Set
  • 特殊情况
    • 如果发生异常,可以直接跳到终止TERMINATED状态,不必再遵循路径,比如可以从WAITING直接到TERMINATED
    • Object.wait()刚被唤醒时,通常不能立刻抢到monitor锁,那就会从WAITING先进入BLOCKED状态,抢到锁后再转换到RUNNABLE状态

2. 手写生产者消费者设计模式

image.png

  • 什么是生产者消费者模式
/**
 *  用wait/notify来实现
 */
public class ProducerConsumerModel {
    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个产品。
仓库中已经有3个产品。
仓库中已经有4个产品。
仓库中已经有5个产品。
仓库中已经有6个产品。
仓库中已经有7个产品。
仓库中已经有8个产品。
仓库中已经有9个产品。
仓库中已经有10个产品。
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下9
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下8
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下7
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下6
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下5
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下4
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下3
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下2
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下1
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下0
仓库中已经有1个产品。
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下0
复制代码

5.2.6 常见面试问题

1. 两个线程交替打印0~100的奇偶数

  1. 基本方式:用synchronized关键字实现
/**
 * @Description: 两个线程交替打印0~100的奇偶数,用synchronized关键字实现
 */
public class WaitNotifyPrintOddEvenSyn {
    public static int count = 0;
    public static final Object lock = new Object();
 
    //新建2个线程
    //1个只处理偶数,第二个只处理奇数(用位运算)
    //用synchronized来通信
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (count < 100) {
                    synchronized (lock) {
                        if ((count & 1) == 0) {
                            System.out.println((Thread.currentThread().getName() + ":" + count++));
                        }
                    }
                }
            }
        }, "偶数").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (count < 100) {
                    synchronized (lock) {
                        if ((count & 1) == 1) {
                            System.out.println(Thread.currentThread().getName() + ":" + count++);
                        }
                    }
                }
            }
        }, "奇数").start();
    }
}
//输出结果
//输出正确,但是实际上如果thread1(偶数线程)一直支持lock,会有不断循环做无效的操作
偶数:0
奇数:1
偶数:2
奇数:3
...
奇数:99
偶数:100
复制代码
  1. 更好的方法:wait/notify
/**
 * 两个线程交替打印0~100的奇偶数,用wait和notify
 */
public class WaitNotifyPrintOddEvenWait {
    private static int count = 0;
    private static Object lock = new Object();
    //1. 拿到锁,我们就打印
    //2. 打印完,唤醒其他线程,自己就休眠
    static class TurningRunner implements Runnable {
        @Override
        public void run() {
            while (count <= 100) {
                synchronized (lock) {
                    //拿到锁就打印
                    System.out.println(Thread.currentThread().getName() + ":" + count++);
                    lock.notify();
                    if (count <= 100) {
                        try {
                            //如果任务还没结束,就让出当前线程,并休眠
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        new Thread(new TurningRunner(),"偶数").start();
        Thread.sleep(100);
        new Thread(new TurningRunner(),"奇数").start();
    }
}
//输出结果
偶数:0
奇数:1
偶数:2
奇数:3
...
奇数:99
偶数:100
复制代码

2. 手写生产者消费者设计模式

3. 为什么wait()需要在同步代码块内使用,而sleep()不需要

我们反过来想,如果不要求wait()必须在同步块里面,而是可以在之外调用的话,那么就会有以下代 码:

class BlockingQueue {

    Queue<String> buffer = new LinkedList<String>();

    public void give(String data){
        buffer.add(data);
        notify(); //Since someone may be waiting in take

    }

    public String take()throws InterruptedException {

        while (buffer.isEmpty()){
            wait();
        }
        return buffer.remove();
    }
复制代码

那么可能发生如下的错误:

  • 消费者线程调用take()并看到了buffer.isEmpty()
  • 在消费者线程继续wait()之前,生产者线程调用一个完整的give(),也就是buffer.add(data)notify()
  • 消费者线程现在调用wait(),但是错过了刚才的notify()
  • 如果运气不好,即使有可用的数据,但是没有更多生产者生产的话,那么消费者会陷入wait的无限期等待

一旦你理解了这个问题,解决方案是显而易见的:synchronized用来确保notify永远不会在isEmpty和 wait之间被调用。如下:

  • 正常逻辑是先执行wait后续在执行notify唤醒。如果wait/notify不放同步代码块,执行wait的时候,线程切换去执行其他任务如notify,导致notify先于wait,就会导致后续切回wait的时候,一直阻塞着,无法释放,导致死锁。
  • 而sleep是针对本身的当前线程的,不影响

参考资料:programming.guide/java/why-wa…

4. 为什么线程通信的方法wait(),notify()和notifyAll被定义在Object类里?而sleep定义在Thread类里?

wait、notify、notifyAll是锁级别的操作,属于Object对象的,而线程实际上是可以持有多把锁的,如果把wait定义到Thread里面,就无法做到这么灵活的控制了

经典回答:www.java67.com/2012/09/top…

5. wait方法是属于Object对象的,那调用Thread.wait会怎么样?

  • 这里就把Thread当成是一个普通的类,和Object没有区别。
  • 但是这样会有一个问题,那就是线程退出的时候会自动notify(),这会让我们自己设计的唤醒流程受到极大的干扰,所以十分不推荐调用Thread类的wait()

6. 如何选择notify还是notifyAll?

Object.notify()可能导致信号丢失这样的正确性问题,而Object.notifyAll()虽然效率不太高(把不需要唤醒的等待线程也给唤醒了),但是其在正确性方面有保障。因此实现通知的一种比较流行的保守性方法是优先使用Object.notifyAll()以保障正确性,只有在有证据表明使用Object.notify()足够的情况下才使用Object.notif()。Object.notify()只有在下列条件全部满足的情况下才能够用于替代notifyAll方法。

  • 条件1:一次通知仅需要唤醒至多一个线程。这一点容易理解,但是光满足这一点还不足以用Object.notify()去替代Object.notifyAll()。在不同的等待线程可能使用不同的保护条件的情况下,Object.notify()唤醒的一个任意线程可能并不是我们需要唤醒的那一个(种)线程。因此,这个问题还需要通过满足条件2来排除。
  • 条件2:相应对象的等待集中仅包含同质等待线程。所谓同质等待线程指这些线程使用同一个保护条件,并且这些线程在Object.wait()调用返回之后的处理逻辑一致。最为典型的同质线程是使用同一个Runnablef接口实例创建的不同线程(实例)或者从同一个Thread子类的new出来的多个实例。

注意:

Object.notify()唤醒的是其所属对象上的一个任意等待线程。Object.notify()本身在唤醒线程时是不考虑 保护条件的。Object.notifyAll()方法唤醒的是其所属对象上的所有等待线程。使用Object.notify()替代 Object.notifyAll()时需要确保以下两个条件同时得以满足:

  • 一次通知仅需要唤醒至多一个线程。
  • 相应对象上的所有等待线程都是同质等待线程。

参考资料: www.jianshu.com/p/5834de089…

7. notifyAll之后所有的线程都会再次抢夺锁,如果某线程抢夺失败怎么办?

实质就跟初始状态一样,多个线程抢夺锁,抢不到的线程就等待,等待上一个线程释放锁

8. 用suspend()和resume()来阻塞线程可以吗?为什么?

这2个方法由于不安全,已经被弃用了。功能类似wait和notify,但是不释放锁,并且容易引起死锁。

5.3 sleep方法详解

5.3.1 作用

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

5.3.2 不释放锁

  • 包括synchronized和lock
  • 和wait不同
/**
 * 展示线程sleep的时候不释放synchronized的monitor,等sleep时间到了以后,正常结束后才释放锁
 */
public class SleepDontReleaseMonitor implements Runnable{
    public static void main(String[] args) {
        SleepDontReleaseMonitor sleepDontReleaseMonitor = new SleepDontReleaseMonitor();
        new Thread(sleepDontReleaseMonitor).start();
        new Thread(sleepDontReleaseMonitor).start();
    }
    @Override
    public void run() {
        syn();
    }
 
    private synchronized void syn() {
        System.out.println("线程" + Thread.currentThread().getName() + "获取到了monitor");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程" + Thread.currentThread().getName() + "退出了同步代码块");
    }
}
//输出结果
线程Thread-0获取到了monitor
线程Thread-0退出了同步代码块(5s后出现)
线程Thread-1获取到了monitor
线程Thread-1退出了同步代码块(5s后出现)
复制代码
/**
 * 演示sleep不释放lock(lock需要手动释放)
 */
public class SleepDontReleaseLock implements Runnable {
    private static final Lock lock = new ReentrantLock();
 
    @Override
    public void run() {
        lock.lock();
        System.out.println("线程" + Thread.currentThread().getName() + "获取到了lock");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        System.out.println("线程" + Thread.currentThread().getName() + "释放了lock");
    }
 
    public static void main(String[] args) {
        SleepDontReleaseLock sleepDontReleaseLock = new SleepDontReleaseLock();
        new Thread(sleepDontReleaseLock).start();
        new Thread(sleepDontReleaseLock).start();
    }
}
//输出结果
线程Thread-0获取到了lock
线程Thread-0释放了lock(5s后)
线程Thread-1获取到了lock
线程Thread-1释放了lock(5s后)
复制代码

5.3.3 sleep方法响应中断

  • 抛出InterruptedException
  • 清除中断状态
/**
 * 每隔1s输出当前时间,被中断,观察
 * Thread.sleep()
 * TimeUnit.SECONDS.sleep()
 */
public class SleepInterrupted implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(new Date());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                System.out.println("我被中断了");
                e.printStackTrace();
            }
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new SleepInterrupted());
        thread.start();
        Thread.sleep(6500);
        thread.interrupt();
    }
}
//输出结果
Wed Apr 15 23:09:55 CST 2020
Wed Apr 15 23:09:56 CST 2020
Wed Apr 15 23:09:57 CST 2020
Wed Apr 15 23:09:58 CST 2020
Wed Apr 15 23:09:59 CST 2020
Wed Apr 15 23:10:00 CST 2020
Wed Apr 15 23:10:01 CST 2020
我被中断了
java.lang.InterruptedException: sleep interrupted
Wed Apr 15 23:10:01 CST 2020
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at ConcurrenceFolder.mooc.threadConcurrencyCore.threadobjectclasscommonmethods.SleepInterrupted.run(SleepInterrupted.java:21)
	at java.lang.Thread.run(Thread.java:748)
Wed Apr 15 23:10:02 CST 2020
Wed Apr 15 23:10:03 CST 2020
复制代码

5.3.4 sleep总结

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

5.3.5 sleep常见面试问题

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

  • 相同

    • 都会阻塞,线程状态为Waiting 或Time_Waiting
    • 都可以响应中断
    • 外层执行thread.interrupt()
    try {
        wait();
        Thread.sleep();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    复制代码
  • 不同

    • wait/notify需要在synchronized方法中,而sleep不需要
    • 释放锁:wait会释放锁,而sleep不释放锁
    • 指定时间:sleep必须传参时间,而wait有多个构造方法,不传时间则直到自己被唤醒
    • 所属类:wait/notify是Object方法,sleep是Thread类的方法

5.4 join方法

5.4.1 作用

因为新的线程加入了“我们”,所以“我们”要等他执行完再出发

5.4.2 用法

(在main方法中thread1.join)main等待thread1执行完毕,注意谁等谁(父等待子)

5.4.3 三个例子

  1. 普通用法
/**
 * 演示join,注意语句输出顺序,会变化
 */
public class Join {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "执行完毕");
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "执行完毕");
            }
        });
        thread1.start();
        thread2.start();
        System.out.println("开始等待子线程运行完毕");
        thread1.join();
        thread2.join();
        System.out.println("所有子线程执行完毕");
    }
}
//输出结果
开始等待子线程运行完毕
Thread-0执行完毕
Thread-1执行完毕
所有子线程执行完毕
复制代码
  1. 遇到中断
/**
 * 演示join期间被中断的效果
 */
public class JoinInterrupt {
    public static void main(String[] args) {
        Thread mainThread = Thread.currentThread();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    mainThread.interrupt();
                    Thread.sleep(5000);
                    System.out.println("Thread1 finished.");
                } catch (InterruptedException e) {
                    System.out.println("子线程中断");
                }
            }
        });
        thread1.start();
        System.out.println("等待子线程运行完毕");
        try {
            thread1.join();
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + "主线程中断了");
            thread1.interrupt();
        }
        System.out.println("子线程已运行完毕");
    }
}
//输出结果
等待子线程运行完毕
main主线程中断了
子线程已运行完毕
子线程中断
复制代码
  1. 在join期间,线程到底是什么状态?:Waiting
/**
 *  先join再mainThread.getState()
 * 通过debugger看线程join前后状态的对比
 */
public class JoinThreadState {
    public static void main(String[] args) throws InterruptedException {
        Thread mainThread = Thread.currentThread();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                    System.out.println(mainThread.getState());
                    System.out.println("Thread-0运行结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        System.out.println("等待子线程运行完毕");
        thread.join();
        System.out.println("子线程运行完毕");
    }
}
//输出结果
等待子线程运行完毕
WAITING
Thread-0运行结束
子线程运行完毕
复制代码

可以使用封装工具类:CountDownLatchCyclicBarrier

5.4.4 join原理

源码:

(1)thread.join();
(2)
public final void join() throws InterruptedException {
        join(0);
    }
(3)
 public final synchronized void join(long millis)
    throws InterruptedException {
        ...
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        }
复制代码

分析:线程在run执行完成后,JVM底层会自动调用一个notifyAll唤醒,所以即使在join()没有notify显示调用执行完run()后,也会唤醒。等价于下面的代码:

//        thread.join();   等价于下面synchronized的代码
        synchronized (thread) {
            thread.wait();
        }
复制代码

5.4.5 常见面试问题

在join期间,线程处于哪种线程状态?Waiting

5.5 yield方法

  • 作用:释放我的CPU时间片。线程状态仍然是RUNNABLE不释放锁,也不阻塞
  • 定位:JVM不保证遵循yield逻辑
  • yield和sleep区别:sleep期间线程调度器不会去调度该线程,而yield方法时只是让线程释放出自己的CPU时间片,线程依然处于就绪状态,随时可能再次被调度。

5.6 获取当前执行线程的引用:Thread.currentThread()方法

同一个方法,不同线程会打印出各自线程的名称

/**
 * 演示打印majn, Thread-0, Thread-1
 */
public class CurrentThread implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
 
    public static void main(String[] args) {
        new CurrentThread().run();
        new Thread(new CurrentThread()).start();
        new Thread(new CurrentThread()).start();
    }
}
//输出
main
Thread-0
Thread-1
复制代码

6. 核心6:线程各属性

6.1 线程各属性纵览

属性名称 用户
编号(ID) 每个线程有自己的ID,用于标识不同的线程
名称(Name) 作用让用户或程序员在开发、调试或运行过程中,更容易区分每个不同的线程、定位问题等
是否是守护线程(isDaemon) true代表该线程是【守护线程】,false代表线程是非守护线程,也就是【用户线程】
优先级(Priority) 优先级这个属性的目的是告诉线程调度器,用户希望哪些线程相对多运行、哪些少运行

6.2 线程ID

/**
 * ID从1开始,JVM运行起来后,我们自己创建的线程的ID早已不是2
 */
public class Id {
    public static void main(String[] args) {
        Thread thread = new Thread();
        System.out.println("主线程id:" + Thread.currentThread().getId());
        System.out.println("子线程id:" + thread.getId());
    }
}
//输出结果
主线程id:1
子线程id:11
复制代码

getId内部调用是nextThreadID

thread.getId = nextThreadID()
private static synchronized long nextThreadID() {
       return ++threadSeqNumber;
}
复制代码

6.3 线程名字、守护线程

6.3.1 线程名字

1. 默认线程名字源码分析

  • "Thread-" + 自增数
public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}
  
private static synchronized int nextThreadNum() {
    return threadInitNumber++;
}
复制代码

2. 修改线程的名字(代码演示、源码分析)

Thread thread = new Thread();
System.out.println("子线程初始名字:" + thread.getName());
thread.setName("FlyThread-1");
System.out.println("子线程修改后的名字:" + thread.getName());
 
//输出结果
子线程初始名字:Thread-0
子线程修改后的名字:FlyThread-1
复制代码

6.3.2 守护线程

1. 概念

Java线程分为用户线程和守护线程,线程的daemon属性为true表示是守护线程,false表示是用户线程。

守护线程: 是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程。

用户线程: 是系统的工作线程,它会完成这个程序需要完成的业务操作。

用户线程一旦退出,守护线程也会结束,即守护线程不能单独存在,必须依赖用户线程存在

代码演示如下,当main线程执行结束退出后,a线程会自动结束,即运行按钮不会是红色了。

public class DaemonDemo
{
    public static void main(String[] args)
    {
        Thread a = new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+" come in:\t"
                    +(Thread.currentThread().isDaemon() ? "守护线程":"用户线程"));
            while (true)
            {

            }
        }, "a");
        a.setDaemon(true);
        a.start();

        //暂停几秒钟线程
        try { 
            TimeUnit.SECONDS.sleep(2); 
        } catch (InterruptedException e) { 
            e.printStackTrace(); 
        }

        System.out.println(Thread.currentThread().getName()+"\t"+" ----task is over");
    }
}
复制代码

重点:

  • 当程序中所有用户线程执行完毕之后,不管守护线程是否结束,系统都会自动退出。如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出了。所以当系统只剩下守护进程的时候,java虚拟机会自动退出。
  • 设置守护线程,需要在start()方法之前进行

2. 三个特性

  • 线程类型默认继承自父线程(守护线程的子线程也是守护线程)
  • 通常守护线程都是由JVM自动启动的
  • 不影响JVM退出:JVM退出只会考虑是否还有用户线程

3. 守护线程的常见面试问题

  • 守护线程和普通线程的区别

    • 整体无区别
    • 唯一区别在于JVM的离开:用户线程会影响JVM的停止,而守护线程不影响
    • 作用不同:用户线程是执行逻辑的,而守护线程是给用户线程提供服务的
  • 我们是否需要给线程设置为守护线程?thread.setDaemon(true)

    • 不应该把自己的用户线程设置为守护线程。
    • 例如:如果设置了用户线程为守护线程,JVM发现只有一个守护线程,就中止退出了,导致程序逻辑没有走完。其实JVM本身提供的守护线程就已经足够了

6.4 线程优先级

10个级别,默认5

引申面试题:为什么程序设计不应依赖于线程优先级?

  • 由于优先级最终是由线程调度器来决定调度方案的,所以优先级高并不能保证就一定比优先级低的先运行:并且如果优先级设置得不合适,可能会导致线程饥饿等问题(优先级低的线程始终得不到运行),所以通常而言,我们不必要设置线程的优先级属性,保持默认的优先级就可以。

6.5、各属性总结

属性名称 用途 注意事项
编号(ID) 标识不同的线程 线程回收后,id被后续创建的线程使用;无法保证id的唯一性(之前线程id,跟后续线程id不一定是同一个线程,可能是回收后后续创建的);不允许修改id
名称(Name) 定位问题 可以设置一个清晰有意义的名字(方便跟踪定位);默认的名称是Thread-0/1/2/3
是否是守护线程(isDaemon) 守护线程/用户线程 二选一;继承父线程;setDaemon
优先级(Priority) 告诉线程调度器,哪些线程相对多运行、哪些少运行 默认和父线程的优先级相等,共有10个等级,默认5;不应依赖优先级

7. 核心7:线程异常处理(全局异常处理UncaughtExceptionHandler)

7.1 为什么需要UncaughtExceptionHandler?

  • 主线程可以轻松发现异常,子线程却不行
  • 子线程异常无法用传统方法(try-catch)捕获(类似main方法中执行thread.start,抛出异常是在子线程的run中,而try-catch的是主线程main,所以捕获不到)
  • 不能直接捕获会导致一些后果(无法捕获到异常,做相应的重试操作逻辑)

7.2 两种解决方案

方案一(不推荐):手动在每个run方法里进行try catch

public class CanCatchDirectly implements Runnable{
    public static void main(String[] args) throws InterruptedException {
        new Thread(new CanCatchDirectly(), "MyThread-1").start();
        Thread.sleep(300);
        new Thread(new CanCatchDirectly(), "MyThread-2").start();
        Thread.sleep(300);
        new Thread(new CanCatchDirectly(), "MyThread-3").start();
        Thread.sleep(300);
        new Thread(new CanCatchDirectly(), "MyThread-4").start();
    }
    
        @Override
    public void run() {
        try {
            throw new RuntimeException();
        } catch (RuntimeException e) {
            System.out.println("Caught Exception");
        }
        
    }
}
//输出结果
Caught Exception
Caught Exception
Caught Exception
Caught Exception
复制代码

方案二(推荐):利用UncaughtExceptionHandler

  1. UncaughtExceptionHandler接口

  2. void uncaughtException(Thread t, Throwable e);

Thread.java

@FunctionalInterface
    public interface UncaughtExceptionHandler {
        /**
         * Method invoked when the given thread terminates due to the
         * given uncaught exception.
         * <p>Any exception thrown by this method will be ignored by the
         * Java Virtual Machine.
         * @param t the thread
         * @param e the exception
         */
        void uncaughtException(Thread t, Throwable e);
    }
复制代码
  1. 异常处理器的调用策略

  2. 自己实现

    • 给程序统一设置
    • 给每个线程单独设置
    • 给线程池设置
    1、自己的UncaughtExceptionHandler
    /**
     * 自己的UncaughtExceptionHandler
     */
    public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
        private String name;
    
        public MyUncaughtExceptionHandler(String name) {
            this.name = name;
        }
    
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            Logger logger = Logger.getAnonymousLogger();
            logger.log(Level.WARNING, "线程异常,终止了" + t.getName(), e);
            System.out.println(name + "捕获了异常" + t.getName() + "异常" + e);
        }
    }
    
    2、使用自己的UncaughtExceptionHandler,触发
    /**
     * UseOwnUncaughtExceptionHandler
     *
     * @author venlenter
     * @Description: 使用自己的UncaughtExceptionHandler
     * @since unknown, 2020-04-28
     */
    public class UseOwnUncaughtExceptionHandler implements Runnable {
        public static void main(String[] args) throws InterruptedException {
            Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler("捕获器1"));
            new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-1").start();
            Thread.sleep(300);
            new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-2").start();
            Thread.sleep(300);
            new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-3").start();
            Thread.sleep(300);
            new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-4").start();
    
        }
        @Override
        public void run() {
            throw new RuntimeException();
        }
    }
    //输出结果
    捕获器1捕获了异常MyThread-1异常java.lang.RuntimeException
    四月 28, 2020 11:34:06 下午 ConcurrenceFolder.mooc.threadConcurrencyCore.uncaughtexception.MyUncaughtExceptionHandler uncaughtException
    警告: 线程异常,终止了MyThread-1
    java.lang.RuntimeException
            at ConcurrenceFolder.mooc.threadConcurrencyCore.uncaughtexception.UseOwnUncaughtExceptionHandler.run(UseOwnUncaughtExceptionHandler.java:24)
            at java.lang.Thread.run(Thread.java:748)
    
    四月 28, 2020 11:34:06 下午 ConcurrenceFolder.mooc.threadConcurrencyCore.uncaughtexception.MyUncaughtExceptionHandler uncaughtException
    捕获器1捕获了异常MyThread-2异常java.lang.RuntimeException
    警告: 线程异常,终止了MyThread-2
    java.lang.RuntimeException
            at ConcurrenceFolder.mooc.threadConcurrencyCore.uncaughtexception.UseOwnUncaughtExceptionHandler.run(UseOwnUncaughtExceptionHandler.java:24)
            at java.lang.Thread.run(Thread.java:748)
    
    四月 28, 2020 11:34:07 下午 ConcurrenceFolder.mooc.threadConcurrencyCore.uncaughtexception.MyUncaughtExceptionHandler uncaughtException
    ...
    复制代码

7.3 线程的未捕获异常-常见面试问题

Java异常体系图: image.png

7.3.1 为什么要全局处理?不处理行不行?

不处理是不行的,因为否则异常信息会抛给前端,这会让重要的信息泄露,不安全。只要是未处理异 常,我们返回给前端就是简单的一句话“意外错误”,而不应该把异常栈信息也告诉前端,否则会被白 帽子、黑客利用。

7.3.2 如何全局处理异常?

  • 给程序统一设置
    • 先自己实现UncaughtException Handler接口,在uncaughtException(Thread t,Throwable e)的实现上,根据业务需要可以有不同策略,最常见的方式是把错误信息写入日志,或者重启线程、或执行其他修复或诊断。
  • 给每个线程或线程池单独设置
    • 刚才我们是给整个程序设置了默认的JncaughtExceptionHandler,这是通常的做法。当然,如果业务有特殊需求,我们也可以给某个线程或者线程池指定单独的特定的UncaughtExceptionHandler,这样可以更精细化处理。

7.3.3 run方法是否可以抛出异常?如果抛出异常,线程的状态会怎么样?

run方法不能抛出异常,如果运行时发生异常,线程会停止运行,状态变成Terminated。

8. 核心8:线程安全-多线程会导致的问题

8.1 线程安全

8.1.1 什么是线程安全

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的————《Java并发编程实战》

8.1.2 线程不安全:get同时set

  • 全都线程安全?:运行速度、设计成本、trade off
  • 完全不用于多线程的代码:不过度设计

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

8.2.1 运行结果错误:a++多线程下出现消失的请求现象

/**
 *  普通a++会导致count叠加错误,以下程序已优化处理
 */
public class MultiThreadsError3 implements Runnable {
    int index = 0;
    final boolean[] marked = new boolean[10000000];
    static AtomicInteger realIndex = new AtomicInteger();
    static AtomicInteger wrongCount = new AtomicInteger();
    static MultiThreadsError3 instance = new MultiThreadsError3();
    static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
    static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);
 
    @Override
    public void run() {
        marked[0] = true;
        for (int i = 0; i < 10000; i++) {
            try {
                cyclicBarrier2.reset();
                cyclicBarrier1.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            index++;
            try {
                cyclicBarrier1.reset();
                cyclicBarrier2.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            realIndex.incrementAndGet();
            //在原基础上加synchronized
            synchronized (instance) {
                if (marked[index] && marked[index - 1]) {
                    System.out.println("发生错误:" + index);
                    wrongCount.incrementAndGet();
                }
                marked[index] = true;
            }
 
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(instance);
        Thread thread2 = new Thread(instance);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("表面上结果是:" + instance.index);
        System.out.println("真正运行的次数:" + realIndex.get());
        System.out.println("错误次数:" + wrongCount.get());
    }
}
//输出结果
表面上结果是:20000
真正运行的次数:20000
错误次数:0
复制代码

8.2.2 活跃性问题:死锁、活锁、饥饿

/**
 * 第二章线程安全问题,演示死锁
 */
public class MultiThreadError implements Runnable {
    int flag = 1;
    static Object o1 = new Object();
    static Object o2 = new Object();
 
    public static void main(String[] args) {
        MultiThreadError r1 = new MultiThreadError();
        MultiThreadError r2 = new MultiThreadError();
        r1.flag = 1;
        r2.flag = 0;
        new Thread(r1).start();
        new Thread(r2).start();
    }
 
    @Override
    public void run() {
        System.out.println("flag = " + flag);
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    System.out.println("1");
                }
            }
        }
        if (flag == 0) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println("0");
                }
            }
        }
    }
}
//输出结果
flag = 1
flag = 0
//程序一直等待不结束
复制代码

8.2.3 对象发布和初始化时的安全问题

1. 什么是发布?

public、return都算是获得对象,发布了该对象出去

2. 什么是溢出?

  • 方法返回一个private对象(定义了private对象的getXX()方法)(private的本意是不让外部访问)
  • 还未完成初始化(构造函数没完全执行完毕)就把对象提供给外界,比如:
  1. 构造函数中未初始化完毕就把this赋值出去
/**
 * 初始化未完毕,就this赋值
 */
public class MultiThreadsError4 {
    static Point point;
 
    public static void main(String[] args) throws InterruptedException {
        new PointMaker().start();
        Thread.sleep(105);
        if (point != null) {
            System.out.println(point);
        }
    }
}
 
class Point {
    private final int x, y;
 
    Point(int x, int y) throws InterruptedException {
        this.x = x;
        //这里先行给point赋值this,此时外部拿到point对象只有x,没有y的值
        MultiThreadsError4.point = this;
        Thread.sleep(100);
        this.y = y;
    }
 
    @Override
    public String toString() {
        return "Point{" +
                "x=" + x +
                ", y=" + y +
                '}';
    }
}
 
class PointMaker extends Thread {
    @Override
    public void run() {
        try {
            new Point(1, 1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
//输出结果
//可能
Point{x=1, y=1}
//也可能
Point{x=1, y=0}
复制代码

2. 隐式逸出————注册监听事件

/**
 *  观察者模式
 */
public class MultiThreadsError5 {
    int count;
 
    public MultiThreadsError5(MySource source) {
        source.registerListener(new EventListener() {
            @Override
            //这里EventListener是一个匿名内部类,实际上也用了count这个外部引用变量,当count未初始化完成,拿到的值就还是0
            public void onEvent(Event e) {
                System.out.println("\n我得到的数字是:" + count);
            }
        });
        for (int i = 0; i < 10000; i++) {
            System.out.print(i);
        }
        count = 100;
    }
 
    public static void main(String[] args) {
        MySource mySource = new MySource();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                mySource.eventCome(new Event() {
                });
            }
        }).start();
        MultiThreadsError5 multiThreadsError5 = new MultiThreadsError5(mySource);
    }
 
    static class MySource {
        private EventListener listener;
 
        void registerListener(EventListener eventListener) {
            this.listener = eventListener;
        }
 
        void eventCome(Event e) {
            if (listener != null) {
                listener.onEvent(e);
            } else {
                System.out.println("还未初始化完毕");
            }
        }
    }
 
    interface EventListener {
        void onEvent(Event e);
    }
 
    interface Event {
 
    }
}
//输出结果
012345678910...
我得到的数字是:0
...
复制代码
  1. 构造函数中运行线程
/**
 *  构造函数中新建线程
 */
public class MultiThreadsError6 {
    private Map<String, String> states;
    public MultiThreadsError6() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                states = new HashMap<>();
                states.put("1", "周一");
                states.put("2", "周二");
                states.put("3", "周三");
                states.put("4", "周四");
            }
        }).start();
    }
 
    public Map<String, String> getStates() {
        return states;
    }
 
    public static void main(String[] args) {
        MultiThreadsError6 multiThreadsError6 = new MultiThreadsError6();
        //在构造函数中states还未初始化完成,就get
        System.out.println(multiThreadsError6.getStates().get("1"));
    }
}
//输出结果
Exception in thread "main" java.lang.NullPointerException
	at ConcurrenceFolder.mooc.threadConcurrencyCore.background.MultiThreadsError6.main(MultiThreadsError6.java:34)
复制代码

3. 如何解决逸出

  • 返回“副本”(返回对象的deepCopy)--对应解决(1.方法返回了private对象)
  • 工厂模式--对应解决(2.还没初始化就吧对象提供给外界)

8.3 各种需要考虑线程安全的情况

  • 访问共享的变量或资源,会有并发风险,比如对象的属性、静态变量、共享缓存、数据库等
  • 所有依赖时序的操作,即使每一步操作都是线程安全的,还是存在并发问题:read-modify-write、check-then-act(a++问题)
  • 不同的数据之间存在捆绑关系的时候(原子操作:要么全部执行,要么全部不执行)
  • 我们使用其他类的时候,如果对方没有声明自己是线程安全的,则我们需要做相应的处理逻辑

8.4 双刃剑:多线程会导致的问题

8.4.1 性能问题有哪些体现、什么是性能问题

  • 服务响应慢、吞吐量低、资源消耗(例如内存)过高等
  • 虽然不是结果错误,但仍然危害巨大
  • 引入多线程不能本末倒置

8.4.2 为什么多线程会带来性能问题

  • 调度:上下文切换

    • 什么是上下文?:线程A执行到某个地方,然后要切换到另一个线程B的时候,CPU会保存当前的线程A在CPU中的状态(上下文)到内存中的某处,等线程B执行完成后,回到线程A需要还原线程A之前保存的状态(这种切换需要耗时)
    • 缓存开销(考虑缓存失效):多线程切换,从线程A切换到线程B,线程A的缓存就失效了,需要重新加载
    • 何时会导致密集的上下文切换:抢锁、IO
  • 协作:内存同步

    • 为了数据的正确性,同步手段往往会使用禁止编译器优化、使CPU内的缓存失效(java内存模型)

8.5 常见面试问题

8.5.1 你知道有哪些线程不安全的情况

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

8.5.2 平时哪些情况下需要额外注意线程安全问题?

  • 访问共享的变量或资源,会有并发风险,比如静态变量。
  • 依赖时序的操作。
  • 不同的数据之间存在捆绑关系的时候

8.5.3 为什么多线程会带来性能问题?

体现在两个方面:线程的调度和协作,这两个方面通常相辅相成,也就是说,由于线程需要协作,所 以会引起调度:

  • 调度:上下文切换
    • 什么时候会需要线程调度呢?当可运行的线程数超过了CPU核心数,那么操作系统就要调度线程,以便于让每个线程都有运行的机会。
  • 缓存开销
    • 除了刚才提到的上下文切换带来的直接开销外,还需要考虑到间接带来的缓存失效的问题。我们知道程序有很大概率会访问刚才访问过的数据,所以CPU为了加快执行速度,会根据不同算法,把常用到的数据缓存到CPU内,这样以后再用到该数据时,可以很快使用。
    • 但是现在上下文被切换了,也就是说,CPU即将执行不同的线程的不同的代码,那么原本缓存的内容有极大概率也没有价值了。这就需要CPU重新缓存,这导致线程在被调度运行后,一开始的启动速度会有点慢。

8.5.4 什么是多线程的上下文切换?

参考资料:www.jianshu.com/p/0fbeee2b2…

9. 多线程八大核心总结

  • 有多少种实现线程的方法?思路有5点
  • 实现Runnable接口和继承Thread类哪种方式更好?
  • 一个线程两次调用start()方法会出现什么情况?为什么?
  • 既然start()方法会调用run()方法,为什么我们选择调用start()方法,而不是直接调用run()方法呢?
  • 如何停止线程
  • 如何处理不可中断的阻塞
  • 线程有哪几种状态?生命周期是什么?
  • 用程序实现两个线程交替打印0~100的奇偶数
  • 手写生产者消费者设计模式
  • 为什么wait()需要在同步代码块内使用,而sleep()不需要
  • 为什么线程通信的方法wait(),notify()和notifyAll()被定义在Object类里?而sleep定义在Thread类里?
  • wait方法是属于Object对象的,那调用Thread.wait会怎么样?
  • 如何选择用notify还是notifyAll?
  • notifyAll之后所有的线程都会再次抢夺锁,如果某线程抢夺失败怎么办?
  • 用suspend()和resume()来阻塞线程可以吗?为什么?
  • wait/notify、sleep异同(方法属于哪个对象?线程状态怎么切换?)
  • 在join期间,线程处于哪种线程状态?
  • 守护线程和普通线程的区别
  • 我们是否需要把线程设置为守护线程?
  • run方法是否可以抛出异常?如果抛出异常,线程的状态会怎么样?
  • 线程中如何处理某个未处理异常?
  • 什么是多线程的上下文切换

思维导图总结: 1线程8大核心基础.png

参考资料

Java并发编程知识体系
Java多线程编程核心技术
Java并发编程的艺术
Java并发实现原理 JDK源码剖析

猜你喜欢

转载自juejin.im/post/7074928102006063140