【java】详解java多线程

目录结构:java

contents structure [+]

在这篇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()等终止线程的方法,线程结束。

线程生命周期以下图所示:
dom

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;
    }
}
SafeStack .java

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);
        }
    }
}
PushThread.java

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();
        }
    }
}
PopThread.java

测试类:

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。

相关文章
相关标签/搜索