线程基础(后篇)

  • 1、线程的优先级
  • 2、线程的让步
  • 3、后台线程
  • 3、编码变体
  • 4、合并线程
  • 5、主线程(UI线程)
  • 6、线程组
  • 7、捕获异常

1、线程的优先级

每一个线程都有自己的优先级,默认不设置线程的优先级的话,就是使用默认的优先级,线程的优先级将该线程的重要性传递给线程调度器,尽管CPU处理现有线程的顺序是不确定的,但是调度器将更加倾向于让优先级最高的线程先执行。这里需要注意,并不是意味着优先级低的线程将得不到执行,(优先级不会导致死锁),意思是优先级比较低的线程执行的频率比较低而已

开发的大多数情况下,所有线程都应该使用默认的优先级运行。不要试图操作线程优先级。

优先级在线程当中是一个属性,我们可以通过get/set来操作它。下面我们来看一个关于优先级的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* 线程的优先级
* @author pengchengliu
*
*/
public class PriorityDemo implements Runnable{

private int mCountDwon = 5 ;
private volatile double mD ;
private int mPriority ;

public PriorityDemo(int priority) {
this.mPriority = priority ;
}
@Override
public void run() {
//设置线程的优先级
Thread.currentThread().setPriority(mPriority);
while(true){
for (int i = 1; i < 10000; i++){
mD += (Math.PI + Math.E) / (double) i ;
if(i % 1000 == 0){
Thread.yield();
}
}
System.out.println(this);
if(--mCountDwon == 0){
return ;
}
}
}

@Override
public String toString() {
return Thread.currentThread() + " :" + mCountDwon;
}

public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();
threadPool.execute(new PriorityDemo(Thread.MIN_PRIORITY));
threadPool.execute(new PriorityDemo(Thread.MAX_PRIORITY));
threadPool.shutdown();
}

}
Thread[pool-1-thread-2,10,main] :5
Thread[pool-1-thread-1,1,main] :5
Thread[pool-1-thread-2,10,main] :4
Thread[pool-1-thread-1,1,main] :4
Thread[pool-1-thread-2,10,main] :3
Thread[pool-1-thread-1,1,main] :3
Thread[pool-1-thread-2,10,main] :2
Thread[pool-1-thread-1,1,main] :2
Thread[pool-1-thread-2,10,main] :1
Thread[pool-1-thread-1,1,main] :1

明白Thread.tostring方法,会自动的打印出线程的名称,还有线程的优先级以及线程所属的线程组,具体打印会是什么样的规则,我们可以通过查看源码来熟悉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

/**
* Returns a string representation of this thread, including the
* thread's name, priority, and thread group.
*
* @return a string representation of this thread.
*/
public String toString() {
ThreadGroup group = getThreadGroup();
if (group != null) {
return "Thread[" + getName() + "," + getPriority() + "," +
group.getName() + "]";
} else {
return "Thread[" + getName() + "," + getPriority() + "," +
"" + "]";
}

}

通过查看tostring的源码,我们知道了在第二个参数会主动的打印当前线程的优先级。

我们可以在一个任务的内部拿到驱动这个任务的线程,并且如果我们要给当前的线程设置优先级的话,因该在run的开头部分进行设定,在构造器当中设定当前线程的优先级我们没有任何好处,因为此时Executor此时还没有开始执行任务。

通过上述的Demo,我们不难看出,确实是优先级为10的先执行了。在JDK当中一共描述了10个优先级,但是在不同的平台上面有着不同的映射,最后可移植且效果比好的,就只剩下三个了,分别就是。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;

/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;

/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;

2、线程的让步

如果在run方法当中已经做完了最重要的工作,就可以给线程调度机制一个暗示,我的重要工作已经做完,可以让别的线程优先使用CPU,这个暗示主要是通过yield方法做出,不过这终究是一个暗示,没有任何机制保证它一定会被线程调度机制所采纳。当调用yield的时候,我们也只是在建议具有相同优先级的其他线程可以运行。

注意:对于任何重要的控制或者在调整应用的时候,我们都不能依赖yield,实际上yield经常被误用。

3、后台线程

后台线程(daemon),指的是在线程运行的时候在后台提供一种通用的服务的线程,并且这种线程并不属于程序中不可获取的部分,因此,当所有非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,只要有任何非后台还在运行,程序就不会终止。那么什么属于非后台线程呢,比如客户端当中控制UI的线程、Java当中的main线程,那么什么属于后台线程呢,比如控制垃圾回收机制的线程。下面Demo来演示如何创建一个后台的线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 创建一个后台线程(守护线程)
* @author pengchengliu
*
*/
public class DaemonDemo implements Runnable{
@Override
public void run() {
try {
while(true){
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread() + "" + this);
}
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("sleep() interrupted");
}
}

public static void main(String[] args) throws InterruptedException {
for(int i = 0; i < 10; i++){
Thread thread = new Thread(new DaemonDemo());
thread.setDaemon(true); //必须在Thread的start方法之前调用有效
thread.start();
}
System.out.println("all daemons started");
TimeUnit.MILLISECONDS.sleep(120);
}

}

注意:必须在线程启动之前调用setDaemon方法,才可以把它设置成后台线程。一旦main线程结束,也就是非后台线程结束,程序也就结束了,后台线程也就结束了,

通过上述例子,我们知道只要我们start之前拿到驱动任务的线程,然后设置它的Daemon,这个线程也就变成了一个后台线程,但是如果我们通过ThreadPool去驱动任务呢?通过ThreadPool驱动任务是sun公司底层已经写好的,所以想要拿到驱动线程还是有点难度,那这个时候,我们应该如何开启后台线程呢,

通过ThreadFactory去定制我们的线程即可,通过ThreadPool驱动线程,都会去调用ThreadFactory去创建线程,我们只要实现这个接口即可,如下例子所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 设置ThreadPool当中的线程为后台线程
* @author pengchengliu
*
*/
public class DaemonThreadFactoryDemo implements Runnable{

@Override
public void run() {
try {
while(true){
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread() + "," +this);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(new ThreadFactory() {

@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(true);
return thread;
}
});
for(int i = 0; i < 10; i++){
newCachedThreadPool.execute(new DaemonThreadFactoryDemo());
}
System.out.println("all daemon started");
TimeUnit.MILLISECONDS.sleep(500);
}
}

注意:如果在一个后台线程当中开启的新的线程也是后台线程,这点需要注意,可以使用isDaemon方法来判断当前的线程是不是后台线程。为了验证这一点,看下面的这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
* 探究由后台线程开启的线程是否是后台线程
* @author pengchengliu
*
*/
public class Daemons {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Daemon());
thread.setDaemon(true);
thread.start();
System.out.println("thread.isDaemon = " + thread.isDaemon() + ",");
TimeUnit.SECONDS.sleep(1);
}
}
class Spawn implements Runnable{
@Override
public void run() {
while(true){
Thread.yield();
}
}
}
class Daemon implements Runnable{
private Thread[] threads = new Thread[10];
@Override
public void run() {
for(int i = 0; i < threads.length; i++){
threads[i] = new Thread(new Spawn());
threads[i].start();
System.out.println("spawn " + i + " started");
}

for(int i = 0; i < threads.length; i++ ){
System.out.println("t[" + i + "].isDaemon = " + threads[i].isDaemon() + ".");
}

while(true){
Thread.yield();
}
}
}
thread.isDaemon = true,
spawn 0 started
spawn 1 started
spawn 2 started
spawn 3 started
spawn 4 started
spawn 5 started
spawn 6 started
spawn 7 started
spawn 8 started
spawn 9 started
t[0].isDaemon = true.
t[1].isDaemon = true.
t[2].isDaemon = true.
t[3].isDaemon = true.
t[4].isDaemon = true.
t[5].isDaemon = true.
t[6].isDaemon = true.
t[7].isDaemon = true.
t[8].isDaemon = true.
t[9].isDaemon = true.

通过上述例子,我们可以明白在后台线程当中开启的线程默认也会是后台线程。

注意:后台进程在不执行finally字句的情况下就会终结其run方法。

为什么会出现上述的这种情况呢?,其实这么java这么设计也不是没有一点道理可言的,试想一下,如果我们所有的非后台线程都关闭了,程序会突然停止,所有的资源都会被系统所回收,那么后台线程所占有的资源同样会被回收,那么就没有在finally字句当中确认的必要了。

3、编码变体

到目前为止,所有的例子都是实现Runnable接口,然后通过Thread或者ThreadPool来驱动,但是在实际的开发当中会有非常灵活的用法,下面就把日常开发当中比较灵巧的用法总结一下。

3.1、直接继承Thread,在构造器当中开启自己,然后在使用的地方,只要new出自己即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 日常开发Thread编码汇总01,
* 直接继承自Thread类,在构造器当中开启。
* @author pengchengliu
*
*/
public class ThreadCoding1 extends Thread{
private int countDown = 5 ;
private static int threadCount = 0 ;

public ThreadCoding1(){
super(Integer.toString(++threadCount));
this.start();
}

@Override
public String toString() {
return "# " + this.getName() + "(" + countDown + ").";
}

@Override
public void run() {
while(true){
System.out.println(this);
if(--countDown == 0)
return ;
}
}

public static void main(String[] args) {
for(int i = 0; i < 6 ; i++){
new ThreadCoding1();
}
}
}

3.2、实现Runnable接口,自管理Runnable接口,让Runnable接口当中含有Thread,然后在构造Runnable接口的时候,通过含有的Thread驱动当前线程。如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 日常开发Thread编码汇总02,
* 实现Runnable接口,然后在runnable接口当中实现相应的内容,
* 内含有Thread对象,在构造方法的时候就去驱动这个Runnable
* @author pengchengliu
*
*/
public class ThreadCoding2 implements Runnable{

private int counDown = 6 ;
private Thread mThread = new Thread(this);

public ThreadCoding2() {
mThread.start();
}
@Override
public void run() {
while(true){
System.out.println(this);
if(--counDown == 0)
return ;
}
}

public static void main(String[] args) {
for(int i = 0; i < 6; i++)
new ThreadCoding2();
}

}

上面两种方式开启线程,在逻辑简单的情况下,没有任何的问题,但是当中逻辑复杂的时候也就会存在诟病,在构造器当中开启线程将会变的非常有问题,因为另一个任务可能会在构造器结束之前开始执行,这便意味着该任务当中存在着不稳定状态的对象。所以上述两种方式这里不是很推荐使用。

3.3、通过内部类来将线程相关的代码隐藏在类中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/**
* 关于线程的变种编码。
* @author pengchengliu
*
*/
public class ThreadCoding4 {
public static void main(String[] args) {
new InnerThread1("innerThread01");
new InnerThread2("innerThread02");
}
}
/**
* 使用内部类来实现,使用内部类的好处在于:
* 1.内部类可以很好的实现隐藏:一般的非内部类,是不允许有 private 与protected权限的,但内部类可以
* 2.内部类拥有外围类的所有元素的访问权限
* 3.可是实现多重继承
* 4.可以避免修改接口而实现同一个类中两种同名方法的调用。
*
* 通过外部的一个类把内部的类包裹一层,这样就对外部的类隐藏了相应的并发的细节
* @author pengchengliu
*
*/
class InnerThread1 {
private int countDown = 5 ;
private Inner inner ;

public InnerThread1 (String name) {
this.inner = new Inner(name);
}

private class Inner extends Thread {
public Inner (String name) {
super(name);
this.start();
}

@Override
public void run() {
try {
while(true) {
System.out.println(this);
if(--countDown == 0)
return ;
TimeUnit.MILLISECONDS.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

@Override
public String toString() {
return this.getName() + " : " + countDown ;
}
}
}
/**
* 通过匿名内部类来实现,
* 一:什么时候选择使用匿名内部类
* 1·只用到类的一个实例。
* 2·类在定义后马上用到。
* 3·类非常小(SUN推荐是在4行代码以下)
* 4·给类命名并不会导致你的代码更容易被理解。
*
* 二:在使用匿名内部类时,要记住以下几个原则:
* 1·匿名内部类不能有构造方法。
* 2·匿名内部类不能定义任何静态成员、方法和类。
* 3·匿名内部类不能是public,protected,private,static。
* 4·只能创建匿名内部类的一个实例。
* 5·一个匿名内部类一定是在new的后面,用其隐含实现一个接口或实现一个类。
* 6·因匿名内部类为局部内部类,所以局部内部类的所有限制都对其生效
*
* @author pengchengliu
*
*/
class InnerThread2 {
private int countDown = 5 ;
private Thread mThread ;
public InnerThread2 (String name) {
mThread = new Thread(name) {
@Override
public void run() {
try {
while(true) {
System.out.println(this);
if(--countDown == 0)
return ;
TimeUnit.MILLISECONDS.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

@Override
public String toString () {
return this.getName() + " : " + countDown ;
}
};
}
}

关于上述实现方式可以根据自己的需求,以及场景灵活改变,毕竟我们要的就是如何实现一个漂亮并且流畅的程序。

4、合并线程

一个线程可以在其他线程上面调用join方法,效果就是等待一段时间直到第二个线程结束才可以执行。如果某个线程在另外一个线程thread1上调用join,那么此线程将被挂起,直到目标线程thread1结束才恢复(即thread1.isAlive返回为假)。下面来看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* 探究Thread.join方法的意义
* @author pengchengliu
*
*/
public class JoinDemo {
public static void main(String[] args) {
Sleeper sleeper01 = new Sleeper("sleeper01", 4000);
Sleeper sleeper02 = new Sleeper("sleeper02", 3000);

Joniner joniner01 = new Joniner("join01", sleeper01);
Joniner joniner02 = new Joniner("join02", sleeper02);

joniner02.interrupt();
}
}
class Sleeper extends Thread {

private int duration ;

public Sleeper (String name, int sleeperTime) {
super(name);
this.duration = sleeperTime ;
this.start();
}

@Override
public void run() {
try {
sleep(duration);
} catch (InterruptedException e) {
System.out.println(this.getName() + " was interrupted. isInterrupted: " + this.isInterrupted() );
return ;
}
System.out.println(this.getName() + " has awakened");
}

}
class Joniner extends Thread {

private Sleeper sleeper ;

public Joniner (String name, Sleeper sleeper) {
super(name);
this.sleeper = sleeper ;
this.start();
}

@Override
public void run() {
try {
sleeper.join();
} catch (InterruptedException e) {
System.out.println("Interrupdate");
}
System.out.println(this.getName() + " join completed ");
}
}
Interrupdate
join02 join completed
sleeper02 has awakened
sleeper01 has awakened
join01 join completed

如果在等待期间,不想继续join的时候,可以通过打断挂起的线程来终结任务。就如上述join2所示的一样。

总结:谁调join方法,就是在当前的线程当中等待谁。这里的概念有点抽象。

也可以在调用join方法的时候携带参数,意思就是如果目标线程在这段时间到期时还没有结束的话,join方法失效。

5、主线程(UI线程)

接触过客户端的开发人员应该明白UI线程到底是什么东西,包括最初线程的概念也是从这里衍生出来的,因为主线程也就是UI线程要一直阻塞,用来监听用户所触发的事件,同时还证明了一点就是主线程不能做过多的耗时操作,因为如果耗时操作过多的话,对于用户而言,就比较的卡顿,所以关于许多耗时操作都是交由后台线程去做的。下面介绍一个例子,来证明这一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* 探究UI线程于后台线程,
* 关于UI线程的监听,则通过从控制台当中读取数据来模拟,
* 耗时操作则通过大量复杂的计算来模拟
* @author pengchengliu
*
*/
public class ResponsiveUI extends Thread{
private static volatile double d = 1 ;

public ResponsiveUI() {
setDaemon(true);
this.start();
}
@Override
public void run() {
while(true){
d = d + (Math.E + Math.PI) /d ;
}
}

public static void main(String[] args) throws IOException {
new UnResponsiveUI();
// new ResponsiveUI();
System.in.read() ;
System.out.println(d);
}

}
/**
* 单线程执行
* @author pengchengliu
*
*/
class UnResponsiveUI {
private volatile double e = 1 ;

public UnResponsiveUI () throws IOException {
while(e > 0){
e += (( Math.PI + Math.E ) /e );
}
System.in.read() ; //导致当前的线程阻塞
}
}

如果想让程序有响应的话,我们就必须把大量的复杂计算放置到后台线程当中。

6、线程组

线程组其实就是线程的集合,它是之前JDK并发里面的一个产物,但是它是一个失败的作品,这里就是了解一下概念即可,以及它产生的原因就是去处理线程的异常。

7、捕获异常

由于线程的特殊性,导致我们不能捕获从线程中逃逸出来的异常。一旦异常逃出了任务的run方法,那么它就会直接向外传播到控制台,除非我们采取特殊的方法捕获这些异常,在JDK5之前,可以通过线程组来捕获这些异常,但是在这之后,我们使用Executor就可以解决这类问题,下面我们一步步来研究如何捕获线程的异常。

7.1、任务产生异常,如果我们不去处理,是否会抛向控制台。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 探究线程当中的异常,如果我们不做处理,将会发生什么事
* @author pengchengliu
*
*/
public class NomalThreadException implements Runnable{

@Override
public void run() {
throw new RuntimeException() ;
}

public static void main(String[] args) {
ExecutorService threadService = Executors.newCachedThreadPool();
threadService.execute(new NomalThreadException());
}
}
Exception in thread "pool-1-thread-1" java.lang.RuntimeException
at com.suansuan.thread.ThreadException.NomalThreadException.run(NomalThreadException.java:15)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)

at java.lang.Thread.run(Thread.java:748)

上述例子很简单,没有什么难度,我们也看出来了,当我们如果在run当中没有处理异常,那么这个异常将会直接抛出到控制台,而我们的程序同时也会崩溃。

7.2、如果我们在线程启动的时候,进行处理,那么我们能否捕获异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 探究线程当中的异常,如果我们在启动线程的时候,通过try-catch能否捕获相应的异常
* @author pengchengliu
*
*/
public class StartThreadCatchException implements Runnable{
@Override
public void run() {
throw new RuntimeException() ;
}

public static void main(String[] args) {
try{
ExecutorService threadPool = Executors.newCachedThreadPool();
threadPool.execute(new StartThreadCatchException());
} catch (RuntimeException e){
System.out.println("Exception has been handled !");
}
}
}
Exception in thread "pool-1-thread-1" java.lang.RuntimeException
at com.suansuan.thread.ThreadException.StartThreadCatchException.run(StartThreadCatchException.java:15)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:748)

看到了上述结果就知道没有成功,没有捕获到相应的异常。

据官方文档描述,我们需要修改Executor产生线程的方式。Thread.UncaughtExceptionHandler,这个接口是在JDK5以后新出的接口,而这个接口产生的目的就是处理线程当中的异常未捕获的问题,其实我们看它的名字也能猜出来是一个干什么的接口。这个接口允许我们在每一个Thread对象上面浮着一个异常处理器。并且只有一个方法,而这个方法会在线程没有被捕获的异常临近死亡的时候回调。我们可以看一下官方描述

1
2
3
4
5
6
7
8
9
10
11
12
@FunctionalInterface
public interface UncaughtExceptionHandler {
/**
* Method invoked when the given thread terminates due to the
* given uncaught exception.
* <p>Any exception thrown by this method will be ignored by the
* Java Virtual Machine.
* @param t the thread
* @param e the exception
*/
void uncaughtException(Thread t, Throwable e);
}

下面我们来看一个如何通过附着在Thread上面的异常处理器处理异常的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* 通过异常处理机制,来处理线程当中产生的异常
* @author pengchengliu
*
*/
public class ExectuorsException {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool(new HandlerThreadFactory());
threadPool.execute(new ExceptionTHread2());
}
}
class ExceptionTHread2 implements Runnable{
@Override
public void run() {
Thread currentThread = Thread.currentThread();
System.out.println("run() by : " + currentThread.toString());
System.out.println("eh = " + currentThread.getUncaughtExceptionHandler());
throw new RuntimeException();
}
}
class MyUncaughException implements UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("caught " + e);
}
}
class HandlerThreadFactory implements ThreadFactory{
@Override
public Thread newThread(Runnable r) {
System.out.println("create new Thread :" + this);
Thread thread = new Thread(r);
thread.setUncaughtExceptionHandler(new MyUncaughException());
return thread;
}

}
create new Thread :com.suansuan.thread.ThreadException.HandlerThreadFactory@41a4555e
run() by : Thread[Thread-0,5,main]
eh = com.suansuan.thread.ThreadException.MyUncaughException@4800002d
create new Thread :com.suansuan.thread.ThreadException.HandlerThreadFactory@41a4555e
caught java.lang.RuntimeException

根据编程原则来说,上面是典型的单一职责的设计理念,我们上面使用了ThreadFactory、UncaughtExceptionHandler、Runnable 这三个接口,而这三个接口分别扮演着不同的角色,通过上述例子,关于线程里面的基础部分也就算是告一段落,下面我们来学习,线程之间的资源共享问题。