简介
虽然相对其他语言来说,Java将线程抽象化为Thread类来控制,做到了一定程度的简化。但是由于多线程自身受到JDK版本、硬件、操作系统等影响带来的不确定性,尤其是当多线程访问临界资源时,我们必须在多线程编程时保持一个谨慎的心态。下面对多线程知识做一个简单的总结,由于个人水平有限有不到之处请指正。
什么时候应该使用多线程
多线程,听起来很高端的样子,仿佛使用了多线程我们就是大牛了。但是真的是这样么?其实不是。我们首先应该了解多线程可以给我们带来什么好处,根据这些好处我们才能确定什么情景下使用多线程。
多线程的好处其实只有两点:1,并行处理(性能需求);2,仿真(功能需求)。
并行处理很简单,由于现在的操作系统普遍支持多核多线程,所以当我们使用多线程的时候就可以更好的利用系统资源,加快应用处理、响应速度,避免丑陋的用户体验。
仿真,仿真可能在游戏编程中会比较多的用到,就是利用多线程每个线程模拟一个或一类资源。例如游戏中打怪兽,怪兽是一个线程,我们控制的任务是一个线程,这就是仿真。当然,在业务应用系统中也会用到,例如常见的生产者和消费者模型中,一个线程作为生产者的仿真,另一个线程作为消费者的仿真。
好了,多线程机制的基本好处已经说明,接下来就靠我们自己来确定自己应用的情景是否适合使用多线程。
JDK线程
除了上文提到的Thread类来作为线程的抽象,Java还有Runnable接口作为线程任务的抽象。所以,我们在实际使用时即可以通过继承Thread类重写run方法来实现线程,也可以通过实现Runnable接口实现run方法来实现线程。但是,都必须通过Thread的start方法启动线程。这里要说明的是,大家可能碰到过是run方法还是start方法来运行/启动/开始线程(一般是运行多)的面试题,其实就是这个运行干扰了我们,记住,线程只会被启动(start),运行(run)指的是线程的内部业务逻辑。
一般情况下无论是Thraed的派生类还是Runnable的实现类,它们的run方法体内都会有根据某个条件运行的while循环保证线程不断执行,直到达到某个条件后循环结束、线程终止。
concurrent类库
java.util.concurrent类库虽然包含在JDK中,但是其来源于Doug Lea的util.concurrent库。这个库包含许多线程安全、测试良好、高性能的并发构建块。主要是为了对Collection数据结构执行并发操作。推荐使用concurrent类库来实现、管理多线程,这样可以避免我们在使用多线程的过程中犯一些不必要的错误。concurrent类库主要包含以下的接口和接口实现类。
ExecutorService接口
ExecutorService接口及其实现类,是concurrent类库中较为的重要的执行控制器,用于统一的管理由控制器启动的线程。常用的实现类有:ThreadPoolExecutor等。一般通过Executors.newXXXX()来得到,也可以通过Executors.newXXXXX(ThreadFactory factory)的方式得到自定义(优先级、是否后台、异常处理等)的控制器。
ExecutorService常用方法:
executor(Runnable),启动Runnable任务,无返回值。
submit(Callable),启动Callable任务,返回Future接口实例。
shutdown(),该执行器就不再接受新的线程任务,只会保证当前的多线程任务顺利执行完。
/**
*
* @ClassName: CachedThreadPool
* @Description: 用来测试concurrent线程工具类库的功能
* @author lixiaodai
* @date 2014-3-11 上午9:18:54
*
*/
public class CachedThreadPool {
public static void main(String[] args) {
//JDK1.5 以后引入了concurrent类库包类辅助我们的线程操作,也可以理解为一个多线程的框架
//常用的方式是通过ExecutorService创建一个为一个的执行器来统一管理多个线程
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i=0;i<5;i++){
executorService.execute(new BasicRunnable());
}
//executor的shutdown()方法调用后,该执行器就不会再接受新的线程任务,只会保证当前的多线程任务顺利执行完
executorService.shutdown();
}
}
/**
*
* @ClassName: FixedThreadPool
* @Description: FixedThreadPool的使用
* @author lixiaodai
* @date 2014-3-12 上午11:15:30
*
*/
public class FixedThreadPool {
public static void main(String[] args) {
//创建FiexedThreadPool作为线程管理器,FixedThreadPool与CachedThreadPool
//不同之处在于FixedThreadPool在创建的时候就已经确定了管理线程的上限,并不是说只能有指定个线程而是说同时只能有指定个线程存在
//也就是在使用前已经完成了高昂的线程分配
//SingleThreadExecutor是线程数量为1的FiexedThreadProol
//SingleThreadExecutor实际上是为了保证线程的按序执行,执行完一个之后再执行另一个,并且不需要处理对共享资源的访问
ExecutorService service = Executors.newFixedThreadPool(5);
for(int i=0;i<10;i++){
service.execute(new BasicRunnable());
}
service.shutdown();
}
}
ThreadFactory接口
ThreadFacotry接口及其实现类,是concurrent类库中定制Thread属性使用的,由于在concurrent的ExecutorService中直接启动的Runnable、Callable接口,而设定后台线程、设定线程优先级以及线程异常捕获是Thread类的方法,所以我们通过ThreadFactory接口设置要执行的Runnable、Callable的Thread属性。
ThreadFactory常用方法:
- newThread(Runnable r),传入的是executor中启动的runnable,返回的是Thread对象。在方法启动创建Thread(r),设置Thread属性后返回该Thread对象。
线程机制
Java中将线程抽象化为Thread类,线程执行的任务抽象化为Runnable接口的实现类。为了很好的使用多线程,我们先来学习一下Java中线程的基本概念以及对应的操作。
线程
多次说过,Java中的线程就是Thread类及其子类,Thread类抽象的只是线程,虽然可以通过继承重写run方法来描述具体的线程任务,但是由于Java不支持多继承,所以并不推荐使用直接继承Thread的方式来实现线程。
public class MyExecutorThread extends Thread{
/**
* 线程要执行的任务,对于并行任务,那么直接写入任务逻辑
* 对于类似于监听/定时等任务一般通过while(true)死循环的方式
*/
@Override
public void run() {
while(true){
System.out.println("一个简单的线程,就是不结束,气死你!");
}
}
}
线程任务
线程任务,即线程要执行的任务逻辑,分为无返回值的任务和有返回值的任务。
无返回值任务
无返回值任务实现Runnable方法即可,在Runnable实现类中的run方法实现任务逻辑即可。通过concurrent类库中的Executor的executor方法启动Runnable实例即可。
/**
*
* @ClassName: BasicRunnable
* @Description: 第一个简单的Runnable实现类,Runnable的实现类本身并不能说明启动线程
* 只是在run()方法中描述了线程的逻辑,必须附着到一个Thread类上才是真正的线程
* @author lixiaodai
* @date 2014-3-10 下午6:56:37
*
*/
public class BasicRunnable implements Runnable {
private int countDown = 10;
private static int taskCount = 0;
private final int id = taskCount++;
public BasicRunnable() {
}
public BasicRunnable(int countDown){
this.countDown = countDown;
}
public String status(){
return "#"+id+"("+(countDown>0?countDown:"Liftoff!")+").";
}
/**
* run方法体内一般都会包含一个根据某条件的不断循环,知道满足条件后循环退出
* 循环退出也就是线程退出
*/
public void run() {
while(countDown-- >0){
System.out.print(status());
//Java线程机制中的yield方法,只是声明该线程以准备好退出资源占用,但是并不是可靠的立刻退出
//资源,而是选择性的不可预测的,所以在使用时一定要小心
Thread.yield();
}
}
public static void main(String[] args) {
Thread thread = new Thread(new BasicRunnable());
//启动线程是使用start()方法,和中文意思一样启动=start
thread.start();
//这里的输出并没有等到thread线程执行完毕之后才执行,而是启动线程后就直接执行了
//这一点正是我们使用线程的理由之一:不阻塞的快速执行某些逻辑,即加快运行速度
System.out.println("Wait For Lift Off");
}
}
有返回值任务
有返回值任务时concurrent类库中提供的,实现Callable接口,Callable接口是一个泛型接口,实现call方法实现业务逻辑,call方法返回值是泛型类型对象。使用Executor的submit(Callable)方法启动,submit方法的返回值是Future类型。
/**
*
* @ClassName: BasicCallable
* @Description: 演示了Callable接口的使用,Callable与Runnable都是用来
* 描述业务逻辑,但是不同之处在于Callable启动的线程可以有返回值
* 而Runnable启动的线程不会有返回值
* Callable和Runnable的区别类似于Python中的eval和exec的区别
* @author lixiaodai
* @date 2014-3-12 下午2:07:10
*
*/
public class BasicCallable implements Callable<String>{
private int id;
public BasicCallable(int id){
this.id = id;
}
/**
* 注意,这里是call而不是run
*/
public String call() throws Exception {
return "result of Cannable : "+id;
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
List<Future<String>> results = new ArrayList<Future<String>>();
for(int i=0;i<10;i++){
/**
* 在执行callable实现类的时候,不是使用execute
* 而是使用submit来启动线程,同时Callable实现类的返回值是Future接口的实现类
* 通过使用Future的get()方法得到Callable实现类的call方法的返回值
* 可调用Future的isDone()判断线程是否完成了
* 可调用Future的isCancel()判断线程在为完成前是否被撤销了
*/
results.add(executorService.submit(new BasicCallable(i)));
}
for(Future<String> fs:results){
if(fs.isDone()){
System.out.println("完成了"+fs.isCancelled());
try {
System.out.println(fs.get());
} catch (InterruptedException e) {
System.out.println(e);
return;
} catch (ExecutionException e) {
System.out.println(e);
return;
}finally{
executorService.shutdown();
}
}
}
}
}
线程休眠
线程休眠可以使用Thread.sleep(休眠时间,毫秒单位)静态方法,来设定当前线程的休眠时间。concurrent类库中提供了TimeUnit枚举类来实现多个时间单位的休眠设定。
/**
*
* @ClassName: SleepThread
* @Description: 线程的休眠,即强制性的将线程退出CPU占用,JDK1.5之前是使用Thread.sleep(毫秒数)来设定线程线程休眠时间
* 而JDK1.5之后,推荐使用concurrent包中的TimeUnit类来设定休眠时间
* @author lixiaodai
* @date 2014-3-12 下午2:35:41
*
*/
public class SleepThread implements Runnable{
private int countDown = 10;
private static int taskCount = 0;
private final int id = taskCount++;
public SleepThread() {
}
public SleepThread(int countDown){
this.countDown = countDown;
}
public String status(){
return "#"+id+"("+(countDown>0?countDown:"Liftoff!")+").";
}
/**
* run方法体内一般都会包含一个根据某条件的不断循环,知道满足条件后循环退出
* 循环退出也就是线程退出
*/
public void run() {
try {
while(countDown-- >0){
System.out.print(status());
/**
* 原来是使用sleep(毫秒数)的方法来设定线程休眠时间
* concurrent类库中提供了更为强大的枚举类来设定线程按不同时间单位的休眠时间
*/
//Thread.sleep(1000)
TimeUnit.MILLISECONDS.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i=0;i<5;i++){
executorService.execute(new SleepThread());
}
executorService.shutdown();
}
}
线程优先级
线程的优先级指线程被执行的概率,并不是线程的执行顺序。不要试图通过设定线程的优先级来控制线程执行顺序。使用Thread.currentThread().setPriority(优先级值)。JDK中提供了10个级别的优先级,但是底层的OS提供的优先级级别并不一定和JDK提供的一样,所以在使用的时候最好使用Thread提供的.MAX_PRIORITY或MIN_PRIORITY或NORM_PRIORITY三个级别来设定优先级。可以通过concurrent中的ThreadFactory来设置线程的优先级。
/**
*
* @ClassName: BasicPriorities
* @Description: 这个类来描述线程优先级的概念,但是要注意的是线程的优先级并不是可靠的
* 优先级只是用来控制线程的执行概率,既然是概率那么就充满了不确定性,不要试图通过
* 控制优先级来控制线程的执行,只是非常错误的
* 使用Thread.currentThread().setPriority(优先级值)
* 虽然JDK为我们提供了10个级别的优先级,但是底层的OS不一定也是10个,所以我们在使用的时候应该使用
* 使用Thread.MAX_PRIORITY或MIN_PRIORITY或NORM_PRIORITY
* @author lixiaodai
* @date 2014-3-12 下午3:04:04
*
*/
public class BasicPriorities implements Runnable {
private int countDown = 5;
private volatile double d;
private int priority;
public BasicPriorities() {
}
public BasicPriorities(int priority){
this.priority = priority;
}
public void run() {
Thread.currentThread().setPriority(priority);
while(true){
for(int i = 1;i<100000;i++){
d+=(Math.PI+Math.E)/(double)i;
if(i%1000 == 0){
Thread.yield();
}
}
System.out.println(this);
if(--countDown == 0){
return;
}
}
}
@Override
public String toString() {
return Thread.currentThread() + " : "+countDown;
}
/**
*
* @Title: main
* @Description:执行结果说明,并不是优先级高的线程就能有限执行,只是执行概率高
* @param @param args 设定文件
* @return void 返回类型
* @throws
*/
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i=0;i<5;i++){
executorService.execute(new BasicPriorities(Thread.MAX_PRIORITY));
executorService.execute(new BasicPriorities(Thread.MIN_PRIORITY));
}
executorService.shutdown();
}
}
线程让步
线程让步,也就是当前线程试图主动让出CPU执行时间。使用Thread.yield()方法来实现。但是和所有的线程一般设定一样,都是不靠谱的,尽量少使用。
后台线程/守护线程
后台线程就是我们通常所说的守护线程,通过使用Thread.setDeamon(true)来设置当前线程为后台线程。后台/守护线程在使用的是由要注意,后台线程在进程中所有的非后台线程结束后会自动结束,同时后台线程派生出的线程自动为后台线程。可以通过concurrent中的ThreadFactory来设置线程的是否为后台线程。
/**
*
* @ClassName: DaemonThread
* @Description: 后台/守护线程的例子,后台/守护线程是作为其他线程的守护来使用,当非后台线程终止时,后台/守护
* 线程自动终止.
* 必须在线程启动(start)前使用对象的setDeamon(true)方法将线程设置为后台线程.
* 由于使用concurrent的executor.executor()方法直接启动的是runnable实现类,如果我们要对
* Thread相关设置就需要用到ThreadFactory
* 后台线程创建中启动的线程自动是后台线程
* @author lixiaodai
* @date 2014-3-12 下午3:31:18
*
*/
public class DaemonThread implements Runnable{
public void run() {
while(true){
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread()+ " "+this);
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<5;i++){
Thread thread = new Thread(new DaemonThread());
/**
* 设置线程为后台线程,必须在start启动线程前设置
*/
thread.setDaemon(true);
thread.start();
}
System.out.println("All deamons started");
TimeUnit.MILLISECONDS.sleep(1000);
}
}
线程合并
线程合并就是将制定线程合并到当前线程,使得随机执行的线程变为顺序执行。即A线程在run方法内调用B线程.join(),那么A线程就会挂起,直到B线程结束后才会继续执行线程A中join()后的任务。
/**
*
* @ClassName: Sleeper
* @Description: 这几个类为了掩饰join()方法如何使用
* @author lixiaodai
* @date 2014-3-13 下午2:54:52
*
*/
public class Sleeper extends Thread{
private int duration;
/**
* <p>Title: </p>
* <p>Description: </p>
*/
public Sleeper() {
}
/**
*
* <p>Title: </p>
* <p>Description: </p>
* @param name
* @param sleepTime
*/
public Sleeper(String name,int sleepTime){
super(name);
duration = sleepTime;
start();
}
/* (non-Javadoc)
* @see java.lang.Thread#run()
*/
@Override
public void run() {
try {
//线程休眠一段时间
TimeUnit.MILLISECONDS.sleep(duration);
} catch (Exception e) {
//如果线程被中断,就捕获中断异常,但是异常被捕获时,将清理线程中断标识
//所以这个的isInterreupt()返回的总是false(默认值)
System.out.println(getName() + " is interrupted." + "isInterrupted(): "+isInterrupted());
return;
}
System.out.println(getName()+" has awakened");
}
public static void main(String[] args) {
Sleeper sleeper = new Sleeper("Sleepy", 1500);
Sleeper grumpy = new Sleeper("Grumpy", 1500);
Joiner dopey = new Joiner("Depey", sleeper);
Joiner cod = new Joiner("Doc", grumpy);
//中断grumpy线程
grumpy.interrupt();
}
}
class Joiner extends Thread{
private Sleeper sleeper;
/**
* <p>Title: </p>
* <p>Description: </p>
*/
public Joiner() {
}
public Joiner(String name,Sleeper sleeper) {
super(name);
this.sleeper = sleeper;
start();
}
/* (non-Javadoc)
* @see java.lang.Thread#run()
*/
@Override
public void run() {
try {
/**
* sleeper线程执行执行,执行完sleeper线程后再执行本线程
*/
sleeper.join();
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println(getName() + " join completed");
}
}
线程销毁
线程的销毁,很多朋友在问如果线程执行完毕了,那么线程会自动销毁么,当然可以,只不过会有一点点延迟,也就是几毫秒的事情。请看demo:
public class CachedThreadPool {
public static void main(String[] args) throws InterruptedException {
//JDK1.5 以后引入了concurrent类库包类辅助我们的线程操作,也可以理解为一个多线程的框架
//常用的方式是通过ExecutorService创建一个为一个的执行器来统一管理多个线程
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i=0;i<5;i++){
executorService.execute(new BasicRunnable());
System.out.println("当前线程数:"+Thread.activeCount());
}
//executor的shutdown()方法调用后,该执行器就不会再接受新的线程任务,只会保证当前的多线程任务顺利执行完
executorService.shutdown();
Thread.sleep(1);
System.out.println("当前线程数:"+Thread.activeCount());
}
}
以上需要其他的类,自己脑补即可。
多线程中的异常
由于多线程的特性,主线程中并不能捕捉到所启动子线程的异常,子线程异常会直接抛出到控制台。那么我们怎么能够捕获子线程的异常呢,Java为我们提供了UncaughtExceptionHandler,只要实现这个接口的uncaughtException()方法来处理异常,然后将实现类通过thread对象的setUncaughtExceptionHandler(UncaughtExceptionHandler实现类)方法将异常处理和子线程绑定在一起即可。
不使用UncaughtExceptionHandler:
/**
* @ClassName: ThreadExceptionDemo
* @Description: 这个类主要来说明在子线程中抛出的异常是无法再主线程中catch到的
* 即try{childrenThread.start()}catch{得不到childrenThread抛出的异常}
* @author lijie
* @date 2014-3-13 下午12:46:03
*
*/
public class SimpleExceptionDemo implements Runnable{
/* (non-Javadoc)
* @see java.lang.Runnable#run()
*/
public void run() {
throw new RuntimeException();
}
public static void main(String[] args) {
try {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(new SimpleExceptionDemo());
} catch (Exception e) {
/**
* 这里的catch并不能捕捉到子线程的异常
*/
System.out.println("捕捉到了子线程的异常");
}
}
}
异常会直接抛出到控制台,所以我们只能通过使用UncaughtExceptionHandler:
/**
*
* @ClassName: ExceptionThread
* @Description: 这个文件演示了利用UncaughtExceptionHandler接口,在Runnable或concurrent类库的ThreadFactory
* 中捕获子线程异常的方式.无论是Runnable还是ThreadFactory中,都是通过threand对象的setUncaughtExceptionHandler、
* 方法来设定子线程异常的捕获/处理
* @author lixiaodai
* @date 2014-3-13 下午12:57:12
* @version V1.0
*
*/
public class ExceptionThread implements Runnable{
/* (non-Javadoc)
* @see java.lang.Runnable#run()
*/
public void run() {
Thread t = Thread.currentThread();
/**
* 下边的输出只是为了起到跟踪的作用
*/
System.out.println("run() by "+t);
System.out.println("eh = "+t.getUncaughtExceptionHandler());
throw new RuntimeException();
}
public static void main(String[] args) {
/**
* 利用ThreadFactory接口的实现类将异常处理类与Runnable绑定到一起
*/
ExecutorService executorService = Executors.newCachedThreadPool(new HandlerThreadFactory());
executorService.execute(new ExceptionThread());
}
}
/**
*
* @ClassName: MyUncaughtExceptionHandler
* @Description: UncaughtExceptionHandler类的实例类,用于异常处理逻辑
* @author lijie
* @date 2014-3-13 下午2:59:28
*
*/
class MyUncaughtExceptionHandler implements UncaughtExceptionHandler{
/* (non-Javadoc)
* @see java.lang.Thread.UncaughtExceptionHandler#uncaughtException(java.lang.Thread, java.lang.Throwable)
*/
public void uncaughtException(Thread t, Throwable e) {
System.out.println("catch thread: "+t.getName()+" exception: "+e.getMessage());
}
}
/**
*
* @ClassName: HandlerThreadFactory
* @Description: ThreadFactory实现类,绑定异常处理类和Runnable实例
* @author lijie
* @date 2014-3-13 下午2:59:59
*
*/
class HandlerThreadFactory implements ThreadFactory{
/* (non-Javadoc)
* @see java.util.concurrent.ThreadFactory#newThread(java.lang.Runnable)
*/
public Thread newThread(Runnable r) {
System.out.println(this + "creating new Thread");
Thread t = new Thread(r);
System.out.println("created "+t.getName());
/**
* 绑定
*/
t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
System.out.println("eh = "+t.getUncaughtExceptionHandler());
return t;
}
}
多线程中的事务
对于Web程序中的事务,无论是Spring还是什么的,由于他们的事务实现一般都是通过ThreadLocal来保证或实现的,所以如果在事务处理中新启动一个线程处理,那么新线程是不会和原线程共享一个事务的。所以如果需要在事务中实现多线程那么就需要人为的操作线程来保证事务。以后会介绍到