Java多线程基础总结
背景
Java採用多线程方式实现并行计算,当然并行计算也能够採用多进程方式实现,可是进程切换耗费比較高。并且进程间是隔离的,进程间通信机制比較麻烦。最后JVM本身在操作系统中就一个进程,由它再启动一个进程不太合适。所以Java採用多线程方式实现并行计算。
Java从诞生之初,多线程就环绕的是Runnable接口和Thread类展开的。它的底层採用的是c的p线程方式,并且因为多线程的复杂性,p线程的非常多概念知识被延伸到了Java层面,这对Java开发人员来说算是一个不幸的消息。
可是因为多线程的复杂性,这样的延伸不得不有。
1.实例化和启动
1.1传统启动方式
Runnable接口
Runnable接口是Java被设计在线程体中运行某项任务的接口。通过实现Runnable接口的run方法,在run方法中运行线程代码。实现某项任务。
《Java编程思想》的作者曾在书中提到觉得Runnable这个接口的命名非常不准确。叫Task比較合适。而实现了该接口的类,表示该类能够并行的运行run方法,也就是run的语句运行并非连续的,非常可能在t1时间运行第一条,t3时间运行第二条。这里面也就带来了原子操作、原语、同步、相互排斥、协同等问题。
Thread类
Thread是Java中的线程类。通过实例化Thread和调用start方法能够启动线程。Thread是java.lang包以下的一个类。它本身实现了Runnable接口。所以线程的另外一种实例化和启动写法能够继承Thread类,并重写Run方法,Threa类的run()方法的源代码例如以下:
public void run() { if(target != null) { target.run(); } }
Thread有非常多重载的构造方法。能够指定线程的名字,Runnable对象,所属线程组等信息。
一般仅仅指定Runnable对象就够了。
Java线程的基本使用方法基本上就是对Thread类里面一些方法的使用方法和注意事项的介绍。
2.JDK1.5 后新的实例化和启动方法
JDK 1.5是Java比較重要的一个版本号。有非常多改进和提高,当中JDK 1.5就有志于改进Java线程的一些内容,并因此引入了java.util.concurrent包。通过该包以下的Executors及相关类,能够获得一个Java标准的线程池,而不採用其它的线程框架。
JDK 1.5之后Java目标尽量降低直接操作Thread类,所以用concurrent包后非常多时候我们常见到的Thread方法调用。Synchronizedkeyword都能够省略。
2.1 Executor相关
Executors有三个基本的static方法,这三个方法採用策略模式。封装了线程池的管理和操作实现。统一返回ExecutorService接口实现对象。
newCachedThreadPool() 该方法会创建一个可依据须要创建新线程的线程池。可是在曾经构造的线程可用时将重用它们。对于执行非常多短期异步任务的程序而言,这些线程池通常可提高程序性能。 newFixedThreadPool() 创建一个可重用固定线程数的线程池,以共享的LinkedBlockQueue方式来执行这些线程。 newSingleThreadExecutor() 创建一个使用单个 worker 线程的 Executor,以LinkedBlockQueue方式来执行该线程。
使用方法:
ExecutorService exec = Executors.newCachedThreadPool(); exec.execute(new MyThread()); exec.shutdown();
调用execute能够将runnable对象增加到线程队列中,executorService的依据自身的实现启动线程执行任务。
shutdown方法执行后表示线程池不再接受新的任务。
2.2 ThreadFactory
上面实例化线程池的时候,能够传入一个实现了ThreadFactory接口的对象来自己定义线程属性,如批量创建守护线程。ThreadFactory这样的project模式,表明Java线程的设计事实上就是完毕某些并发任务。而Executor能够运行一组相似的任务,所以这些Thread能够通过一个工厂产出。
ExecutorService exec =Executors.newCachedThreadPool(new ThreadFactory(){ ThreadnewThread(Runnable r){ Thread t = new Thread(r); t.setDaemon(true); } }); for(int i=0;i++;i<5) exec.execute(new MyThread()); exec.shutdown();
2.3带返回值的任务:
多线程(并发计算)的一个优点是,我们能够把耗时的计算放到后台,这样就能够创建有响应不会假死的UI界面。
Runnable接口的run方法为void方法,不带有不论什么返回值,对于一些计算需求不是非常方便。
JDK 1.5之后。能够实现带有返回值的Thread。
想要让任务带有返回值。那么我们的类就要实现Callable<V>接口,这是一个泛型接口。能够觉得这个接口是Runnable的一个平级兄弟接口。仅仅只是Runnable接口的run方法不能抛出受检查的异常,不带有返回值,可是这个接口能够带有返回值,能够跑出异常。该接口规定了一个泛型方法:
V call() throws Exception;
能够在这种方法中抛出受检查的异常和返回值。返回值被Future<T>对象包围,这么做能够保证我们在多线程环境下获取到计算完成的值。
由于假设直接返回一个变量的地址。那么我们无法确定这个变量是否被计算完成或者说call方法是否运行完成,通过Future能够由JVM来帮我们确保这项工作。而且Future还有更深的使用方法。比方中断线程池中的单个线程。
3.Thread类的一些其它操作
Thread类还提供了Sleep、setPriority、setDaemon、join等操作,这些种操作都是针对线程的,因此他们都属于Thread里面的方法。
并且这些操作不会涉及到线程锁,因此他们的操作一般都是安全的。
3.1休眠Sleep
Thread类的静态方法,让线程休眠一段时间,是一个native方法。单位是毫秒。会抛出InterruptedException异常,须要捕捉,该异常在Thread中断中再解释。
比方一般我们都会在Demo中通过Sleep方法模拟一些耗时操作。让线程等待一段时间,让Thread慢下来以便重现一些现象。
public class A extends Thread{ public void run(){ try { <span style="white-space:pre"> </span>TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { } } }
3.2设置优先级。setPriority
Thread类的对象方法。设置线程优先级。优先级越高的能够被尽可能多的轮询。JDK规定了10个优先级,可是JVM于多数操作系统都不能非常好的映射。并且要注意优先级仅仅是一种建议,并非低优先级的一定在高优先级的Thread完毕后才干运行。
线程的优先级终于是由JVM映射到OS层面实现的,可是不同OS的优先级数不一样,少的比方Windows也就7个并且不固定,Solairs有2的31次方个优先级。
所以我们在设置的时候一般设置为Thread类的三个常量MAX_PRIORITY/NORM_PRIORITY/MIN_PRIORITY三个级别就够了。
<span style="white-space:pre"> </span>public class B implements Runnable { <span style="white-space:pre"> </span>public static void main(String[] args) { <span style="white-space:pre"> </span>ExecutorService exec = Executors <span style="white-space:pre"> </span>.newCachedThreadPool(new DaemonThreadFactory()); <span style="white-space:pre"> </span>for (int i = 0; i < 10; i++) <span style="white-space:pre"> </span>exec.execute(new DaemonFromFactory()); <span style="white-space:pre"> </span>} <span style="white-space:pre"> </span>@Override <span style="white-space:pre"> </span>public void run() { <span style="white-space:pre"> </span>Thread.currentThread().setPriority(Thread.MAX_PRIORITY); <span style="white-space:pre"> </span>} }
3.3让步yield
Thread类的静态方法,native方法,表示让出当前CPU时间。可是让步操作不过一个建议,详细会不会让出CPU还要由JVM调度决定。
这种方法能够在測试和演示时加快线程切换频率,让一些问题更快的发生。须要注意的是让步操作不会操作锁,也就是说,假设让步操作中发生在同步块儿内,线程让出CPU可是不会让出对象锁。
public class C implements Runnable { public static void main(String[] args) { ExecutorService exec = Executors .newCachedThreadPool(new DaemonThreadFactory()); for (int i = 0; i < 10; i++) exec.execute(new DaemonFromFactory()); } @Override public void run() { Thread.yield(); synchronized (this) { Thread.yield(); } } }
3.4获取当前线程currentThread
Thread类的静态方法。native方法,返回当前的线程对象。
通过这种方法。能够在线程中获取自身的一些属性,状态等,比方演示或做日志时,经经常使用Thread.currentThread.getName()输出当前Thread的名字,方便观察。
3.5后台线程setDaemon
Thread对象方法,final方法不可覆盖。后台线程或叫守护线程,是指程序执行的时候后台提供的一种通用服务线程,并且这样的线程不是程序不可或缺的一部分,当全部非后台线程结束时。程序终止。它们也就自己主动被终止了,并且从后台线中程构造启动的线程都默认是后台线程(也就是Thread会继承创建它的Thread的一些属性)
public class D implements Runnable { public static void main(String[] args) { ExecutorService exec = Executors .newCachedThreadPool(new DaemonThreadFactory()); for (int i = 0; i < 10; i++) exec.execute(new DaemonFromFactory()); } @Override public void run() { Thread.currentThread().setDaemon(true); } }
3.6增加join
Thread对象方法,final方法,并且是同步的。抛出InterruptedExecption异常。join同意让一个线程增加到还有一个线程中,可是要注意join的时候是将谁(Thread t1)增加到谁(Threadt2),由于join的意思是将当前线程(t2)挂起,直到目标线程(t1)结束才恢复当前线程(t2)。
join有多个重载的方法。调用时能够加上超时參数,单位是毫秒。这样在超时时间内。join总能够返回,并且join能够被打断。
public class E extends Thread { private double num = 0; public static void main(String[] args) { E e = new E(); Thread f = new F(e); f.start(); e.start(); } @Override public void run() { System.out.println("计算E"); for (int i = 0; i < 10; i++) { num = Math.PI * Math.E + num; } System.out.println("E计算完毕" + num); } public synchronized double getNum() { return num; } } class F extends Thread { private E e = null; public F(E e) { this.e = e; } @Override public void run() { try { System.out.println("运行F"); e.join(); System.out.println("E运行完毕" + e.getNum()); } catch (InterruptedException e) { System.out.print("被打断"); } } }
3.7是否存活isAlive
Thread对象方法,final方法,native方法。通过它能够推断一个Thread是否存活。可是要注意,Thread死亡是从run退出,也就是run运行完成或运行了return语句。中断并不代表线程死亡,或者即使任务正确处理了中断,Thread应该结束了,可是也不要马上推断Thread对象状态。
比方。以下的假设不Sleep,两次输出都会是true。
import java.util.concurrent.TimeUnit; public classTestAlive { public static void main(String[] args) throws InterruptedException { TMt = new TM(); t.start(); System.out.println(t.isAlive()); TimeUnit.SECONDS.sleep(1); t.interrupt(); //TimeUnit.SECONDS.sleep(1); System.out.println(t.isAlive()); } } class TM extendsThread { public void run() { while (true) { // System.out.println("存活……"); if (Thread.interrupted()){ return; } } } }
3.8其它
其它的Thread方法。Interrupt和isInterrupted会在Thread中断中总结。剩下的stop、resume、suspend等有些是JDK废弃的了,有些是与ThreadGroup有关的。废弃方法知道即可。不是必需浪费精力,ThreadGroup是一次失败的尝试。不值得在浪费精力学习了。
4.线程组:
线程组是Java一次不成功的尝试,并且JDK也一直在有意的慢慢的遗忘、淡化它,能够不学习。Java线程组是承诺升级理论的现实写照:“继续错误的代价由别人来承担,而承认错误的代价由自己承担”。因此Java一直没有官方表明线程组是好还是坏。
5.异常捕捉:
线程是非常特殊的,因此我们不能捕获从线程中逃逸出来的异常,仅仅能在线程内处理。可是假设代码中抛出了一个RuntimeException,这个报错会抛给控制台。JDK5之后能够使用Executor创建线程池,然后给它加入一个异常处理器,来捕获线程抛出的不论什么异常。
PS:不能捕捉异常的意思是,我们在线程代码外,无法用try-catch方式捕获run抛出的不论什么异常。也就是以下的代码是不正确的。
try { Threadm =new Thread(new Runnable() { public void run() { throw newRuntimeException(); } }); m.start(); } catch (Exceptione) { System.out.println("捕捉到了异常"); }
Thread.UncaughtExceptionHandler接口
这个接口是JDK1.5之后出现的接口,Thread的内部接口。把该接口的实例对象设置给线程对象(t.setUncaughtExceptionHandler)或者设置为全局默认的异常处理器(Thread.setDefalutUncaughtException)就可以捕捉Thread中抛出的异常。
PS:这里讨论的异常。是我们的任务代码抛出的异常,不应把线程的中断异常也包含进来。
publicclassUncatchExceptionThread extends Thread { publicvoid run() { throw newRuntimeException("出错了!"); } publicstatic void main(String[]args) { UncatchExceptionThreadt =new UncatchExceptionThread(); Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler(){ public void uncaughtException(Threadt, Throwablee) { System.out.println("发生了异常:" + e.getLocalizedMessage()); } }); try { ExecutorServiceexec = Executors.newCachedThreadPool(); exec.execute(t);//这样的异常更友好!// t.start(); }catch(Exceptione) { System.out.println("捕捉了异常" + e); } } }
6.TimeUnit
TimeUnit是JDK 1.5新增的一个枚举类,位于java.util.concurrent包下。
该枚举的主要意图是将程序员从毫秒的“暴政”中解脱出来。
在JDK 1.5之前。假设我们想基于时间单位做一些事情,比方Sleep 3秒,那么我们须要计算3等于多少毫秒。尽管这个计算并不复杂,可是有了TimeUnit,我们能够直接调用TimeUnit的SECONDS的Sleep传入long值,TimeUnit的相关方法自己主动帮我们转换时间单位。
使用TimeUnit类能让代码更清晰易读,毕竟我们读代码的时间可能比写代码的时间要长。
@Override public void run() { try { // 传统的时间写法 long mills = 3 * 1000;// 休眠3秒,一秒等于1000毫秒 Thread.sleep(mills); // 用TimeUnit休眠 TimeUnit.SECONDS.sleep(3);// 休眠3秒 } catch (InterruptedException e) { System.out.print("被打断"); } }
总结:
Java的多线程是Java实现并发的基础。多线程编程本身不是什么新奇的,也不是Java特有的,可是Java语言本身支持多线程,这比C等语言编写多线程代码要easy的多,并且Java本身努力消除OS层面的线程差异。Java的多线程基本的语法都非常easy,比較困难的是基本概念。假设有计算机操作系统进程调度管理等方面的知识的话这些学习理解起来就比較easy了。
Java多线程的基础是首先要理解Thread的各个方法、概念,明确Thread的四种状态,状态之间的转换。可以合理的利用Thread类提供的各种方法完毕一些事情。理解了Java多线程基础,之后才干更好的研究Java多线程之间的同步、协同机制。对于JDK 1.5提供的concurrent包,先明确基础之后再研究这个包比較妥当。并发专家非常多都建议先使用Java传统的做法。等有特殊场景或者须要性能优化的时候再用concurrent包相关的工具替换自己的实现。比較concurrent包里面提供的非常多东西都太高级了。并且多线程性能调优本身就应依据实际场景甚至是机器、JVM版本号来调试。
posted on 2017-06-06 18:22 cynchanpin 阅读(200) 评论(0) 编辑 收藏 举报