【java】详解java多线程
目录结构:
在这篇Blog中,在这边文章中,笔者将结合自己对多线程的理解,以及《java疯狂讲义》一书中多线程一章,对这篇文章做详细的阐述。
都知道线程是进程的执行单元,一个进程可以拥有多个线程,每个线程都拥有独立的栈堆,程序计数器。线程之间是独立运行的,但他们之间也可以相互通信、相互影响。
1. 线程的创建与启动
接下来介绍创建线程的三种方式,当然,方式远不止这三种。
1.1 继承Thread类创建线程类
这是一种比较常见的一种方式,通过继承Thread类重写其中的run()方法。
栗如:
public class ExtendsThreadTest extends Thread { private int i=0; public ExtendsThreadTest(String name) { super(name); } @Override public void run(){ for(;i<100;i++){ System.out.println(getName()+" -> "+i); } } public static void main(String[] args) { new ExtendsThreadTest("线程一").start(); new ExtendsThreadTest("线程二").start(); } }
注:通过继承Thread类的方法来创建线程类的时,多个线程之间无法共享线程类的实例变量。
1.2 实现Runnable接口创建线程类
在创建Thread类时,可以指定一个Runnable参数,所以我们可以将Runnable接口的实现类传给Thread类,以实现线程。
public class RunnableImpTest implements Runnable{ public int i=0; @Override public void run() { for(;i<100;i++){ System.out.println(Thread.currentThread().getName()+" -> "+i); } } public static void main(String[] args) { RunnableImpTest runnableImp=new RunnableImpTest(); new Thread(runnableImp,"线程一").start(); new Thread(runnableImp,"线程二").start(); } }
注:通过实现Runnable接口,线程间可以共享Runnable实现类的实例变量。
1.3 使用Callable和Future创建线程
java5开始提供了一个Callable接口,Callable接口提供了一个call()方法作为线程的执行体。call()方法比传统的run()方法功能更强大,call()方法运行有返回值,允许抛出异常。
java5提供了Future接口作为Callable接口里call()方法的返回值,Future实现类提供了FutureTask实现类,FutureTask不仅仅实现了Future接口,还实现了Runnable接口。该类可以作为线程的target使用。
class CallableTest implements Callable<Boolean>{ private int prime=0; public CallableTest(int prime){ this.prime=prime; } @Override public Boolean call() throws Exception { //计算prime是否是素数 if(prime<2){ return false; } if(prime==2 || prime==3){ return true; } for(int i=2;i<=Math.floor(Math.sqrt(prime));i++){ if(prime%i==0){ return false; } } return true; } } public class CallableThreadTest { public static void main(String[] args) { //创建Callable对象 CallableTest callableTest=new CallableTest(15); //创建一个FutureTask任务 FutureTask<Boolean> task=new FutureTask<Boolean>(callableTest); //启动线程 new Thread(task).start(); //获取线程的返回值 try { System.out.println("is prime="+task.get());//一直阻塞,直到返回值 } catch (Exception e) { e.printStackTrace(); } System.out.println("执行完毕"); } }
2. 线程的生命周期
当线程被创建启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在线程的生命周期中,它要经过新建(New),就绪(Runnable),运行(Running),阻塞(Blocked)和死亡(Dead)5中状态。在线程启动以后,它不可能一直“霸占”着CPU独自运行,线程的状态也会在多次运行、就绪之间切换。
新建,当程序使用New关键字创建一个线程后,该线程就处于新建状态。此时,它和其他的java对象一样,仅仅由java虚拟机分配内存,并初始化成员变量的值。此时的线程对象并没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。
就绪,当线程调用了start()方法之后,该线程就处于就绪状态。java会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有开始运行,只是表示该线程可以运行了。
运行,如果处于就绪状态的线程获得了CPU,开始执行run()方法的方法体,则该线程处于运行状态。如果计算机只有一个CPU,那么任何时刻都只有一个线程处于运行。如果是在多处理器上,将会有多个线程并行执行。如果当前线程调用了yield()方法,那么线程将会重新进行就绪状态。
阻塞,当一个线程开始运行后,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就被执行结束了),线程在运行的过程中需要被中断(阻塞),目的是使其他线程获得执行的机会。被阻塞的线程会在合适的时候重新进入就绪状态。
死亡,一般情况下,当线程方法体执行结束后,线程结束;线程抛出未捕获的异常,线程结束;线程调用stop()等终止线程的方法,线程结束。
线程生命周期如下图所示:
3. 控制线程
java中的线程提供了一些快捷的工具方法,通过这些工具可以很好的控制线程。接下来介绍一些工具的使用。
3.1 join线程
Thread提供了让一个线程等待另一个线程完成的方法-join方法。当某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完毕,当前线程才会重新回到就绪状态。
join()方法有如下三种重载形式:
join():等待被join的线程执行完毕
join(long millis):等待被join的线程的时间最长为millis毫秒。如果在millis毫秒内被join()的线程还没有执行结束,则不再等待。
join(long millis,int nanos):等待被join的线程最长为millis豪秒,nanos豪微秒。
class JoinThread extends Thread{ public JoinThread(String name){ super(name); } @Override public void run(){ for(int i=0;i<100;i++){ try{ Thread.sleep(200); }catch(Exception e){ e.printStackTrace(); } System.out.println(getName()+"->"+i); } } } public class JoinTest { public static void main(String[] args) throws InterruptedException { JoinThread joinThread = new JoinThread("被Join的线程"); joinThread.start(); joinThread.join(); System.out.println("执行完毕");//在joinThread线程执行完毕后,才会继续执行。 } }
3.2 后台线程
有一种线程,它是在后台运行的,它的任务是为其他线程提供服务,这种线程被称为“后台线程(Daemon Thread)”,又称为“守护线程”或“精灵线程”。JVM的垃圾回收线程就是典型的后台线程。
后台线程的特征:如果所有的前台线程都死亡了,后台线程也会自动死亡。
调用Thread对象的setDaemon(true)可将制定线程设置为后台线程。下面的程序将执行线程设置为后台线程,所有的前台线程都死亡时,后天线程也死亡,程序就退出了。
class DaemonThread extends Thread{ public DaemonThread(String name){ super(name); } @Override public void run(){ for(int i=0;i<100;i++){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName()+"->"+i); } } } public class DaemonThreadTest { public static void main(String[] args) { DaemonThread daemonThread= new DaemonThread("后台线程"); daemonThread.setDaemon(true);//设置为后台线程 daemonThread.start(); try { Thread.sleep(5000);//睡眠5秒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("执行完毕"); } }
从结果上面的结果可以看出,前台线程执行完毕后,后台线程也就结束了。
3.3 线程睡眠:sleep
如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep()方法来实现。当当前线程调用sleep()方法进入阻塞状态后,在其睡眠时间内,该线程不会获得执行的机会,即使系统中没有其它可执行的线程,处于sleep()的线程也不会执行,sleep()是用来暂停线程的执行。
在上面的案例中,已经展示了sleep()方法的使用了。
3.4 线程让步:yield
yield()方法是一个和sleep()方法有点相似的方法,它也是Thread类提供的一个静态方法。它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,只是将该线程转入就绪状态。yeild()只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield()线程暂停之后,线程调度器又将其调度出来重新执行。
当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行机会。
栗子:
class Yield implements Runnable{ int i=0; @Override public void run(){ for(;i<50;i++){ System.out.println(Thread.currentThread().getName()+"->"+i); //当i等于20时,当前线程让步,让线程调度器重新调度 if(i==20){ Thread.yield(); } } } } public class YieldThreadTest extends Thread{ public YieldThreadTest(Runnable runnable,String name){ super(runnable,name); } public static void main(String[] args) { Yield yd=new Yield(); YieldThreadTest ytt1=new YieldThreadTest(yd,"高级"); ytt1.setPriority(Thread.MAX_PRIORITY);//设置优先级最高 ytt1.start(); YieldThreadTest ytt2=new YieldThreadTest(yd,"低级"); ytt2.setPriority(Thread.MIN_PRIORITY);//设置优先级最低 ytt2.start(); } }
运行上面的程序会发现,在一般情况下会发现,“高级”和“低级”线程是交叉执行的,这是因为多CPU的原因。如果用户的计算机是单核的,那么就可以清楚看到上面的运行效果。
yield()方法和sleep()的区别如下:
1.sleep()方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级;但yield()只会给优先级相同,或优先级更高的线程执行机会。
2.sleep()方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态;而yield()不会讲线程转入阻塞状态,它只是将当前线程进入就绪状态。
3.sleep()方法的声明抛出了InterruptedException异常,所以调用sleep()方法时要么捕捉改异常,要么抛出该异常。
4.sleep()方法比yield()方法具有更好的可移动性,所以建议不要使用yield()方法来控制并发线程的执行。
3.5 改变线程优先级
每个线程执行时都具有一定的优先级,优先级高的线程具有更高的执行机会,优先级低的线程具有更少的执行机会。每个线程默认的优先级都与创建它的父级优先级相同。在默认情况下,main线程具有普通优先级。
Thread对象提供了setPrority(int newPrority)、getPrority()来设置和返回优先级。其中setPrority的参数是一个int类型的整数,Thread类提供如下三个静态常量MAX_PRIORITY、NORM_PRIORITY、MIN_PRIORITY,顾名思义分别是最高、普通、最低优先级。
该方法在上面的案例中以及使用过了,这里不再赘述。
3.6 终止线程
3.6.1 使用退出标志终止线程
当run方法执行完后,线程就会退出。但有时run方法是永远不会结束的。如在服务端程序中使用线程进行监听客户端请求,或是其他的需要循环处理的任务。在这种情况下,一般是将这些任务放在一个循环中,如while循环。如果想让循环永远运行下去,可以使用while(true){……}来处理。但要想使while循环在某一特定条件下退出,最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出。下面给出了一个利用退出标志终止线程的例子。
public class ThreadFlag extends Thread { public volatile boolean exit = false; public void run() { while (!exit); } public static void main(String[] args) throws Exception { ThreadFlag thread = new ThreadFlag(); thread.start(); sleep(5000); // 主线程延迟5秒 thread.exit = true; // 终止线程thread thread.join(); System.out.println("线程退出!"); } }
在上面代码中定义了一个退出标志exit,当exit为true时,while循环退出,exit的默认值为false.在定义exit时,使用了一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值,
3.6.2 使用stop强行终止线程
使用stop方法可以强行终止正在运行或挂起的线程。我们可以使用如下的代码来终止线程:
thread.stop();
虽然使用上面的代码可以终止线程,但使用stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,因此,并不推荐使用stop方法来终止线程。
3.6.3 使用interrupt终止线程
使用interrupt方法来终端线程可分为两种情况:
(1)线程处于阻塞状态,如使用了sleep方法。
(2)使用while(!isInterrupted()){……}来判断线程是否被中断。
在第一种情况下使用interrupt方法,sleep方法将抛出一个InterruptedException例外,而在第二种情况下线程将直接退出。
下面的代码演示了在第一种情况下使用interrupt方法。
public class ThreadInterrupt extends Thread { public void run() { try { sleep(50000); // 延迟50秒 } catch (InterruptedException e) { System.out.println(e.getMessage()); } } public static void main(String[] args) throws Exception { Thread thread = new ThreadInterrupt(); thread.start(); System.out.println("在50秒之内按任意键中断线程!"); System.in.read(); thread.interrupt(); thread.join(); System.out.println("线程已经退出!"); } }
上面代码的运行结果如下:
在50秒之内按任意键中断线程!
sleep interrupted
线程已经退出!
在调用interrupt方法后, sleep方法抛出异常,然后输出错误信息:sleep interrupted.
注意:在Thread类中有两个方法可以判断线程是否通过interrupt方法被终止。一个是静态的方法interrupted(),一个是非静态的方法isInterrupted(),这两个方法的区别是interrupted用来判断当前线是否被中断,而isInterrupted可以用来判断其他线程是否被中断。因此,while (!isInterrupted())也可以换成while (!Thread.interrupted())。
4. 线程同步
4.1 概述
关于线程同步的知识,很多都是关于对象的。这里笔者从另一个角度来尝试解释线程同步,首先给出如下结论:
线程同步问题是指线程访问线程作用域之外的资源引发的资源混乱问题(这里的资源不仅仅是对象,静态字段等等。)
看看如下这个栗子:
public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { TestMethod(0); } }).start(); new Thread(new Runnable() { @Override public void run() { TestMethod(10); } }).start(); } public static void TestMethod(int i){ System.out.println(Thread.currentThread().getName()+">i="+ i); try { Thread.sleep(1000);//休眠一秒钟 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+">i="+(++i)); }
两个线程同时访问TestMethod方法,并且传入了不同的参数,在TestMethod上会引发线程同步的问题吗?答案是不会。虽然两个线程都访问了TestMethod方法,但是在TestMethod方法中,并没有访问在该方法作用域之外的任何资源,变量i一直都在TestMethod方法的作用域之内。所以这里不会引发线程同步的问题。
结果为:
Thread-0>i=0
Thread-1>i=10
Thread-0>i=1
Thread-1>i=11
其实上面的代码可以看做如下这样,就更好理解了。
public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { int i=0; System.out.println(Thread.currentThread().getName()+">i="+ i); try { Thread.sleep(1000);//休眠一秒钟 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+">i="+(++i)); } }).start(); new Thread(new Runnable() { @Override public void run() { int i=10; System.out.println(Thread.currentThread().getName()+">i="+ i); try { Thread.sleep(1000);//休眠一秒钟 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+">i="+(++i)); } }).start(); }
如果把上面的代码修改为如下代码,则就会引发线程问题了。
public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { TestMethod(0); } }).start(); new Thread(new Runnable() { @Override public void run() { TestMethod(10); } }).start(); } static int c=-1; public static void TestMethod(int i){ c=i; System.out.println(Thread.currentThread().getName()+">c="+ c); try { Thread.sleep(1000);//休眠一秒钟 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+">c="+(++c)); }
输出如下:
Thread-0>c=0
Thread-1>c=10
Thread-0>c=11
Thread-1>c=12
上面笔者介绍了一下,在什么情况下会引发线程同步问题。在项目中,如果线程访问的资源超过了它的作用域,那么就应该考虑是线程同步了,java多线程中引入了同步监视器来进行线程同步。
4.2 同步锁(synchronized)
synchronized有两种用法,它即可作为作为同步代码块,也可以同步方法。
同步代码块的语法是:
synchronized(obj){ ... }
格式中synchronized后括号里的obj就是同步监视器,线程开始执行时,必须先获得对同步监视器的锁定。
虽然java程序运行任何对象作为同步监视器,但同步监视器的目的是为了阻止多线程对共享资源的并发访问,通常推荐使用可能被并发访问的共享资源才作为同步监视器。
同步方法,就是使用synchronized来修饰某个方法,则该方法被称为同步方法。对于Synchronized修饰的实例方法(非static方法,synchronized修饰static方法没什么意义,因为static最终是由类调用,跟线程对象无关。),无须显式指定同步监视器,同步方法的监视器就是this,也就是调用该方法的特征。
同步方法的语法是:
public synchronized void testMethod(){ ... }
下面使用synchronized同步代码块来模拟银行取钱:
class Account{ private String name=null; private Integer amount=0; public Account(String name,Integer amount){ this.name=name; this.amount=amount; } public void Draw(int drawAmount){ synchronized (amount) {//使用同步锁锁住amount System.out.println(name+" 开始取钱"); if(amount>=drawAmount){//判断 amount-=drawAmount;//取钱 System.out.println(name+" 账户余额:"+amount); }else{ System.out.println(name+" 余额不足"); } } } } public class DrawThread extends Thread{ private Account account=null; private int amount=0; public DrawThread (Account account,int amount){ this.account=account; this.amount=amount; } @Override public void run(){ account.Draw(amount); } public static void main(String[] args) { Account account=new Account("富人甲", 1000); new DrawThread(account, 800).start(); new DrawThread(account, 800).start(); } }
使用synchronized修饰同步代码块或是同步方法,都是为了达到线程同步。如果有两个线程需要同时访问同一个字段,那么可以使用volatile来修改该字段。
4.3 同步锁(Lock)
java5开始,提供了功能更强大的的线程同步机制-通过显式定义同步锁对象来实现同步,这种机制下,同步锁由Lock对象充当,Lock比synchronized更灵活。
Lock和ReadLock是java5提供的两个根接口,并且为Lock提供了ReentrantLock实现类,为ReadWriteLock提供了Reentrant实现类,为ReadWriteLock提供了ReentrantReadWriteLock实现类。
ReadLock的语法格式为:
class X { private final ReentrantLock lock = new ReentrantLock(); // ... public void m() { try { lock.lock(); // block until condition holds // ... method body } finally { lock.unlock() } } }
使用Lock的时候,强烈建议使用try{}finally{}的格式,并且在try块中加锁,在finally块中显示释放锁。这样的话,即使try中发生未捕获的异常,那么也可以释放锁对象。
接下来使用ReenLock来重从上面银行取钱的Account类。
class Account{ //定义锁 private final ReentrantLock lock = new ReentrantLock(); private String name=null; private Integer amount=0; public Account(String name,Integer amount){ this.name=name; this.amount=amount; } public void Draw(int drawAmount){ try{ lock.lock(); System.out.println(name+" 开始取钱"); if(amount>=drawAmount){//判断 amount-=drawAmount;//取钱 System.out.println(name+" 账户余额:"+amount); }else{ System.out.println(name+" 余额不足"); } }finally{ lock.unlock(); } } }
4.4 死锁
当两个锁相互等待对方释放同步监视器时就会发生死锁,java虚拟机没有提供检测,也没有采取任何措施来处理死锁的情况,所以多线程编程中,应该采取措施避免死锁。一旦出现死锁,整个程序既不会发生任何错误,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
例如:
abstract class Car{ protected String name=null; protected Car(String name){ this.name=name; } public synchronized void entrySingleRoadWith(Car car){ System.out.println(name+" 进入道路"); try { Thread.sleep(1000);//睡眠一秒钟 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(car.name+" 准备释放道路所有权"); car.release(); } //释放道路所有权,在释放之前必须要获得对象同步锁。 public synchronized void release(){ System.out.println(name+" 释放道路所有权"); } } class CarA extends Car{ public CarA() { super("carA"); } } class CarB extends Car{ public CarB() { super("carB"); } } public class DeadLockTest extends Thread{ private Car car1=null; private Car car2=null; public DeadLockTest(Car car1,Car car2){ this.car1=car1; this.car2=car2; } @Override public void run() { car1.entrySingleRoadWith(car2);//car1和car2同时进入道路 } public static void main(String[] args) { //创建两个Car对象 CarA carA=new CarA(); CarB carB=new CarB(); //创建两个线程,并且开始运行 new DeadLockTest(carA,carB).start(); new DeadLockTest(carB,carA).start(); } }
5. 线程通信
当线程在系统内运行时候,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换执行。但java中提供了一些机制,来保证线程的协调运行。
线程通信类的问题可以按照如下的思路进行思考:
a.确定临界资源
b.确定需要哪些线程类
c.确定线程的通信逻辑
d.确定线程的退出条件
5.1 使用wait和notify控制线程通信
Object类提供了wait()、notify()和notifyAll(),可以通过调用Object对象的这三个方法来实现线程的通信。
wait():导致当前线程等待,知道其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。
notify():唤醒在此同步监视器上等待的单个线程。如果有多个线程都在此同步监视器上等待,则只唤醒其中一个。
notifyAll():唤醒在此同步监视器上等待的所有线程。
如果使用synchronized修饰的同步方法,那么该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法。
如果使用synchronized修饰的是同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这三个方法。
下面的栗子是一个生产者-消费者的模型:
safeStack.java
public class SafeStack { /** * 下标 */ private int top=0; /** * 存储产生的随机整数 */ private int[] values=new int[10]; /* * 压栈和出栈的标志,通过dataAvailable变量控制push()方法和pop()方法中线程的等待。 * dataAvailable的值默认是false,最开始让pop()方法中线程中等待。 */ private boolean dataAvailable=false; /** * 入栈 */ public synchronized void push(int val){ if(dataAvailable){ try{ wait(); }catch(InterruptedException e){ e.printStackTrace(); } } values[top]=val; System.out.println("压入数字"+val+"完成"); top++;//入栈完成 if(top>=values.length-1){//当values数组满后,才改变状态。 dataAvailable=true;//状态变为出栈 notifyAll();//唤醒线程 } } /** * 出栈 */ public synchronized int pop(){ if(!dataAvailable){ try{ wait(); }catch(InterruptedException e){ e.printStackTrace(); } } int res=values[top]; System.out.println("弹出数字"+res+"完成"); top--; if(top<=0){ dataAvailable=false; notifyAll(); } return res; } }
PushThread.java
public class PushThread extends Thread{ private SafeStack safeStack=null; public PushThread(SafeStack safeStack){ super(); this.safeStack=safeStack; } @Override public void run() { while(true){//假设一个生产者生产次数无限生产;也可以指定生产次数。 //获得随机数 int randomInt=new Random().nextInt(100); safeStack.push(randomInt); } } }
PopThread.java
public class PopThread extends Thread{ private SafeStack safeStack=null; public PopThread(SafeStack safeStack){ super(); this.safeStack=safeStack; } @Override public void run(){ while(true){//假设一个消费者,无限消费;也可以指定消费次数。 int value=safeStack.pop(); } } }
测试类:
public class TestSafeStack { public static void main(String[] args) { SafeStack safeStack=new SafeStack(); PushThread pushThread=new PushThread(safeStack);//创建了一个生产者 PopThread popThread=new PopThread(safeStack);//创建消费者 pushThread.start(); popThread.start(); } }
5.2 使用Condition控制线程通信
如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象阿里保证同步,则系统中不存在隐式的同步监视器,也就不能使用wait()、notify()和notifyAll()方法来保证线程间的通信了。
当使用Lock对象来保证同步时,java提供了一个Condition类保持协调,使用Condition可以让那些已经得到Lock对象却无法执行的线程释放Lock对象,Condition对象也可以唤醒其它处于等待的线程。
当Lock与Condition联合使用时,Lock代替了同步代码块或是同步方法,Condition代表了同步监视器的功能。Condition的实例被绑定在Lock对象上,要获得指定Lock实例的Condition实例,调用Lock对象的newCondition()方法即可。
Condition类提供了如下三个方法:
await():类似于隐式同步监视器的上的wait()方法,导致当前线程等待,直到其他线程调用该Condition的signal()方法或signalAll()方法来唤醒该线程。该await()方法有更多的变体,如long awaitNanos(long nanosTimeout)、void awaitUninterruptibly()、awaitUntil(Date deadline)等,可以完成更丰富的功能。
signal():唤醒在此Lock对象上等待的单个线程。如果有多个线程在该Lock对象上等待,则会选择任意唤醒其中一个线程。
signalAll():唤醒在此Lock对象上等待的所有线程。只有当前线程放弃对该Lock对象的锁定后,才可以执行被唤醒的线程。
接下来,使用Condition来重写上面的生产者-消费者案例中的临界资源类SafeStack:
public class SafeStack { //显示定义Lock对象 private final Lock lock=new ReentrantLock(); //获得Lock对象上的Condition private final Condition cond=lock.newCondition(); /** * 下标 */ private int top=0; /** * 存储产生的随机整数 */ private int[] values=new int[10]; /* * 压栈和出栈的标志,通过dataAvailable变量控制push()方法和pop()方法中线程的等待。 * dataAvailable的值默认是false,最开始让pop()方法中线程中等待。 */ private boolean dataAvailable=false; /** * 入栈 */ public void push(int val){ try{ lock.lock();//加锁 if(dataAvailable){ try{ cond.await();//等待 }catch(InterruptedException e){ e.printStackTrace(); } } values[top]=val; System.out.println("压入数字"+val+"完成"); top++;//入栈完成 if(top>=values.length-1){//当values数组满后,才改变状态。 dataAvailable=true;//状态变为出栈 cond.signalAll();//唤醒其他线程 } }finally{ lock.unlock();//释放锁 } } /** * 出栈 */ public int pop(){ int res=0; try{ lock.lock();//加锁 if(!dataAvailable){ try{ cond.await();//等待 }catch(InterruptedException e){ e.printStackTrace(); } } res=values[top]; System.out.println("弹出数字"+res+"完成"); top--; if(top<=0){ dataAvailable=false; cond.signalAll();//唤醒线程 } }finally{ lock.unlock();//解锁 }; return res; } }
5.3 使用阻塞队列(BlockingQueue)控制通信
Java5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但它的主要用途不是用作容器,而是作为线程同步的工具。BlockingQueue具有一个特征,当生产者试图向BlockingQueue中放入元素时,如果该队列已经满了,则线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列为空,则该线程被阻塞。程序的两个线程通过交替想BlockingQueue中放入元素和取出元素,即可很好的控制线程通信。
笔者认为,该机制比上面提供的两种线程异步通信的机制更加的灵活、便捷,上面提供的两种方式都需要同一样东西,那就是需要显示声明临界资源,以及指定阻塞和恢复执行的位置。而BlockingQueue对这一步进行了简化,开发者只需要提供被同步资源的类型就可以了,而无需关心具体的实现细节。在实际开发中,很有可能遇到异步线程通信的问题(并且没有临界资源),我们可以上面提供的两种机制自己封装能满足要求的异步线程通信类,如果BlockingQueue可以满足要求的话,那么为什么不用别人已经封装好的呢!
BlockingQueue提供如下两个支持阻塞的方法:
put(E e):尝试把e元素放入队列中,如果该队列中的元素已满,那么则阻塞该线程。
take():尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞线程。
BlockingQueue继承了Queue接口,当然也可以使用Queue接口中的方法,大致有如下三类:
a.在队列尾部插入元素。包括add(E e)、Offer(E e)和put(E e)方法,当队列已满时,这三个方法分别会抛出异常,返回false,阻塞队列。
b.在队列头部删除并返回删除的元素。包括remove(),poll()和take()方法。当队列已为空时,这三个方法会分别抛出异常、返回false、阻塞队列。
c.在队列头部取出但不删除元素。包括element()和peek()方法,当队列为空时,这两个方法分别抛出异常,返回false。
下面这张表映射它们之间的关系
抛出异常 | 不同返回值 | 阻塞线程 | 指定超时时长 | |
队尾插入元素 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
队头插入元素 | remove() | poll() | take() | poll(time,unit) |
获取、不删除元素 | element() | peek() | 无 | 无 |
BlockingQueue与其实现关系的类图:
ArrayBlockingQueue:基于数组实现的BlockingQueue队列。
LinkedBlockingQueue:基于链表实现BlockingQueue队列。
PriorityBlockingQueue:并不是标准的阻塞队列,该队列使用remove()、poll()、take()等方法取元素时,并不是取队列中时间存在最长的元素,而是队列中最小的元素。PriorityBlockingQueue<E>判断元素大小可以更具元素本身的大小排序(实现Comparable接口),也可以使用Comparator进行定制排序。
SynchronousQueue:同步队列。对该队列的存取必须交替进行。
DelayQueue:它是一个特殊的BlockingQueue,底层依靠PriorityBlockingQueue实现。不过,DelayQueue要求集合元素都实现Delay接口(该接口里有一个long getDelay()方法),DelayQueue根据集合元素的getDelay()方法的返回值排序。
下面是使用ArrayBlockingQueue实现异步线程通信的一个案例:
public class BlackingQueueTest { public static void main(String[] args) { //创建一个容量为1的BlockingQueue BlockingQueue<String> bq=new ArrayBlockingQueue<String>(1); //启动三个线程 new Producer(bq).start(); new Producer(bq).start(); new Consumer(bq).start(); } } class Producer extends Thread{ private BlockingQueue<String> bq; public Producer(BlockingQueue<String> bq){ this.bq=bq; } public void run(){ String[] strArr=new String[]{ "JAVA", "STRUTS", "SPRING" }; for(int i=0;i<99999;i++){ System.out.println(getName()+"生产者准备生产集合元素!"); try{ Thread.sleep(200); //尝试放入元素,如果元素已经满,则线程会被阻塞 bq.put(strArr[i%3]); }catch(Exception e){ e.printStackTrace(); } System.out.println(getName()+"生产完成:"+bq); } } } class Consumer extends Thread{ private BlockingQueue<String> bq; public Consumer(BlockingQueue<String> bq){ this.bq=bq; } public void run(){ while(true){ System.out.println(getName()+"消费者准备消费集合元素"); try{ Thread.sleep(200); //尝试取出元素,如果队列已空,则线程会被阻塞 bq.take(); }catch(Exception e){ e.printStackTrace(); } System.out.println(getName()+"消费完成"+bq); } } }
6. 线程组
java使用过ThreadGroup来表示线程组,它可以对一组线程进行分类管理,java允许程序直接对线程组进行控制。对线程组的控制,相当于同时控制这批线程。用户创建的所有的线程都属于指定线程组,如果没有显示指定线程属于哪个线程组,则该线程属于默认线程组。在默认情况下,子线程和创建它的父线程处于同一个线程组内,例如A线程创建了B线程,并且没有指定B线程的线程组,则B线程属于A线程所在线程组。
一旦某个线程加入了指定线程组后,该线程将一直属于该线程组,直到线程死亡,线程运行中不能改变它的所属线程组。
Thread类提供了一个getThreadGroup()方法来返回该线程所属线程组,getThreadGroup()方法的返回值是ThreadGroup对象,表示一个线程组。ThreadGroup类提供了如下两个简单的构造器来创建示例。
ThreadGroup(String name):以指定的线程组名字来创建新的线程组。
ThreadGroup(ThreadGroup parent,String name):以指定的名字和指定的父线程组创建一个新的线程组。
ThreadGroup类提供了如下几个常用的方法来创建操作整个线程组里的所有线程。
int activeCount():返回此线程组中活动线程的数目。
interrupt():中断此线程中的所有线程。
isDaemon():判断该线程组是否是后台线程组。
setDaemon(boolean daemon):把该线程组设置为后台线程组(后台线程组的一个特征,当后台线程组中的最后一个线程执行完毕或最后一个线程被销毁后,后台线程将自动销毁)。
setMaxPriority(int pri):设置线程组的最高优先级。
7. 线程池
系统新启动一个线程的成本是很高的,它涉及到与操作系统的交互。在这种情况下,使用线程池可以很好的提升性能,尤其是线程中需要创建大量生存期短暂的线程时,更应该考虑使用线程池。
与数据库池类似的是,线程池在启动时即会创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个线程来执行他们的run()或call()方法,当run()或call()方法执行结束后,该线程并不会死亡,而是再次回到线程池后成为空闲状态。
从java5开始,java内建支持线程池。java5新增加了一个Executors工厂类来生产线程池,该工厂类有如下静态方法:
newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。
ExecutorService newFixedThreadPool(int nThreads):创建一个可重用、具有固定线程数的线程池。
ExecutorService newSignleThreadExecutor():创建一个只有单线程的线程池,它相当于调用newFixedThreadPool()方法时传入参数为1。
ExecutorService newScheduledThreadPool(int corePoolSize):创建一个具有指定线程数的线程池,它可以在指定延迟后执行线程任务。corePoolSize指池中所保存的线程数,即使线程空闲也被保存。
ExecutorService newSignleScheduledExecutor():创建只有一个线程的线程池,它可以在指定延迟后执行线程任务。
使用线程池来执行线程任务的步骤如下:
a.调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池。
b.创建Runnable实现类或Callable实现类的实例,作为线程执行任务。
c.调用ExecutorService对象的submit()方法来提交Runnable或Callable的实例。
d.当不想提交任务时,就调用ExecutorService的shutdown()方法来关闭线程池,shutdown()方法也会将以前所有已提交的任务执行完毕。
栗子:
ExecutorService pool= Executors.newFixedThreadPool(10); Runnable target=new Runnable() { @Override public void run() { for(int i=0;i<10;i++){ System.out.println(Thread.currentThread().getName()+"的i值为:"+i); } } }; //向线程池中提交两个线程 pool.submit(target); pool.submit(target); //关闭线程池,执行池中已有的任务 pool.shutdown();
8. 线程相关的类和方法
8.1 线程未处理的异常
java5开始,java加强了对线程异常的处理,如果线程执行过程中抛出了一个未处理的异常,JVM在结束线程之前会自动检查是否有对应的Thread.UncaughtExceptionHandler对象,如果找到该处理器对象,则调用该对象uncaughtException(Thread t,Throwable e)方法来处理该异常。
Thread.UncaughtExeceptionHandler是Thread类的一个静态内部接口,该接口内只有一个方法:void uncaughtException(Thread t,Throwable e),该方法中的t代表出现的异常,而e代表该线程抛出的异常。
Thread类提供了一下两个方法来设置异常处理器。
static setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):为该线程类的所有线程实例设置默认的异常处理器。
setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):为指定的线程实例设置异常处理器。
注意:设置异常处理器只能够监听到未处理的异常,而不能阻止它继续向上抛出。
栗子:
class MyExHandler implements Thread.UncaughtExceptionHandler{ @Override public void uncaughtException(Thread t, Throwable e) { System.out.println(t+" 线程出现了异常:"+e); } } public class ExHandler { public static void main(String[] args) { Thread.currentThread().setDefaultUncaughtExceptionHandler(new MyExHandler()); int a =5/0; System.out.println("程序正常结束"); } }
打印为:
Thread[main,5,main] 线程出现了异常:java.lang.ArithmeticException: / by zero
从打印看出,设置UncaughtExceptionHandler不会阻止异常继续往上抛出,若要阻止的话可以使用try catch来完成。
8.2 包装线程不安全的集合
java集合中的ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是线程不安全的,也就是说,多个线程并发访问这些集合中的元素时,就有可能破坏数据的完整性。
针对这个问题,java提供提供了Collections类,使用Collections类可以把这些集合包装成线程安全的。
例如:
//将一个普通的HashMap包装为线程安全的HashMap对象
HashMap m=Collections.synchronizedMap(new HashMap());
关于Collections的更多使用,可以详见java API。