Java多线程编程核心技术
Java多线程编程核心技术 #
这本书有利于对Java多线程API的理解,但不容易从中总结规律。
JDK文档
1. Thread类
部分源码:
public class Thread implements Runnable {
private volatile char name[];
private int priority;
private Thread threadQ;
private long eetop;
/* Whether or not to single_step this thread. */
private boolean single_step;
/* Whether or not the thread is a daemon thread. */
private boolean daemon = false;
/* What will be run. */
private Runnable target;
/* The group of this thread */
private ThreadGroup group;
/* The context ClassLoader for this thread */
private ClassLoader contextClassLoader;
/* For autonumbering anonymous threads. */
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* The requested stack size for this thread, or 0 if the creator did
* not specify a stack size. It is up to the VM to do whatever it
* likes with this number; some VMs will ignore it.
*/
private long stackSize;
/*
* Thread ID
*/
private long tid;
/* For generating thread ID */
private static long threadSeqNumber;
/* Java thread status for tools,
* initialized to indicate thread 'not yet started'
*/
private volatile int threadStatus = 0;
private static synchronized long nextThreadID() {
return ++threadSeqNumber;
}
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
/**
* Returns a reference to the currently executing thread object.
*
* @return the currently executing thread.
*/
public static native Thread currentThread();
优先级
- 线程可以具有的最高优先级10
- 线程可以具有的最低优先级1
- 分配给线程的默认优先级5
常用方法
- static Thread currentThread() 返回对当前正在执行的线程对象的引用。
- long getId()返回该线程的标识符。
- String getName()返回该线程的名称。
- int getPriority() 返回线程的优先级。
- void interrupt() 中断线程。
- static boolean interrupted() 测试当前线程是否已经中断,执行后具有将状态标志置清除为false的功能(即连续两次调用该方法可能会显示第一次true,第二次false)。
- boolean isInterrupted() 测试线程是否已经中断,但不清除状态标志(即连续两次调用该方法显示结果一致)。
- boolean isAlive()测试线程是否处于活动状态。
- void join()等待该线程终止。
- void join(long millis)等待该线程终止的时间最长为 millis 毫秒。
- void join(long millis, int nanos)等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
- void setDaemon(boolean on)将该线程标记为守护线程或用户线程。
- void setPriority(int newPriority)更改线程的优先级。
- static void sleep(long millis)在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
- static void sleep(long millis, int nanos)在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
- void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
- static void yield()暂停当前正在执行的线程对象,并执行其他线程。
2. Runnable接口
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
第1章 Java多线程技能
1.1进程和多线程的概念及线程的优点
1.2 Java 启动线程
Java 启动线程主要有三种方式(其实还有一种实现Callable接口的方式):
- 继承Thread类
- 实现Runnable接口
- 直接在函数体使用
1.2.1继承 Thread 类
先来看看Thread类的结构,如下:
public class Thread implements Runnable
Thread类实现了 Runnable接口,它们之间具有多态关系。其实,使用继承Thread类的方式创建新线程时,最大的局限就是不支持多继承,因为 Java语言的特点就是单继承,为了改变这种限制,完全可以实现Runnable接口的方式。用这两种方式创建的线程在工作时的性质是一样的,没有本质的区别。下面给出继承 Thread 类例子(注意代码中将两个类放到了一起,运行时请放到不同的.java文件中)。
public class MyThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("MyThread");
}
}
public class Run {
public static void main(String[] args) {
MyThread mythread = new MyThread();
mythread.start();
System.out.println("运行结束!");
}
}
输出:
运行结束!
MyThread
从运行结果来看,MyThread.java类中的run方法执行的时间比较晚,这也说明在使用多线程技术时,代码的运行结果与代码执行顺序或调用顺序是无关的,多线程是异步的。线程是一个子任务,CPU以不确定的方式,或者说是以随机的时间来调用线程中的run 方法,所以就会出现先打印“运行结束!”后输出“MyThread”这样的结果了。同时注意:执行start()方法的顺序不代表线程启动的顺序。
如果直接在main函数中调用MyThread.java的run方法:Thread.java类中的start()方法通知“线程规划器”此线程已经准备就绪,等待调用线程对象的run()方法。这个过程其实就是让系统安排一个时间来调用Thread中的run()方法,也就是使线程得到运行,启动线程,具有异步执行的效果。如果调用代码mythread.run()就不是异步执行了,而是同步,那么此线程对象并不交给“线程规划器”来进行处理,而是由main主线程来调用run()方法,也就是必须等run()方法中的代码执行完后才可以执行后面的代码。
1.2.2 实现Runnable接口
如果欲创建的线程类已经有一个父类了,这时就不能再继承自Thread类了,因为Java 不支持多继承,所以就需要实现Runnable接口来应对这样的情况。
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("运行中!");
}
}
public class Run {
public static void main(String[] args) {
Runnable runnable=new MyRunnable();
Thread thread=new Thread(runnable);
thread.start();
System.out.println("运行结束!");
}
}
输出:
运行结束!
运行中!
在这里说明一下Thread.java类的构造函数:
注意上面的代码中不可以runnable.start()
,因为Runnable接口中的唯一方法就是run方法。Thread.java构造函数支持传人一个Runnable接口的对象。由于Thread 实现 Runnable 接口,构造函数Thread(Runnable target)不光可以传人Runnable接口的对象,还可以传入一个Thread类的对象,这样做完全可以将一个Thread对象中的run()方法交由其他的线程进行调用。
1.2.3 直接在函数体使用
public class MyThread {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
public void run() {
System.out.println("运行中!");
}
});
t.start();
System.out.println("运行结束!");
}
}
1.2.4 实例变量与线程安全
自定义线程类中的实例变量针对其他线程可以有共享与不共享之分,这在多个线程之间进行交互时是很重要的一个技术点。
**(1)不共享数据的情况 **
下面通过一个示例来看下数据不共享情况。
public class MyThread extends Thread {
private int count = 5;
public MyThread(String name) {
super();
this.setName(name);
}
@Override
public void run() {
super.run();
while (count > 0) {
count--;
System.out.println("由 " + this.currentThread().getName() + " 计算,count=" + count);
}
}
}
public class Run {
public static void main(String[] args) {
MyThread a = new MyThread("A");
MyThread b = new MyThread("B");
MyThread c = new MyThread("C");
a.start();
b.start();
c.start();
}
}
输出:
由 A 计算,count=4
由 B 计算,count=4
由 B 计算,count=3
由 C 计算,count=4
由 C 计算,count=3
由 C 计算,count=2
由 C 计算,count=1
由 C 计算,count=0
由 B 计算,count=2
由 A 计算,count=3
由 A 计算,count=2
由 A 计算,count=1
由 A 计算,count=0
由 B 计算,count=1
由 B 计算,count=0
可以看到,一共创建了 3个线程,每个线程都有各自的count变量,自己减少自己的count变量的值。这样的情况就是变量不共享,此示例并不存在多个线程访问同一个实例变量的情况。
**(2 )共享数据的情况 **
public class MyThread extends Thread {
private int count=5;
@Override
synchronized public void run() {
super.run();
count--;
System.out.println("由 "+this.currentThread().getName()+" 计算,count="+count);
}
}
public class Run {
public static void main(String[] args) {
MyThread mythread=new MyThread();
Thread a=new Thread(mythread,"A");
Thread b=new Thread(mythread,"B");
Thread c=new Thread(mythread,"C");
Thread d=new Thread(mythread,"D");
Thread e=new Thread(mythread,"E");
a.start();
b.start();
c.start();
d.start();
e.start();
}
}
输出:
由 A 计算,count=4
由 E 计算,count=3
由 D 计算,count=2
由 C 计算,count=1
由 B 计算,count=0
在某些JVM中,i--的操作要分成如下3步:取得原有i值;计算i-1;对i进行赋值。在这3个步骤中,如果有多个线程同时访问,那么很可能会出现非线程安全问题。可以考虑:
- 使用在java.util.concurrent.atomic包中的一些原子变量类,用于实现在数值和对象引用上的原子状态转换。
- 使用synchronized同步count--操作即可。
1.3 currentThread()方法
currentThread()方法可返回代码段正在被哪个线程调用的信息。首先给出一个正常的例子:
public class CountOperate extends Thread {
public CountOperate() {
System.out.println("CountOperate---begin");
System.out.println("Thread.currentThread().getName()="
+ Thread.currentThread().getName());
System.out.println("this.getName()=" + this.getName());
System.out.println("CountOperate---end");
}
@Override
public void run() {
System.out.println("run---begin");
System.out.println("Thread.currentThread().getName()="
+ Thread.currentThread().getName());
System.out.println("this.getName()=" + this.getName());
System.out.println("run---end");
}
}
public class Run {
public static void main(String[] args) {
CountOperate c = new CountOperate();
c.setName("A");
c.start();
}
}
输出:
CountOperate---begin
Thread.currentThread().getName()=main
this.getName()=Thread-0
CountOperate---end
run---begin
Thread.currentThread().getName()=A
this.getName()=A
run---end
我们解释一下,首先输出Thread.currentThread().getName()=main
很正常,实例化MyThread类,调用MyThread构造方法是主线程main。输出this.getName()=Thread-0
,在这里this是MyThread的引用,是个线程类,但是这个线程类并没有设置名字,所以Thread默认给了一个Thread-0,默认名字的规则定义如下:
之后是run()中的代码结果,当前线程名字为A,A是我们手动赋予的c.setName("A");,并且它是运行着的。但如果将Run类换成:
public class Run {
public static void main(String[] args) {
CountOperate c = new CountOperate();
Thread t1 = new Thread(c);
t1.setName("A");
t1.start();
}
}
将输出:
CountOperate---begin
Thread.currentThread().getName()=main
this.getName()=Thread-0
CountOperate---end
run---begin
Thread.currentThread().getName()=A
this.getName()=Thread-0
run---end
这里将线程对象以构造参数的方式传递给Thread对象进行start()启动线程 。 run()方法前的代码没什么好说的。在run()中的代码结果表示,this 与 Thread.currentThread()不是同一个引用。将线程对象以构造参数的方式传递给Thread对象进行start()启动线程,我们直接启动的线程实际是newThread(即t1),而作为构造参数的c线程对象,赋给Thread类中的属性target,之后在Thread的run方法中调用target.run();
此时Thread.currentThread()是Thread的引用newThread, 而this依旧是c的引用,所以是不一样的,打印的内容也不一样。
1.4 isAlive()方法
方法isAlive()的功能是判断当前的线程是否处于活动状态。活动状态就是:线程已经启动且尚未终止,线程处于正在运行或准备开始运行的状态,就认为线程是 “存活”的。
1.5 sleep()方法
方法sleep()的作用是在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。 这个“正在执行的线程”是指this.currentThread()返回的线程。注意:调用sleep()方法不会释放锁
1.6 getld()方法
getld()方法的作用是取得线程的唯一标识Id。比如一般main线程的id值为1。
1.7 停止线程
停止一个线程意味着在线程处理完任务之前停掉正在做的操作,也就是放弃当前的操作。停止一个线程可以使用Thread.stop()方法,但最好不用它,因为这个方法是不安全的(unsafe),而且是已被弃用作废的(deprecated)。大多数停止一个线程的操作使用Thread.interrupt()方法,尽管方法的名称是“停止,中 止”的意思,但这个方法不会终止一个正在运行的线程,还需要加入一个判断才可以完成线程的停止。
在Java中有以下3种方法可以终止正在运行的线程:(注意:执行完线程中的所有代码后,线程就自动结束并自我销毁,无法再次调用start()方法)
- 使用interrupt方法中断线程。
- 使用退出标志
- 使用stop方法强行终止线程,但是不推荐使用这个方法,因为stop和suspend及 resume—样,都是作废过期的方法,使用它们可能产生不可预料的结果。
1.7.1 interrupt方法
interrupt()方法的使用效果并不像 for+break语句那样,马上就停止循环。调用interrupt()方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程。
首先看一个例子:
public class MyThread extends Thread {
@Override
public void run() {
super.run();
for (int i = 0; i < 500000; i++) {
System.out.println("i=" + (i + 1));
}
}
}
public class Run {
public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(2000);
thread.interrupt();
} catch (InterruptedException e) {
System.out.println("main catch");
e.printStackTrace();
}
}
}
运行后的结果是:
...
i=499997
i=499998
i=499999
i=500000
...
应用程序并不会退出,启动的线程没有因为调用interrupt而终止,可是从调用isInterrupted方法返回的结果可以清楚地知道该线程已经中断了。那为什么会出现这种情况呢?到底是interrupt方法出问题了还是isInterrupted方法出问题了?在Thread类中还有一个测试中断状态的方法(静态的)interrupted,换用这个方法测试,得到的结果是一样的。实际上,在JAVA API文档中对该方法进行了详细的说明。该方法实际上只是设置了一个中断状态,当该线程由于下列原因而受阻时,则其中断状态将被清除,它还将收到一个 InterruptedException:
(1)如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个InterruptedException异常。这个时候,我们可以通过捕获InterruptedException异常来终止线程的执行,具体可以通过return等退出或改变共享变量的值使其退出。
(2)如果该线程在可中断的通道上的 I/O 操作中受阻,则该通道将被关闭,该线程的中断状态将被设置并且该线程将收到一个 ClosedByInterruptException。这时候处理方法一样,只是捕获的异常不一样而已。
interrupt方法可以和下面方法结合使用停止线程:
- interrupt+抛出异常(推荐):
public class MyThread extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 500000; i++) {
if (this.interrupted()) {
System.out.println("已经是停止状态了!我要退出了!");
throw new InterruptedException();
}
System.out.println("i=" + (i + 1));
}
System.out.println("我在for下面");
} catch (InterruptedException e) {
System.out.println("进MyThread.java类run方法中的catch了!");
e.printStackTrace();
}
}
}
public class Run {
public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(2000);
thread.interrupt();
} catch (InterruptedException e) {
System.out.println("main catch");
e.printStackTrace();
}
System.out.println("end!");
}
}
输出:
...
i=256699
i=256700
i=256701
i=256702
end!
已经是停止状态了!我要退出了!
进MyThread.java类run方法中的catch了!
java.lang.InterruptedException
at exthread.MyThread.run(MyThread.java:11)
建议使用“抛异常”的方法来实现线程的停止,因为在catch块中可以对异常的信息进行相关的处理,而且使用异常流能更好、更方便的控制程序的运行流程,不至于代码中出现很多个return;污染代码。
- interrupt+标志位:
上面的MyThread.java改为:
public class MyThread extends Thread {
private volatile boolean stop =false; //必须用volatile保证可见性
@Override
public void run() {
int i = 0;
while(!stop){
System.out.println("i=" + i++);
if (this.interrupted()) {
System.out.println("已经是停止状态了!我要退出了!");
stop = true;
}
}
}
}
输出:
...
i=261075
i=261076
i=261077
end!
已经是停止状态了!我要退出了!
- interrupt+reurn:
上面的MyThread.java改为:
public class MyThread extends Thread {
@Override
public void run() {
while (true) {
if (this.isInterrupted()) {
System.out.println("停止了!");
return;
}
System.out.println("timer=" + System.currentTimeMillis());
}
}
}
输出:
...
timer=1500947629722
timer=1500947629722
timer=1500947629722
停止了!
1.7.2 使用退出标志
public class MyThread extends Thread {
public volatile boolean stop =false; //必须用volatile保证可见性
@Override
public void run() {
int i = 0;
while(!stop){
System.out.println("i=" + i++);
}
}
}
public class Run {
public static void main(String[] args) throws InterruptedException {
MyThread t=new MyThread();
t.start();
Thread.sleep(2000);
t.stop = true;
System.out.println(t.isAlive());
}
}
输出:
i=256163
i=256164
i=256165
true
1.7.3 使用stop方法
方法stop()已经被作废,因为如果强制让线程停止则有可能使一些清理性的工作得不到完成。就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果。另外一个情况就是对锁定的对象进行了 “解锁”,导致数据得不到同步的处理,出现数据不一致的问题(比如一个线程正在修改数据,刚改了一半被stop了,那么就会出现数据不一致)。
1.8 暂停线程
暂停线程意味着此线程还可以恢复运行。在Java多线程中,不推荐使用suspend()方法暂停线程,使用resume()方法恢复线程的执行,原因如下:
假设两个线程A、B和一个资源P,B锁定了资源P,A调用suspend()方法中断线程B,但同时A又想获取资源P。对任何线程来说,如果它们想中断目标线程,同时又试图使用这个线程锁定的资源,就会造成死锁,也就是暂停未释放锁。但如果暂停释放了锁,又可能出现不同步问题,比如线程B正在修改P中的数据,刚修改一半被suspend了,然后他释放了P的锁,A线程获得P会发现P中的数据不一致。
如何正确的挂起一个线程:可以在Thread实例外设置一个volatile 修饰的boolean变量,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用 wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。同时 ,wait() 和 notify() 这一对方法必须在 synchronized 方法或块中调用,理由也很简单,只有在 synchronized 方法或块中当前线程才占有锁,才有锁可以释放。后面章节我们会介绍。
1.9 yield方法
yield()方法的作用是放弃当前的CPU资源,将它让给其他的任务去占用CPU执行时间。但它不会阻塞该线程,它只是将该线程转入到就绪状态。即让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行。
当某个线程调用了yield()方法之后,只有优先级与当前线程相同或者比当前线程更高的处于就绪状态的线程才会获得执行机会。
1.10 线程的优先级
在操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU资源较多,也就是CPU优先执行优先级较高的线程对象中的任务。设置线程优先级有助于帮“线程规划器”确定下一次选择哪一个线程来优先执行。
设置线程的优先级使用setPriority()方法,在Java中,线程的优先级分为1 ~ 10这10个等级,如果小于1或大于10,则JDK抛出异常 throw new IllegalArgumentException()。默认优先级是5。
在Java中,线程的优先级具有继承性,比如A线程启动B线程,则B线程的优先级与 A是一样的。
优先级高只能保证更大概率的被优先执行,但并不保证一定被先执行完。所以不要把线程的优先级与运行结果的顺序作为衡量的标准,优先级较高的线程并不一定每一次都先执行完run()方法中的任务,也就是说, 线程优先级与打印顺序无关,不要将这两者的关系相关联,它们的关系具有不确定性和随机性。
1.11 守护线程
在Java线程中有两种线程,一种是用户线程,另一种是守护Daemon[ˈdi:mən]线程。
守护线程是一种特殊的线程,又称服务进程、后台线程,是指在程序运行时在后台提供一种通用服务的线程,这种线程并不属于程序中不可或缺的部分。当进程中不存在非守护线程了,则守护线程自动销毁。典型的守护线程就是垃圾回收线程,当进程中没有非守护线程了,则垃圾回收线程也就没有存在的必要了,自动销毁。用个比较通俗的比喻来解释一下 “守护线程”:任何一个守护线程都是整个JVM中所有非守护线程的“保姆”,只要当前JVM 实例中存在任何一个非守护线程没有结束,守护线程就在工作,只有当最后一个非守护线程结束时,守护线程才随着JVM 一同结束工作。
第2章 对象及变量的并发访问
2.1 synchronized 同步方法
2.1.1 synchronized方法与锁对象
首先给出例子:
public class PublicVar {
public String username = "A";
public String password = "AA";
synchronized public void setValue(String username, String password) {
try {
this.username = username;
Thread.sleep(5000);
this.password = password;
System.out.println("setValue method thread name=" + Thread.currentThread().getName() + " username=" + username + " password=" + password);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void getValue() {
System.out.println("getValue method thread name=" + Thread.currentThread().getName() + " username=" + username + " password=" + password);
}
}
public class ThreadA extends Thread {
private PublicVar publicVar;
public ThreadA(PublicVar publicVar) {
this.publicVar = publicVar;
}
@Override
public void run() {
publicVar.setValue("B", "BB");
}
}
public class Test {
public static void main(String[] args) {
try {
PublicVar p = new PublicVar();
ThreadA thread = new ThreadA(p);
thread.start();
Thread.sleep(200);// 打印结果受此值大小影响
p.getValue();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出:
getValue method thread name=main username=B password=AA
setValue method thread name=Thread-0 username=B password=BB
上面的代码大家很容易找到解决方法,使用synchronized声明getValue方法即可,我们主要在于分析:
- 当A线程调用anyObject对象加入synchronized关键字的X方法时,A线程就获得了 X 方法锁,更准确地讲,是获得了对象的锁,所以其他线程必须等A线程执行完毕才可以调用 X方法,但B线程可以随意调用其他的非synchronized同步方法。
- 同理,当A线程调用anyObject对象加入synchronized关键字的X方法时,A线程就获得了 X 方法所在对象的锁,而B线程如果调用声明了 synchronized关键字的非X方法时,也必须等A线程将X方法执行完,也就是释放对象锁后才可以调用。这时A线程已经执行了一个完整的任务,也就是说usemame和 password这两个实例变量已经同时被赋值,也就能保证getValue的线程安全。
2.1.2 synchronized 锁重入
关键字synchronized拥有锁重人的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。
2.1.3 出现异常,锁自动释放
当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
2.1.4 同步不具有继承性
同步不能继承,如果子类覆盖了父类的synchronized方法,还得在子类的对应方法中添加synchronized关键字。
2.2 synchronized同步语句块
用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个长时间的任务,那么B线程则必须等待比较长时间。在这样的情况下可以使用 synchronized同步语句块来解决,但要保证每个共享的和可变的变量都应该只由一个锁来保护。同时,对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。详细可以看用锁来保护状态
2.2.1 synchronized(this)同步语句块
在使用同步synchronized(this)代码块时需要注意的是,当一个线程访问object的一个 synchronized(this)同步代码块时,其他线程对同一个object中所有其他synchronized(this)同步代码块的访问或者同一个object中所有其他synchronized方法的访问都将被阻塞,这说明synchronized使用的“对象监视器”是一个,即synchronized(this)代码块也是锁定当前对象的。
2.2.2 synchronized(非this对象x)同步语句块
当然这个非this对象x
一般指实例变量或者方法的参数。
同样,在多个线程持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this对象x)同步代码块中的代码。但注意:锁非this对象具有一定的优点:如果在一个类中有很多个synchronized方法,这时虽然能实现同步,但会受到阻塞,所以影响运行效率;但如果使用同步代码块锁非this对象,则synchronized(非this)代码块中的程序与同步方法是异步的,不与其他锁this同步方法争抢this锁,则可大大提高运行效率。(即synchronized(this)锁的是当前对象,synchronized(非this对象x)锁的是x对象)
比如下面两个synchronized同步语句块可以异步执行,不会互相阻塞,从而提高运行效率。(但synchronized (list) 会阻塞另一个对list对象同步语句块的调用,synchronized (this) 会阻塞另一个对this对象同步语句块或者同步方法的调用)。在将任何数据类型作为同步锁时,需要注意的是,是否有多个线程同时持有锁对象,如果同时持有相同的锁对象,则这些线程之间就是同步的;如果分别获得不同的锁对象,这些线程之间就是异步的。
public class MyService {
public MyOneList addServiceMethod(MyOneList list, String data) {
try {
synchronized (list) {
if (list.getSize() < 1) {
Thread.sleep(2000);
list.add(data);
}
}
synchronized (this) {
i=2;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return list;
}
}
2.2.3 静态同步 synchronized 方法与 synchronized(class)代码块
synchronized关键字加到static 静态方法上是给Class类上锁,而synchronized关键字加到非static静态方法上是给对象上锁。Class锁可以对类的所有对象实例起作用。
同步synchronized (class)代码块的作用其实和synchronized static方法的作用类似,都是给Class类上锁。
2.2.4 多线程的死锁
2.3 volatile 关键字
volatile 关键字可以保证变量的可见性。
第3章 线程间通信
3.1 等待/通知机制
3.1.1 等待/通知机制的实现
Java多线程的等待/通知机制是用来完成线程之间的通信的,包括wait(),notify(),notifyAll()方法,注意它们都是Object类的方法。
wait()方法
wait()的作用:使当前执行代码的线程在wait()所在的代码行处停止执行,并置入“预执行队列”中,直到接到通知或被中断为止。
在调用wait()之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法。在执行wait()方法后,当前线程释放锁。在从 wait()返回前(即恢复执行前),线程与其他线程竞争重新获得锁。如果调用wait()时没有持有适当的锁,则抛出 IllegalMonitorStateException。
notify()方法
notify()的作用:用来随机通知一个等待获得该对象锁的其他线程,并在当前线程将程序执行完,也就是退出synchronized代码块后,将对象的对象锁给被通知的线程。
需要说明的是:
- 在执行notify()方法后,当前线程不会马上释放该对象锁,呈WAITING状态的线程也并不能马上获取该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出synchronized代码块后,当前线程才会释放锁,而呈WAITING状态所在的线程才可以获取该对象锁。
- 当第一个获得了该对象锁的WAITING线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用notify语句(其实是执行完对象锁锁住的区域后再释放锁),则即便该对象已经空闲,其他WAITING状态等待的线程由于没有得到该对象的通知,还会继续阻塞在WAITING状态,直到这个对象发出一个notify或notifyAll。
- notify()也要在同步方法或同步块中调用,即在调用前,线程也必须获得该对象的对象级别锁。如果调用notify()时没有持有适当的锁,也会抛出 IllegalMonitorStateException。
经典例子
public class WaitNotify {
static boolean flag = true;
volatile static Object lock = new Object();
public static void main(String[] args) throws Exception {
Thread waitThread = new Thread(new Wait(), "WaitThread");
waitThread.start();
Thread.sleep(1000);
Thread notifyThread = new Thread(new Notify(), "NotifyThread");
notifyThread.start();
}
static class Wait implements Runnable {
public void run() {
synchronized (lock) {
while (flag) {// 当条件满足的时候,进入WAITTING状态,同时释放lock锁
System.out.println("flag is true");
try {
lock.wait();
System.out.println("回到wait()之后");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("doSomething");
}
}
}
static class Notify implements Runnable {
public void run() {
synchronized (lock) {
// 获取lock的锁,然后进行通知,通知不会释放lock锁,直到发出通知的线程执行完毕释放了lock锁,WaitThread线程才能从wait方法返回
lock.notify();
System.out.println("flag is false now");
flag = false;
System.out.println("直到发出通知的线程执行完毕释放了lock锁");
}
}
}
}
输出:
flag is true
flag is false now
直到发出通知的线程执行完毕释放了lock锁
回到wait()之后
doSomething
关键字synchronized可以将任何一个Object对象作为同步对象来看待,而Java为每个Object都实现了 wait()和notify()方法,它们必须用在被synchronized同步的Object的临界区内。通过调用wait方法可以使处于临界区内的线程进人等待状态,同时释放被同步对象的锁。而notify操作可以唤醒一个因调用了wait操作而处于阻塞状态中的线程,使其进入就绪状态。被重新唤醒的线程会试图重新获得临界区的控制权,也就是锁,并继续执行临界区内wait之后的代码(可以看到上面的代码中flag==false,但依然从wait之后的代码执行)。如果发出notify操作时没有处于阻塞状态中的线程,那么该命令会被忽略。
notifyAll()方法
notifyAll()的作用:可以使所有正在等待队列中等待同一共享资源的“全部”线程从等待状态退出,进入BLOCKED状态。此时,由于只有一个线程可以获得锁,优先级最高的那个线程最先执行,但也有可能是随机执行,因为这要取决于JVM虚拟机的实现。
3.1.2 当interrupt方法遇到wait方法
当线程呈wait()状态时,调用线程对象的interrupt()方法会出现InterruptedException异常。在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放。
3.1.3 生产者/消费者模式实现
等待/通知模式最经典的案例就是“生产者/消费者”模式。但此模式在使用上有几种“变形",还有一些小的注意事项,但原理都是基于wait/notify的。
一生产与一消费:操作值
例子:
三个实体类:
public class ValueObject {
public static String value = "";
}
//生产者实体
public class P {
private String lock;
public P(String lock) {
this.lock = lock;
}
public void setValue() {
try {
synchronized (lock) {
if (!ValueObject.value.equals("")) { //值不为空,则等待消费者消费;否则创建产品
lock.wait();
}
String value = System.currentTimeMillis() + "_" + System.nanoTime();
System.out.println("set的值是" + value);
ValueObject.value = value;
lock.notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费者实体
public class C {
private String lock;
public C(String lock) {
this.lock = lock;
}
public void getValue() {
try {
synchronized (lock) {
if (ValueObject.value.equals("")) {
lock.wait();
}
System.out.println("get的值是" + ValueObject.value);
ValueObject.value = "";
lock.notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
生产者和消费者线程类:
public class ThreadP extends Thread {
private P p;
public ThreadP(P p) {
this.p = p;
}
@Override
public void run() {
while (true) {
p.setValue();
}
}
}
public class ThreadC extends Thread {
private C c;
public ThreadC(C c) {
this.c = c;
}
@Override
public void run() {
while (true) {
c.getValue();
}
}
}
测试类:
public class Run {
public static void main(String[] args) {
String lock = new String("");
P p = new P(lock);
C c = new C(lock);
ThreadP pThread = new ThreadP(p);
ThreadC cThread = new ThreadC(c);
pThread.start();
cThread.start();
}
}
输出:
...
set的值是1500982553158_1043197026048019
get的值是1500982553158_1043197026048019
set的值是1500982553158_1043197026063020
get的值是1500982553158_1043197026063020
本示例是1个生产者和1个消费者进行数据的交互,在控制台中打印的日志get和set是交替运行的。但如果在此实验的基础上,设计出多个生产者和多个消费者,那么在运行的过程中极有可能出现“假死”的情况,也就是所有的线程都呈WAITING等待状态。
多生产与多消费:操作值
例子:
三个实体类:
public class ValueObject {
public static String value = "";
}
//生产者实体
public class P {
private String lock;
public P(String lock) {
this.lock = lock;
}
public void setValue() {
try {
synchronized (lock) {
while (!ValueObject.value.equals("")) {
System.out.println("生产者 " + Thread.currentThread().getName() + " WAITING了★");
lock.wait();
}
System.out.println("生产者 " + Thread.currentThread().getName() + " RUNNABLE了");
String value = System.currentTimeMillis() + "_" + System.nanoTime();
ValueObject.value = value;
lock.notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费者实体
public class C {
private String lock;
public C(String lock) {
this.lock = lock;
}
public void getValue() {
try {
synchronized (lock) {
while (ValueObject.value.equals("")) {
System.out.println("消费者 " + Thread.currentThread().getName() + " WAITING了☆");
lock.wait();
}
System.out.println("消费者 " + Thread.currentThread().getName() + " RUNNABLE了");
ValueObject.value = "";
lock.notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
生产者和消费者线程类:
public class ThreadP extends Thread {
private P p;
public ThreadP(P p) {
this.p = p;
}
@Override
public void run() {
while (true) {
p.setValue();
}
}
}
public class ThreadC extends Thread {
private C c;
public ThreadC(C c) {
this.c = c;
}
@Override
public void run() {
while (true) {
c.getValue();
}
}
}
测试类:
public class Run {
public static void main(String[] args) throws InterruptedException {
String lock = new String("");
P p = new P(lock);
C c = new C(lock);
ThreadP[] pThread = new ThreadP[2];
ThreadC[] cThread = new ThreadC[2];
for (int i = 0; i < 2; i++) {
pThread[i] = new ThreadP(p);
pThread[i].setName("生产者" + (i + 1));
cThread[i] = new ThreadC(c);
cThread[i].setName("消费者" + (i + 1));
pThread[i].start();
cThread[i].start();
}
Thread.sleep(5000);//5秒之后打印所有线程状态
Thread[] threadArray = new Thread[Thread.currentThread().getThreadGroup().activeCount()];
Thread.currentThread().getThreadGroup().enumerate(threadArray);
for (int i = 0; i < threadArray.length; i++) {
System.out.println(threadArray[i].getName() + " " + threadArray[i].getState());
}
}
}
输出:
从打印的信息来看,呈假死状态的进程中所有的线程都呈WAITING状态,为什么会出现这样的情况呢?
在代码中确实已经通过wait/notify进行通信了,但不保证notify唤醒的是异类,也许是同类,比如“生产者”唤醒“生产者”,或“消费者”唤醒“消费者”这样的情况。如果按这样情况运行的比率积少成多,就会导致所有的线程都不能继续运行下去,大家都在等待, 都呈WAITING状态,程序最后也就呈“假死”状态,不能继续运行下去了。我们分析一下上面的结果:
- 生产者1进行生产,生产完毕后发出通知(但此通知属于“通知过早”,因为没有线程处于waiting状态),并释放锁, 准备进入下一次的while循环。
- 生产者1进入了下一次while循环,迅速再次持有锁,发现产品并没有被消费,所以生产者1呈等待状态★。
- 生产者2被start()启动,生产者2发现产品还没有被消费,所以生产者2也呈等待状态★
- 消费者2被start()启动,消费者2持有锁,将产品消费并发出通知(发出的通知唤醒了第7行生产者1 ),运行结束后释放锁,等待消费者2进入下次循环。
- 消费者2进入了下一次的while循环,并持有锁,发现产品并未生产,所以释放锁并呈等待状态☆。
- 消费者1被start()启动,快速持有锁,发现产品并未生产,所以释放锁并呈等待状态☆。
- 由于消费者2在第4行已经将产品进行消费,唤醒了第7行的生产者1进行顺利生产后释放锁,并发出通知(此通知唤醒了第9行的生产者2),生产者1准备进入下一次的 while循环。
- 这时生产者1进入下一次的while循环再次持有锁,发现产品还并未消费,所以生产者1也呈等待状态★。
- 由于第7行的生产者1唤醒了生产者2,生产者2发现产品还并未被消费,所以生产者2也呈等待状态★。
出现★符号就代表本线程进人等待状态,需要额外注意这样的执行结果。
假死出现的主要原因是有可能连续唤醒同类。怎么能解决这样的问题呢?
解决:解决“假死”的情况很简单,将P.java和C.java文件中的notify()改成notifyAll()方法即可,它的原理就是不光通知同类线程,也包括异类。这样就不至于出现假死的状态了,程序会一直运行下去。
多生产与多消费:操作栈
本示例是使生产者向堆栈List对象中放人数据,使消费者从List堆栈中取出数据。List 最大容量是1,实验环境多个生产者与多个消费者。
例子:
三个实体类:
public class MyStack {
private List list = new ArrayList();
synchronized public void push() {
try {
while (list.size() == 1) { //这里用while的原因:如果用if,可能两个消费线程先后被唤醒,第一个线程删除数据后,第二个会报IndexOutofBoundException,越界异常。
this.wait();
}
list.add("anyString=" + Math.random());
this.notifyAll();
System.out.println("push=" + list.size());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized public String pop() {
String returnValue = "";
try {
while (list.size() == 0) {
System.out.println("pop操作中的" + Thread.currentThread().getName() + " 线程呈wait状态");
this.wait();
}
returnValue = "" + list.get(0);
list.remove(0);
this.notifyAll();
System.out.println("pop=" + list.size());
} catch (InterruptedException e) {
e.printStackTrace();
}
return returnValue;
}
}
//生产者实体
public class P {
private MyStack myStack;
public P(MyStack myStack) {
this.myStack = myStack;
}
public void pushService() {
myStack.push();
}
}
//消费者实体
public class C {
private MyStack myStack;
public C(MyStack myStack) {
this.myStack = myStack;
}
public void popService() {
System.out.println("pop=" + myStack.pop());
}
}
生产者和消费者线程类:
public class P_Thread extends Thread {
private P p;
public P_Thread(P p) {
this.p = p;
}
@Override
public void run() {
while (true) {
p.pushService();
}
}
}
public class C_Thread extends Thread {
private C c;
public C_Thread(C c) {
this.c = c;
}
@Override
public void run() {
while (true) {
c.popService();
}
}
}
测试类:
public class Run {
public static void main(String[] args) throws InterruptedException {
MyStack myStack = new MyStack();
P p1 = new P(myStack);
P p2 = new P(myStack);
P p3 = new P(myStack);
P p4 = new P(myStack);
P p5 = new P(myStack);
P p6 = new P(myStack);
P_Thread pThread1 = new P_Thread(p1);
P_Thread pThread2 = new P_Thread(p2);
P_Thread pThread3 = new P_Thread(p3);
P_Thread pThread4 = new P_Thread(p4);
P_Thread pThread5 = new P_Thread(p5);
P_Thread pThread6 = new P_Thread(p6);
pThread1.start();
pThread2.start();
pThread3.start();
pThread4.start();
pThread5.start();
pThread6.start();
C c1 = new C(myStack);
C c2 = new C(myStack);
C c3 = new C(myStack);
C c4 = new C(myStack);
C c5 = new C(myStack);
C c6 = new C(myStack);
C c7 = new C(myStack);
C c8 = new C(myStack);
C_Thread cThread1 = new C_Thread(c1);
C_Thread cThread2 = new C_Thread(c2);
C_Thread cThread3 = new C_Thread(c3);
C_Thread cThread4 = new C_Thread(c4);
C_Thread cThread5 = new C_Thread(c5);
C_Thread cThread6 = new C_Thread(c6);
C_Thread cThread7 = new C_Thread(c7);
C_Thread cThread8 = new C_Thread(c8);
cThread1.start();
cThread2.start();
cThread3.start();
cThread4.start();
cThread5.start();
cThread6.start();
cThread7.start();
cThread8.start();
}
}
3.1.4 通过管道进行线程间通信:字节流
在Java语言中提供了各种各样的输入/输出流Stream,使我们能够很方便地对数据进行操作,其中管道流(pipeStream)是一种特殊的流,用于在不同线程间直接传送数据。一个线程发送数据到输出管道,另一个线程从输人管道中读数据。
通过使用管道,实现不同线程间的通信,而无须借助于类似临时文件之类的东西。在Java的JDK中提供了 4个类来使线程间可以进行通信:
- PipedlnputStream 和 PipedOutputStream
- PipedReader 和 PipedWriter
例子:
public class WriteData {
public void writeMethod(PipedOutputStream out) {
try {
System.out.println("write :");
for (int i = 0; i < 300; i++) {
String outData = "" + (i + 1);
out.write(outData.getBytes());
System.out.print(outData);//打印写入的结果
}
System.out.println();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class ReadData {
public void readMethod(PipedInputStream input) {
try {
System.out.println("read :");
byte[] byteArray = new byte[20];
int readLength = input.read(byteArray);
while (readLength != -1) {
String newData = new String(byteArray, 0, readLength);
System.out.print(newData);//打印读取的结果
readLength = input.read(byteArray);
}
System.out.println();
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class ThreadWrite extends Thread {
private WriteData write;
private PipedOutputStream out;
public ThreadWrite(WriteData write, PipedOutputStream out) {
this.write = write;
this.out = out;
}
@Override
public void run() {
write.writeMethod(out);
}
}
public class ThreadRead extends Thread {
private ReadData read;
private PipedInputStream input;
public ThreadRead(ReadData read, PipedInputStream input) {
this.read = read;
this.input = input;
}
@Override
public void run() {
read.readMethod(input);
}
}
public class Run {
public static void main(String[] args) {
try {
WriteData writeData = new WriteData();
ReadData readData = new ReadData();
PipedInputStream inputStream = new PipedInputStream();
PipedOutputStream outputStream = new PipedOutputStream();
// inputStream.connect(outputStream);
outputStream.connect(inputStream);
ThreadRead threadRead = new ThreadRead(readData, inputStream);
threadRead.start();
Thread.sleep(2000);
ThreadWrite threadWrite = new ThreadWrite(writeData, outputStream);
threadWrite.start();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出(两行数字相同,分别表示写入和读出):
在此实验中,首先是读取线程newThreadRead(inputStream)启动,由于当时没有数据被写入, 所以线程阻塞在int readLength = input.read(byteArray);代码中,直到有数据被写入,才继续向下运行。
3.2 方法join的使用
在很多情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时运算,主线程往往将早于子线程结束之前结束。这时,如果主线程想等待子线程执行完成之后再结束, 比如子线程处理一个数据,主线程要取得这个数据中的值,就要用到join()方法了。方法 join()的作用是等待线程对象销毁。
例子:
public class MyThread extends Thread {
@Override
public void run() {
try {
int secondValue = (int) (Math.random() * 10000);
System.out.println(secondValue);
Thread.sleep(secondValue);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Test {
public static void main(String[] args) {
try {
MyThread threadTest = new MyThread();
threadTest.start();
threadTest.join();
System.out.println("我想当threadTest对象执行完毕后我再执行,我做到了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出:
655
我想当threadTest对象执行完毕后我再执行,我做到了
方法join的作用是使所属的线程对象x正常执行run()方法中的任务,而使当前线程z进行无限期的阻塞,等待线程x销毁后再继续执行线程z后面的代码。方法join具有使线程排队运行的作用,有些类似同步的运行效果。join与synchronized 的区别是:join在内部使用wait()方法进行等待,而sychronized关键字使用的是排它锁原理进行同步。
3.2.1 方法join与异常
方法join()与interrupt()方法如果彼此遇到,则会出现异常InterruptedException。
3.2.2 方法 join(long)与 sleep(long)的区别
方法join(long)的功能在内部是使用wait(long)方法来实现的,所以join(long)方法具有释放锁的特点。从源代码中可以了解到,当执行wait(long)方法后,当前线程的锁被释放,那么其他线程就可以调用此线程中的同步方法了。而Thread.sleep(long)方法却不释放锁。
join(long)方法的源码:
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
3.2.3 方法 wait()与 sleep()的区别
1.所属的类不同:
- sleep是来自Thread类的静态类方法,用于使本线程暂停执行一段时间。
- wait来自Object类的成员方法,用于线程间的通信,这个方法会使当前拥有对象锁的线程等待一段时间或者无限期等待,直到到达时间或者其他线程调用notify/notifyAll方法将其唤醒。
2.对锁的处理机制不同:
- sleep方法主要是使线程暂停执行一段时间,时间一到则自动恢复,不涉及线程间的通信,因此调用sleep方法不会释放锁。
- wait方法被调用后,锁被解除,由其它需要使用该对象锁的线程去争夺。
3.使用范围不同:
- wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用。
- sleep可以在当前线程的任何地方使用。
3.2.4 方法 yield()与 sleep()的区别
1、sleep()方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级;但yield()方法只会给优先级高或者相同的线程执行机会。
2、sleep()方法会将线程转入到阻塞状态,直到经过阻塞时间才会转入就绪状态;而yield()不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程调用了yield()方法暂停之后,立即再次获取处理器资源被执行。
3、sleep()方法声明抛出InterruptedException异常,所以调用sleep()方法时要么捕捉该异常,要么显式声明抛出该异常;而yield()方法则没有声明抛出任何异常。
4、sleep()方法比yield()方法有更好的可移植性,通常不建议使用yield()方法来控制并发线程执行。
3.3 类 ThreadLocal 的使用
3.4 类 InheritableThreadLocal 的使用
使用类InheritableThreadLocal可以在子线程中取得父线程继承下来的值。
但在使用InheritableThreadLocal类需要注意一点,如果子线程在取得值的同时,主线程将InheritableThreadLocal中的值进行更改,那么子线程取到的值还是旧值。
第 4 章 Lock的使用
4.1 使用 ReentrantLock类
在Java多线程中,可以使用synchronized关键字来实现线程之间同步互斥,但在JDK1.5中新增加了 ReentrantLock类也能达到同样的效果,并且在扩展功能上也更加强大,比如具有嗅探锁定、多路分支通知等功能,而且在使用上也比synchronized更加的灵活(实际推荐:只要不使用重入锁的高级功能,提倡选择synchronized,这一点可以看Java并发编程实战13.4节)。
例子:
private Lock lock = new ReentrantLock();
public void methodA() {
try {
lock.lock();
for (int i = 0; i < 5; i++) {
System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1)));
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
效果和下面代码相同:
public void methodA() {
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1)));
}
}
}
4.1.1 使用Condition实现等待/通知
关键字synchronized与wait()和notify()/notifyAll()方法相结合可以实现等待/通知模式,类ReentrantLock也可以实现同样的功能,但需要借助于Condition对象。Condition类是在JDK5中出现的技术,使用它有更好的灵活性,比如可以实现多路通知功能,也就是在一个Lock对象里面可以创建多个Condition (即对象监视器)实例,线程对象可以注册在指定的Condition中,从而可以有选择性地进行线程通知,在调度线程上更加灵活。
在使用notify()/notifyAll()方法进行通知时,被通知的线程却是由JVM随机选择的。但使用ReentrantLock结合Condition类可以实现前面介绍过的“选择性通知”,这个功能是非常重要的,而且在Condition类中是默认提供的。而synchronized就相当于整个Lock对象中只有一个单一的Condition对象,所有的线程都注册在它一个对象的身上。线程开始notifyAll()时,需要通知所有的WAITING线程,没有选择权,会出现相当大的效率问题。
- Object类中的wait()方法相当于Condition类中的await()方法。
- Object类中的wait(long timeout)方法相当于Condition类中的await(long time,TimeUnit unit)方法。
- Object类中的notify()方法相当于Condition类中的signal()方法。
- Object类中的notifyAll()方法相当于Condition类中的signalAll()方法。
4.1.2 使用多个Condition实现通知部分线程
例子:
public class MyService {
private Lock lock = new ReentrantLock();
public Condition conditionA = lock.newCondition();
public Condition conditionB = lock.newCondition();
public void awaitA() {
try {
lock.lock();
System.out.println("begin awaitA时间为" + System.currentTimeMillis() + " ThreadName=" + Thread.currentThread().getName());
conditionA.await();
System.out.println("end awaitA时间为" + System.currentTimeMillis() + " ThreadName=" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void awaitB() {
try {
lock.lock();
System.out.println("begin awaitB时间为" + System.currentTimeMillis() + " ThreadName=" + Thread.currentThread().getName());
conditionB.await();
System.out.println("end awaitB时间为" + System.currentTimeMillis() + " ThreadName=" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signalAll_A() {
try {
lock.lock();
System.out.println("signalAll_A时间为" + System.currentTimeMillis() + " ThreadName=" + Thread.currentThread().getName());
conditionA.signalAll();
} finally {
lock.unlock();
}
}
public void signalAll_B() {
try {
lock.lock();
System.out.println("signalAll_B时间为" + System.currentTimeMillis() + " ThreadName=" + Thread.currentThread().getName());
conditionB.signalAll();
} finally {
lock.unlock();
}
}
}
public class ThreadA extends Thread {
private MyService service;
public ThreadA(MyService service) {
this.service = service;
}
@Override
public void run() {
service.awaitA();
}
}
public class ThreadB extends Thread {
private MyService service;
public ThreadB(MyService service) {
this.service = service;
}
@Override
public void run() {
service.awaitB();
}
}
public class Run {
public static void main(String[] args) throws InterruptedException {
MyService service = new MyService();
ThreadA a = new ThreadA(service);
a.setName("A");
a.start();
ThreadB b = new ThreadB(service);
b.setName("B");
b.start();
Thread.sleep(3000);
service.signalAll_A();
}
}
输出:
begin awaitA时间为1500993578296 ThreadName=A
begin awaitB时间为1500993578296 ThreadName=B
signalAll_A时间为1500993581296 ThreadName=main
end awaitA时间为1500993581297 ThreadName=A
通过此实验可以得知,使用ReentrantLock对象可以唤醒指定种类的线程,这是控制部分线程行为的方便方式。
4.1.3 实现生产者/消费者模式:多对多交替打印
实现和3.1.3很相似,例子:
public class MyService {
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private boolean hasValue = false;
public void set() {
try {
lock.lock();
while (hasValue == true) {
System.out.println("有可能★★连续");
condition.await();
}
System.out.println("打印★");
hasValue = true;
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void get() {
try {
lock.lock();
while (hasValue == false) {
System.out.println("有可能☆☆连续");
condition.await();
}
System.out.println("打印☆");
hasValue = false;
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class MyThreadA extends Thread {
private MyService myService;
public MyThreadA(MyService myService) {
this.myService = myService;
}
@Override
public void run() {
while(true) {
myService.set();
}
}
}
public class MyThreadB extends Thread {
private MyService myService;
public MyThreadB(MyService myService) {
this.myService = myService;
}
@Override
public void run() {
while(true) {
myService.get();
}
}
}
public class Run {
public static void main(String[] args) throws InterruptedException {
MyService service = new MyService();
MyThreadA[] threadA = new MyThreadA[10];
MyThreadB[] threadB = new MyThreadB[10];
for (int i = 0; i < 10; i++) {
threadA[i] = new MyThreadA(service);
threadB[i] = new MyThreadB(service);
threadA[i].start();
threadB[i].start();
}
}
}
输出:
...
打印★
有可能★★连续
有可能★★连续
有可能★★连续
打印☆
有可能☆☆连续
有可能☆☆连续
控制台中“打印★”和“打印☆”是交替输出的,但是“有可能★★连续”和“有可能☆☆连续”却不是交替输出的,有时候出现连续打印的情况。原因是程序中使用了一个 Condition对象,再结合signalAIl()方法来唤醒所有的线程,那么唤醒的线程就有可能是同类,所以就出现连续打印“有可能★★连续”或“有可能☆☆连续”的情况了
4.1.4 公平锁与非公平锁
公平与非公平锁:锁Lock分为“公平锁”和“非公平锁”,公平锁表示线程获取锁的顺序是按照线程请求锁的顺序来分配的,即先来先得的FIFO先进先出顺序。而非公平锁就是一 种获取锁的抢占机制,是随机获得锁的,和公平锁不一样的就是先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁,结果也就是不公平的了。
使用方式:
Lock lock = new ReentrantLock(false);
或者Lock lock = new ReentrantLock();
表示获得非公平锁;Lock lock = new ReentrantLock(true);
表示获得公平锁。
4.1.5 常用方法
- getHoldCount():查询当前线程保持此锁的次数,也就是调用 lock〇方法的次数。
- getQueueLength() :返回正等待获取此锁的线程估计数。
- getWaitQueueLength(Condition condition) : 返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程。
- hasQueuedThread(Thread thread) :查询给定线程是否正在等待获取此锁。
- hasQueuedThreads() :查询是否有些线程正在等待获取此锁。
- hasWaiters(Condition condition) :查询是否有些线程正在等待与此锁有关的给定条件。
- isFair() :如果此锁的公平设置为 true,则返回 true。
- isHeldByCurrentThread() :查询当前线程是否保持此锁。
- isLocked() :查询此锁是否由任意线程保持。
- lockInterruptibly() :如果当前线程未被中断(interrupt()方法),则获取锁;否则抛出异常。
- tryLock() :仅在调用时锁未被另一个线程保持的情况下,才获取该锁。
- getOwner() :返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null。
- getQueuedThreads() :返回一个 collection,它包含可能正等待获取此锁的线程。
4.1.6 使用Condition实现顺序执行
使用Condition对象可以对线程执行的业务进行排序规划。
public class Run {
volatile private static int nextPrintWho = 1;
private static ReentrantLock lock = new ReentrantLock();
final private static Condition conditionA = lock.newCondition();
final private static Condition conditionB = lock.newCondition();
final private static Condition conditionC = lock.newCondition();
public static void main(String[] args) {
Thread threadA = new Thread() {
public void run() {
try {
lock.lock();
while (nextPrintWho != 1) {
conditionA.await();
}
for (int i = 0; i < 3; i++) {
System.out.println("ThreadA " + (i + 1));
}
nextPrintWho = 2;
conditionB.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
};
Thread threadB = new Thread() {
public void run() {
try {
lock.lock();
while (nextPrintWho != 2) {
conditionB.await();
}
for (int i = 0; i < 3; i++) {
System.out.println("ThreadB " + (i + 1));
}
nextPrintWho = 3;
conditionC.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
};
Thread threadC = new Thread() {
public void run() {
try {
lock.lock();
while (nextPrintWho != 3) {
conditionC.await();
}
for (int i = 0; i < 3; i++) {
System.out.println("ThreadC " + (i + 1));
}
nextPrintWho = 1;
conditionA.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
};
Thread[] aArray = new Thread[5];
Thread[] bArray = new Thread[5];
Thread[] cArray = new Thread[5];
for (int i = 0; i < 5; i++) {
aArray[i] = new Thread(threadA);
bArray[i] = new Thread(threadB);
cArray[i] = new Thread(threadC);
aArray[i].start();
bArray[i].start();
cArray[i].start();
}
}
}
输出:
...
ThreadB 1
ThreadB 2
ThreadB 3
ThreadC 1
ThreadC 2
ThreadC 3
4.2 使用 ReentrantReadWriteLock 类
类ReentrantLock具有完全互斥排他的效果,即同一时间只有一个线程在执行 ReentrantLock.lock()方法后面的任务。这样做虽然保证了实例变量的线程安全性,但效率却是非常低下的。所以在JDK中提供了一种读写锁ReentrantReadWriteLock类。读写锁表示有两个锁,一个是读操作相关的锁,也称为共享锁;另一个是写操作相关 的锁,也叫排他锁。也就是多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。在没有线程Thread进行写入操作时,进行读取操作的多个Thread都可以获取读锁,而进行写入操作的Thread只有在获取写锁后才能进行写人操作。即多个Thread可以同时进行读取操作, 但是同一时刻只允许一个Thread进行写人操作。
由于“读写”、“写读”和“写写”都是互斥的;而“读读”是异步的,非互斥的,所以仅仅给出“读读”的代码。
public class Service {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void read() {
try {
try {
lock.readLock().lock();
System.out.println("获得读锁" + Thread.currentThread().getName() + " " + System.currentTimeMillis());
Thread.sleep(10000);
} finally {
lock.readLock().unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ThreadA extends Thread {
private Service service;
public ThreadA(Service service) {
this.service = service;
}
@Override
public void run() {
service.read();
}
}
public class ThreadB extends Thread {
private Service service;
public ThreadB(Service service) {
super();
this.service = service;
}
@Override
public void run() {
service.read();
}
}
public class Run {
public static void main(String[] args) {
Service service = new Service();
ThreadA a = new ThreadA(service);
a.setName("A");
ThreadB b = new ThreadB(service);
b.setName("B");
a.start();
b.start();
}
}
输出说明两个线程几乎同时进入读锁:
获得读锁B 1501032283053
获得读锁A 1501032283053
- lock.readLock().lock();
- lock.readLock().unlock();
- lock.writeLock().lock();
- lock.writeLock().unlock();
第5章 定时器Timer
5.1 定时器Timer的使用
在JDK库中Timer类主要负责计划任务的功能,即安排在指定的时间通过后台线程执行的任务。与每个 Timer 对象相对应的是单个后台线程,用于顺序地执行所有计时器任务。默认情况下,任务执行线程并不作为守护线程 来运行,所以它能够阻止应用程序终止。此类是线程安全的:多个线程可以共享单个 Timer 对象而无需进行外部同步。
构造函数:
- Timer(): 创建一个新计时器。
- Timer(boolean isDaemon) :创建一个新计时器,可以指定其相关的线程作为守护程序运行。
- Timer(String name) :创建一个新计时器,其相关的线程具有指定的名称。
- Timer(String name, boolean isDaemon) :创建一个新计时器,其相关的线程具有指定的名称,并且可以指定作为守护程序运行。
方法:
- cancel(): 终止此计时器,丢弃所有当前已安排的任务。
- purge() :从此计时器的任务队列中移除所有已取消的任务。
- schedule(TimerTask task, Date time) :安排在指定的时间执行指定的任务。
- schedule(TimerTask task, Date firstTime, long period) :安排指定的任务在指定的时间开始进行重复的固定延迟执行,period时间单位:毫秒。
- schedule(TimerTask task, long delay) :安排在指定延迟后执行指定的任务。
- schedule(TimerTask task, long delay, long period) :安排指定的任务从指定的延迟后开始进行重复的固定延迟执行(注意:如果由于任何原因(如垃圾回收或其他后台活动)而延迟了某次执行,则后续执行也将被延迟,即保证前一个任务和后一个任务的延迟时间固定)。
- scheduleAtFixedRate(TimerTask task, Date firstTime, long period) :安排指定的任务在指定的时间开始进行重复的固定速率执行(注意:如果由于任何原因(如垃圾回收或其他后台活动)而延迟了某次执行,则将快速连续地出现两次或更多的执行,从而使后续执行能够“追赶上来”,适用于对绝对时间敏感的情况)。
- scheduleAtFixedRate(TimerTask task, long delay, long period) :安排指定的任务在指定的延迟后开始进行重复的固定速率执行。
Timer类的主要作用就是设置计划任务,但封装任务的类却是TimerTask类。
所有已实现的接口: Runnable
方法:
- cancel() :将自身从任务队列中清除,即仅仅清除Timer中的当前任务,其他任务不受影响,注意和Timer的cancel()区分。
- run() :此计时器任务要执行的操作。
- scheduledExecutionTime() : 返回此任务最近实际执行的已安排执行时间。
实例1:
public class Run {
private static Timer timer = new Timer();
static public class MyTask1 extends TimerTask {
@Override
public void run() {
System.out.println("任务1执行时间为:" + new Date());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static public class MyTask2 extends TimerTask {
@Override
public void run() {
System.out.println("任务2执行时间为:" + new Date());
}
}
public static void main(String[] args) {
try {
MyTask1 task1 = new MyTask1();
MyTask2 task2 = new MyTask2();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateString1 = "2014-10-12 10:39:00";
String dateString2 = "2014-10-12 10:40:00";
System.out.println("计划1时间为:" + dateString1 + "现在时间为:" + sdf.format(new Date()));
System.out.println("计划2时间为:" + dateString2 + "现在时间为:" + sdf.format(new Date()));
timer.schedule(task1, sdf.parse(dateString1));
timer.schedule(task2, sdf.parse(dateString2));
} catch (ParseException e) {
e.printStackTrace();
}
}
}
输出:
如果计划执行任务的时间晚于当前时间:在未来执行;如果计划执行任务的时间早于当前时间:立刻执行。
同时,由于没有将任务执行线程设置为守护线程 来运行,故此线程一直在后台运行。如果设为守护线程 ,会在任务执行完毕后立刻结束当前线程。
当一个Timer中有多个TimerTask任务时,TimerTask是以队列的方式一个一个被顺序执行的,所以执行的时间有可能和预期的时间不一致,因为前面的任务有可能消耗的时间较长,则后面的任务运行的时间也会被延迟。
第 6 章 单例模式与多线程
6.1 立即加载/“饿汉模式”
什么是立即加载?立即加载就是在调用方法前,实例已经被创建了(其实是加载类的时候已经将对象创建完毕),常见的实现办法就是直接new一个private static的实例,然后通过public static的方法返回实例。而立即加载从中文的语境来看,有“着急”、“急迫”的含义,所以也称 为“饿汉模式”。
实例:
/**
* 立即加载方式==饿汉模式
* 此代码版本为立即加载,此版本代码的缺点是不能有其它实例变量
*/
public class Test {
private Test() {}
private static Test uniqueInstance = new Test();
public static Test getInstance() {
return uniqueInstance;
}
}
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(Test.getInstance().hashCode());
}
}
public class Run {
public static void main(String[] args) {
MyThread tl = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
tl.start();
t2.start();
t3.start();
}
}
输出:
350646954
350646954
350646954
控制台打印的hashCode是同一个值,说明对象是同一个,也就实现了立即加载型单例设计模式。
6.2 延迟加载/“懒汉模式”
延迟加载就是在调用get()方法时实例才被创建,常见的实现办法就是在get()方法中进行new实例化。而延迟加载从中文的语境来看,是“缓慢”、“不急迫” 的含义,所以也称为“懒汉模式”。
实例:
/**
* 延迟加载方式==懒汉模式
* 使用双检测锁(DCL,Double-Check Locking)机制,尽量减小同步块的大小,同时保证线程安全。
* DCL也是大多数多线程结合单例模式使用的解决方案。
*/
public class Test {
private Test() {}
private volatile static Test uniqueInstance;
public static Test getInstance() {
try {
if (uniqueInstance == null) {
// 模拟在创建对象之前做一些准备性的工作
Thread.sleep(3000);
synchronized (Test.class) {
if (uniqueInstance == null) {
uniqueInstance = new Test();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return uniqueInstance;
}
}
其他代码和输出与立即加载方式相同。说明:
- 如果不进行同步,可能多个线程同时检测到uniqueInstance == null,就会出现取出多个实例的情况。
- 如果用synchronized同步整个getInstance()方法,会将线程中耗时较长,并且不需要同步的代码也锁上,导致效率太低。
- 如果没有第二个if (uniqueInstance == null) 检测,同样可能出现:多个线程同时检测到uniqueInstance == null,从而取出多个实例的情况。
第7章 拾遗增补
7.1 线程的状态
7.2 线程组
可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程。这样的组织结构有些类似于树的形式,如图所示。
自动归属:在实例化一个ThreadGroup线程组或者线程时,如果不指定所属的线程组, 则自动归到当前线程对象所属的线程组中。JVM的根线程组是system。
7.2.1 线程对象关联线程组:1级关联
所谓的1级关联就是父对象中有子对象,但并不创建子孙对象。这种情况经常出现在开发中,比如创建一些线程时,为了有效地对这些线程进行组织管理,通常的情况下是创建一 个线程组,然后再将部分线程归属到该组中。这样的处理可以对零散的线程对象进行有效的组织与规划。
public class Run {
public static void main(String[] args) {
ThreadA aRunnable = new ThreadA();
ThreadB bRunnable = new ThreadB();
ThreadGroup group = new ThreadGroup("xxx的线程组");
Thread aThread = new Thread(group, aRunnable);
Thread bThread = new Thread(group, bRunnable);
aThread.start();
bThread.start();
System.out.println("活动的线程数为:" + group.activeCount());
System.out.println("线程组的名称为:" + group.getName());
}
}
7.2.2 线程对象关联线程组:多级关联
所谓的多级关联就是父对象中有子对象,子对象中再创建子对象,也就是出现子孙对象的效果了。但是此种写法在开发中不太常见,如果线程树结构设计得非常复杂反而不利于线程对象的管理,但JDK却提供了支持多级关联的线程树结构。
7.2.3 组内的线程批量停止
当调用ThreadGroup的interrupt()方法时,可以将该组中的所有正在运行的线程批量停止,当然这些线程需要有类似下面的判断代码来停止线程:
while (!this.isInterrupted()) {
...
}
7.3 使线程具有有序性
正常的情况下,线程在运行时多个线程之间执行任务的时机是无序的。可以通过改造代 码的方式使它们运行具有有序性。
public class MyThread extends Thread {
private Object lock; //公共的对象锁
private String showChar; //当前线程显示的字母
private int showNumPosition; //当数字是showNumPosition时该线程工作
private int printCount = 0;// 当前线程统计打印了几次字母
volatile private static int addNumber = 1; // 所有线程统计打印了几个字母
public MyThread(Object lock, String showChar, int showNumPosition) {
this.lock = lock;
this.showChar = showChar;
this.showNumPosition = showNumPosition;
}
@Override
public void run() {
try {
synchronized (lock) {
while (true) {
if (addNumber % 3 == showNumPosition) {
System.out.println("ThreadName=" + Thread.currentThread().getName() + " runCount=" + addNumber + " " + showChar);
lock.notifyAll();
addNumber++;
printCount++;
if (printCount == 3) {
break;
}
} else {
lock.wait();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Run {
public static void main(String[] args) {
Object lock = new Object();
MyThread a = new MyThread(lock, "A", 1);
MyThread b = new MyThread(lock, "B", 2);
MyThread c = new MyThread(lock, "C", 0);
a.start();
b.start();
c.start();
}
}
输出:
ThreadName=Thread-0 runCount=1 A
ThreadName=Thread-1 runCount=2 B
ThreadName=Thread-2 runCount=3 C
ThreadName=Thread-0 runCount=4 A
ThreadName=Thread-1 runCount=5 B
ThreadName=Thread-2 runCount=6 C
ThreadName=Thread-0 runCount=7 A
ThreadName=Thread-1 runCount=8 B
ThreadName=Thread-2 runCount=9 C
7.4 SimpleDateFormat 非线程安全
类SimpleDateFormat主要负责日期的转换与格式化,但在多线程的环境中,使用此类容易造成数据转换及处理的不准确,因为SimpleDateFormat类并不是线程安全的。
比如:
public class MyThread extends Thread {
private SimpleDateFormat sdf;
private String dateString;
public MyThread(SimpleDateFormat sdf, String dateString) {
this.sdf = sdf;
this.dateString = dateString;
}
@Override
public void run() {
try {
Date dateRef = sdf.parse(dateString);
String newDateString = sdf.format(dateRef);
if (!newDateString.equals(dateString)) {
System.out.println("ThreadName=" + this.getName() + "报错了 日期字符串:" + dateString + " 转换成的日期为:" + newDateString);
}
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public class Test {
public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String[] dateStringArray = new String[] { "2000-01-01", "2000-01-02", "2000-01-03", "2000-01-04", "2000-01-05",
"2000-01-06", "2000-01-07", "2000-01-08", "2000-01-09", "2000-01-10" };
MyThread[] threadArray = new MyThread[10];
for (int i = 0; i < 10; i++) {
threadArray[i] = new MyThread(sdf, dateStringArray[i]);
}
for (int i = 0; i < 10; i++) {
threadArray[i].start();
}
}
}
输出:
解决方法:
- 创建多个SimpleDateFormat实例:首先创建DateTools.java类,然后在MyThread.java中调用
DateTools.parse("yyyy-MM-dd", dateString);
和DateTools.format("yyyy-MM-dd", dateRef);
。
public class DateTools {
public static Date parse(String formatPattern, String dateString) throws ParseException {
return new SimpleDateFormat(formatPattern).parse(dateString);
}
public static String format(String formatPattern, Date date) {
return new SimpleDateFormat(formatPattern).format(date);
}
}
- 利用ThreadLocal类将新SimpleDateFormat实例绑定到对应的线程:首先创建DateTools.java类,然后在MyThread.java中调用
DateTools.getSimpleDateFormat("yyyy-MM-dd").parse( dateString);
和DateTools.getSimpleDateFormat("yyyy-MM-dd").format(dateRef);
。
public class DateTools {
private static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>();
public static SimpleDateFormat getSimpleDateFormat(String datePattern) {
SimpleDateFormat sdf = null;
sdf = tl.get();
if (sdf == null) {
sdf = new SimpleDateFormat(datePattern);
tl.set(sdf);
}
return sdf;
}
}
7.5 线程中出现异常的处理
在Java的多线程技术中,可以对多线程中的异常进行“捕捉”,使用的是UncaughtExceptionHandler类,从而可以对发生的异常进行有效的处理。
public class MyThread extends Thread {
@Override
public void run() {
String username = null;
System.out.println(username.hashCode());
}
}
public class Main1 {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.setName("线程t1");
t1.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("线程:" + t.getName() + "出现了异常:");
e.printStackTrace();
}
});
t1.start();
MyThread t2 = new MyThread();
t2.setName("线程t2");
t2.start();
}
}
输出:
可以看到线程t2仅仅抛出了空指针异常,而线程t1还打印其他信息。
方法setUncaughtExceptionHandler()是给指定线程对象设置的异常处理器。在Thread类中还可以使用方法setDefaultUncaughtExceptionHandler()为指定线程类的所有线程对象设置异常处理器,示例代码如下:
public class Main3 {
public static void main(String[] args) {
MyThread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("线程:" + t.getName() + "出现了异常:");
e.printStackTrace();
}
});
MyThread t1 = new MyThread();
t1.setName("线程t1");
t1.start();
MyThread t2 = new MyThread();
t2.setName("线程t2");
t2.start();
}
}