java多線程的一點整理

     多線程是提升效率性能的一個重要技術,企業級開發中運用的很普遍,我在學習了一段時間以後,仍是有不少地方是很懵比的,後續的會繼續學習更新。html

    1.  多線程簡單的概述java

    進程:正在進行的程序,好比任務管理器上的進程,QQ等等編程

    線程:就是進程中負責執行程序的控制單元(或者執行路徑),一個進程中有多個執行路徑稱之爲多線程安全

 開啓多線程是爲了同時運行多部分代碼,每一個線程都有本身的運行內容,這個內容稱之爲線程的任務。多線程

 多線程的好處:解決了多程序同時運行的問題。多線程的弊端:線程太多回到效率下降。併發

其實應用程序的執行是CPU在作着快速切換完成的,這個切換是隨機的。dom

 

下面是多線程的幾種狀態ide

jdk 1.5以前的 多線程兩種建立方式就很少說了。函數

1.extend Thread ,覆寫run方法(run 方法就是描述的線程任務),將該類new一個對象就是建立了一個線程對象,用start()方法開啓線程性能

2.實現Runnable接口,覆寫run()方法,方法裏面也是描述線程任務,new一個Thread線程對象,將任務構造的時候傳遞進去,用start()方法開啓線程

注意:實現Runnable接口的好處

1.將線程的任務從線程的子類中分離出來,進行的單獨的封裝,按照面向對象的思想將任務進行了封裝

2.避免了java單繼承的侷限性。

2.0 多線程安全問題

public class ticket implements Runnable{
    private  int num=500;

    public void sale(){
        while (true){
            synchronized(this) {
                if (num>0){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"...sale=="+num--);
                }else {
                    break;
                }
            }


        }
    }


    @Override
    public void run() {
        sale();
    }
}
public static void main(String[] args){
  
         //屢次啓動一個線程是非法的。
         ticket t1= new ticket();   
         Thread thread1= new Thread(t1);
         Thread thread2= new Thread(t1);
         Thread thread3= new Thread(t1);
         Thread thread4= new Thread(t1);
         thread1.start();
         thread2.start();
         thread3.start();
         thread4.start();

     }

這段代碼很簡單,4 個線程操做同一個ticket對象,num是一個全局變量,每一個線程進來休眠10ms後都減1,直到爲0,能夠觀察到裏面加了synchronized關鍵字(同步),若是不加synchronized,將會出現線程不安全的因素。

附:屢次啓動一個線程對象是非法的!

打印的結果是

分析:出現這種緣由就是0線程進來後,此時num爲1,判斷(num>0)以後,線程休眠了,cpu釋放執行權(由於cpu執行那個線程是隨機切換的),1線程進來以後也出現這種狀況num=1的時候,cpu釋放執行權,同理其餘的2個線程也發生這種狀況,致使出現數據不一樣步的狀況,要解決這種就要加鎖

線程安全問題產生的緣由:

1.多個線程操做共享數據

2.操做共享數據的代碼有多條

用鎖去解決線程安全的問題,synchronized(鎖對象){須要被同步的代碼},this表示當前線程任務對象

同步的好處: 解決了線程的安全問題

同步的弊端:相對下降了效率,由於同步外的線程都會判斷同步鎖。

同步的前提:同步必須有多個線程並使用同一個鎖。

同步函數的鎖對象是當前固定的this,同步代碼塊的鎖是任意的對象。

靜態同步函數鎖的對象是該函數所屬的字節碼文件對象,能夠用this.getClass()或者類名.class表示

死鎖的狀況:同步鎖的的嵌套,死鎖可能會出現和諧的狀況。

2.1 單例模式下多線程的安全問題

/**
    * 懶漢式
    * */
    public static class  Single{
        private static  Single s= null;

        private Single(){

        }

        public static Single getInstance(){
          

            /*
            * 加鎖是爲了解決線程的安全的問題,鎖以前加個判斷 是由於線程進來判斷鎖,比較消耗效率(解決多線程下單例的效率問題)
            * */
            if (s==null){
                synchronized (Single.class){
             //多線程訪問須要同步,例如a,b兩個線程,a線程執行到s==null時失去了cpu的執行權,
            // 而後b線程同理執行到s==null失去cpu的執行權,這個就不是單例模式了。
            //懶漢式在多線程狀況下,操做s的共享數據有多條代碼,所以存在線程不安全的狀況,須要同步操做s共享數據的多條代碼
                    if (s==null){
                        s= new Single();
                    }
                }
            }


            return s;
        }

    }

上述懶漢式在多線程狀況下就不是單例模擬了。

2.3 多線程之間的通訊 wait和notify機制(生成者-消費者模式)

先看一下簡單的例子,2個線程,一個輸入線程負責讀,一個輸出線程負責寫

/**
 * Created by ZC-16-012 on 2018/2/2.
 *
 * 定義多線程的資源共享數據對象
 */
public class Resource {
    private String  name;

    private String sex;

    private boolean flag=false;

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    //輸入線程set方法
    public synchronized  void set(String name,String sex){
        if (flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            this.name= name;
            this.sex= sex;
            flag=true;
            this.notify();
        }
    }

    //輸出線程
    public synchronized void out(){
        if (!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            System.out.println(this.getName()+"===="+this.getSex());
            this.setFlag(false);
            this.notify();
        }
    }
}
public class InputTask implements Runnable{
    Resource r;

    public InputTask(Resource r){
        this.r = r;
    }

    @Override
    public void run() {
        int x=0;

        while (true){
            /**
            * 分析:因爲資源Resource是共享數據,當輸入線程獲取的cpu執行權時,第一次賦值name是weiAnKang,sex是男,
             *     第二次輸入線程再次賦值name 是'麗麗'時,因爲線程之間是cpu執行權的切換,此時輸出線程獲取cpu的執行權,
             *     輸出線程的結果就是'麗麗====男',這樣致使多線程操做同一個資源時出現的安全問題
            * */
            //todo:此時在輸入線程加上同步鎖依然沒有解決,Resource共享數據同步的問題,出現這種問題應該考慮多個線程之間是否是判斷同一個鎖
             
            
            if (x==0){
                r.set("weiAnKang","nan");

            }else {
                r.set("麗麗","女女女");

            }

            x=(x+1)%2;
        }

    }
}
/**
 * Created by ZC-16-012 on 2018/2/2.
 *
 * 定義輸出線程的任務
 */
public class OutputTask implements Runnable{

    Resource r;

    public OutputTask(Resource r){
        this.r=r;
    }

    @Override
    public void run() {
        while (true){        
            r.out();

        }
    }
}
/**
 * Created by ZC-16-012 on 2018/2/2.
 *
 * 線程之間的通訊
 *
 * 等待/喚醒機制
 * wait(): 讓線程處於凍結狀態,被wait的線程會被存儲到線程池中
 * notify():喚醒線程池中的一個線程(任意的)
 * notifyAll():喚醒線程池中的全部線程
 * 這些方法必須定義在同步中,由於這些方法是用於操做線程的狀態的方法,
 * 必需要明確到底操做的是那個鎖上的線程
 *
 */
public class ThreadCommunicationDemo {

    public static void main(String[] args){

        Resource r= new Resource();

        InputTask input= new InputTask(r);
        OutputTask output= new OutputTask(r);

        //建立線程,並將線程的任務傳遞給線程對象
        Thread t1= new Thread(input);
        Thread t2= new Thread(output);

        //開啓線程
        t1.start();
        t2.start();

    }
}

等待/喚醒機制
 * wait(): 讓線程處於凍結狀態,被wait的線程會被存儲到線程池中
 * notify():喚醒線程池中的一個線程(任意的)
 * notifyAll():喚醒線程池中的全部線程
 * 這些方法必須定義在同步中,由於這些方法是用於操做線程的狀態的方法,
 * 必需要明確到底操做的是那個鎖上的線程

2.4 jdk1.5以後的java.util.concurrent.locks.Lock的應用

假若有多個線程進行讀的操做,多個線程進行寫的操做呢

/**
 * Created by ZC-16-012 on 2018/2/5.
 *
 * 生產者和消費者
 * while判斷標記,解決了線程獲取cpu執行權後,是否要執行
 * notifyAll 解決了本方線程必定會喚醒對方線程
 */
public class ResourceClass {

    private String name;

    private int  count=1;

    private boolean flag=false;

    private Lock lock= new ReentrantLock();//使用jdk1.5版本以後的鎖對象

    //經過已有的鎖對象獲取該鎖上的監視器對象,建立兩組監視器,一組監視生產者,一組監視消費者
    Condition producerCon= lock.newCondition();

    Condition consumerCon= lock.newCondition();

    /**
    * 分析:0,1 是生產者線程,2 3是消費者線程,假設0先獲取cpu的執行權,2線程消費,有一種狀況當0,1線程
     *  t0(活),notify以後正好是 喚醒t1 線程,t1判斷flag=true,t0,t1一直在while循環中等待,出現了死鎖狀況
     *  出現這種狀況就是生成者線程喚醒的是另外一個生成者線程,應該生成者要保證喚醒的至少有消費者線程
    * */

    /**
    * 對於synchronized 修飾的同步代碼塊,鎖是隱式的
     * 可使用interrupt()方法將線程從凍結狀態強制恢復到運行狀態中,讓線程具備cpu的執行資格
     * 該強制動做會發生 InterruptedException 異常
     *
    * */
    public  void set(String name) {
        lock.lock();
        try {
            while (flag) {
                try {
//                    this.wait();// t0(活),notify 正好喚醒t1
                    //經過condition的await 讓線程凍結
                    producerCon.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            this.name = name + count;
            count++;
            System.out.println(Thread.currentThread().getName() + "..生產者.." + this.name);
            flag = true;
//            notifyAll();//喚醒了全部線程池中等待的線程
            //經過condition的signalAll 喚醒消費者的全部線程
            consumerCon.signal();
        } finally {
            lock.unlock();//釋放鎖
        }

    }

    public  void out (){
        lock.lock();
        try {
            if (!flag) {
                try {
//                    this.wait();
                    consumerCon.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                System.out.println(Thread.currentThread().getName() + ".......消費者.." + this.name);
                flag = false;
//                notifyAll();
                //消費者喚醒全部生成者的線程
                producerCon.signal();
            }
        } finally {
            lock.unlock();
        }
    }

}
public class Producer implements Runnable{
    ResourceClass r;

    public Producer(ResourceClass r){
        this.r=r;
    }

    @Override
    public void run() {
        while (true){
            r.set("饅頭");
        }
    }
}
public class Consumer implements Runnable{
    ResourceClass r;

    public Consumer(ResourceClass r){
        this.r = r;
    }

    @Override
    public void run() {
        while (true){
            r.out();
        }

    }
}
/**
 * Created by ZC-16-012 on 2018/2/5.
 */
public class ProducerAndConsumer {

    /**
    *  Thread.currentThread().toString() 會打印線程的名稱,優先級,線程組
     *  線程的優先級 MAX_PRIORITY 10,MIN_PRIORITY 1, NORM_PRIORITY 5,數字越高,表示優先級越大,
     *  cpu執行該線程的機率也就越大,例如:t0.setPriority(Thread.MAX_PRIORITY); 將t0線程的優先級設置成最大
    *
    * */

    public static  void  main(String[] args){
        ResourceClass r= new ResourceClass();
        Producer pro= new Producer(r);
        Consumer con= new Consumer(r);
        Thread t0 = new Thread(pro);
        Thread t1 = new Thread(pro);
        Thread t2 = new Thread(con);
        Thread t3 = new Thread(con);

        t0.start();
        /**
        * try {
                t0.join(); //t0線程要申請加入進來,運行。讓這個臨時線程先執行完
            } catch (InterruptedException e) {
             e.printStackTrace();
           }
        * */
        //t0.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
        /**
        *  t2.setDaemon(true) 設置成守護線程(後臺線程),前臺線程其餘線程結束,這個線程也結束
        * */

        t3.start();



    }
}

以上例子即是多生產者多消費者的例子

3.0 volatile關鍵字

不少時候不會區分volatile和synchronized,先看一下這個例子

public class RunThread extends Thread{
    private  boolean flag = true;

    public  void  setFlag(boolean flag){
        this.flag=flag;
    }

    @Override
    public  void run() {
      while (flag){

      }
      System.out.println(Thread.currentThread().getName() + "...線程運行結束");
    }
    /**
    * 分析:main方法中建立了t0線程,休眠3s後,將t線程中的全局變量設置了false,主線程結束後,卻沒有打印t0線程中的 "...線程運行結束"
    * 說明t0線程一直while循環中運行,對於t0線程而言,flag的值並無改變
     *
     * 總結:在java中,每個線程都有一個內存區,其中存放着全部線程共享的主內存中的變量值的拷貝,當線程執行時,他在本身的工做
     * 內存區中操做這些變量,爲了存取一個共享的變量,一個線程一般先獲取鎖定並去清楚它的內存工做區,把這些變量從全部線程的共享內存區中
     * 正確的裝入到他本身所在的工做內存區中,當線程解鎖時保證該工做內存區中變量的值寫回到共享內存中
     *
     * volatile 關鍵字強制線程到主內存中(共享內存中)去讀取變量,而不是去線程工做內存區中讀取,從而實現了多個線程見的變量可見
    * */
    public static void main(String[] args) throws InterruptedException {
        RunThread t0= new RunThread();
        t0.start();
        Thread.sleep(3000);
        t0.setFlag(false);
        System.out.println("flag的值已經被設置成了false。。");
        Thread.sleep(1000);
        System.out.println(t0.flag);
    }

}

網上也搜了不少博客,https://www.cnblogs.com/yzwall/p/6661528.html 這遍博客說的很清楚

3.0 Atomic原子類

/**
 * Created by ZC-16-012 on 2018/2/8.
 */
public class VolatileNoAtomic extends Thread{

    @Override
    public void run() {
        addCount();
    }

    /**
    * volatile關鍵字具備可見性,並不具有原子性
     * Atomic原子類 能夠保證結果是原子性,並不能保證方法自己是原子性
    * */

//    private volatile static  int count;

    private static  AtomicInteger count =new AtomicInteger(0);

    private static void addCount(){

            for (int i=0;i<1000;i++){
//                count++;
                count.incrementAndGet();
            }
            System.out.println(count);


    }

    public static void main(String[] args){
        VolatileNoAtomic[] arr= new VolatileNoAtomic[10];
        for (int i=0;i<10;i++){
            arr[i]=new VolatileNoAtomic();
        }
        for (int i=0;i<10;i++){
            arr[i].start();
        }
    }

}

能夠經過這個例子看一下Atomic對象和 volatile關鍵字的一些區別

3.1 網上有一個聯繫題 有4個線程分別獲取C D E F 盤的大小,第5個線程統計總大小

public class ContactThread {

    public static void main(String[] args){
        /**
        * CountDownLatch 是一個同步倒數計數器,構造時傳入int參數,每調用一次countDown()方法
         * 計數器減1
        * */
        final CountDownLatch countDownLatch= new CountDownLatch(4);
        ExecutorService executorService= Executors.newFixedThreadPool(6);
        final DiskMemory diskMemory= new DiskMemory();
        for (int i=0;i<4;i++)
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    int timer = new Random().nextInt(5);
                    try {
                        Thread.sleep(timer * 1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    int diskSize = diskMemory.getSize();
                    System.out.printf("完成磁盤的統計任務,耗時%d秒,磁盤大小爲%d.\n", timer, diskSize);
                    //統計每一個磁盤的大小
                    diskMemory.setSize(diskSize);
                    //任務完成後,計數器減一
                    countDownLatch.countDown();
                    System.out.println("count num=" + countDownLatch.getCount());
                }
            });

        //主線程一直被阻塞,直到全部的線程統計完
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.printf("所有線程統計完,全部磁盤總大小.\n" + ",totalSize = " + diskMemory.getTotalSize());
        executorService.shutdown();
    }
}
/**
 * Created by ZC-16-012 on 2018/2/27.
 *
 * 定義磁盤對象
 */
public class DiskMemory {
    private int totalSize;

    public int getSize(){
        return (new Random().nextInt(3)+1)*100;
    }

    public void setSize(int size){
        totalSize+=size;
    }

    public int getTotalSize(){
        return totalSize;
    }
}

這邊用到了java.util.concurrent.CountDownLatch的一個同步倒數計數器。

固然java併發編程是一個很複雜的技術,遠遠不止於這些,後面學習到會陸續更新,好比java線程池的運用。 

3.2synchronized關鍵字總結

上述synchronized關鍵字經過鎖的機制,保證了多線程之間共享變量的安全。那麼synchronized關鍵字何時釋放鎖?

要麼synchronized裏面的代碼執行完,要麼執行過程當中出現異常,synchronized 都將自動釋放鎖。synchronized 具備可重入性和不可中斷性。

可重入性:指的是同一線程的外層函數獲取到鎖後,內層函數能夠直接再次持有該鎖。

不可中斷性:指的是當前線程一旦持有該鎖,其餘線程只能等待,直到當前線程釋放鎖。

能夠看出來,synchronized關鍵字 經過指令monitorenter,monitorexit等指令進行控制鎖,monitorenter=0的時候說明該線程沒有持有鎖,一旦持有鎖,就經過該計數器加1,進入內層函數也是加1,同理monitorexit 進行減1,執行完計算器等於0.

可見性:因爲Java線程內存模型,每一個線程的線程內存 存儲的是主內存(共享內存)變量的一個副本,可見性保證了當前線程內存存儲的變量副本和主內存一致(讀寫)。

synchronized關鍵字進入鎖以前,先把共享變量從主內存中讀取出來,釋放鎖以前再把共享變量從當前線程內存中寫到主內存中。

synchronized 的缺陷:

1.效率低:鎖釋放的狀況少,試圖得到鎖時不能設置超時,不能中斷一個正在試圖得到鎖的過程

2.不夠靈活:加鎖和釋放鎖的時機單一,好比讀寫鎖,讀的操做不會加鎖,寫的時候加鎖

3.沒法知道是否成功獲取到鎖。還應該避免死鎖

相關文章
相關標籤/搜索