第4章 Java并发编程基础
4.1 线程简介
4.1.1 什么是线程
线程是轻量级进程,一个进程可以创建多个线程,各个线程拥有各自的计数器、堆栈和局部变量等属性。
注意:一个Java程序的运行不仅仅是main()方法的运行,而是main线程和多个其他线程的同时运行。其他线程:
Signal Dispatcher(分发处理发送给jvm信号的线程)、Finalizer(调用对象finalize方法的线程)、Reference Handler(清除Reference的线程)、main(main线程,用户程序入口)
4.1.2 为什么要使用多线程
原因有:更多的处理器核心;更快的响应时间;更好的编程模型
4.1.3 线程优先级
在java中,通过整型成员变量priority来控制优先级,优先级范围:1~10,可以使用setPriority(int)修改优先级,默认是5.优先级高的线程分配时间片的数量多。
注意:线程优先级不能依赖线程的优先级高低。
4.1.4 线程状态
Java线程的状态:NEW、RUNNABLE、BLOCKED、WAITING、TIME_WAITING、TERMINATED
4.1.5 Daemon线程
当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过Thread.setDaemon(true)设置。
注意:1、Daemon属性需要在启动线程之前设置,不能在启动线程之后设置。
2、在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。
4.2 启动和终止线程
4.2.1 构造线程
继承Thread类,实现Runnable接口、Callable接口
4.2.2 启动线程
调用start()方法
注意:启动一个线程前,最好为这个线程设置线程名称,因为这样在使用jstack分析程序或者进行问题排查时,就会给开发人员提供一些提示,自定义的线程最好能够起个名字。
4.2.3 理解中断
线程可以通过调用其它线程的interrupt()对其他线程进行终端操作。
线程可以通过isInterrupted()判断是否被中断
也可以通过静态方法Thread.interrupted()对当前线程的中断标识位进行复位。
4.2.4 过期的suspend()、resume()和stop()
suspend()(暂停)、resume()(恢复)和stop()(停止)
suspend()方法,在调用后,线程不会释放已经占有的资源(比如锁),而是占着资源进入睡眠状态,容易发生死锁问题。
同样,stop()方法在终结一个线程时不会保证线程的资源的正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。
注意:正因为suspend()、resume()和stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法,而暂停和恢复操作可以用后面提到的等待/通知机制来替代。
4.2.5 安全地终止线程
线程可以通过中断操作和cancel()方法均可使Thread终止。
这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,所以这种方式显得更加安全和优雅。
4.3 线程间通信
4.3.1 volatile和synchronized关键字
线程开始运行,拥有自己的栈空间,Java支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个变量的拷贝(虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是可以拥有一份拷贝,这样做的目的是加速程序的执行,这是现代多核处理器的一个显著特性),所以程序在执行过程中,一个线程看到的变量并不一定是最新的。
关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。
同步代码就不在贴出来,对于同步块的实现使用了monitorenter和monitorexit指令,而同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的。无论采用哪种方式,其本质是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。
任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态。
从图4-2中可以看到,任意线程对Object(Object由synchronized保护)的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。
4.3.2等待/通知机制
这个机制背景就是为了解耦生产者、消费者的问题,简单的办法是使用轮询,但是轮询缺点是及时性、性能不能保证,所以采用通知机制避免轮询带来的性能损失。
等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类java.lang.Object上,方法和描述如表4-2所示。
等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
注意:调用wait()、notify()以及notifyAll()时需要注意的细节,如下:
1)使用wait()、notify()和notifyAll()时需要先对调用对象加锁。
2)调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列。
3)notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifAll()的线程释放锁之后,等待线程才有机会从wait()返回。
4)notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED。
5)从wait()方法返回的前提是获得了调用对象的锁。
所以:等待/通知机制依托于同步机制,其目的就是确保等待线程从wait()方法返回时能够感知到通知线程对变量做出的修改。
在图4-3中,WaitThread首先获取了对象的锁,然后调用对象的wait()方法,从而放弃了锁并进入了对象的等待队列WaitQueue中,进入等待状态。由于WaitThread释放了对象的锁,
NotifyThread随后获取了对象的锁,并调用对象的notify()方法,将WaitThread从WaitQueue移到SynchronizedQueue中,此时WaitThread的状态变为阻塞状态。NotifyThread释放了锁之后,
WaitThread再次获取到锁并从wait()方法返回继续执行。
4.3.3等待/通知的经典范式
从上节的示例中可以提取经典范式,分为等待方(消费者)和通知方(生产者)
等待方:
1)获取对象锁;
2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
3)条件满足则执行对应的逻辑。
对应的伪代码如下:
通知方:
1)获得对象的锁;
2)改变条件;
3)通知所有等待在对象上的线程。
对应的伪代码如下:
4.3.4 管道的输入、输出流
管道的输入、输出流主要用于线程间的数据传输,传输的媒介为内存。
这块只是淡出提了下,属于nio的范畴,需单独整理,例子就不贴了。
4.3.5 Thread.join的使用
如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(longmillis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回。
下图是JDK中Thread.join()方法的源码(进行了部分调整)
4.3.6 ThreadLocal的使用
作者只是举例演示使用方式,这里注意使用场景,具体参见这篇文章:ThreadLock.
4.4线程应用实例
这里不细写了,作者分别介绍了数据库连接池示例、线程池技术、基于线程池的简单web服务器。可以参照原书去理解。