文章目录
进程是指运行中的应用程序,每一个进程都有自己独立的内存空间,对一个应用程序可以同时启动多个进程。线程是指进程中的一个执行流程,有时也称为执行情景。一个进程可以由多个线程组成,即在一个进程中可以同时运行多个不同的线程,它们分别执行不同的任务。当进程内的多个线程同时运行时,这种运行方式称为并发运行。
线程与进程的主要区别在于:每个进程都需要操作系统为其分配独立的内存地址空间,而同一进程中的所有线程在同一块地址空 间中工作,这些线程可以共享同一块内存和系统资源。
13.1 Java 线程的运行机制
在 Java 虚拟机进程中,执行程序代码的任务是由线程来完成的。每个线程都有一个独立的程序计数器和方法调用栈(method invocation stack):
- 程序计数器:也称为 PC 寄存器,当线桯执行一个方法时,程序计数器指向方法区中下一条要执行的字节码指令。
- 方法调用栈:简称方法栈,用来跟踪线程运行中一系列的方法调用过程,栈中的元素称为栈桢。每当线程调用一个方法时,就会向方法栈压入一个新桢,桢用来存储方法的参数、局部变量和运算过程中的临时数据。
栈桢由 3 部分组成:
- 局部变量区:存放局部变量和方法参数 。
- 操作数栈:是线程的工作区,用来存放运算过程中生成的临时数据。
- 栈数据区:为线程执行指令提供相关的信息,包括:如何定位到位于堆区和方法区的特定数据,如何正常退出方法或者异常中断方法。
每当用 java 命令启动一个 Java 虚拟机进程时,Java 虚拟机就会创建一个主线程,该线程从程序入口 main()方法开始执行。
public class Sample {
private int a; //实例变量
public int method() {
int b = 0; //局部变量
a++;
b = a;
return b;
}
public static void main(String[] args) {
Sample s = null; //局部变量
int a = 0; //局部变量
s = new Sample();
a = s.method();
System.out.println(a);
}
}
/*
当线程开始执行 method() 方法的 a++ 操作时,运行时数据区的状态如下
____Java栈区_____ _______堆区_____ _________方法区________
|method方法的栈帧 | | | | |
| ·局部变量a | | | | Sample类的数据结构 |
|main方法的栈帧 | | | | ·method方法的字节码 |
| ·局部变量s --|--|-->Sample对象--|--|---> ·(具体指令) <--|---主线程程序计数器
| ·局部变量a | | ·实例变量a | | |
*/
当主线程执行 a++ 操作时,它能根据 method() 方法的栈桢的栈数据区中的有关信息,正确地定位到堆区的 Sample 对象的实例变量 a, 把它的值加 1。当 method() 方法执行完毕后,它的栈桢就会从方法栈中弹出,它的局部变量 b 结束生命周期。main() 方法的栈桢成为当前桢,主线程继续执行 main() 方法。
方法区存放了线程所执行的字节码指令,堆区存放了线程所操作的数据(以对象的形式存放),Java 栈区则是线程的工作区,保存线程的运行状态。
13.2 线程的创建和启动
Java 虚拟机的主线程,它从启动类的 main() 方法开始运行。此外,用户还可以创建自己的线程,它将和主线程并发运行。创建线程有两种方式:
扩展 java.lang.Thread 类
Thread 类代表线程类,它的最主要的两个方法是:
run()
:包含线程运行时所执行的代码。start()
:用于启动线程。
用户的线程类只需继承 Thread 类,覆盖 Thread 类的 run() 方法。在 Thread 类中,run() 方法的定义如下:public void run()
。该方法没有声明抛出任何异常,根据方法覆盖的规则,Thread 子类的 run() 方法也不能声明抛出任何异常。
- 主线程于用户自定义的线程并发运行
package allrun; public class Machine extends Thread { public void run() { for (int i = 0; i < 50; i++) { //Thread 类的 currentThread() 静态方法返回当前线程的引用 //Thread 类的 getName()实例方法则返回线程的名字 // 相当于 Thread thread = Thread.currentThread(); // String name = Thread.getName(); System.out.println(currentThread().getName() + i); try { sleep(100); //给其他线程运行机会 } catch (InterruptedException e) { throw new RuntimeException(e); } } } public static void main(String[] args) { Machine m1 = new Machine(); Machine m2 = new Machine(); m1.start(); //启动线程 m2.start(); m1.run(); //主线程执行m1对象的run()方法 } }
- 多个线程共享同一个对象的实例变量
package sharevar; public class Machine extends Thread { private int i = 0; //实例变量 public void run() { for (i = 0; i < 50; i++) { System.out.println(currentThread().getName() + i); try { sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } } } public static void main(String[] args) { //主线程和 Machine 线程并发执行 Machine 对象的 run() 方法,操作同一个实例变量轮流加1 Machine machine = new Machine(); machine.start(); //启动一个Machine线程 m1.run(); //主线程执行run()方法 /** 若修改方法: Machine m1 = new Machine(); Machine m2 = new Machine(); m1.start(); m2.start(); 实例方法和静态方法的字节码都位于方法区,被所有的线程共享。 意味着当 m1 线程执行 run()方法时,会把 run() 方法中的变量 i 解析为 m1 对象的实例变量 同理当 m2 线程执行 run()方法时,会把 run() 方法中的变量 i 解析为 m2 对象的实例变量 即 m1 m2 线程分别操作不同的实例变量 i */ } }
- 不要随便覆盖 Thread 类的 start() 方法
创建了一个线程对象后,线程并不自动开始运行,必须调用它的 start() 方法才能启动线程。JDK 为 Thread 类的 start() 方法提供了默认的实现。当用 new 语句创建 Machine 对象时,仅仅在堆区内出现一个新的对象但 Machine 线程并没有被启动。当主线程执行 Machine 对象的 start() 方法时才会启动线程,并在 Java 栈区为其创建相应的方法调用栈。
故在 Thread 子类中不应该随意覆盖 start() 方法,假如一定要覆盖 start() 方法,那么应该先调用package wrongstart; public class Machine extends Thread { private int i = 0; public void start() { //覆盖start()方法 run(); } public void run() { for (int i = 0; i < 50; i++) { System.out.println(currentThread().getName() + i); try { sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } } } public static void main(String[] args) { Machine machine = new Machine(); machine.start(); //当主线程执行 machine.start(), start() 方法并没有启动一个新的 Machine 线程, //而是去调用 Machine 对象的 run() 方法,这只是普通的方法调用 //即 Machine 对象的 run() 方法是由主线程执行的 //打印结果: main:0 main:1 ... main:49 //而非:thread0:1 thread0:2 ...thread0:49 } }
super.start()
方法。package correctstart; public class Machine extends Thread { private int i = 0; private static int count = 0; //统计被启动的 Machine 线程的数目 public void start() { super.run(); System.out.println(currentThread().getName() + (++count) + "isStart"); //本行代码由主线程执行 } public void run() { for (int i = 0; i < 50; i++) { System.out.println(currentThread().getName() + i); try { sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } } } public static void main(String[] args) { Machine m1 = new Machine(); Machine m2 = new Machine(); m1.start(); m2.start(); } }
- 一个线程只能被启动一次
Machine m = new Machine(); m.start(); m.start(); //第二次调用时会抛出 Java.lang.IllegalThreadStateException 异常
实现 Runnable 接口
Java 不允许一个类继承多个类,因此一旦一个类继承了 Thread 类,就不能再继承其他的类。为了解决这一问题,Java 提供了 java.lang.Runnable 接口,它有一个 run() 方法,它的定义如下:public void run()
。
package runimpl;
public class Machine implements Runnable {
private int i = 0;
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(currentThread().getName() + i);
try {
sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) {
//主线程创建的 t1 和 t2 两个线程对象启动后都会执行 machine 变量所引用的 Machine 对象的 run() 方法
//t1 和 t2 共享同一个 machine对象,因此在执行 run() 方法时操纵同一个实例变量 i
Machine machine = new Machine();
Thread t1 = new Thread(machine);
Thread t2 = new Thread(machine);
t1.start();
t2.start();
/** 若修改方法:
Machine m1 = new Machine();
Machine m2 = new Machine();
Thread t1 = new Thread(m1);
Thread t2 = new Thread(m2);
t1.start();
t2.start();
启动 t1 和 t2 线程后,将分别执行 machine1 和 machine2 变量各自引用的 Machine 对象的 run() 方法
因此 t1 和 t2 线程操纵不同的 Machine 对象的实例变量 i
*/
}
}
在 Thread 类中定义了如下形式的构造方法:
Thread(Runnable runnble) //当线程启动时,会执行参数 runnable 所引用对象的 run() 方法
13.3 线程的状态转换
线程在它的生命周期中会处于各种不同的状态,
- 新建状态
用 new 语句创建的线程对象处于新建状态(New),此时它和其他Java对象一样仅仅在堆区内被分配了内存。 - 就绪状态
当一个线程对象创建后,其他线程调用其 start() 方法,该线程进入就绪状态(Runnable),Java 虚拟机为其创建方法调用栈和程序计数器。处于该状态的线程位于可运行池中,等待CPU的使用权。、 - 运行状态
处于运行状态(Running) 的线程占用 CPU,执行程序代码。在并发运行环境中,如果计算机只有一个 CPU, 那么任何时刻只会有一个线程处于这个状态。如果计算机有多个 CPU, 那么同一时刻可以让几个线程占用不同的 CPU, 使它们都处于运行状态。只有处于就绪状态的线程才有机会转到运行状态。 - 阻塞状态
阻塞状态(Blocked)是指线程因为某些原因放弃 CPU,暂时停止运行。当线程处于阻塞状态时,Java 虚拟机不会给线程分配 CPU, 直到线程重新进入就绪状态,它才有机会转到运行状态。
阻塞状态可分为 3 种:- 位于对象等待池中的阻塞状态(Blocked in object’s wait pool):当线程处于运行状态时,如果执行了某个对象的
wait()
方法,Java 虚拟机就会把线程放到这个对象的等待池中。 - 位于对象锁池中的阻塞状态( Blocked in object’s lock pool):当线程处于运行状态,试图获得某个对象的 同步锁 时,如果该对象的同步锁已经被其他线程占用,Java 虚拟机就会把这个线程放到这个对象的锁池中。
- 其他阻塞状态(Otherwise Blocked):当前线程执行了
sleep()
方法,或者调用了其他线程的join()
方法,或者发出了 I/O 请求时,就会进入这个状态。
//主线程启动一个 Machine 线程后,就等待用户的标准输入,主线程进入阻塞状态, //Machine 线程占用 CPU 继续运行。直到用户输入数据,主线程才会恢复运行 package waitio; public class Machine extends Thread { private static StringBuffer log = new StringBuffer(); private static int count = 0; public void run() { for(int i = 0; i < 50; i++) System.out.println(currentThread().getName() + i); } public static void main(String[] args) throws Exception { Machine machine = new Machine(); machine.start(); //主线程进入阻塞状态,等待用户的输入,直到获得用户输入的数据,才退出阻塞 int data = System.in.read(); machine.run(); }
- 位于对象等待池中的阻塞状态(Blocked in object’s wait pool):当线程处于运行状态时,如果执行了某个对象的
- 死亡状态
当线程退出 run() 方法以后,就进入死亡状态(Dead),该线程结束生命周期。线程有可能是正常执行完 run() 方法而退出的,也有可能是遇到异常而退出的。不管线程是正常结束还是异常结束,都不会对其他线程造成影响。package withex; public class Machine extends Thread { public void run() { for(int i = 0; i < 3; i++) { System.out.println(currentThread().getName() + i); if (i ==1 && currentThread().getName().equals("m1")) throw new RuntimeException("Wrong from Machine"); try{ sleep(100); } catch(InterruptedException e) { throw new RuntimeException(e); } } } public static void main(String[] args) throws Exception{ Machine machine = new Machine(); machine.setName("m1"); machine.start(); machine.run(); //Thread 类的 isAlive() 方法判断一个线程是否活着,当线程处于死亡状态或者新建状态时返回false System.out.println("Is machine alive: "+ machine.isAlive()); System.out.println("main:end"); } }
13.4 线程调度
计算机通常只有 一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得 CPU 的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线和轮流获得 CPU 的使用权,分别执行各自的任务。在可运行池中,会有多个处于就绪状态的线程在等待CPU,Java 虚拟机的一项任务就是负责线程的调度。线程的调度是指按照特定的机制为多个线和分配 CPU 的 使用权,有两种调度模型:分时调度模型和抢占式调度模型。
分时调度模型是指让所有线程轮流获得 CPU 的使用权, 并且平均分配每个线程占用CPU的时间片。Java 虚拟机采用抢占式调度栈型,它是指优先让可运行池中优先级高的线程占用CPU, 如果可运行池中线程的优先级相同,那么就随机地选择一个线程,使其占用 CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。值得注意的是,线程的调度不是是跨平台的,它不仅取决于 Java 虚拟机,还依赖于操作系统。
Java 线桯的调度不是分时的,同时启动多个线程后,不能保证各个线程轮流获得均等的 CPU 时间片。如果希望明确的让一个线程给另一个线程运行的机会,可以采用一下方法:
- 调整各个线程的优先级。
- 让处于运行状态的线程调用 Thread.sleep()方法。
- 让处于运行状态的线程调用 Thread.yield()方法。
- 让处于运行状态的线程调用另-个线程的 join()方法。
调整优先级
所有处于就序状态的线程根据优先级存放在可运行池中,优先级低的线桯获得较少的运行机会,优先级高的线程获得较多的运行机会。Thread 类的 setPriority(int i)
和 getPriority()
方法分别用来设置优先级和读取优先级。优先级用整数表示,取值范围是1~10, Thread 类有 3 个静态常量:
- MAX_PRIORITY: 取值为 10, 表示最高优先级。
- MIN_PRIORITY: 取值为 1, 表示最低优先级。
- NORM_PRIORITY: 取值为 5, 表示默认的优先级。
线程睡眠
当一个线程在运行中执行了 sleep()
方法,它就会放弃 CPU,转到阻塞状态。Thread 类的 sleep(long millis)
方法是静态的,millis 参数设定睡眠的时间,以亳秒为单位。当线程结束睡眠时,首先转到就绪状态,假如存在另一线桯正在运行,该线程不一定会立即运行,而是在可运行池中等待获得 CPU。线程在睡眠时如果被中断,就会收到一个 InterruptedException 异常,线程跳到异常处理代码块。
// Sleeper 类的 main() 方法中,主线程启动 Sleeper 线程, Sleeper 线程睡眠 1 分钟,
// 主线程睡眠 10 毫秒后中断 Sleeper 线程的睡眠
public class Sleeper extends Thread{
public void run() {
try{
sleep(60000); // 睡眠 l 分钟
System.out.println("sleep over");
} catch (InterruptedException e){
System.out.println("sleep interrupted");
}
System.out.println("end");
}
public static void main(String[] args) throws Exception{
Sleeper sleeper = new Sleeper();
sleeper.start();
Thread.sleep(10);
sleeper.interrupt(); //中断 Sleeper 线程的睡眠
}
}
线程让步
当线程在运行中执行了 Thread 类的 yield()
静态方法时,如果此时具有相同优先级的其他线程处于就绪状态,yield() 方法将把当前运行的线程放到可运行池中并使另一个线程运行。如果没有相同优先级的可运行进程, yield() 方法什么都不做。
public void run() {
for (int i = 0; i < 20; i++) {
log.append(currentThread().getName() + i);
if (++ count % 10 == 0) log.append("\n");
yeild();
}
}
sleep() 方法和 yield() 方法都是 Thread 类的静态方法,都会使当前处于运行状态的线程放弃 CPU,把运行机会让给别的线程。 两者的 区别 在于:
- sleep() 方法会给其他线程运行的机会,不考虑其他线程的优先级,因此会给较低优先级线程一个运行的机会;yield() 方法只会给相同优先级或者更高优先级的线程一个运行的机会。
- 当线程执行了 sleep() 方法以后,将转到阻塞状态,参数 millis 指定睡眠时间;当线程执行了 yield() 方法以后,将转到就绪状态。
- sleep() 方法声明抛出 InterruptedException 异常,而 yield() 方法没有声明抛出任何异常。
- sleep() 方法比 yield() 方法具有更好的可移植性。不能依靠 yield() 方法来提高程序的并发行能。对于大多数程序员来说,yield() 方法的唯一用途是在测试期间入为地提高程序的并发性能,以帮助发现一些隐藏的错误。
等待其他线程结束
当前运行的线程可以调用另一个线程的 join()
方法,当前运行的线程将转到阻塞状态直至另一个线程运行结束,它才会恢复运行。
package join;
public class Machine extends Thread {
public void run() {
for(int i = 0; i < 50; i++)
System.out.println(getName() + i);
}
public static void main(String[] args) throws Exception {
Machine machine = new Machine();
machine.setName("m1");
machine.start();
System.out.println("main:join machine");
machine.join(); //主线程等待 machine 线程运行结束
System.out.println("main:end");
}
}
join() 方法有两种重载形式:
public void join()
public void join(long timeout) //timeout 参数设定当前线程被阻塞的时间,以毫秒为单位
13.5 获得当前线程对象的引用
Thread 类的 currentThread()
静态方法返回当前线程对象的引用。
package threadref;
public class Machine extends Thread {
public void run() {
for(int i = 0; i < 3; i++) {
System.out.println(currentThread().getName() + i);
/**
打印结果 main:0 m1:0 main:1 m1:1 main:2 m1:2
如果修改 currentThread() 为 this
System.out.println(this.getName() + i);
则打印结果:m1:0 m10 m1:1 m1:1 m1:2 m1:2
*/
yield();
}
}
public static void main(String[] args) throws Exception {
Machine machine = new Machine();
machine.setName("m1");
machine.start(); //启动线程m1
machine.run(); //主线程普通调用run()方法
}
}
不管是主线程还是 machine 线程,都执行 machine 对象的 run() 方法, run() 方法中的 this 关键字引用当前的 machine 对象,因此 this.getName() 方法总是返回 machine 对象的 name 属性。
13.6 后台线程
后台线程的特点是:后台线程与前台线程相伴相随,只有当所有前台线程结束生命周期后,后台线程才会结束生命周期。只要有一个前台线程还没有运行结束,后台线程就不会结束生命周期。
// Machine 线程是前台线程,负责把它的实例变量 a 的值不断地加 1
// 在 Machine 线程的 start() 方法中的匿名线程类的实例作为后台线程,定期把 Machine 对象的实例变量归零
package withdaemon;
public class Machine extends Thread {
private int a;
private static int count;
public void start() {
super.start();
Thread deamon = new Thread() { //匿名线程类
public void run() {
while(true) {
reset();
try {
sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
deamon.setDaemon(true); //Thread 类的 setDaemon(true) 方法设置线程为后台线程
deamon.start();
}
public void reset() {
a = 0;
}
public void run() {
while (true) {
System.out.println(getName() + i++);
if (count++ == 100) break;
yield();
}
}
public static void main(String[] args) throws Exception {
Machine m = new Machine();
m.start();
}
}
尽管以上匿名的后台线程的 run() 方法执行的是无限循环,但只要其他前台线程都运行结束,Java 虚拟机就会终止这个后台线程。
在使用后台线程时,有以下注意点;
- Java 虚拟机所能保证的是:当所有前台线程运行结束 ,假如后台线程还在运行,Java 虚拟机会终止后台线程。此外,后台线程是否一定在前台线程的后面结束生命周期,还取决于程序的实现。
- 只有在线程启动前(即调用 start() 方法以前),才能把它设置为后台线程。如果线程启动后,再调用这个线程的 setDaemon() 方法 ,会导致 IllegalThreadStateException 异常。
- 由前台线程创建的线程默认悄况下仍然是前台线程,由后台线程创建的线程默认情况下仍然是后台线桯。
13.7 定时器
在 JDK 的 java.util 包中提供了一个实用类定时器 Timer, 它能够定时执行特定的任务。TimerTask 类表示定时器执行的一项任务。java.util.TimerTask 类是一个抽象类,它实现了 Runnable 接口。
java.util.Timer 类的构造方法有几种重载形式,有一种构造方法 Timer(boolean isDaemon)
允许把与 Timer 关联的线程设为后台线程。 Timer 类本身并不是线程类,但是在它的实现中,利用线程来执行定时任务。
Timer 类的 schedule(TimerTask task, long delay, long period)
方法用来设置定时器需要定时执行的任务。task 参数表示任务,delay 参数表示延迟执行的时间,以毫秒为单位,period 参数表示每次执行任务的间隔时间,以毫秒为单位。schedule 方法还有一种重载形式:schedule(TimerTask task, long delay)
表示仅仅执行一次任务 ,
package usetimer;
import java.util.Timer;
import java.util.TimerTask;
public class Machine extends Thread {
private int a;
public void start() {
super.start();
Timer timer = new Timer(true); //把与 Timer 关联的线程设为后台线程
TimerTask task = new TimerTask() { //匿名类
public void run() {
reset();
}
};
timer.schedule(task, 10, 50); //设置定时任务
}
public void reset() {a = 0;}
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(getName() + a++);
yield();
}
}
public static void main(String[] args) throws Exception {
Machine m = new Machine();
m.start();
}
}
13.8 线程的同步
线程的职责就是执行一 些操作,而多数操作都涉及处理数据。原子操作由相关的一组操作完成,这些操作可能会操作与其他线程共享的资源。为了保证得到正确的运算结果,一个线程在执行原了操作的期间,应该采取措施使得其他线程不能操作共享资源。
下面再以生产者-消费者的例子来演示多个线程对共享资源的竞争。
package producerconsumer;
public class SyncTest {
public static void main(String[] args) {
Stack stack = new Stack("stack1");
Producer producer1 = new Producer(stack, "producer1");
Consumer consumer1 = new Consumer(stack, "consumer1");
}
}
//生产者线程,不断想堆栈内加入产品
class Producer extends Thread {
private Stack theStack;
public Producer(Stack s, String name) {
super(name);
theStack = s;
start(); // 启动自身生产者线程
}
public void run() {
String goods;
for (int i = 0; i < 200; i++) {
//当一个生产者线程试图执行以下代码时,必须先获得 theStack 变量引用的 Stack 对象的锁
synchronized(theStack) {
goods = "goods" + (theStack.getPointer() + 1);
theStack.push(goods);
System.out.println(getName() + "push" + goods + "to" + theStack.getName());
yield();
}
}
}
}
//消费者线程,不断从堆栈中取出产品
class Consumer extends Thread {
public Consumer(Stack s, String name) {
super(name);
theStack = s;
start(); // 启动自身消费线程
}
public void run() {
String goods;
for (int i = 0; i < 200; i++) {
goods = theStack.pop();
System.out.println(getName() + "pop" + goods + "from" + theStack.getName());
yield();
}
}
}
// 堆栈类
class Stack {
private String name;
private final int SIZE = 100;
private String[] buffer = new String[SIZE];
int pointer = -1;
public Stack(String name) {this.name = name;}
public String getName() {return name;}
public synchronized int getPointer() {return pointer;}
public synchronized String pop() {
if (pointer == -1) return null;
String goods = buffer[pointer];
buffer[pointer] = null;
Thread.yield();
pointer--;
return goods;
}
public synchronized boolean push(String goods) {
if (pointer == SIZE - 1) return false;
pointer++;
Thread.yield();
buffer[pointer] = goods;
}
}
当生产者线程开始执行 push() 方法以后,或者消费者线程开始执行 pop() 方法时,都必须先获得 Stack 对象的锁,如果这把锁已经被其他线程占用,那么另一个线程就只能在锁池中等待。另外,对于生产者线程而言,getPoint() 和 push() 方法必须作为一个原子操作,当 producer1 线程执行这个原子操作时,其他线程不允许修改 Stack 对象的 pointer 属性。
为了保证每个线程能正常执行原子操作, Java 引入了同步机制,具体作法是在代表原子操作的程序代码前加上 synchronized
标记,这样的代码被称为同步代码块。
每个 Java 对象都有且只有一个同步锁,在任何时刻,最多只允许一个线程拥有这把锁。当消费者线程试图执行以上带有 synchronized(this) 标记的代码块时,消费者线程必须首先获得 this 关键字引用的 Stack 对象的锁。在以下两种情况,消费者线程有着不同的命运:
- 假如这个锁已经被其他线程占用, Java 虚拟机就会把这个消费者线程放到 Stack 对象的锁池中,消费者线程进入 阻塞状态。 在 Stack 对象的锁池中可能会有许多等待锁的线程。等到其他线程释放了锁,Java 虚拟机会从锁池中随机地取出一个线程,使这个线程拥有锁,并且转到就绪状态。
- 假如这个锁没有被其他线程占用,消费者线程就会获得这把锁,开始执行同步代码块。一般情况下,消费者线程只有执行完同步代码块,才会释放锁,使得其他线程能够获得锁 , 当然也有一些例外情况。
如果一个方法中的所有代码都属于同步代码,那么可以直接在方法前用 synchronized
修饰。下面两种方式是等价的:
public synchronized String pop() {...}
// 等价于
public String pop() {
synchronized(this) {...}
}
线程同步的特征
线程同步具有以下特征 :
- 如果一个同步代码块和非同步代码块操作共享资源,这仍然会造成对共享资源的竞争。
- 每个对象都有唯一的同步锁。
//如果修改SynTest类的main方法如下 public static void main(String[] args) { Stack stack1 = new Stack("stack1"); Stack stack2 = new Stack("stack2"); Producer producer1 = new Producer(stack1, "producer1"); Consumer consumer1 = new Consumer(stack2, "consumer1"); } //生产者线桯与消费者线程分别操作不同的 Stack 对象, //当生产者线程试图执行stack1 对象的 push()方法时,只需获得 stack1 对象的锁 //当消费者线程试图执行 stack2 对象的 pop()方法时,只需获得 stack2 对象的锁 //因此这两个线程之间不会同步
- 在静态方法前面也可以使用 synchronized 修饰符。
package synstatic; public class Machine implements Runnable { private static int a = 1; //静态变量 public synchronized static void go(int i) { a += i; Thread.yield(); a -= i; System.out.println(a); } public void run() { for (int i = 0; i < 1000; i++) go(i); } public static void main(String[] args) throws InterruptedException { Machine machine = new Machine(); Thread t1 = new Thread(machine); Thread t2 = new Thread(machine); t1.start(); t2.start(); } } //每个被加载到 Java 虚拟机的方法区的类也有唯一的同步锁 //当 t1 线程试图执行Machine 类的 go() 静态方法时,必须获得 Machine 类的锁, //假如这把锁已经被 t2 线程占用,那么 Java 虚拟机会把 t1 线程放到 Machine 类的锁池中,t1 线程进入阻塞状态。
- 当一个线程开始执行同步代码块,并不意味在着必须以不中断的方式运行。进入同步代码块的线程也可以执行 sleep() 或者 yield() 方法,此时它并没有释放锁,只是把运行机会(即 CPU) 让给了其他的线程。
package synsleep; public class Machine implements Runnable { private int a= 1; //共享数据 public void run() { for (int i = 0; i < 1000; i++){ synchronized(this){ a += i; try { Thread.sleep(500); //给其他线程运行的机会 } catch (InterruptedExccption e){ throw new RuntimeException(e); } a -= i; System.out.println(Thread.currentThread().getName() + a); } } } public void go() { for (int i = 0; i < 1000; i++) { System.out.println(Thread.currentThread().getName() + i); Thread.yield(); } } public static void main(String[] args) throws InterruptedException { Machine machine = new Machine(); Thread t1 = new Thread(machine); Thread t2 = new Thread(machine); t1.start(); t2.start(); machine.go() } } //在 Machine 类的 run () 方法中包含操作实例变量 a 的原子操作,因此把它声明为同步代码块 //Machine 类的 go() 方法仅仅操纵局部变量无须同步 //当t1线程执行同步代码块中的sleep()方法时就开始睡眠,此时t1线程放弃CPU,但仍然持有 Machine 对象的锁 // 主线程有机会获得 CPU, 进入运行状态,而 t2 线程因为没有得到 Machine 对象的锁,依然在锁池中等待
- synchronized 声明不会被继承。如果一个用 synchronized 修饰的方法被了类覆盖,那么子类中这个方法不再保持同步,除非也用 synchronized 修饰。
同步与并发
同步是解决共享资源竞争的有效手段,但是,多线程的同步与并发是一对此消彼长的矛盾。为了提高并发性能,应该使得同步代码块中包含尽可能少的操作,使得一个线程能尽快释放锁,减少其他线程等待锁的时间 。
//有 10 个人到同一口井里打水,每个人都要打 10 桶水,人代表线程,井代表共享资源
//注释部分实现的是每人依次打10桶水的同步方式
//代码部分实现的是每人一次打1桶水轮10次的同步方式(对well尽早释放锁)
public class Person extends Thread {
private Well well;
public Person(Well well) {
this.well = well;
start(); //启动自身线程
}
/*
尽管调用了 yield 方法让出CPU,但是并没有释放Well对象的锁,导致其他Person线程不能进行
public void run() {
synchronized(well) {
for (int i = 0; i < 10; i++) {
well.withdraw();
yield();
}
}
}
*/
public void run() { //取消同步
for (int i = 0; i < 10; i++) {
well.withdraw();
yield();
}
}
public static void main(String[] args) {
Well well = new Well();
Person[] persons = new Person[10];
for (int i = 0; i < 10; i++)
person[i] = new Person(well);
}
}
class Well {
private int water = 1000; //共享数据
public synchronized void withdraw() { //对 withdraw方法同步
water--;
System.out.println(Thread.currentThread().getName() + "warter left:"+water);
}
/*
public void withdraw() {
water--;
System.out.println(Thread.currentThread().getName() + "warter left:"+water);
}
*/
}
线程安全的类
一个线程安全的类满足以下条件:
- 这个类的对象可以同时被多个线程安全地访问。
- 每个线程都能正常执行原子操作,得到正确的结果。
- 每个线程的原子操作完成后,对象处于逻辑上合理的状态。
不可变类总是线程安全的,因为它的对象的状态始终不会变化,任何线程只能读取对象的状态,而不能改变它的状态。对于可变类,如果要保证它线程安全,必须根据实际情况,对某些原子操作进行同步。
可变类的线程安全往往以降低并发性能为代价,为了减小这一负面影响,可以采取以下措施:
- 只对可能导致资源竞争的代码进行同步。
- 如果一个可变类有两种运行环境:单线程运行环境和多线程运行环境,那么可以为这个类提供两种实现,在单线程运行环境中使用未采取同步的类的实现,在多线桯运行环境中使用采取同步的类的实现。
释放对象的锁
由于等待一个锁的线程只有获得这把锁之后,才能恢复运行,所以让持有锁的线程在不再需要锁的时候及时释放锁是很重要的。在以下情况,持有锁的线程会释放锁:
- 执行完同步代码块,就会释放锁。
- 在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放。
- 在执行同步代码块的过程中,执行了锁所属对象的 wait() 方法,这个线程会释放锁,进入对象的等待池。
除了以上情况,只要持有锁的线程还没有执行完同步代码块,就不会释放锁。因此在以下情况,线程不会释放锁:
- 在执行同步代码块的过程中,执行了 sleep() 方法,当前线程放弃 CPU,开始睡眠,在睡眠中不会释放锁。
- 在执行同步代码块的过程中,执行了 yield() 方法,当前线程放弃 CPU,但不会释放锁。
- 在执行同步代码块的过程中,其他线程执行了当前线程对象的 suspend() 方法,当前线程被暂停,但不会释放锁。
死锁
当一个线程等待由另一个线程持有的锁,而后者正在等待已被第一个线程持有的锁时,就会发生死锁。Java 虚拟机不监测也不试图避免这种情况,因此保证不发生死锁就成了程序员的责任。
package deadlock;
public class Machine extends Thread {
private Counter counter; //共享数据
public Machine(Counter counter){
this.counter = counter;
start(); //启动自身线程
}
public void run() {
for(int i = 0; i < 1000; i++)
counter.add();
}
public static void main(String[] args) throws InterruptedException {
Counter counter1 = new Counter();
Counter counter2 = new Counter();
counter1.setFriend(counter2);
counter2.setFriend(counter1);
Machine machine1 = new Machine(counter1);
Machine machine2 = new Machine(counter2);
}
}
class Counter {
private int a;
private Counter friend;
public void setFriend(Counter friend) {
this.friend = friend;
}
public synchronized void add(){
a++;
Thread.yield();
friend.delete();
System.out.println(Thread.currentThread().getName()+ ":add");
}
public synchronized void delete() {
a--;
System.out.println(Thread.currentThread().getNameQ+ ":delete");
}
}
在 Machine 类的 main() 方法中创建了 machine1 和 machine2 线程,这两个线程分别执行 counter1 和 counter2 对象的 add() 方法。而 counter1 对象的 add() 方法会调用 counter2 对象的 delete() 方法, counter2 对象的 add() 方法会调用 counterl 对象的 delete() 方法。machine1 线程和 machine2 线程分别持有 counter1 对象和 counter2 对象的锁,都在等待对方的锁,死锁就这样产生了,这两个线程都被阻塞,谁都无法恢复运行。因此,避免死锁的一个通用的经验法则是:当几个线程都要访问共享资源 A、B 和 C 时,保证每个线程都按照同样的顺序去访问它们。
13.9 线程通信
不同的线程执行不同的任务,如果这些任务有某种联系,线程之间必须能够通信,协调完成工作。java.lang.Object 类中提供了两个用于线程通信的方法:
wait()
:执行该方法的线程释放对象的锁,Java 虚拟机把该线程放到该对象的 等待池 中,该线程等待其他线程将它唤醒。notify()
:执行该方法的线程唤醒在对象的等待池中等待的一个线程。Java 虚拟机从对象的等待池中随机地选择一个线程,把它转到对象的 锁池 中。
Object 类还有一个 notifyAll()
方法,该方法会把对象等待池中的所有线程都转到对象的锁池中。
//在之前生产消费模型进一步修改,使得生产者线程与消费者线程能够相互通信
package communication;
public class SyncTest {
public static void main(String[] args) {
Stack stack1 = new Stack("stack1");
Producer producer1 = new Producer(stack1, "producer1");
Consumer consumer1 = new Consumer(stack1, "consumer1");
Consumer consumer2 = new Consumer(stack1, "consumer2");
Stack stack2 = new Stack("stack2");
Producer producer2 = new Producer(stack2, "producer2");
Producer producer3 = new Producer(stack2, "producer3");
Consumer consumer3 = new Consumer(stack1, "consumer3");
}
}
class Producer extends Thread {...}
class Consumer extends Thread {...}
class Stack {
...
public synchronized int getPointer() {return pointer;}
public synchronized String pop() {
//唤醒 this 引用的 Stack 对象的等待池中的线程
this.notifyAll();
while (pointer == -1) {
System.out.println(Thread.currentThread().getName() + ":Wait");
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
String goods = buffer[pointer];
buffer[pointer] = null;
Thread.yield();
pointer--;
return goods;
}
public synchronized boolean push(String goods) {
this.notifyAll();
while (pointer == buffer.length - 1) {
System.out.println(Thread.currentThread().getName() + ":Wait");
try {
this.wait(); //this 引用Stack对象
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
pointer++;
Thread.yield();
buffer[pointer] = goods;
}
}
值得注意的是,wait() 方法必须放在一个循环中,因为在多线程环境中,共享对象的状态随时可能被改变。当一个在对象等待池中的线程被唤醒后,并不一定立即恢复运行,等到这个线程获得了锁及 CPU 以 后才能继续运行,有可能此时对象的状态已经发生了变换。如果 wait() 方法放在一个 if 语句中,那么当 consumer2 线程恢复运行时,不会再判断 pointer 的值是否为 -1,因此会导致访问空的堆栈的错误。此外,对一 个对象的 wait() 和 notify() 的调用应该放在同步代码块中,并且同步代码块采用这个对象的锁。如果违反了这一规则,尽管在编译时不会检查这种错误,但在运行时会抛出 IllegalMonitorStateException 异常。
13.10 中断阻塞
当线程 A 处于阻塞状态时,如果线程 B 调用线程 A 的 interrupt()
方法 ,那么线程 A 会接收到一个 InterruptedException
,线程 A 退出阻塞状态,开始进行异常处理。
//Machine 线程不断把变量 a 的值加 1 如果变量 a 的值大于 3 ,就会进入 Machine 对象的等待池
//定时器从 Machine 线程转到阻塞状态开始计时,如果 Machine 线程在等待池中的时间超过 3 秒,
//则定时器就会调用 Machine 线程的 interrupt() 方法,使 Machine 线程中断阻塞,并开始执行异常处理代码块
package interrupt;
import java.util.*;
public class Machine extends Thread{
private int a = 0;
private Timer timer = new Timer(true);
public synchronized void reset() {a = 0;}
public void run() {
final Thread thread = Thread.currentThread();
TimerTask timerTask = new TimerTask() { //继承 TimerTasker 的匿名类
public void run() {
System.out.println(thread.getName() + "has waited for 3s");
thread.interrupt(); //中断Machine线程的阻塞
}
};
while (true) {
synchronized(this) {
while (a > 3) {
timer.schedule(timerTask, 3000); //三秒后执行定时任务
try {
this.wait(); //如果等待时间超过3秒,则会收到InterruptedException
} catch (InterruptedException e) {
System.out.println(thread.getName() + "is interrupted");
/* DO SOMETHING */
return;
}
}
a++;
System.out.println(a);
}
}
}
public static void main(String[] args) throws Exception {
Machine machine = new Machine();
machine.start();
}
}
13.11 线程控制
当线程执行完 run() 方法后,它将自然终止运行。在前面的例子中,许多线程的 run() 方法都是由一个 for 循环组成的,线程执行的循环次数是固定的,这主要是为了简化范例。但在实际应用中,线程执行的循环次数往往是不确定的,到底何时结束线程,由外部程序决定。Thread 类中也提供了一些控制线程的方法:
start()
:启动线程。suspend()
:使线程暂停。resume()
:使暂停的线程恢复运行。stop()
:终止线程。
不过,从 JDK1.2 开始, 除了 start() 方法,其他 3 个控制线程的方法都被废弃(Deprecated)。
被废弃的 suspend() 和 resume() 方法
这两个方法被废弃 ,因为它们会导致以下危险:
- 容易导致死锁。
- 一个线程强制中断另一个线程的运行,会造成另一个线程操作的数据停留在逻辑上不合理的状态。
假设线程 A 获得了某个对象的锁,正在执行一个同步代码块 ,如果线程 B 调用线程 A 的 suspend() 方法,使线程 A 暂停,那么线程 A 会放弃 CPU,但是不会放弃占用的锁,这是导致死锁的根源所在。
//machine 线程和主线程都处于阻塞状态, machine 线程等待主线程将其恢复运行,
//而主线程等待 machine 线程释放 machine 对象的锁,死锁就这样产生了 。
package suspend;
public class Machine extends Thread {
private int a; //共享数据
public void run() {
for(int i = 0; i < 1000; i++) {
synchronized(this){
a += i;
yield(); //给其他线程运行的机会
a -= i;
}
}
}
public synchronized void reset() { a == 0;}
public static void main(String[] args) throws InterruptedException {
Machine machine = new Machine();
machine.start(); //machine线程启动,获得machine对象的锁执行run()方法
yield(); //给 machine 线程运行的机会
machine.suspend(); //主线程调用suspend 让 machine 线程暂停
machine.reset(); //主线程调用 machine 对象的同步代码块
machine.resume(); //主线程使 machine 线程恢复运行
}
}
当主线程调用 machine.suspend() 时,此时 machine 线程正在执行一个原子操作,会操作共享数据 a,machine 线程被迫中断运行,使共享数据停留在不稳定的中间状态。为了安全起见,只有 machine 线程本身才可以决定自己何时暂停运行。
应该使用 wait() 和 notify() 机制来 代替 suspend() 和 resume()。前者由线程自身执行一个对象的 wait()方法,从而进入阻塞状态。线程可以确保处理的数据稳定后,再使自己进入阻塞状态。此外,当线程执行 wait() 方法时,这个线程会自动释放锁,给予其他线程获得锁的机会,从而避免了死锁。
被废弃的 stop() 方法
Thread 类的 stop() 方法可以强制终止一个线程。但是从 JDK1.2 开始,废弃了 stop() 方法。假设线程 A 获得了某个对象的锁,正在执行一个同步代码块,如果线程 B 调用线程 A 的 stop() 方法,线程 A 就会终止,线程 A 在终止之前释放它持有的锁,这避免了 suspend() 和 resume() 方法引起的死锁问题。
但是,当线程 B 调用线程 A 的 stop() 方法时,如果线程 A 正在执行一个原子操作,会操作共享数据,那么会使共享数据停留在不稳定的中间状态。为了安全起见,只有线程 A 本身才可以决定何时终止运行。
以编程的方式控制线程
在实际编程中,一般是在受控制的线程中定义一个标志变量,其他线程通过改变标志变量的值,来控制线程的暂停、恢复运行以及自然终止。
package control;
public class ControlledThread extends Thread {
public static final int RUN = 0;
public static final int SUSP = 1;
public static final int STOP = 2;
private int state = RUN;
public synchronized void setState(int state) {
this.state = state;
if (state == RUN) notify();
}
public synchronized boolean checkState() {
while (state == SUSP) {
try {
System.out.println(Thread.currentThread().getName() + ":Wait");
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e.getMessage());
}
}
if (state == STOP) {
return false;
}
return true;
}
}
//Machine 类继承 ControlledThread 类,因此 Machine 类是受控制的线程
//在 Machine 类的 run() 方法的循环中,每次循环结束前都会调用 checkState()方法判断接下来的操作状态
package control;
public class Machine extends ControlledThread {
private int count; //共享数据
public synchronized int getCount() { return count; }
public synchronized void reset() {
count = 0;
System.out.println(Thread.currentThread().getName() + ":Reset");
}
public void run() {
while (true) {
synchronized (this) {
count++;
System.out.println(Thread.currentThread().getName() + ":Run" + count + "times");
}
if (!checkState()) {
System.out.println(Thread.currentThread().getName() + ":Stop");
break;
}
}
}
public static void main(String[] args) {
Machine machine = new Machine();
machine.start();
for (int i = 0; i < 200; i++) {
if (machine.getCount() > 5) {
//暂停 machine 线程
machine.setState(ControlledThread.SUSP);
yield();
machine.reset();
//恢复 machine 线程
machine.setState(ControlledThread.RUN);
}
yield()
}
//终止 machine 线程
machine.setState(ControlledThread.STOP);
}
}
值得注意的是,以上线程控制方式不是实时的,当主线程在某个时刻执行了 machine.setState(SUSP)
方法时,machine 线程并不会立即进入暂停状态。machine 线程必须先获得 CPU,开始执行 checkState() 方法,才会进入暂停状态。
13.12 线程组
Java 中的 ThreadGroup 类表示线程组,它能够对一组线程进行集中管理。用户创建的每个线程均属于某线程组。在创建一个线程对象时,可以通过以下构造方法指定它所属的线程组:
ThreadGroup(ThreadGroup group, String name)
ThreadGroup(ThreadGroup parent, String name)
假设线程 A 创建了线程 B, 如果创建线程 B 时没有在构造方法中指定线程组,那么线程 B 会加入到线程 A 所属的线程组中 。一旦线程加入某线程组,该线程就一直存在于该线程组中,直至线程死亡,不能在中途改变线程所属的线程组。当 Java 应用程序运行时,Java 虚拟机会创建名为 main 的线程组,默认情况下,所有线程都属于这个线程组。
用户创建的线程组都有一个父亲线程组,默认情况下,如果线程 A 创建了一个新的线程组,那么这个线程组以线程 A 所属的线程组作为父亲线程组。 此外,在构造线程组实例时 ,也可以显式地指定父亲线程组。
package group;
public class Machine extends Thread{
public Machine(ThreadGroup group, String name) {
super(group, name);
}
public void run() {
for (int a = 0; a < 1000; a++) {
System.out.println(Thread.currentThread().getName() + a);
yield();
}
}
public static void main(String[] args) throws Exception {
ThreadGroup group = new ThreadGroup("machines");
for (int i = 1; i < 5; i++) {
Machine machine = new Machine(group, "machine" + i);
machines.start();
}
int activeCount = group.activeCount(); //获得当前活着的线程数目
Thread[] machines = new Thread[activeCount];
/* 假如此时activeCount值为5,再启动一个 Machine 线程
Machine machine = new Machine(group, "machine" + 6); //第六个线程
machine.start();
再执行enumerate()方法只会存放5个线程引用而忽略一个线程
*/
group.enumerate(machines); //把当前活着的线程的引用存复制在数组中
for (int i = 0; i < activeCount; i++)
System.out.println(machines[i].getName() + "is Alive");
}
}
由此可见,JDK 提供的 ThreadGroup API 不 是很健壮,因此不推荐使用 ThreadGroup 类。Thread Group 类的唯一比较有用的功能是它的 uncaughtException()
方法,详见下节。
13.13 处理线程未捕获的异常
在 Thread 类中提供了一个公共的静态的 UncaughtExceptionHandler 内部接口,它负责处理线程未捕获的异常,这个接口的完整名字为 Thread.UncaughtExceptionHandler
,它的唯一的方法是 uncaughtException(Thread t, Throwable e)
,参数 t 表示抛出异常的线程,参数 e 表示具体的异常。
从 JDK5 版本开始,加强了对线程的异常处理。如果线程没有捕获异常,那么 Java 虚拟机会寻找相关的 UncaughtExceptionHandler
实例,如果找到,就调用它的 uncaughtException(Thread t, Throwable e)
方法。
Thread 类中提供了两个设置异常处理类的方法:
setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
第一个方法是静态方法,设置线桯类的默认异常处理器,第二个方法是实例方法,设置线程实例的当前异常处理器。
每个线程实例都属于一个 ThreadGroup 线程组。 ThreahGroup 类实现了 Thread.UncaughtExceptionHandler 接口。当一个线程实例抛出未捕获的异常时,Java 虚拟机首先寻找线程实例的当前异常处理器,如果找到就调用它的 uncaughtException() 方法 。否则,把线程实例所属的线程组作为异常处理器,调用它的 uncaughtException() 方法。
ThreadGroup 类的 uncaughtException() 方法的处理流程如下:
- 如果这个线程组有一个父亲线程组,那么就调用它的 uncaughtException() 方法。
- 否则,如果线程实例所属的线程类具有默认异常处理器,那么就调用这个默认异常处理器的 uncaughtException() 方法。
- 否则,就把来自方法调用堆栈的异常信息打印到标准输出流 System.err 中。
package exhandler;
class MachineGroup extends ThreadGroup {
public MachineGroup() {
super("MachineGroup");
}
public void uncaughtException(Thread t, Throwable e) {
System.out.println(getName() + "Catch an Exception from" + t.getName());
super.uncaughtException(t, e);
}
}
class MachineHandler implements Thread.UncaughtExceptionHandler {
private String name;
public MachineHandler(String name) {
this.name = name;
}
public void uncaughtException(Thread t, Throwable e) {
System.out.println(name + "Catch an Exception from" + t.getName());
}
}
public class Machine extends Thread {
public Machine(ThreadGroup group, String name) {
super(group, name);
}
public void run() {
int a = 1 / 0; //ArithmeticException
}
public static void main(String[] args) throws Exception {
ThreadGroup group = new MachineGroup();
//设置 Machine 类的默认异常处理器
UncaughtExceptionHandler defaultHandler = new MachineHandler("DefaultHandler");
Machine.setDefaultUncaughtExceptionHandler(deaultHander);
Machine m1 = new Machine(group, "m1");
Machine m2 = new Machine(group, "m2");
//设置 m2 的当前异常处理器
UncaughtExceptionHandler currHandler = new MachineHandler("m2.hander");
m2.setDefaultUncaughtExceptionHandler(currHandler);
m1.start();
m2.start();
}
}
/* 打印结果
MachineGroup Catches an Exception from m1
DefaultHandler Catches an Exception from m1
m2.handler Catches an Exception from m2
*/
当 ml 和 m2 线程运行时,由于执行了整数除以零的操作,因此会分别抛出 ArithmeticException 异常。对于m2 抛出的异常,直接由当前异常处理器 currHandler 处理,对于 m1 抛出的异常,由于没有设置当前异常处理器,因此由 MachineGroup 处理,MachineGroup 向控制台打印些信息后接着再转手由 Machine 类的默认异常处理器 defaultHander 处理。
13.14 ThreadLocal 类
java.lang.ThreadLocal 可用来存放线程的局部变量,每个线程都有单独的局部变量,彼此之间不会共享。ThreadLocal <T>
类主要包括以下 3 个方法:
public T get()
:返同当前线程的局部变量。protected T initialValue()
:返同当前线程的局部变量的初始值。public void set(T value)
:设置当前线程的局部变量。
initialValue() 方法为 protected 类型,它是为了被子类覆盖而特意提供的,该方法返回当前线程的局部变量的初始值,这个方法是一个延迟调用方法,当线程第一次调用 ThreadLocal 对象的 get() 或者 set() 方法时才执行, 并且仅执行一次。在 ThreadLocal 类本身的实现中,initialValue() 方法直接返同一个 null:
protected T initialValue() { return null; }
ThreadLocal 是如何做到为每一个线程提供一个单独的局部变量的呢?其实很简单,在 ThreadLocal 类中有个 Map 缓存,用于存储每一个线程的局部变量。
// ThreadLocal实现的总体思路
package mypack;
import java.util.*;
public class ThreadLocal<T> {
private Map<Runnable, T> values =
Collection.synchronizedMap(new HashMap<Runnable, T>());
public T get() {
Thread curThread = Thread.currentThread();
T o = values.get(curThread);
if (o == null && !values.containsKey(curThread)) {
o = initialValue();
values.put(curThread, o);
}
return o;
}
public void set(T newValue) {
values.put(Thread.currentThread(), new Value);
}
protected T initialValue() {
return null;
}
}
下面的例程演示了 Java.lang.ThreadLocal 类的用法。
//Counter 类用来为每个线程分配一个序列号 serialCount,引用自 ThreadLocal 的匿名子类的实例
//LocalTester 是个线程类,在它的 run() 方法中打印门己的序列号,然后把序列号加 2
class Counter {
private static int count;
//每个线程的序列号声明为 ThreadLocal 类型
//引用一个 ThreadLocal 的匿名子类的实例
private static ThreadLocal<Integer> serialCount = new ThreadLoacl<Intger>() {
protected synchronized Integer initialValue() {
return new Integer(count++);
}
};
public static int get() {
return serialCount.get();
}
public static void set(int i) {
serialCount.set(i);
}
public class LocalTester extends Thread{
public void run() {
for (int i = 0; i < 3; i++) {
int c = Counter.get();
System.out.println(getName() + c);
Counter.set(c += 2);
}
}
}
public static void main(String[] args) {
Thread t1 = new LocalTester();
Thread t2 = new LocalTester();
t1.start();
t2.start();
}
}
//打印结果 Thread-0:0 Thread-1:1 Thread-0:2 Thread-0:4 Thread-1:3 Thread-1:5
13.15 concurrent 并发包
编写多线程的程序代码时,既要保证线程的同步,又要避免死锁,还要考虑并发性能,因此对开发人员的要求很高。为了帮助开发人员编写出高效安全的多线程代码,从 JDK5 开始,增加了 Java.util.cuncurrent 并发包,它提供了许多实用的处理多线程的接口和类。
用于线程同步的 Lock 外部锁
每个 Java 对象都有一个用于同步的锁,但这实际上是概念上的锁。而在 Java.util.cuncurrent.locks
包中,Lock 接口及它的实现类 专门 表示用于同步的锁。为了叙述方便 ,本章把 Lock 锁称为外部锁,前文所讲的 Java 对象的锁称为 Java 对象内部锁。
Lock 锁的优势在于它提供了更灵活地获得同步代码块的锁的方式:
lock()
:当前线程获得同步代码块的锁,如果锁被其他线程占用,那就进入阻塞状态。这种处理机制和 Java 对象内部锁是一样的。tryLock()
:当前线程试图获得同步代码块的锁,如果锁被其他线程占用,那就立即返回 false,否则返回true。tryLock(long time, TimeUnit unit)
:该方法和上面的不带参数的 tryLock() 方法的作用相似。区别在于本方法设定了时间限制。如果锁被其他线程占用,当前线程会先进入阻塞状态。如果在时间限定范围内获得了锁,那就返同 true;如果超过时间限定范围还没有获得锁,那就返同 false。例如lock.tryLock(50L, TimeUnit.SECONDS)
表示设定的时间限制为 50 秒。
和以上 lock() 方法及 tryLock() 方法对应,Lock 接口的 unlock()
方法用于释放线程所占用的同步代码块的锁。
Lock 接口有一个实现类 ReentrantLock
,它有以下构造方法:
Reentrantlock()
: 默认构造方法,创建一个常规的锁。ReentrantLock(boolean fair)
:如果 fair 参数为 true,会创建一个带有公平策略的锁。否则就创建一个常规的锁。所谓 公平策略,是指会保证让阻塞较长时间的线程有机会获得锁。使用公平锁时,有两个注意事项:一是公平锁的公平机制是以降低运行性能为代价的;二是公平锁依赖于底层线程调度的实现,不能完全保证公平。
在利用外部锁来对线程同步时,一般采用以下编程模式:
public class Sample {
private final Reentrant Lock lock = new ReentrantLock(); //创建锁
...
public void method() {
lock.lock(); //获得锁
try {
//...需要同步的原子操作
} finally {
lock.unlock(); //释放锁
}
}
}
使用外部锁时,需要同步的操作放在 try
代码块中。为保证出现异常的情况下,也会执行释放锁的操作,特意把解锁语句放在 finally
代码块中。
package lock;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
public class Machine implements Runnable {
private final Lock machineLoack = new ReentrantLoack(); //创建外部锁
int data = 100; //共享数据
public void run() {
machineLock.lock(); //用外部锁来同步
try {
data++;
Thread.currentThread().sleep(1000);
data--;
System.out.println(Thread.currentThread().getName() + data);
} catch (Exception e) {
e.printStackTrace();
} finally {
machineLock.unlock(); //释放锁
}
}
public static void main(String[] args) throws Exception {
Machine machine = new Machine();
for (int i = 0; i < 10; i++){ //创建10个线程
Thread thread = new Thread(machine); //都执行同一个 Machine对象的 run() 方法
thread.start();
}
}
}
用于线程连信的 Condition 条件接口
java.lang.concurrent.locks.Condition
条件接口用于线程之间的通信。Lock 接口的 newCondition()
方法返回实现了 Condition 接口的实例。
Condition 接口中有以下方法:
await()
: 作用和 Object 类的wait()
方法相似。当前线程释放外部锁,进入等待池中,等待其他线程将它唤醒。await(long time, TimeUnit unit)
:当前线程释放外部锁,进入等待池中,等待其他线程将它唤醒。如果在参数设定的时间范围内没有被唤醒,就不再等待,直接返回 false,否则返同 true。signal()
:作用和 Object 类的notify()
方法相似。当前线程唤醒等待池中的个线程。signalAll()
:作用和 Object 类的notifyAll()
方法相似。当前线程唤醒等待池中的所有线程。
//重写 SyncTest 类,利用Condition 接口来进行线程之间通信
package lockcommunication;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
public class SyncTest {
public static void main(String[] args) {
Stack stack1 = new Stack("stack1");
Producer producer1 = new Producer(stack1, "producer1");
Consumer consumer1 = new Consumer(stack1, "consumer1");
Consumer consumer2 = new Consumer(stack1, "consumer2");
}
}
class Producer extends Thread {
private Stack theStack;
public Producer(Stack s, String name) {
super(name);
theStack = s;
start(); // 启动自身生产者线程
}
public void run() {
String goods;
Lock stackLock = theStack.getLock();
for (int i = 0; i < 200; i++) {
stackLock.lock();
try {
goods = "goods" + (theStack.getPointer() + 1);
theStack.push(goods);
} finally {
stackLock.unlock();
}
System.out.println(getName() + "push" + goods + "to" + theStack.getName());
sleep(100);
}
}
public void sleep(int m) {
try {
super.sleep(m);
} catch (Exception e) {
e.printStackTrace();
}
}
}
class Consumer extends Thread {
public Consumer(Stack s, String name) {
super(name);
theStack = s;
start(); // 启动自身消费线程
}
public void run() {
String goods;
for (int i = 0; i < 200; i++) {
goods = theStack.pop();
System.out.println(getName() + "pop" + goods + "from" + theStack.getName());
yield();
}
}
}
class Stack {
private String name;
private final int SIZE = 100;
private String[] buffer = new String[SIZE];
int pointer = -1;
private final Lock stackLock;
private Condition condition;
public Stack(String name) {
this.name = name;
stackLock = new ReenterantLock();
condition = stackLock.newCondition();
}
public String getName() {return name;}
public Lock getLock() {return stackLock;}
public synchronized int getPointer() {
stackLock.lock();
try {
return pointer;
} finally {
stackLock.unlock();
}
}
public synchronized String pop() {
stackLock.lock();
try {
condition.signalAll();
while(pointer == -1) {
System.out.println(Thread.currentThread().getName() + ":Wait");
condition.await();
}
String goods = buffer[pointer];
buffer[pointer] = null;
Thread.yield();
pointer--;
return goods;
} catch (InterruptedException e) {
throw new RuntimeException(e)''
} finally {
stackLock.unlock();
}
}
public synchronized void push(String goods) {
stackLock.lock();
try {
condition.signalAll();
while (pointer == buffer.length - 1) {
System.out.println(Thread.currentThread().getName() + ":Wait");
condition.await();
}
pointer++;
Thread.yield();
buffer[pointer] = goods;
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
stackLock.unlock();
}
}
}
以上程序在 Stack 类的 push() 和 pop() 方法中通过 Condition 对象的 await() 方法和 signalAll() 方法来进行生产者线程和消费者线程之间的通信。另外 ,在生产者 Producer 类的 run() 方法中, 也利用 Stack 对象的外部锁来对操作 Stack 对象的代码进行同步。
支持异步计算的 Callable 接口和 Future 接口
Runnable 接口的 run() 方法的返回类型为 void。假如线桯 A 执行一个运算任务,线程 B 需要获取线程 A 的运算结果,这该如何实现呢?如果直接靠编程来实现,就需要定义一个存放运算结果的共享变量,线程 A 和线程 B 都可以访问这个共享变量,并且需要对操作共享变量的代码块进行同步。
在 JDK5 中,java.util.concurrent 包中的一些接口和类提供了更简单的支持异步运算的方法。涉及 java.util.concurrent 包中的以下类和接口:
Callable
接口:它和 Runnable 接口有点类似,都指定了线程所要执行的操作。区别在于,Callable 接口是在call()
方法中指定线程所要执行的操作的, 并且该方法有泛型的返回值<V>
。 此外,Callable 实例不能像 Runnable 实例那样,直接作为 Thread 类的构造方法的参数。Future
接口:能够保存异步运算的结果。它有以下方法:get()
:返回异步运算的结果。如果运算结果还没有出来,当前线程就会被阻塞,直到获得运算结果,才结束阻塞。get(long timeout, TimeUnit unit)
:和第一个不带参数的 get() 方法的作用相似,区别在于本方法为阻塞设了限定时间。如果超过参数设置的限定时间,还没有获得运算结果,就会抛出 TimeoutException。cancel(boolean mayInterrupt)
:取消该运算。如果运算还没开始,那就立即取消。如果运算已经开始,并且 mayInterrupt 参数为 true, 那么会取消该运算。否则,如果运算已经开始,并且 mayInterrupt 参数为 false,那么不会取消该运符,而是让其继续执行下去。isCancelled()
:判断运算是否已经被取消,如果取消,就返回 true。isDone()
:判断运算是否已经完成,如果已经完成,就返回 true。
FutureTask
类 :它是一个适配器,同时 实现 了Runnable
接口和Future
接口,又会 关联 一个 Callable 实例。它实际上把 Callable 接口转换成了 Runnable 接口。FutureTask 实例可以作为 Thread 类的构造方法的参数。例如 ,以下代码把一个 Callable 实例传给 FutureTask 类 的构造方法,接着把 FutureTask 实例传给 Thread 类的构造方法:Callable myComputation = ... FutureTask<Integer> task = new FutureTask<Integer>(myComputation); Thread thread = new Thread(task); thread.start(); // 当以上线程启动后,会执行 Callable 实例中的 call() 方法, // 并且运符结果保存在 FutureTask 实例中 。
// Machine 类实现了 Callable 接口 machine 线程负责执行 call() 方法,并且返回运算的结果
package future;
import java.util.concurrent.*;
public class Machine implements Callable<Integer> {
public Integer call() {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
try {
Thread.currentThread().sleep(20);
} catch (Exception e) {
e.printStackTrace();
}
}
return sum;
}
public static void main(String[] args) throws Exception {
FutureTask<Integer> task = new FutureTask<Integer>(new Machine());
Thread machine = new Thread(task);
machine.start(); // machine线程执行Machine类的call()方法
System.out.println("The Sum Is" + task.get()); //获取运算结果
}
}
线程调用 task.get() 方法, 当 machine 线程还没运算完毕时,主线程就会阻塞,machine 线程执行完 call() 方法,中线程才会获得运算结果,并从 task.get() 方法中退出。
通过线程池来高效管理多个线程
每个线程执行完一个 Runnable 实例的 run() 方法,就会结束生命周期。在多线程环境中,不断地创建和销毁线程既费时又消耗系统资源。为了提高程序的性能,java.util.concurrent 并发包提供了用线程池来管理多线程的机制。它的基本原理是仅仅创建数量有限的线程,每个线程都会待续不断地执行各种任务。
Executor
接口 表示线程池,它的 execute(Runnable command)
方法用来提交 Runnable 类型参数 command 的 run() 方法所指定的任务,线程池会调度空闲的线程来执行该任务。至于到底何时执行该任务,这是由线程池在运行时动态决定的。它的 ExecutorService
子接口 具有管理线程池的一些方法:
shutdown()
:预备关闭线程池。如果已经有任务开始执行,那么要等这些任务执行完毕后,才会真正地关闭线程池。那些还没有开始执行的任务则会被忽略不再执行。shutdownNow()
:终止已经开始执行的任务,立即关闭线程池。awaitTennination()
:等待线程池关闭。isTenninated()
:判断线程池是否关闭,如果关闭,就返回 true,否则返回 false。
ExecutorService 接口的 submit(Callable<T> task)
和 submit(Runnable task)
方法的作用与 Executor 接口的 execute(Runnable command)
方法相似,都用于向线程池提交任务。区别在于前两个 submit() 方法支持异步运算,它们都会返回表示异步运算结果的 Future 对象。
Excutors
类 包含一些静态方法,负责生成各种类型的 ExecutorService 线程池实例:
newCachedThreadPool()
:创建拥有缓存的线程池。有任务时才创建新线程,空闲的线程在缓存中被保留 60 秒。newFixedThreadPool(int nThreads)
:创建拥有固定数目线程的线程池。空闲线程会一直保留。参数 nThreads 用于设定线程池中线程的数目。newSingleThreadExecutor()
:创建只有一个线桯的线程池。这一单个线程会依次执行每个任务。如果这个线程因为异常而终止,不会重新创建替代它的线程,这是与newFixedThreadPool(1)
方法的不同之处。 newFixedThreadPool(1) 方法尽管也只创建一个线程,但是如果这个线程意外终止,线程池会重新创建一个新的线程来替代它继续执行任务。newScheduledThreadPool(int corePoolSize)
:线程池会按时间计划来执行任务,允许用户设定计划执行任务的时间。参数 corePoolSize 设定线程池中线程的最小数目。任务较多时,线程池可能会创建更多的线桯来执行任务。newSingleThreadScheduledExecutor()
:创建只有一个线程的线程池。这一单个线程能按照时间计划来执行任务。
// Machine 类利用线程池来执行多个 Machine 对象的 run() 方法指定的任务
package pool;
import java.util.concurrent.*;
public class Machine implements Runnable {
private int id;
public Machine(int id) {this.id = id;}
public void run() {
for (int a = 0; a < 10; a++) {
System.out.println(Thread.currentThread().getName() + id + a);
Thread.currentThread().yield();
}
}
public static void main(String[] args) throws Exception {
ExecutorService service = Executors.newFixedThreadPool(2); //线程池有2个线程
for (int i = 0; i < 5; i++) {
service.execute(new Machine(i)); //向线程池递交五个Machine任务
//或者 service.submit(new Machine(i));
}
service.shutdown();
}
}
线程池会调度池中的两个线程来执行这些任务。当程序执行 service.shutdown() 方法时,该方法不会立即关闭线程池,只有当已经开始执行的任务全部执行完毕后,才会关闭线程池。
BlockingQueue 阻塞队列
java.util.concurrent.BlockingQueue
接口继承了 Java.util.Queue
接口,BlockingQueue 接口为多个线程同时操作同一个队列提供了 4 种处理方案,方案一和方案二来自 Queue 接口,方案三和方案四是在 BlockingQueue 接口中新增的。
操作 | 处理方案一:抛出异常 | 处理方案二:返回特定值 | 处理方案三:线程阻塞 | 处理方案四:超时 |
---|---|---|---|---|
添加元素 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
删除元素 | remove() | poll() | take() | poll(time,unit) |
读取元素 | element() | peek() | - | - |
- 线程阻塞。在通过
put(e)
方法向队列尾部添加元素时,如果队列已满,则当前线程进入阻塞状态,直到队列有剩余的容量来添加元素,才退出阻塞。在通过take()
方法从队列头部删除并返回元素时,如果队列为空,则当前线程进入阻塞状态,直到从队列中成功删除并返回元素为止。 - 超时。超时和以上线程阻塞有一些共同之处。两者的区别在于当线程在特定条件下进入阻塞状态后,如果超过了
offer(e, time, unit)
和poll(time , unit)
方法的参数所设置的时间限制,那么也会退出阻塞状态,分别返回 false 和 null。
如 java.util.concurrent 包中,BlockingQueue 接口主要有以下实现类:
LinkedBlockingQueue
类:默认情况下,LinkedBlockingQueue 的容量是没有上限的(确切地说,默认的容量为 Integer.MAX_VALUE),但是也可以选择指定其最大容量。它是基于链表的队列,此队列按 FIFO(先进先出)的原则存取元素。ArrayBlockingQueue
类:它的ArrayBlockingQueue(int capacity, boolean fair)
构造方法允许指定队列的容量,并可以选择是否采用公平策略。如果参数 fair 被设置为 true , 那么等待时间最长的线程会优先访问队列(其底层实现是通过将ReentrantLock
设置为使用公平策略来达到这种公平性的)。通常,公平
性以降低运行性能为代价,所以只有在确实非常需要的时候才使用这种公平机制。 ArrayBlockingQueue 是基于数组的队列,此队列按 FIFO(先进先出)的原则存取元素。PriorityBlockingQueue
是一个带优先级的队列,而不是先进先出队列。元素按优先级顺序来删除,该队列的容量没有上限。DelayQueue
: 在这个队列中存放的是延期元素,也就是说这些元索必须实现java.util.concurrent.Delayed
接口。只有延迟期满的元素才能被取出或删除。当一个元素的getDelay(TimeUnit unit)
方法返回一个小于或等于零的值时,则表示延迟期满。DelayQueue 队列的容量没有上限。
//重写 SyncTest 类,生产消费线程同时操作同一个阻塞队列
package blockque;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
public class SyncTest {
public static void main(String[] args) {
BlockingQueue<String> queue = new ArrayBlockingQueue<String>(100);
Producer producer1 = new Producer(queue, "producer1");
Consumer consumer1 = new Consumer(queue, "consumer1");
Consumer consumer2 = new Consumer(queue, "consumer2");
}
}
class Producer extends Thread {
private BlockingQueue<String> queue;
public Producer(BlockingQueue<String> queue, String name) {
super(name);
this.queue = queue;
start(); // 启动自身生产者线程
}
public void run() {
String goods;
for (int i = 0; i < 200; i++) {
try {
goods = "goods" + i;
queue.put(goods);
System.out.println(getName() + "put" + goods + "to queue");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
class Consumer extends Thread {
private BlockingQueue<String> queue;
public Consumer(BlockingQueue<String> queue, String name) {
super(name);
this.queue = queue;
start(); // 启动自身消费线程
}
public void run() {
String goods;
for (int i = 0; i < 200; i++) {
try {
goods = queue.take();
yield();
System.out.println(getName() + "take" + goods + "from queue");
} catch (Exception e) {
e.printlnStackTrace();
}
}
}
}
在 Consumer 类的 run() 方法中,通过 queue.take() 方法从队列中取出元素,如果队列为空,消费者线程会进入阻塞状态,直到生产者线程加入了产品 ,消费者线程才会退出阻塞。