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 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 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 public final static int MIN_PRIORITY = 1 ;public final static int NORM_PRIORITY = 5 ; 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 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(); } 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 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 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 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 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 public class ThreadCoding4 { public static void main (String[] args) { new InnerThread1("innerThread01" ); new InnerThread2("innerThread02" ); } } 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 ; } } } 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 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 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(); System.in.read() ; System.out.println(d); } } 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 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 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 { 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 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@41 a4555e run() by : Thread[Thread-0 ,5 ,main] eh = com.suansuan.thread.ThreadException.MyUncaughException@4800002 d create new Thread :com.suansuan.thread.ThreadException.HandlerThreadFactory@41 a4555e caught java.lang.RuntimeException
根据编程原则来说,上面是典型的单一职责的设计理念,我们上面使用了ThreadFactory、UncaughtExceptionHandler、Runnable 这三个接口,而这三个接口分别扮演着不同的角色,通过上述例子,关于线程里面的基础部分也就算是告一段落,下面我们来学习,线程之间的资源共享问题。