• 《Java并发编程的艺术》读书笔记 - 第四章 - Java并发编程基础


    目录

    前言

    线程简介

    什么是线程?

    为什么要使用多线程?

    线程优先级

    线程的状态

    Daemon线程(守护线程) 

    启动和终止线程

    构造线程

    启动线程

    理解中断

    过期的suspend()、resume()和stop()

    安全地终止线程

    等待/通知机制

    Thread.join()的使用

    ThreadLocal 的使用

    等待超时模式


    前言

    Java从诞生开始就明智地选择了内置对多线程的支持,这使得Java语言相比同一时期的其他语言具有明显的优势。线程作为操作系统调度的最小单元,多个线程能够同时执行,这将显著提升程序性能,在多核环境中表现得更加明显。但是,过多地创建线程和对线程的不当管理也容易造成问题。


    线程简介

    什么是线程?

    现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。现代操作系统调度的最小单元是线程,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程是在同时执行。

    为什么要使用多线程?

    • 更多的处理器核心:在多个处理器核心运行多线程程序可以显著减少程序的处理时间,并且随着更多处理器核心的加入而变得更有效率。
    • 更快的响应时间:多线程可以在我们编写较为复杂的业务逻辑时提供更快的响应时间。
    • 更好的编程模型: Java为多线程编程提供了良好、考究并且一致的编程模型。

    线程优先级

    在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从 1 ~ 10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。

    线程的状态

    线程的6种状态
    状态名称说明
    NEW初始状态,线程被构建,但是还没有调用start()方法
    RUNNABLE运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”
    BLOCKED阻塞状态,表示线程阻塞于锁
    WAITING等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定的动作(通知或中断)
    TIME_WAITING超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的
    TERMINATED终止状态,表示当前线程已经执行完毕

    线程 6 种状态之前的变迁

    图片来源:百度

    注意:阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在java.concurrent包中Lock接口的线程状态却是等待状态,因为java.concurrent包中Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法。 

    Daemon线程(守护线程) 

    Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。此设置需要在线程启动之前,否则会爆IllegalThreadStateException

    1. public class DaemonTest {
    2. public static void main(String[] args) {
    3. Thread thread01 = new Thread(() -> {
    4. System.out.println("thread01 run...");
    5. }, "thread01");
    6. thread01.start();
    7. thread01.setDaemon(true);
    8. }
    9. }


    启动和终止线程

    构造线程

    在运行线程之前首先要构造一个线程对象,线程对象在构造的时候需要提供线程所需要的属性,如线程所属的线程组、线程优先级、是否是Daemon线程等信息。一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了parent是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程。

    启动线程

    线程对象在初始化完成之后,调用start()方法就可以启动这个线程。这个方法的含义是当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。

    1. public static void main(String[] args) {
    2. Thread thread01 = new Thread(() -> {
    3. System.out.println("thread01 run...");
    4. }, "thread01");
    5. thread01.start();
    6. }

    理解中断

            中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法 Thread.interrupted()对当前线程的中断标识位进行复位。如果线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返回false。
            在Java的API中有许多声明抛出InterruptedException的方法(例如Thread.sleep)这些方法在抛出异常前,Java虚拟机会先将该线程的中断标识位清除,此时调用isInterrupted方法将会返回false。

    1. public class InterruptedDemo {
    2. public static void main(String[] args) {
    3. Thread sleepThread = new Thread(new SleepRunner());
    4. Thread busyThread = new Thread(new BusyRunner());
    5. // 线程运行
    6. sleepThread.start();
    7. busyThread.start();
    8. System.out.println("两个线程运行前中断标识如下...");
    9. System.out.println("sleepThread: " + sleepThread.isInterrupted());
    10. System.out.println("busyThread: " + busyThread.isInterrupted());
    11. // 中断两个线程
    12. sleepThread.interrupt();
    13. busyThread.interrupt();
    14. System.out.println("两个线程中断后标识如下...");
    15. System.out.println("sleepThread: " + sleepThread.isInterrupted());
    16. System.out.println("busyThread: " + busyThread.isInterrupted());
    17. }
    18. // 该线程一直处于等待状态
    19. static class SleepRunner implements Runnable {
    20. @Override
    21. public void run() {
    22. while (true) {
    23. try {
    24. Thread.sleep(500);
    25. } catch (InterruptedException e) {
    26. e.printStackTrace();
    27. }
    28. }
    29. }
    30. }
    31. // 该线程一直处于运行状态
    32. static class BusyRunner implements Runnable {
    33. @Override
    34. public void run() {
    35. while (true) {
    36. }
    37. }
    38. }
    39. }

    从结果可以看出,抛出InterruptedException的线程sleepThread,其中断标识位被清除了,而busyThread线程中断标识位没有被清除。 

    过期的suspend()、resume()和stop()

    这些API是不建议使用的(过期方法),以suspend()为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。

    安全地终止线程

    除了中断以外,还可以利用一个boolean变量来控制是否需要停止任务并终止该线程 

    1. public class BooleanForInterruptedDemo {
    2. public static void main(String[] args) throws InterruptedException {
    3. Runner runner = new Runner();
    4. Thread thread = new Thread(runner);
    5. thread.start();
    6. // 睡眠500ms
    7. Thread.sleep(500);
    8. // 中断线程
    9. runner.stop();
    10. }
    11. static class Runner implements Runnable {
    12. private volatile long num = 0;
    13. // 中断标识位
    14. private volatile boolean flag = true;
    15. @Override
    16. public void run() {
    17. while (flag) {
    18. num++;
    19. }
    20. System.out.println("num: " + num);
    21. }
    22. // 终止线程方法
    23. public void stop() {
    24. flag = false;
    25. }
    26. }
    27. }

    这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅。


    等待/通知机制

    等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类java.lang.Object上。

    等待/通知的相关方法
    方法名称描述
    notify()通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到了对象的锁
    notifyAll()通知所有等待在该对象上的线程
    wait()调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回。调用该方法后,会释放对象的锁
    wait(long)超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回
    wait(long, int)对于超时时间更细粒度的控制,可以达到纳秒

    等待/通知代码示例 :

    1. public class WaitNotifyDemo {
    2. static boolean flag = true;
    3. // 锁对象
    4. static final Object lock = new Object();
    5. public static void main(String[] args) throws InterruptedException {
    6. Thread waitThread = new Thread(new Wait());
    7. Thread notifyThread = new Thread(new Notify());
    8. waitThread.start();
    9. // 睡眠1秒
    10. Thread.sleep(1000);
    11. notifyThread.start();
    12. }
    13. static class Wait implements Runnable {
    14. @Override
    15. public void run() {
    16. synchronized (lock) {
    17. long start = System.currentTimeMillis();
    18. // flag = false 则说明收到notify通知
    19. while (flag) {
    20. System.out.println("wait... flag: " + flag + " current time:" + start);
    21. try {
    22. lock.wait();
    23. } catch (InterruptedException e) {
    24. e.printStackTrace();
    25. }
    26. }
    27. System.out.println("break... flag: " + flag + " cost time:" + (System.currentTimeMillis() - start));
    28. }
    29. }
    30. }
    31. static class Notify implements Runnable {
    32. @Override
    33. public void run() {
    34. synchronized (lock) {
    35. flag = false;
    36. lock.notifyAll();
    37. }
    38. }
    39. }
    40. }

    需要注意的细节:

    • 使用wait()、notify()和notifyAll() 时需要先对调用对象加锁
    • 调用wait()方法后,线程状态由RUNNABLE变为WAITING,并将当前线程放置到对象的等待队列
    • notify()和notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用它们的线程释放锁之后,等待线程才有机会从wait()返回
    • notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING -> BLOCKED
    • 从wait()方法返回的前提是获得了调用对象的锁

    Thread.join()的使用

    如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis) 和 join(long millis, int nanos) 两个具备超时特性的方法。

    1. public class ThreadJoinTest {
    2. static int loopCount = 100;
    3. // 保证累加的原子性
    4. static AtomicInteger num = new AtomicInteger(0);
    5. public static void main(String[] args) throws InterruptedException {
    6. List threads = new ArrayList<>();
    7. for (int i = 0; i < loopCount; i++) {
    8. Thread thread = new Thread(new JoinDemo());
    9. threads.add(thread);
    10. }
    11. // 遍历调用
    12. for (Thread thread : threads) {
    13. thread.start();
    14. }
    15. // 遍历join等待
    16. for (Thread thread : threads) {
    17. thread.join();
    18. }
    19. System.out.println("num: " + num);
    20. }
    21. static class JoinDemo implements Runnable {
    22. @Override
    23. public void run() {
    24. for (int i = 0; i < 10000; i++) {
    25. num.getAndIncrement();
    26. }
    27. }
    28. }
    29. }

    ThreadLocal 的使用

    ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。

    1. public class ThreadLocalTest {
    2. private static final ThreadLocal TIME_THREADLOCAL = new ThreadLocal<>();
    3. public static void main(String[] args) throws InterruptedException {
    4. begin();
    5. Thread.sleep(1000);
    6. end();
    7. // 手动调用remove方法,防止内存泄露
    8. TIME_THREADLOCAL.remove();
    9. }
    10. public static void begin() {
    11. TIME_THREADLOCAL.set(System.currentTimeMillis());
    12. }
    13. public static void end() {
    14. Long begin = TIME_THREADLOCAL.get();
    15. System.out.println("cost time: " + (System.currentTimeMillis() - begin) + " ms");
    16. }
    17. }


    等待超时模式

    1. public class ConnectionPoolTest {
    2. private LinkedList pool = new LinkedList<>();
    3. // 构造器:初始化连接池
    4. public ConnectionPoolTest(int initialSize) {
    5. if (initialSize > 0) {
    6. for (int i = 0; i < initialSize; i++) {
    7. pool.addLast(new Connection());
    8. }
    9. }
    10. }
    11. public Connection fetchConnection(long mills) throws InterruptedException {
    12. synchronized (pool) {
    13. // 完全超时,一直等待
    14. if (mills <= 0) {
    15. while (pool.isEmpty()) {
    16. pool.wait();
    17. }
    18. return pool.removeFirst();
    19. } else {
    20. // 暂存未来应该到达的超时时间点
    21. long future = System.currentTimeMillis() + mills;
    22. long remaining = future;
    23. while (pool.isEmpty() && remaining > 0) {
    24. pool.wait(remaining);
    25. remaining = future - System.currentTimeMillis();
    26. }
    27. // 若因超时退出循环直接返回null
    28. Connection connection = null;
    29. if (!pool.isEmpty()) {
    30. connection = pool.removeFirst();
    31. }
    32. return connection;
    33. }
    34. }
    35. }
    36. public void releaseConnection(Connection connection) {
    37. if (connection != null) {
    38. synchronized (pool) {
    39. pool.addLast(connection);
    40. pool.notifyAll();
    41. }
    42. }
    43. }
    44. }
    45. // mock连接池对象
    46. class Connection {
    47. }

    等待超时模式就是在等待/通知范式基础上增加了超时控制,这使得该模式相比原有范式更具有灵活性,因为即使方法执行时间过长,也不会 “永久” 阻塞调用者,而是会按照 “调用者”的要求 “按时” 返回。

  • 相关阅读:
    LTE MAC2 SR-BSR
    【LC刷题】DAY24:122 55 45 1005
    【云原生进阶之数据库技术】第四章-GaussDB-1-简介
    韩顺平java 515-520即时笔记
    DFS剪枝
    NodeJS入门以及文件模块fs模块
    Java 基础(继承、接口、抽象)
    CVPR最佳论文:谷歌基于Spectral Volume从单图生成视频
    Qi标准无线供电模块如何处理噪声抑制语音通讯接收灵敏度
    R函数optim()最小化或者最大化多参数函数
  • 原文地址:https://blog.csdn.net/shuttlepro/article/details/127724995