Java多线程详解

线程对象是能够产生线程的对象。好比在Java平台中Thread对象,Runnable对象。线程,是指正在执行的一个指点令序列。在java平台上是指从一个线程对象的start()开始,运行run方法体中的那一段相对独立的过程。相比于多进程,多线程的优点有:html

    (1)进程之间不能共享数据,线程能够;java

    (2)系统建立进程须要为该进程从新分配系统资源,故建立线程代价比较小;程序员

    (3)Java语言内置了多线程功能支持,简化了java多线程编程。算法

1、建立线程和启动数据库

  (1)继承Thread类建立线程类编程

经过继承Thread类建立线程类的具体步骤和具体代码以下:数组

   • 定义一个继承Thread类的子类,并重写该类的run()方法;安全

   • 建立Thread子类的实例,即建立了线程对象;多线程

   • 调用该线程对象的start()方法启动线程。并发

复制代码
 class SomeThead extends Thraad   { 
    public void run()   { 
     //do something here  
    }  
 } 
 
public static void main(String[] args){
 SomeThread oneThread = new SomeThread();   
  步骤3:启动线程:   
 oneThread.start(); 
}
复制代码

    (2)实现Runnable接口建立线程类

经过实现Runnable接口建立线程类的具体步骤和具体代码以下:

   • 定义Runnable接口的实现类,并重写该接口的run()方法;

   • 建立Runnable实现类的实例,并以此实例做为Thread的target对象,即该Thread对象才是真正的线程对象。

复制代码
class SomeRunnable implements Runnable   { 
  public void run()   { 
  //do something here  
  }  
} 
Runnable oneRunnable = new SomeRunnable();   
Thread oneThread = new Thread(oneRunnable);   
oneThread.start(); 
复制代码

    (3)经过Callable和Future建立线程

经过Callable和Future建立线程的具体步骤和具体代码以下:

   • 建立Callable接口的实现类,并实现call()方法,该call()方法将做为线程执行体,而且有返回值。
   • 建立Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
   • 使用FutureTask对象做为Thread对象的target建立并启动新线程。
   • 调用FutureTask对象的get()方法来得到子线程执行结束后的返回值其中,Callable接口(也只有一个方法)定义以下: 

复制代码
public interface Callable   { 
  V call() throws Exception;  
 } 
  步骤1:建立实现Callable接口的类SomeCallable(略);   
  步骤2:建立一个类对象: 
      Callable oneCallable = new SomeCallable(); 
  步骤3:由Callable建立一个FutureTask对象:   
    FutureTask oneTask = new FutureTask(oneCallable); 
  注释: FutureTask是一个包装器,它经过接受Callable来建立,它同时实现了 Future和Runnable接口。 
  步骤4:由FutureTask建立一个Thread对象:   
    Thread oneThread = new Thread(oneTask);   
  步骤5:启动线程:  
    oneThread.start(); 
复制代码

2、线程的生命周期

                        

一、新建状态

       用new关键字和Thread类或其子类创建一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有本身的内存空间,经过调用start方法进入就绪状态(runnable)。

注意:不能对已经启动的线程再次调用start()方法,不然会出现Java.lang.IllegalThreadStateException异常。

二、就绪状态

       处于就绪状态的线程已经具有了运行条件,但尚未分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。由于cpu的调度不必定是按照先进先出的顺序来调度的),等待系统为其分配CPU。等待状态并非执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动做称之为“cpu调度”。一旦得到CPU,线程就进入运行状态并自动调用本身的run方法。

提示:若是但愿子线程调用start()方法后当即执行,可使用Thread.sleep()方式使主线程睡眠一伙儿,转去执行子线程。

三、运行状态

      处于运行状态的线程最为复杂,它能够变为阻塞状态、就绪状态和死亡状态。

处于就绪状态的线程,若是得到了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。若是该线程失去了cpu资源,就会又从运行状态变为就绪状态。从新等待系统分配资源。也能够对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。

注: 当发生以下状况是,线程会从运行状态变为阻塞状态:

     ①、线程调用sleep方法主动放弃所占用的系统资源

     ②、线程调用一个阻塞式IO方法,在该方法返回以前,该线程被阻塞

     ③、线程试图得到一个同步监视器,但更改同步监视器正被其余线程所持有

     ④、线程在等待某个通知(notify)

     ⑤、程序调用了线程的suspend方法将线程挂起。不过该方法容易致使死锁,因此程序应该尽可能避免使用该方法。

当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。

四、阻塞状态

      处于运行状态的线程在某些状况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时中止本身的运行,进入阻塞状态。 

在阻塞状态的线程不能进入就绪队列。只有当引发阻塞的缘由消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,从新到就绪队列中排队等待,被系统选中后从原来中止的位置开始继续运行。有三种方法能够暂停Threads执行:

五、死亡状态

      当线程的run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,可是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 若是在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

3、线程管理

     Java提供了一些便捷的方法用于会线程状态的控制。具体以下:

一、线程睡眠——sleep

      若是咱们须要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则能够经过调用Thread的sleep方法。

注:

   (1)sleep是静态方法,最好不要用Thread的实例对象调用它,由于它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。以下面的例子:

复制代码
public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        System.out.println(Thread.currentThread().getName());  
        MyThread myThread=new MyThread();  
        myThread.start();  
        myThread.sleep(1000);//这里sleep的就是main线程,而非myThread线程  
        Thread.sleep(10);  
        for(int i=0;i<100;i++){  
            System.out.println("main"+i);  
        }  
    }  
} 
复制代码

     (2)Java线程调度是Java多线程的核心,只有良好的调度,才能充分发挥系统的性能,提升程序的执行效率。可是无论程序员怎么编写调度,只能最大限度的影响线程执行的次序,而不能作到精准控制。由于使用sleep方法以后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会从新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,咱们不可能精准的去干涉它,因此若是调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。

二、线程让步——yield

      yield()方法和sleep()方法有点类似,它也是Thread类提供的一个静态的方法,它也可让当前正在执行的线程暂停,让出cpu资源给其余的线程。可是和sleep()方法不一样的是,它不会进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,从新进入就绪的线程池中,让系统的线程调度器从新调度器从新调度一次,彻底可能出现这样的状况:当某个线程调用yield()方法以后,线程调度器又将其调度出来从新进入到运行状态执行。

实际上,当某个线程调用了yield()方法暂停以后,优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程更有可能得到执行的机会,固然,只是有可能,由于咱们不可能精确的干涉cpu调度线程。用法以下:

复制代码
public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        new MyThread("低级", 1).start();  
        new MyThread("中级", 5).start();  
        new MyThread("高级", 10).start();  
    }  
}  
  
class MyThread extends Thread {  
    public MyThread(String name, int pro) {  
        super(name);// 设置线程的名称  
        this.setPriority(pro);// 设置优先级  
    }  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 30; i++) {  
            System.out.println(this.getName() + "线程第" + i + "次执行!");  
            if (i % 5 == 0)  
                Thread.yield();  
        }  
    }  
} 
复制代码

注:关于sleep()方法和yield()方的区别以下:

①、sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,因此有可能刚进入就绪状态,又被调度到运行状态。

②、sleep方法声明抛出了InterruptedException,因此调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。

③、sleep方法比yield方法有更好的可移植性,一般不要依靠yield方法来控制并发线程的执行。

三、线程合并——join

线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另外一个线程执行完毕才能执行时,Thread类提供了join方法来完成这个功能,注意,它不是静态方法。
从上面的方法的列表能够看到,它有3个重载的方法:

void join()      
当前线程等该加入该线程后面,等待该线程终止。 void join(long millis)
当前线程等待该线程终止的时间最长为 millis 毫秒。 若是在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,从新等待cpu调度 void join(long millis,int nanos)
等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。若是在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,从新等待cpu调度

四、设置线程的优先级

     每一个线程执行时都有一个优先级的属性,优先级高的线程能够得到较多的执行机会,而优先级低的线程则得到较少的执行机会。与线程休眠相似,线程的优先级仍然没法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的几率较大,优先级低的也并不是没机会执行。

每一个线程默认的优先级都与建立它的父线程具备相同的优先级,在默认状况下,main线程具备普通优先级。

注:Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,范围是1~·0之间,也可使用Thread类提供的三个静态常量:

MAX_PRIORITY   =10

MIN_PRIORITY   =1

NORM_PRIORITY   =5
复制代码
    public class Test1 {  
        public static void main(String[] args) throws InterruptedException {  
            new MyThread("高级", 10).start();  
            new MyThread("低级", 1).start();  
        }  
    }  
      
    class MyThread extends Thread {  
        public MyThread(String name,int pro) {  
            super(name);//设置线程的名称  
            setPriority(pro);//设置线程的优先级  
        }  
        @Override  
        public void run() {  
            for (int i = 0; i < 100; i++) {  
                System.out.println(this.getName() + "线程第" + i + "次执行!");  
            }  
        }  
    }  
复制代码

注:虽然Java提供了10个优先级别,但这些优先级别须要操做系统的支持。不一样的操做系统的优先级并不相同,并且也不能很好的和Java的10个优先级别对应。因此咱们应该使用MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY三个静态常量来设定优先级,这样才能保证程序最好的可移植性。

五、后台(守护)线程

     守护线程使用的状况较少,但并不是无用,举例来讲,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在作数据库应用时候,使用的数据库链接池,链接池自己也包含着不少后台线程,监控链接个数、超时时间、状态等等。调用线程对象的方法setDaemon(true),则能够将其设置为守护线程。守护线程的用途为:

     • 守护线程一般用于执行一些后台做业,例如在你的应用程序运行时播放背景音乐,在文字编辑器里作自动语法检查、自动保存等功能。

     • Java的垃圾回收也是一个守护线程。守护线的好处就是你不须要关心它的结束问题。例如你在你的应用程序运行的时候但愿播放背景音乐,若是将这个播放背景音乐的线程设定为非守护线程,那么在用户请求退出的时候,不只要退出主线程,还要通知播放背景音乐的线程退出;若是设定为守护线程则不须要了。

setDaemon方法的详细说明:

复制代码
public final void setDaemon(boolean on)        将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。    
         该方法必须在启动线程前调用。 该方法首先调用该线程的 checkAccess 方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)。   
  参数:
     on - 若是为 true,则将该线程标记为守护线程。    
  抛出:    
    IllegalThreadStateException - 若是该线程处于活动状态。    
    SecurityException - 若是当前线程没法修改该线程。
复制代码

注:JRE判断程序是否执行结束的标准是全部的前台执线程行完毕了,而无论后台线程的状态,所以,在使用后台县城时候必定要注意这个问题

六、正确结束线程

Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit这些终止线程运行的方法已经被废弃了,使用它们是极端不安全的!想要安全有效的结束一个线程,可使用下面的方法:

    • 正常执行完run方法,而后结束掉;

    • 控制循环条件和判断条件的标识符来结束掉线程。

复制代码
class MyThread extends Thread {  
    int i=0;  
    boolean next=true;  
    @Override  
    public void run() {  
        while (next) {  
            if(i==10)  
                next=false;  
            i++;  
            System.out.println(i);  
        }  
    }  
}
复制代码

4、线程同步

     java容许多线程并发控制,当多个线程同时操做一个可共享的资源变量时(如数据的增删改查),将会致使数据不许确,相互之间产生冲突,所以加入同步锁以免在该线程没有完成操做以前,被其余线程的调用,从而保证了该变量的惟一性和准确性。

一、同步方法     

      即有synchronized关键字修饰的方法。因为java的每一个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,须要得到内置锁,不然就处于阻塞状态。

1
public  synchronized  void  save(){}

 注: synchronized关键字也能够修饰静态方法,此时若是调用该静态方法,将会锁住整个类

 二、同步代码块     

     即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。

复制代码
    public class Bank {  
     
        private int count =0;//帐户余额  
     
        //存钱  
        public   void addMoney(int money){  
     
            synchronized (this) {  
                count +=money;  
            }  
            System.out.println(System.currentTimeMillis()+"存进:"+money);  
        }  
     
        //取钱  
        public   void subMoney(int money){  
     
            synchronized (this) {  
                if(count-money < 0){  
                    System.out.println("余额不足");  
                    return;  
                }  
                count -=money;  
            }  
            System.out.println(+System.currentTimeMillis()+"取出:"+money);  
        }  
     
        //查询  
        public void lookMoney(){  
            System.out.println("帐户余额:"+count);  
        } 
    }
 
 
复制代码

注:同步是一种高开销的操做,所以应该尽可能减小同步的内容。一般没有必要同步整个方法,使用synchronized代码块同步关键代码便可。

 三、使用特殊域变量(volatile)实现线程同步      

   • volatile关键字为域变量的访问提供了一种免锁机制;

   • 使用volatile修饰域至关于告诉虚拟机该域可能会被其余线程更新;

   • 所以每次使用该域就要从新计算,而不是使用寄存器中的值;

   • volatile不会提供任何原子操做,它也不能用来修饰final类型的变量。

复制代码
 public class SynchronizedThread {
 
        class Bank {
 
            private volatile int account = 100;
 
            public int getAccount() {
                return account;
            }
 
            /**
             * 用同步方法实现
             * 
             * @param money
             */
            public synchronized void save(int money) {
                account += money;
            }
 
            /**
             * 用同步代码块实现
             * 
             * @param money
             */
            public void save1(int money) {
                synchronized (this) {
                    account += money;
                }
            }
        }
 
        class NewThread implements Runnable {
            private Bank bank;
 
            public NewThread(Bank bank) {
                this.bank = bank;
            }
 
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    // bank.save1(10);
                    bank.save(10);
                    System.out.println(i + "帐户余额为:" +bank.getAccount());
                }
            }
 
        }
 
        /**
         * 创建线程,调用内部类
         */
        public void useThread() {
            Bank bank = new Bank();
            NewThread new_thread = new NewThread(bank);
            System.out.println("线程1");
            Thread thread1 = new Thread(new_thread);
            thread1.start();
            System.out.println("线程2");
            Thread thread2 = new Thread(new_thread);
            thread2.start();
        }
 
        public static void main(String[] args) {
            SynchronizedThread st = new SynchronizedThread();
            st.useThread();
        }
 
复制代码

注:多线程中的非同步问题主要出如今对域的读写上,若是让域自身避免这个问题,则就不须要修改操做该域的方法。用final域,有锁保护的域和volatile域能够避免非同步的问题。

四、使用重入锁(Lock)实现线程同步

      在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具备相同的基本行为和语义,而且扩展了其能力。ReenreantLock类的经常使用方法有:    

 ReentrantLock() : 建立一个ReentrantLock实例         
 lock() : 得到锁        
 unlock() : 释放锁

注:ReentrantLock()还有一个能够建立公平锁的构造方法,但因为能大幅度下降程序运行效率,不推荐使用

复制代码
//只给出要修改的代码,其他代码与上同
        class Bank {
            
            private int account = 100;
            //须要声明这个锁
            private Lock lock = new ReentrantLock();
            public int getAccount() {
                return account;
            }
            //这里再也不须要synchronized 
            public void save(int money) {
                lock.lock();
                try{
                    account += money;
                }finally{
                    lock.unlock();
                }
                
            }
        } 
复制代码

5、线程通讯

一、借助于Object类的wait()、notify()和notifyAll()实现通讯

     线程执行wait()后,就放弃了运行资格,处于冻结状态;

     线程运行时,内存中会创建一个线程池,冻结状态的线程都存在于线程池中,notify()执行时唤醒的也是线程池中的线程,线程池中有多个线程时唤醒第一个被冻结的线程。
      notifyall(), 唤醒线程池中全部线程。
注: (1) wait(), notify(),notifyall()都用在同步里面,由于这3个函数是对持有锁的线程进行操做,而只有同步才有锁,因此要使用在同步中;
       (2) wait(),notify(),notifyall(),  在使用时必须标识它们所操做的线程持有的锁,由于等待和唤醒必须是同一锁下的线程;而锁能够是任意对象,因此这3个方法都是Object类中的方法。

单个消费者生产者例子以下:

复制代码
 class Resource{  //生产者和消费者都要操做的资源  
    private String name;  
    private int count=1;  
    private boolean flag=false;  
    public synchronized void set(String name){  
        if(flag)  
            try{wait();}catch(Exception e){}  
        this.name=name+"---"+count++;  
        System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);  
        flag=true;  
        this.notify();  
    }  
    public synchronized void out(){  
        if(!flag)  
            try{wait();}catch(Exception e){}  
        System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);  
        flag=false;  
        this.notify();  
    }  
}  
class Producer implements Runnable{  
    private Resource res;  
    Producer(Resource res){  
        this.res=res;  
    }  
    public void run(){  
        while(true){  
            res.set("商品");  
        }  
    }  
}  
class Consumer implements Runnable{  
    private Resource res;  
    Consumer(Resource res){  
        this.res=res;  
    }  
    public void run(){  
        while(true){  
            res.out();  
        }  
    }  
}  
public class ProducerConsumerDemo{  
    public static void main(String[] args){  
        Resource r=new Resource();  
        Producer pro=new Producer(r);  
        Consumer con=new Consumer(r);  
        Thread t1=new Thread(pro);  
        Thread t2=new Thread(con);  
        t1.start();  
        t2.start();  
    }  
}//运行结果正常,生产者生产一个商品,紧接着消费者消费一个商品。
复制代码

      可是若是有多个生产者和多个消费者,上面的代码是有问题,好比2个生产者,2个消费者,运行结果就可能出现生产的1个商品生产了一次而被消费了2次,或者连续生产2个商品而只有1个被消费,这是由于此时共有4个线程在操做Resource对象r,  而notify()唤醒的是线程池中第1个wait()的线程,因此生产者执行notify()时,唤醒的线程有多是另1个生产者线程,这个生产者线程从wait()中醒来后不会再判断flag,而是直接向下运行打印出一个新的商品,这样就出现了连续生产2个商品。
为了不这种状况,修改代码以下:

复制代码
    class Resource{  
        private String name;  
        private int count=1;  
        private boolean flag=false;  
        public synchronized void set(String name){  
            while(flag) /*原先是if,如今改为while,这样生产者线程从冻结状态醒来时,还会再判断flag.*/  
                try{wait();}catch(Exception e){}  
            this.name=name+"---"+count++;  
            System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);  
            flag=true;  
            this.notifyAll();/*原先是notity(), 如今改为notifyAll(),这样生产者线程生产完一个商品后能够将等待中的消费者线程唤醒,不然只将上面改为while后,可能出现全部生产者和消费者都在wait()的状况。*/  
        }  
        public synchronized void out(){  
            while(!flag) /*原先是if,如今改为while,这样消费者线程从冻结状态醒来时,还会再判断flag.*/  
                try{wait();}catch(Exception e){}  
            System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);  
            flag=false;  
            this.notifyAll(); /*原先是notity(), 如今改为notifyAll(),这样消费者线程消费完一个商品后能够将等待中的生产者线程唤醒,不然只将上面改为while后,可能出现全部生产者和消费者都在wait()的状况。*/  
        }  
    }  
    public class ProducerConsumerDemo{  
        public static void main(String[] args){  
            Resource r=new Resource();  
            Producer pro=new Producer(r);  
            Consumer con=new Consumer(r);  
            Thread t1=new Thread(pro);  
            Thread t2=new Thread(con);  
            Thread t3=new Thread(pro);  
            Thread t4=new Thread(con);  
            t1.start();  
            t2.start();  
            t3.start();  
            t4.start();  
        }  
    }  
复制代码

二、使用Condition控制线程通讯

      jdk1.5中,提供了多线程的升级解决方案为:

     (1)将同步synchronized替换为显式的Lock操做;

     (2)将Object类中的wait(), notify(),notifyAll()替换成了Condition对象,该对象能够经过Lock锁对象获取;

     (3)一个Lock对象上能够绑定多个Condition对象,这样实现了本方线程只唤醒对方线程,而jdk1.5以前,一个同步只能有一个锁,不一样的同步只能用锁来区分,且锁嵌套时容易死锁。

复制代码
    class Resource{  
        private String name;  
        private int count=1;  
        private boolean flag=false;  
        private Lock lock = new ReentrantLock();/*Lock是一个接口,ReentrantLock是该接口的一个直接子类。*/  
        private Condition condition_pro=lock.newCondition(); /*建立表明生产者方面的Condition对象*/  
        private Condition condition_con=lock.newCondition(); /*使用同一个锁,建立表明消费者方面的Condition对象*/  
          
        public void set(String name){  
            lock.lock();//锁住此语句与lock.unlock()之间的代码  
            try{  
                while(flag)  
                    condition_pro.await(); //生产者线程在conndition_pro对象上等待  
                this.name=name+"---"+count++;  
                System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);  
                flag=true;  
                 condition_con.signalAll();  
            }  
            finally{  
                lock.unlock(); //unlock()要放在finally块中。  
            }  
        }  
        public void out(){  
            lock.lock(); //锁住此语句与lock.unlock()之间的代码  
            try{  
                while(!flag)  
                    condition_con.await(); //消费者线程在conndition_con对象上等待  
            System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);  
            flag=false;  
            condition_pro.signqlAll(); /*唤醒全部在condition_pro对象下等待的线程,也就是唤醒全部生产者线程*/  
            }  
            finally{  
                lock.unlock();  
            }  
        }  
    }  
复制代码

三、使用阻塞队列(BlockingQueue)控制线程通讯

       BlockingQueue是一个接口,也是Queue的子接口。BlockingQueue具备一个特征:当生产者线程试图向BlockingQueue中放入元素时,若是该队列已满,则线程被阻塞;但消费者线程试图从BlockingQueue中取出元素时,若是队列已空,则该线程阻塞。程序的两个线程经过交替向BlockingQueue中放入元素、取出元素,便可很好地控制线程的通讯。

BlockingQueue提供以下两个支持阻塞的方法:

  (1)put(E e):尝试把Eu元素放如BlockingQueue中,若是该队列的元素已满,则阻塞该线程。

  (2)take():尝试从BlockingQueue的头部取出元素,若是该队列的元素已空,则阻塞该线程。

BlockingQueue继承了Queue接口,固然也可使用Queue接口中的方法,这些方法概括起来能够分为以下三组:

  (1)在队列尾部插入元素,包括add(E e)、offer(E e)、put(E e)方法,当该队列已满时,这三个方法分别会抛出异常、返回false、阻塞队列。

  (2)在队列头部删除并返回删除的元素。包括remove()、poll()、和take()方法,当该队列已空时,这三个方法分别会抛出异常、返回false、阻塞队列。

  (3)在队列头部取出但不删除元素。包括element()和peek()方法,当队列已空时,这两个方法分别抛出异常、返回false。

BlockingQueue接口包含以下5个实现类:

 

复制代码
ArrayBlockingQueue :基于数组实现的BlockingQueue队列。

LinkedBlockingQueue:基于链表实现的BlockingQueue队列。

PriorityBlockingQueue:它并非保准的阻塞队列,该队列调用remove()、poll()、take()等方法提取出元素时,并非取出队列中存在时间最长的元素,而是队列中最小的元素。
它判断元素的大小便可根据元素(实现Comparable接口)的自己大小来天然排序,也可以使用Comparator进行定制排序。 SynchronousQueue:同步队列。对该队列的存、取操做必须交替进行。 DelayQueue:它是一个特殊的BlockingQueue,底层基于PriorityBlockingQueue实现,不过,DelayQueue要求集合元素都实现Delay接口(该接口里只有一个long getDelay()方法),
DelayQueue根据集合元素的getDalay()方法的返回值进行排序。
复制代码

copy的一个示例:

复制代码
 1 import java.util.concurrent.ArrayBlockingQueue;
 2 import java.util.concurrent.BlockingQueue;
 3 public class BlockingQueueTest{
 4     public static void main(String[] args)throws Exception{
 5         //建立一个容量为1的BlockingQueue
 6         
 7         BlockingQueue<String> b=new ArrayBlockingQueue<>(1);
 8         //启动3个生产者线程
 9         new Producer(b).start();
10         new Producer(b).start();
11         new Producer(b).start();
12         //启动一个消费者线程
13         new Consumer(b).start();
14         
15     }
16 }
17 class Producer extends Thread{
18     private BlockingQueue<String> b;
19     
20     public Producer(BlockingQueue<String> b){
21         this.b=b;
22         
23     }
24     public synchronized void run(){
25         String [] str=new String[]{
26             "java",
27             "struts",
28             "Spring"
29         };
30         for(int i=0;i<9999999;i++){
31             System.out.println(getName()+"生产者准备生产集合元素!");
32             try{
33             
34                 b.put(str[i%3]);
35                 sleep(1000);
36                 //尝试放入元素,若是队列已满,则线程被阻塞
37                 
38             }catch(Exception e){System.out.println(e);}
39             System.out.println(getName()+"生产完成:"+b);
40         }
41         
42     }
43 }
44 class Consumer extends Thread{
45     private BlockingQueue<String> b;
46     public Consumer(BlockingQueue<String> b){
47         this.b=b;
48     }
49     public  synchronized  void run(){
50     
51         while(true){
52             System.out.println(getName()+"消费者准备消费集合元素!");
53             try{
54                 sleep(1000);
55                 //尝试取出元素,若是队列已空,则线程被阻塞
56                 b.take();
57             }catch(Exception e){System.out.println(e);}
58             System.out.println(getName()+"消费完:"+b);
59         }
60     
61     }
62 }
复制代码

6、线程池

   合理利用线程池可以带来三个好处。

  1. 下降资源消耗。经过重复利用已建立的线程下降线程建立和销毁形成的消耗。
  2. 提升响应速度。当任务到达时,任务能够不须要等到线程建立就能当即执行。
  3. 提升线程的可管理性。线程是稀缺资源,若是无限制的建立,不只会消耗系统资源,还会下降系统的稳定性,使用线程池能够进行统一的分配,调优和监控。

一、使用Executors工厂类产生线程池

      Executor线程池框架的最大优势是把任务的提交和执行解耦。客户端将要执行的任务封装成Task,而后提交便可。而Task如何执行客户端则是透明的。具体点讲,提交一个Callable对象给ExecutorService(如最经常使用的线程池ThreadPoolExecutor),将获得一个Future对象,调用Future对象的get方法等待执行结果。线程池实现原理类结构图以下:

               

      上图中涉及到的线程池内部实现原理的全部类,不利于咱们理解线程池如何使用。咱们先从客户端的角度出发,看看客户端使用线程池所涉及到的类结构图:

                               

    由上图可知,ExecutorServiceJava中对线程池定义的一个接口,它java.util.concurrent包中。  Java API对ExecutorService接口的实现有两个,因此这两个便是Java线程池具体实现类以下:

 ThreadPoolExecutor
 ScheduledThreadPoolExecutor

    除此以外,ExecutorService还继承了Executor接口(注意区分Executor接口和Executors工厂类),这个接口只有一个execute()方法,最后咱们看一下整个继承树:

    使用Executors执行多线程任务的步骤以下:

  • 调用Executors类的静态工厂方法建立一个ExecutorService对象,该对象表明一个线程池;

  • 建立Runnable实现类或Callable实现类的实例,做为线程执行任务;

  • 调用ExecutorService对象的submit()方法来提交Runnable实例或Callable实例;

  • 当不想提交任务时,调用ExecutorService对象的shutdown()方法来关闭线程池。                                                                 

(1)使用Executors的静态工厂类建立线程池的方法以下:

一、newFixedThreadPool() : 
     做用:该方法返回一个固定线程数量的线程池,该线程池中的线程数量始终不变,即不会再建立新的线程,也不会销毁已经建立好的线程,自始自终都是那几个固定的线程在工做,因此该线程池能够控制线程的最大并发数。 
栗子:假若有一个新任务提交时,线程池中若是有空闲的线程则当即使用空闲线程来处理任务,若是没有,则会把这个新任务存在一个任务队列中,一旦有线程空闲了,则按FIFO方式处理任务队列中的任务。
二、newCachedThreadPool() : 
     做用:该方法返回一个能够根据实际状况调整线程池中线程的数量的线程池。即该线程池中的线程数量不肯定,是根据实际状况动态调整的。 
栗子:假如该线程池中的全部线程都正在工做,而此时有新任务提交,那么将会建立新的线程去处理该任务,而此时假如以前有一些线程完成了任务,如今又有新任务提交,那么将不会建立新线程去处理,而是复用空闲的线程去处理新任务。那么此时有人有疑问了,那这样来讲该线程池的线程岂不是会越集越多?其实并不会,由于线程池中的线程都有一个“保持活动时间”的参数,经过配置它,若是线程池中的空闲线程的空闲时间超过该“保存活动时间”则马上中止该线程,而该线程池默认的“保持活动时间”为60s。
三、newSingleThreadExecutor() : 
     做用:该方法返回一个只有一个线程的线程池,即每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待这一个线程空闲,当这个线程空闲了再按FIFO方式顺序执行任务队列中的任务。
四、newScheduledThreadPool() : 
     做用:该方法返回一个能够控制线程池内线程定时或周期性执行某任务的线程池。
五、newSingleThreadScheduledExecutor() : 
     做用:该方法返回一个能够控制线程池内线程定时或周期性执行某任务的线程池。只不过和上面的区别是该线程池大小为1,而上面的能够指定线程池的大小。

注:Executors只是一个工厂类,它全部的方法返回的都是ThreadPoolExecutorScheduledThreadPoolExecutor这两个类的实例。

(2) ExecutorService有以下几个执行方法:

- execute(Runnable)
- submit(Runnable)
- submit(Callable)
- invokeAny(...)
- invokeAll(...)

execute(Runnable)

     这个方法接收一个Runnable实例,而且异步的执行,请看下面的实例:

复制代码
ExecutorService executorService = Executors.newSingleThreadExecutor();

executorService.execute(new Runnable() {
public void run() {
    System.out.println("Asynchronous task");
}
});

executorService.shutdown();
复制代码

submit(Runnable)

   submit(Runnable)execute(Runnable)区别是前者能够返回一个Future对象,经过返回的Future对象,咱们能够检查提交的任务是否执行完毕,请看下面执行的例子:

复制代码
Future future = executorService.submit(new Runnable() {
public void run() {
    System.out.println("Asynchronous task");
}
});

future.get();  //returns null if the task has finished correctly.
复制代码

注:若是任务执行完成,future.get()方法会返回一个null。注意,future.get()方法会产生阻塞。

submit(Callable)

   submit(Callable)submit(Runnable)相似,也会返回一个Future对象,可是除此以外,submit(Callable)接收的是一个Callable的实现,Callable接口中的call()方法有一个返回值,能够返回任务的执行结果,而Runnable接口中的run()方法是void的,没有返回值。请看下面实例:

复制代码
Future future = executorService.submit(new Callable(){
public Object call() throws Exception {
    System.out.println("Asynchronous Callable");
    return "Callable Result";
}
});

System.out.println("future.get() = " + future.get());
复制代码

    若是任务执行完成,future.get()方法会返回Callable任务的执行结果。另外,future.get()方法会产生阻塞。

invokeAny(…)

invokeAny(...)方法接收的是一个Callable的集合,执行这个方法不会返回Future,可是会返回全部Callable任务中其中一个任务的执行结果。这个方法也没法保证返回的是哪一个任务的执行结果,反正是其中的某一个。请看下面实例:

复制代码
ExecutorService executorService = Executors.newSingleThreadExecutor();

Set<Callable<String>> callables = new HashSet<Callable<String>>();

callables.add(new Callable<String>() {
public String call() throws Exception {
    return "Task 1";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
    return "Task 2";
}
});
callables.add(new Callable<String>() {
    public String call() throws Exception {
    return "Task 3";
}
});
String result = executorService.invokeAny(callables);
System.out.println("result = " + result);
executorService.shutdown();
复制代码

     你们能够尝试执行上面代码,每次执行都会返回一个结果,而且返回的结果是变化的,可能会返回“Task2”也但是“Task1”或者其它。

invokeAll(…)

   invokeAll(...)与 invokeAny(...)相似也是接收一个Callable集合,可是前者执行以后会返回一个Future的List,其中对应着每一个Callable任务执行后的Future对象。状况下面这个实例:

   

复制代码
ExecutorService executorService = Executors.newSingleThreadExecutor();

Set<Callable<String>> callables = new HashSet<Callable<String>>();

callables.add(new Callable<String>() {
public String call() throws Exception {
    return "Task 1";
}
});
callables.add(new Callable<String>() {
    public String call() throws Exception {
    return "Task 2";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
    return "Task 3";
}
});

List<Future<String>> futures = executorService.invokeAll(callables);
for(Future<String> future : futures){
    System.out.println("future.get = " + future.get());
}
executorService.shutdown();
复制代码

(3) ExecutorService关闭方法

     当咱们使用完成ExecutorService以后应该关闭它,不然它里面的线程会一直处于运行状态。举个例子,若是的应用程序是经过main()方法启动的,在这个main()退出以后,若是应用程序中的ExecutorService没有关闭,这个应用将一直运行。之因此会出现这种状况,是由于ExecutorService中运行的线程会阻止JVM关闭。

     要关闭ExecutorService中执行的线程,咱们能够调用ExecutorService.shutdown()方法。在调用shutdown()方法以后,ExecutorService不会当即关闭,可是它再也不接收新的任务,直到当前全部线程执行完成才会关闭,全部在shutdown()执行以前提交的任务都会被执行。

    若是想当即关闭ExecutorService,咱们能够调用ExecutorService.shutdownNow()方法。这个动做将跳过全部正在执行的任务和被提交尚未执行的任务。可是它并不对正在执行的任务作任何保证,有可能它们都会中止,也有可能执行完成。

二、使用Java8加强的ForkJoinPool产生线程池

      在Java 8中,引入了自动并行化的概念。它可以让一部分Java代码自动地以并行的方式执行,前提是使用了ForkJoinPool。

     ForkJoinPool同ThreadPoolExecutor同样,也实现了Executor和ExecutorService接口。它使用了一个无限队列来保存须要执行的任务,而线程的数量则是经过构造函数传入,若是没有向构造函数中传入但愿的线程数量,那么当前计算机可用的CPU数量会被设置为线程数量做为默认值。

      ForkJoinPool主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题。典型的应用好比快速排序算法。这里的要点在于,ForkJoinPool须要使用相对少的线程来处理大量的任务。好比要对1000万个数据进行排序,那么会将这个任务分割成两个500万的排序任务和一个针对这两组500万数据的合并任务。以此类推,对于500万的数据也会作出一样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,中止这样的分割处理。好比,当元素的数量小于10时,会中止分割,转而使用插入排序对它们进行排序。那么到最后,全部的任务加起来会有大概2000000+个。问题的关键在于,对于一个任务而言,只有当它全部的子任务完成以后,它才可以被执行。因此当使用ThreadPoolExecutor时,使用分治法会存在问题,由于ThreadPoolExecutor中的线程没法像任务队列中再添加一个任务而且在等待该任务完成以后再继续执行。而使用ForkJoinPool时,就可以让其中的线程建立新的任务,并挂起当前的任务,此时线程就可以从队列中选择子任务执行。好比,咱们须要统计一个double数组中小于0.5的元素的个数,那么可使用ForkJoinPool进行实现以下:

复制代码
public class ForkJoinTest {
    private double[] d;
    private class ForkJoinTask extends RecursiveTask {
        private int first;
        private int last;
        public ForkJoinTask(int first, int last) {
            this.first = first;
            this.last = last;
        }
        protected Integer compute() {
            int subCount;
            if (last - first < 10) {
                subCount = 0;
                for (int i = first; i <= last; i++) {
                    if (d[i] < 0.5){
                        subCount++;
} } }else { int mid = (first + last) /2; ForkJoinTask left = new ForkJoinTask(first, mid); left.fork(); ForkJoinTask right = new ForkJoinTask(mid + 1, last); right.fork(); subCount = left.join(); subCount += right.join(); } return subCount; } } public static void main(String[] args) { ForkJoinPool pool=new ForkJoinPool(); pool.submit(new ForkJoinTask(0, 9999999));
pool.awaitTermination(2,TimeUnit.SECONDS); System.out.println("Found " + n + " values"); } }
复制代码

     以上的关键是fork()和join()方法。在ForkJoinPool使用的线程中,会使用一个内部队列来对须要执行的任务以及子任务进行操做来保证它们的执行顺序。

注:使用ThreadPoolExecutor和ForkJoinPool的性能差别:

  (1)首先,使用ForkJoinPool可以使用数量有限的线程来完成很是多的具备父子关系的任务,好比使用4个线程来完成超过200万个任务。可是,使用ThreadPoolExecutor时,是不可能完成的,由于ThreadPoolExecutor中的Thread没法选择优先执行子任务,须要完成200万个具备父子关系的任务时,也须要200万个线程,显然这是不可行的。

  (2)ForkJoinPool可以实现工做窃取(Work Stealing),在该线程池的每一个线程中会维护一个队列来存放须要被执行的任务。当线程自身队列中的任务都执行完毕后,它会从别的线程中拿到未被执行的任务并帮助它执行。所以,提升了线程的利用率,从而提升了总体性能。

  (3)对于ForkJoinPool,还有一个因素会影响它的性能,就是中止进行任务分割的那个阈值。好比在以前的快速排序中,当剩下的元素数量小于10的时候,就会中止子任务的建立。

结论:

  1. 当须要处理递归分治算法时,考虑使用ForkJoinPool;
  2. 仔细设置再也不进行任务划分的阈值,这个阈值对性能有影响;
  3. Java 8中的一些特性会使用到ForkJoinPool中的通用线程池。在某些场合下,须要调整该线程池的默认的线程数量。

7、死锁

   产生死锁的四个必要条件以下。当下边的四个条件都知足时即产生死锁,即任意一个条件不知足既不会产生死锁。

 (1)死锁的四个必要条件

 

  • 互斥条件:资源不能被共享,只能被同一个进程使用
  • 请求与保持条件:已经获得资源的进程能够申请新的资源
  • 非剥夺条件:已经分配的资源不能从相应的进程中被强制剥夺
  • 循环等待条件:系统中若干进程组成环路,该环路中每一个进程都在等待相邻进程占用的资源

 

      举个常见的死锁例子:进程A中包含资源A,进程B中包含资源B,A的下一步须要资源B,B的下一步须要资源A,因此它们就互相等待对方占有的资源释放,因此也就产生了一个循环等待死锁。

 

  (2)处理死锁的方法

 

  • 忽略该问题,也即鸵鸟算法。当发生了什么问题时,无论他,直接跳过,无视它;
  • 检测死锁并恢复;
  • 资源进行动态分配;
  • 破除上面的四种死锁条件之一。

  8、线程相关类

 

(1)ThreadLocal

 

      ThreadLocal它并非一个线程,而是一个能够在每一个线程中存储数据的数据存储类,经过它能够在指定的线程中存储数据,数据存储以后,只有在指定线程中能够获取到存储的数据,对于其余线程来讲则没法获取到该线程的数据。 即多个线程经过同一个ThreadLocal获取到的东西是不同的,就算有的时候出现的结果是同样的(偶然性,两个线程里分别存了两份相同的东西),但他们获取的本质是不一样的。使用这个工具类能够简化多线程编程时的并发访问,很简洁的隔离多线程程序的竞争资源。

     对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不一样的线程排队访问,然后者为每个线程都提供了一份变量,所以能够同时访问而互不影响。ThreadLocal类提供了以下的三个public方法:

ThreadLocal()
          建立一个线程本地变量。
T get()
          返回此线程局部变量的当前线程副本中的值,若是这是线程第一次调用该方法,则建立并初始化此副本。
protected  T initialValue()
          返回此线程局部变量的当前线程的初始值。

     下面经过系统源码来分析出现这个结果的缘由。 在ThreadLocal中存在着两个很重要的方法,get()和set()方法,一个读取一个设置。

复制代码
    /**
    * Returns the value of this variable for the current thread. If an entry
    * doesn't yet exist for this variable on this thread, this method will
    * create an entry, populating the value with the result of
    * {@link #initialValue()}.
    *
    * @return the current value of the variable for the calling thread.
    */
    @SuppressWarnings("unchecked")
    public T get() {
       // Optimized for the fast path.
       Thread currentThread = Thread.currentThread();
       Values values = values(currentThread);
       if (values != null) {
            Object[] table = values.table;
            int index = hash & values.mask;
            if (this.reference == table[index]) {
              return (T) table[index + 1];
             }
        } else {
        values = initializeValues(currentThread);
      }
      return (T) values.getAfterMiss(this);
    }
    /**
    * Sets the value of this variable for the current thread. If set to
    * {@code null}, the value will be set to null and the underlying entry will
    * still be present.
    *
    * @param value the new value of the variable for the caller thread.
    */
    public void set(T value) {
         Thread currentThread = Thread.currentThread();
       Values values = values(currentThread);
       if (values == null) {
          values = initializeValues(currentThread);
       }
       values.put(this, value);
    }
复制代码

     从注释上能够看出,get方法会返回一个当前线程的变量值,若是数组不存在就会建立一个新的。另外,对于“当前线程”和“数组”,数组对于每一个线程来讲都是不一样的 values.table。而values是经过当前线程获取到的一个Values对象,所以这个数组是每一个线程惟一的,不能共用,而下面的几句话也更直接了,获取一个索引,再返回经过这个索引找到数组中对应的值。这也就解释了为何多个线程经过同一个ThreadLocal返回的是不一样的东西。

     Java中为何要这么设置呢?

  • ThreadLocal在平常开发中使用到的地方较少,可是在某些特殊的场景下,经过ThreadLocal能够轻松实现一些看起来很复杂的功能。通常来讲,当某些数据是以线程为做用域而且不一样线程具备不一样的数据副本的时候,就能够考虑使用ThreadLocal。例如在Handler和Looper中。对于Handler来讲,它须要获取当前线程的Looper,很显然Looper的做用域就是线程而且不一样的线程具备不一样的Looper,这个时候经过ThreadLocal就能够轻松的实现Looper在线程中的存取。若是不采用ThreadLocal,那么系统就必须提供一个全局的哈希表供Handler查找指定的Looper,这样就比较麻烦了,还须要一个管理类。
  • ThreadLocal的另外一个使用场景是复杂逻辑下的对象传递,好比监听器的传递,有些时候一个线程中的任务过于复杂,就可能表现为函数调用栈比较深以及代码入口的多样性,这种状况下,咱们又须要监听器可以贯穿整个线程的执行过程。这个时候就可使用到ThreadLocal,经过ThreadLocal可让监听器做为线程内的全局对象存在,在线程内经过get方法就能够获取到监听器。若是不采用的话,可使用参数传递,可是这种方式在设计上不是特别好,当调用栈很深的时候,经过参数来传递监听器这个设计太糟糕。而另一种方式就是使用static静态变量的方式,可是这种方式存在必定的局限性,拓展性并非特别的强。好比有10个线程在执行,就须要提供10个监听器对象。

 

注:ThreadLocal和其余全部的同步机制同样,都是为了解决多线程中对于同一变量的访问冲突。值普通的同步机制中,经过对象加锁来实现多线程对同一变量的安全访问,且该变量是多线程共享的,全部须要使用这种同步机制来明确分开是在何时对变量进行读写,在何时须要锁定该对象。此种状况下,系统并无将这个资源复制多份,而是采起安全机制来控制访问而已。ThreadLocal只是从另外一个角度解决多线程的并发访问,即将须要并发访问的资源复制多份,每一个线程拥有一份资源,每一个线程都有本身的资源副本。

总结:若多个线程之间须要共享资源,以达到线程间的通讯时,就使用同步机制;若仅仅须要隔离多线程之间的关系资源,则可使用ThreadLocal。

 

原文:https://www.cnblogs.com/snow-flower/p/6114765.html

相关文章
相关标签/搜索