高性能專題

JDK多線程核心原理

多線程原理基礎

java程序運行原理分析

image.png

圖片參考:https://www.processon.com/view/link/5e586f5fe4b069f82a16e8a0

線程狀態

image.png

package com.xbin;

public class ThreadTest {
    private static final Object object=new Object();
    private static final Object object2=new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(new Runnable() {
            public void run() {
                try {
                    Thread.sleep(5000);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        });
        System.out.println("t1 NEW 狀態:"+t1.getState());
        t1.start();
        System.out.println("t1 RUNNABLE 狀態:"+t1.getState());
        Thread.sleep(3000);
        System.out.println("t1 TIMED_WAITING 狀態:"+t1.getState());
        Thread.sleep(3000);
        System.out.println("t1 TERMINATED 狀態:"+t1.getState());
        Thread t2=new Thread(()->{
                synchronized (object){
                        while (true){

                        }
                }
        });

        Thread t3=new Thread(()->{
                synchronized (object){

                }
        });
        t2.start();
        Thread.sleep(200);
        t3.start();
        Thread.sleep(200);
        System.out.println("t3 BLOCKED 狀態"+t3.getState());

        Thread t4=new Thread(()->{
            synchronized (object2){
                try {

                    object2.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread t5=new Thread(()->{
            synchronized (object2){

            }
        });
        t4.start();
        Thread.sleep(200);
        t5.start();
        System.out.println("t4 WAITING 狀態 "+t4.getState());
    }
}

如何正確讓線程中止
  1. Thread.stop() 方法(已過時)
package com.xbin;

public class ThreadStop extends Thread {

    private int i ,j;

    @Override
    public void run() {
        //添加 synchronized 保證原子操作
       synchronized (this){
           i++;
           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           j++;
       }
    }
    public void print(){
        System.out.println("i="+i+",j="+j);
    }
}

package com.xbin;

public class ThreadStop1 {

    public static void main(String[] args) throws InterruptedException {
       ThreadStop threadStop=new ThreadStop();
       threadStop.start();
       Thread.sleep(200);
       //會破壞原子性操作
       threadStop.stop();
//        threadStop.interrupt();
        while (threadStop.isAlive()){

        }
       threadStop.print();

    }
}


運行結果

從運行結果可以看出這已經破壞了操作的原子性

  1. Thread.interrupt()中止線程
package com.xbin;

/**
 *使用 interrupt 中止線程
 */
public class ThreadStop2 {
    public static void main(String[] args) throws InterruptedException {
        ThreadStop threadStop=new ThreadStop();
        threadStop.start();
        Thread.sleep(200);
        //會破壞原子性操作
//        threadStop.stop();
        threadStop.interrupt();
        while (threadStop.isAlive()){

        }
        threadStop.print();

    }
}

運行結果

從結果可以看出這不會破壞操作的原子性

  1. 標誌位中止線程
package com.xbin;

/**
 * 標誌位中止線程
 */
public class ThreadStop3 {
    private static volatile boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {

            try {
                while (flag) {
                    System.out.println("正在運行------");
                    Thread.sleep(300);
                }
                System.out.println("結束運行————————————");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }


        });
        thread.start();
        Thread.sleep(3000);
        flag=false;
    }
}

**小結:**一般都會使用標識中止線程,有利用程序編寫一些事後處理工作。

內存屏障和CPU緩存
  1. 緩存
    L1 Cache 是CPU第一層高速緩存,分爲數據緩存和指令緩存。一般服務器CPU的L1緩存的容量通常在32–4096kb.

    L2 Cache 由於L1級高速緩存容量的限制,爲了再次提高CPU的運算速度,在CPU外部放置一高速存儲器,即二級緩存。

    L3 Cache 現在的都是內置的。而它的實際作用即是。L3緩存的應用可以通過進一步的降低內存延遲,同時提升大數據量計算時處理器的性能。具有較大L3緩存的處理器提供更有效的文件系統緩存行爲及較短消息和處理器隊列長度。一般是多核共享一個L3緩存

  2. 緩存同步協議

在這種高速緩存回寫的場景下,有一個緩存一致性協議多數CPU廠商對它進行了實現。
MESI協議 ,它規定每條緩存有個狀態位,同時定義了下面四個狀態:

修改態 此cache行已被修改過,內存不同於主存,爲此cache專有。
專有態 此cache行內容同於主存,但不出現於其它cache中。
共享態 此cache行內容同於主存,但也出現於其它cache中。
無效態 此cache行內容無效。

多處理器時,單個CPU對緩存中數據進行了改動,需要通知給其他CPU。
CPU處理要控制自己的讀寫操作,還要監聽其他CPU發出的通知,從而保證最終一致。

  1. 運行時指令重排

image.png

指令重排的場景:當CPU寫緩存時發現緩存區塊正被其它CPU佔用,爲了提高CPU處理性能,可能將後面的讀緩存命令優先執行。

並非隨便重排,需要遵守as-if-serial語義
as-if-serial語義的意思指:不管怎麼重排序(編譯器和處理器爲了提高並行度),(單線程)程序的執行結果不能被改變。編譯器,runtime和處理器都必須遵守as-if-serial語義。
編譯器和處理器不會對存在數據依賴關係的操作做重排序。

  1. 緩存和指令重排引起的兩個問題。

    CPU高速緩存問題:緩存中數據和主內存的數據並不是實時同步的,各CPU間緩存的數據也不是實時同步。同一個時間點,各CPU所看到同一內存地址的數據的值可能是不一致的。

    CPU執行指令重排序問題:雖然遵守了as-if-serial語義,單僅在單CPU自己執行的情況下能保證結果正確。
    多核多線程中,指令邏輯無法分辨因果關聯,可能出現亂序執行,導致程序運行結果錯誤。

  2. 內存屏障

處理器提供了兩個內存屏障指令用於解決上述兩個問題。

寫內存屏障;在指令後插入Store Barrier,能讓寫入緩存中的最新數據更新寫入主內存,讓其線程可見。強制寫入主內存,這種顯示調用,CPU就不會因爲性能考慮而去對指令重排。

讀內存屏障(Load Memory Barrier):在指令前插入Load Barrier,可以讓高速緩存中數據失效,強制從新的主內存加載數據。強制讀取主內存內容,讓CPU緩存與主內存保持一致,避免了緩存導致的一致性問題。

線程通信及控制
  1. 文件共享
package com.xbin;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

/**
 * 文件共享
 */
public class FileSharding {
    public static void main(String[] args) {
        Thread write = new Thread(() -> {
            try {
                while (true) {

                    Files.write(Paths.get("log.txt"), ("當前時間" + System.currentTimeMillis()).getBytes());
                    Thread.sleep(1000L);

                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        write.start();

        Thread read = new Thread(() -> {
            try {
                while (true) {
                    Thread.sleep(1000L);
                    byte[] bytes = Files.readAllBytes(Paths.get("log.txt"));
                    System.out.println(new String(bytes));
                }
            } catch (Exception e) {

            }
        });
        read.start();

    }

}

運行結果

  1. 變量共享
package com.xbin;

/**
 * 變量共享
 */
public class VariableSharding {

    private static String context="";

    public static void main(String[] args) {
        Thread write =new Thread(()->{
            try {
                while (true){
                    context="當前時間:"+System.currentTimeMillis();
                    Thread.sleep(1000L);
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        });
        write.start();
        Thread read =new Thread(()->{
            try {
                while (true){
                    Thread.sleep(1000L);

                    System.out.println(context);
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        });
        read.start();

    }
}

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-9LsecR2m-1591524544496)(https://upload-images.jianshu.io/upload_images/22932958-0b7155eac6cc06e0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

  1. 線程協作

JDK中對於需要多線程協作完成某一任務的場景,提供了對應API支持。
多線程協作的典型場景是:生產者,消費者模型。(線程阻塞,線程喚醒)

suspend和resume的使用

package com.xbin;

public class SuspendResumeTest {

    private Object object=null;
    /**
     * 方法的調用順序。(如果先調用resume,在調用suspend就會引起死鎖)
     */
    public void suspendResumeTest03() {
        Thread t1 = new Thread(() -> {
            while (object == null) {
                System.out.println("1.等待包子生產");
                try {
                    Thread.sleep(5000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                Thread.currentThread().suspend();
            }
            System.out.println("3.買到包子了");
        });
        t1.start();
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        object = new Object();
        t1.resume();
        System.out.println("2.生產包子");
    }
    /**
     * 對象鎖操作 --會導致死鎖 --suspend()不會釋放鎖
     */
    public void suspendResumeTest02() {
        Thread t1 = new Thread(() -> {
            while (object == null) {
                synchronized (this) {
                    System.out.println("1.等待包子生產");
                    Thread.currentThread().suspend();
                }
            }
            System.out.println("3.買到包子了");
        });
        t1.start();
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        object = new Object();
        synchronized (this) {
            t1.resume();


        }
        System.out.println("2.生產包子");
    }
    /**
     * suspend 和 resume 正常演示
     */
    public void suspendResumeTest() {
        Thread t1 = new Thread(() -> {
            while (object == null) {
                System.out.println("1.等待包子生產");
                Thread.currentThread().suspend();


            }


            System.out.println("3.買到包子了");
        });
        t1.start();
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        object = new Object();
        t1.resume();
        System.out.println("2.生產包子");
    }

    public static void main(String[] args) {
        SuspendResumeTest suspendResumeTest=new SuspendResumeTest();
//        suspendResumeTest.suspendResumeTest03();
//        suspendResumeTest.suspendResumeTest02();
        suspendResumeTest.suspendResumeTest();
    }
}

wait/notify的使用

wait/notify:方法只能由同一對象鎖的持有者線程調用,也就是寫在同步塊裏面,否則會拋出IllegalMonitorStateException異常。
wait方法會導致當前線程等待,加入該對象的等待集合中,並且放棄當前持有的對象鎖。
notify/notifyAll方法喚醒一個或所有正在等待這個對象鎖的線程。
注意:雖然wait會自動解鎖,但是對順序有要求,如果在notify被調用之後,纔開始wait方法的調用,線程就會永遠處於WAITING狀態。

package com.xbin;

public class WaitNotifyTest {

    private Object object=null;
    /**
     * 雖然wait() 和notify() 會釋放鎖,但是有先後順序
     */
    public void waitNotifyTest02(){
        new Thread(() -> {


            while (object == null) {
                System.out.println("1.等待包子生產");
                try {
                    Thread.sleep(3000L);
                    synchronized (this) {
                        this.wait();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("3.成功買到包子");
        }).start();
        try {
            Thread.sleep(200L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        object=new Object();
        synchronized (this){
            this.notify();
        }
        System.out.println("2.包子生產完成");
    }

    /**
     * 正確地處理方式
     */
    public void watiNotifyTest() {
        new Thread(() -> {


            while (object == null) {
                System.out.println("1.等待包子生產");
                try {
                    synchronized (this) {
                        this.wait();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("3.成功買到包子");
        }).start();
        try {
            Thread.sleep(200L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        object=new Object();
        synchronized (this){
            this.notify();
        }
        System.out.println("2.包子生產完成");
    }

    public static void main(String[] args) {
        WaitNotifyTest waitNotifyTest=new WaitNotifyTest();
        waitNotifyTest.waitNotifyTest02();
//        waitNotifyTest.watiNotifyTest();
    }
}

pack/unpack機制的使用

pack/unpack機制:線程調用park則等待“許可”,unpack方法爲指定線程提供“許可”。
不要求park和unpark方法的調用順序,多次調用unpark之後,再調用park,線程會直接運行,但不會疊加。
也就是說,連續多次調用park方法,第一次會拿到“許可”直接運行,後續調用會進入等待。
park方法不會釋放鎖,也可能會引起死鎖操作。

package com.xbin;

import java.util.concurrent.locks.LockSupport;

public class ParkUnParkTest {
    private Object object=null;

    /**
     * 會產生死鎖 parK() 方法不會釋放鎖
     */
    public void  parkUnpackTest02(){
        Thread t1=new Thread(()->{
            while (object==null){
                System.out.println("1.等待包子生產");
                synchronized (this){
                    LockSupport.park();
                }


            }
            System.out.println("3.成功買到包子");
        });
        t1.start();
        try {
            Thread.sleep(200L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        object=new Object();
        synchronized (this){
            LockSupport.unpark(t1);
        }


        System.out.println("2.店家成功生產包子");
    }
    /**
     * park() 和 unpacke() 對調用的方法的先後順序沒有問題。
     */
    public void  parkUnpackTest(){
        Thread t1=new Thread(()->{
            while (object==null){
                System.out.println("1.等待包子生產");
                try {
                    Thread.sleep(2000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                LockSupport.park();
            }
            System.out.println("3.成功買到包子");
        });
        t1.start();
        try {
            Thread.sleep(200L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        object=new Object();
        LockSupport.unpark(t1);
        System.out.println("2.店家成功生產包子");
    }

    public static void main(String[] args) {
        ParkUnParkTest parkUnParkTest=new ParkUnParkTest();
//        parkUnParkTest.parkUnpackTest02();
        parkUnParkTest.parkUnpackTest();
    }
}

僞喚醒
警告!之前的代碼中如果用把while改成if語句來判斷是否進入等待狀態是錯誤的
官方建議應該在循環中檢查等待條件,原因是處於等待狀態的線程可能會收到錯誤警報和僞喚醒,
如果不在循環中檢查等待條件,程序就會在沒有滿足結束條件的情況下退出。

僞喚醒是指線程並非因爲notify,notifyall,unpark等api調用而喚醒,是更底層原因導致的。

線程封閉之Threadlocal和棧封閉
  1. 線程封閉的概念
    多線程訪問共享可變數據時,涉及到線程間數據同步的問題。並不是所有時候,都要用到共享數據,所以線程封閉概念就提出來了。
    數據都被封閉在各自的線程之中,就不需要同步,這種通過將數據封閉在線程中而避免使用同步的技術稱爲線程封閉。
    線程封閉具體的體現有:ThreadLocal,局部變量

  2. ThreadLocal
    ThreadLocal是java裏一種特殊的變量。
    它是一個線程級別變量,每個線程都有一個ThreadLocal就是每個線程都擁有了自己獨立的一個變量,競爭條件被徹底消除了,在併發模式下是絕對安全的變量。
    用法:ThreadLocal local=new ThreadLocal();
    會自動在每一個線程上創建一個T的副本,副本之間彼此獨立,互不影響。可以用ThreadLocal存儲一些參數,以便在線程中多個方法中使用,用來代替方法傳參的做飯。(線程中控制同一數據庫連接對象就是使用ThreadLocal)
    實在難以理解的,可以理解未,JVM維護了一個Map<Thread,T>,每個線程要用這個T的時候,用當前的線程去Map裏面取。僅作爲一個概念理解

  3. 棧封閉

局部變量的固有屬性之一就是封閉在線程中。它們位於執行線程的棧中,其他線程無法訪問這個棧。

線程池及實現原理剖析

######線程池的概念原理
1 線程池管理器:用於創建並管理線程池,包括創建線程池,銷燬線程池,添加新任務;
2 工作線程:線程池中線程,在沒有任務時處於等待狀態,可以循環的執行任務;
3 任務接口:每個任務必須實現的接口,以供工作線程調度任務的執行,它主要規定了任務的入口,任務執行完後的收尾工作,任務的執行狀態等;
4 任務隊列:用於存放沒有處理的任務。提供一種緩存機制。

image.png

ExecutorService–API
方法名稱 方法描述
awaitTermination(long timeout,TimeUnit unit) ExecutorService是否已經關閉,直到所有任務完成執行,或超時發生,或當前線程被中斷
invokeAll(Collection<? extends Callable> tasks) 執行給定的任務集合,執行完畢後,放回結果
invokeAll(Collection<? extends Callable> tasks,long timeout,TimeUnit unit) 執行給定的任務集合,執行完畢或者超時後,放回結果,其它任務終止
invokeAny(Collection<? extends Callable>tasks ) 執行給定的任務,任意一個執行成功則返回結果,其他任務終止
invokeAny(Collection<? extends Callable>tasks,long timeout,TimeUnit unit) 執行給定的任務,任意一個執行成功或者超時後,則返回結果,其他任務終止
isShudown() 如果此線程池已關閉,則返回true
isTerminated() 如果關閉後所有任務都已完成,則返回true
shutdown() 優雅關閉線程池,之前提交的任務將被執行,但是不會接受新的任務。
shutdownNow() 嘗試停止所有正在執行的任務,停止等待任務的處理,並放回等待執行任務的列表
submit(Callable task) 提交一個用於執行的Callable返回任務,並放回一個Future,用於獲取Callable執行結果
submit(Runnable task) 提交可運行任務以執行,並放回一個Future對象,執行結果未null
submit(Runnable task,T result) 提交可運行任務以執行,並返回Future,執行結果爲傳入的result
ScheduledExecutorService --API
方法名稱 方法描述
schedule(Callable callable,long delay,TimeUnit unit) 創建並執行一個一次性任務
chedule(Runnable command,long delay,TimeUnit unit) 創建並執行一個一次性任務
scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit 創建並執行一個週期性任務過了給定的初始延遲時間,會第一次被執行,執行過程中發生了異常,那麼任務就終止 。(一次任務執行時長超過了週期時間,下一次任務會等到該次任務執行結束後,立即執行,這就是它和scheduleWithFixedDelay的重要區別)
scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit) 創建並執行一個週期性任務過了給定的初始延遲時間,會第一次被執行,執行過程中發生了異常,那麼任務就終止 。(一次任務執行時長超過了週期時間,下一次任務會在該次任務執行結束的時間基礎上,計算執行延遲,對於超過週期的長時間處理任務的不同處理方式,這是它和scheduleAtFixedRate的重要區別)
Executors工具類 --API
方法名稱 方法描述
newFixedThreadPool(int nThreads) 創建一個固定大小,任務隊列容量無界的線程池。核心線程數=最大線程數。
newCachedThreadPool() 創建的是一個大小無界的緩衝線程池。它的任務隊列是一個同步隊列。任務加入到池中,如果池中有空閒線程,則用空閒線程執行,如無則創建新線程執行。池中的線程空閒超過60秒,將被銷燬釋放。線程數隨任務的多少變化,適用於執行耗時較小的異步任務。池的核心線程數=0,最大線程數=Integer.MAX_VALUE
newsingleThreadExecutor() 只有一個線程來執行無界任務隊列的單一線程池。該線程池確保任務按加入的順序一個一個依次執行,當唯一的線程因任務異常終止時,將創建一個新的線程來繼續執行後續的任務。與newFixedThreadPool(1)的區別在於,單一線程池的池大小在newSingleThreadExecutor方法中硬編碼,不能再改變
newScheduledThreadPool(int corePoolSize) 能定時執行任務的線程池。該池的核心線程數由參數指定,最大線程數=Integer.MAX_VALUE
任務execute過程
  1. 是否達到核心線程數量?沒達到,創建一個工作線程來執行任務。
  2. 工作隊列是否已滿?沒滿,則將新提交的任務存儲在工作隊列裏。
  3. 是否達到線程池最大數量?沒達到,則創建一個新的工作線程來執行任務。
  4. 最後,執行拒絕策略來處理這個任務。

image.png

如何確定線程池中線程的數量
  1. 計算型任務:cup數量的1-2倍
  2. IO型任務:相對計算型任務,需多一些線程,需根據具體的IO阻塞時長進行考量決定。

如果Tomcat中默認的最大線程數爲:200.也可考慮根據需要在一個最小數量和最大數量間自動增減線程數。

從本質去了解及線程安全

可見性問題

JAVA內存模型定義

前面章節中的大部分討論僅涉及代碼得行爲,即一次執行單個語句或表達式,即通過單個線程來執行。 JAVA虛擬機可以同時支持多個執行線程,若未正確同步線程的行爲,線程的行爲可能會出現混淆和違反直覺。
本章描述了多線程程序的語義;它包含了,當多個線程修改了共享內存中的值時,應該讀取到哪個值得規則。由於這部分規範類似於不同硬件體系結構的內存模型,因此這些語義稱爲java編程語言內存模型。
這些語言沒有規定如何執行多線程程序。相反,它們描述了允許多線程程序的合法行爲。
----《Java語言規範》

多線程中的問題
  1. 所見非所得
  2. 無法肉眼去檢測程序的準確性
  3. 不同的運行平臺有不同的表現
  4. 錯誤很難重現
package com.xbin;

/**
 * 線程安全-可見性問題
 */
public class ThreadSafeDemo {
    private static boolean flag=false;


    private static int i=0;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (!flag){
                i++;
            }
            System.out.println(i);
        }).start();
        Thread.sleep(3000L);
        flag=true;
        System.out.println("shutdown");
    }

}

image.png

**結論:**不同的環境產生的結果就不同

上述代碼運行的內存分析圖

image.png

上述程序不i輸出i的值得原因,雖然和高速緩存沒有關係,但高速緩存會在很短的時間內引起可見性的問題,在一些極限的場景需要注意;產生問題的主要原因跟JIT有關。

CPU指令重排

java編程語言的語義允許java編譯器和微處理器進行執行優化,這些優化導致了與其交互的代碼不再同步,從而導致看似矛盾的行爲。

image.png

JIT編譯器(Just In Time Compiler)

腳本語言與編譯語言的區別

  • 解釋執行:即咱們說的腳本,在執行時,由語言的解釋器將其一條條編譯成機器可識別的指令。
  • 編譯執行:將我們編寫的程序,直接編譯成機器可以識別的指令嗎。

下面分析JIT編譯器對上述代碼的行爲

image.png

上述問題是JIT對重複執行的代碼進行優化產生的問題,解決上述的問題是在flag添加volatile關鍵字修飾。

volatile關鍵字

可見性問題:讓一個線程對共享變量的修改,能夠及時的被其他線程看到。

Java內存模型規定:
對volatile變量v的寫入,與所有其他線程後續對v的讀同步

要滿足這些條件,所以volatiel關鍵字就有下面兩個功能。

  1. 禁止緩存

volatile變量的訪問控制符會加個ACC_VOLATILE
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-6U4g0yFO-1591524544549)(https://upload-images.jianshu.io/upload_images/22932958-9475b32c28d417ce.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

  1. 對volatile變量相關的指令不做重排序。
共享變量(Shared Variables)定義

可以在線程之間共享的內存稱爲共享內存或堆內存。
所有實例字段,靜態字段和數組元素都存儲在堆內存中,這些字段和數組都是標題中提到的共享變量。
衝突:如果至少有一個訪問是寫操作,那麼對同一個變量的兩次訪問是衝突的。
這些能被多個線程訪問的共享變量是內存模型規範的對象。

線程間操作的定義
  1. 線程間操作值:一個程序執行的操作可被其他線程感知或被其他線程直接影響。
  2. Java內存模型只描述線程間操作,不描述線程內操作,線程內操作按照線程內語義執行。

線程間操作有:

  • read操作(一般讀,即非volatile讀)

  • write操作(一般寫,即非volatile寫)

  • volatile read

  • volatile write

  • Lock(鎖monitor),Unlock

  • 線程的第一個和最後一個操作

  • 外部操作

所以線程間操作,都存在可見性問題,JMM需要對其進行規範

對於同步的規則定義
  • 對volatile變量v的寫入,與所有其他線程後續對v的讀同步
  • 對於監視器m的解鎖與所有後續操作對於m的加鎖同步
  • 對於每個屬性寫入默認值(0,false,null)與每個線程對其進行的操作同步
  • 啓動線程的操作與線程中的第一個操作同步
  • 線程T2的最後操作與線程T1發現線程T2已經結束同步。(isAlive,join可以判斷線程是否終結)
  • 如果線程T1中斷了T2,那麼線程T1的中斷操作與其他所有線程發現T2被中斷了同步通過拋InterruptedException異常,或者調用Thread.interrupted或Thread.isInterrupted
final關鍵字在JMM中的處理
  1. final在該對象的構造函數中設置對象的字段,當線程看到該對象時,將始終看到該對象的final字段的正確構造版本。僞代碼示例:f=new FinalDemo();讀取到的f.x一定最新,x爲final字段。
  2. 如果在構造函數中設置字段後發生讀取,則會看到該final字段分配的值,否則它將看到默認值;僞代碼示例:public finalDemo(){x=1;y=x;}; y會等於1;
  3. 讀取該共享對象的final成員變量之前,先要讀取共享對象。僞代碼示例:r=new ReferenceObj(); k=r.f;這兩個操作不能重排序。
  4. 通常被static fianl 修飾的字段,不能被修改。然而System.in,System.out,System.err被static final修飾,卻可以修改,遺留問題,必須允許通過set方法改變,我們將這些字段稱爲寫保護,已區別於普通final字段;

原子性

package com.xbin;

/**
 * 計數工具類
 */
public class Counter {
    int i;


    public void add(){
        i++;
    }
}


package com.xbin;

public class CounterDemo {
    public static void main(String[] args) throws InterruptedException {
        Counter counter=new Counter();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int j = 0; j < 10000; j++) {
                    counter.add();
                }
            }).start();
        }
        Thread.sleep(5000L);
        System.out.println(counter.i);
    }
}

運行結果

上述代碼按照正常邏輯執行的結果應該是:10000 ,但結果確實小於10000

產生上述問題的原因是i++ 不是原子操作,通過javap -v -p 操作把.class 文件解析成字節碼操作指令。
image.png

從第2步到第7步就是i++操作的字節碼指令。

image.png

image.png

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-nPvN4eFg-1591524544596)(https://upload-images.jianshu.io/upload_images/22932958-1bf41bf665f968f3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-2Tje4El6-1591524544609)(https://upload-images.jianshu.io/upload_images/22932958-99e8cfbeb9354fe9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

image.png

分析完這些操作後,就可以很明顯的發現問題。在多個線程訪問的情況下,就會有多個線程同時操作,

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Ra7P5qCE-1591524544626)(https://upload-images.jianshu.io/upload_images/22932958-f80b4305c83cffa3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

原子操作
  1. 原子操作可以是一個步驟,也可以是多個步驟,但是其順序不可以被打擾,也不可以被切割而只執行其中的一部分(不可中斷性)。
  2. 將整個操作視爲一個整體,資源在該次操作中保持一致,這是原子性地核心特徵。
原子性問題的場景
  1. 判斷了某種狀態後,這個狀態失效了
if(owner==null){
    owner=currentThread();
}
  1. 加載了一個值,這個值失效了。列如:i++操作。
原子操作問題的解決方案
  1. 使用synchronized 添加排他鎖(互斥鎖)
public class Counter {
    int i;


    public synchronized void add(){
        i++;
    }
}
  1. 使用ReetrantLocak鎖。
public class Counter {
    int i;
    Lock lock=new ReentrantLock();


    public synchronized void add(){
        lock.lock();
        try{
            i++;
        }finally {
            lock.unlock();
        }
    }
}
  1. 使用JUC 包下的AtomicInteger進行自增操作
public class Counter {
    AtomicInteger i=new AtomicInteger(0);
    public synchronized void add(){
        i.getAndIncrement();
    }
}

CAS(Compare and swap)

  • compare and swap 比較和交換。屬於硬件同步原語,處理器提供了基本內存操作的原子性保證。
  • CAS操作需要輸入兩個數值,一箇舊值A(期望操作前的值)和一個新值B,在操作期間先對舊值進行比較,若沒有發生變化,才交換新值,發生了變化則不交換。
  • java中sum,misc,Unsafe類,提供了compareAndSwapInt()和compareAndSwapLong()等幾個方法實現CAS

CAS在J.U.C併發編碼包下被廣泛運用
image.png

AtomicInteger的底層使用的就是CAS,首先CAS 是CPU指令級的操作,只有一步原子操作,所以非常快;

package com.xbin.atomic;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * 使用Unsafe 實現i++操作
 */
public class CounterUnsafe {
    private static Unsafe unsafe = null;
    private static long valueOff;
    int i = 0;
    static {
        //使用反射獲取Unsafe對象
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);
            //計數類屬性的偏移量
            Field field = CounterUnsafe.class.getDeclaredField("i");
            valueOff = unsafe.objectFieldOffset(field);


        } catch (Exception e) {
            e.printStackTrace();




        }
    }
    public void add() {
        try {
            while (true) {//自旋
                //獲取對象屬性的值
                int intVolatile = unsafe.getIntVolatile(this, valueOff);
                boolean b = unsafe.compareAndSwapInt(this, valueOff, intVolatile, intVolatile + 1);
                if (b) {
                    break;
                }
            }


        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


package com.xbin.atomic;

public class CounterDemo {
    public static void main(String[] args) throws InterruptedException {
        CounterUnsafe counter=new CounterUnsafe();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int j = 0; j < 10000; j++) {
                    counter.add();
                }
            }).start();
        }
        Thread.sleep(5000L);
        System.out.println(counter.i);
    }
}

運行結果

從上述運行結果中可以得到的結論是:使用Unsafe可以實現i++的原子性。

CAS的3個問題
  1. 循環+CAS,自旋的實現讓所有線程都處於高頻運行,爭搶CPU執行時間的狀態。如果操作長時間不成功,會帶來很大的CPU資源消耗。
  2. 僅針對單個變量的操作,不能擁於多個變量來實現原子操作。
  3. ABA問題。
ABA問題

image.png

  1. thread1,thread2同時讀取到i=0後;
  2. thread1,thread2都要執行CAS(0,1)操作;
  3. 假設thread1操作稍之後與thread2,則thread2執行成功;
  4. thread2緊接着執行CAS(1,0),將i的值改回0;
  5. thread1也會執行成功;
ABA問題的後果

下圖是使用CAS對stack進行操作的過程

  1. 步驟1 thread1,thread2 中同一時刻讀取棧中頂部元素的值A。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Howzkgbv-1591524544655)(https://upload-images.jianshu.io/upload_images/22932958-23b0a6262c1ab38c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

  1. 步驟2 thread2對stack進行操作CAS(A,B)

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-IaIfiWUB-1591524544661)(https://upload-images.jianshu.io/upload_images/22932958-7ee565a1bee2603b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

  1. 步驟3 thread2進行pop(B)把元素從stack中彈出。

image.png

  1. 步驟4 添加元素到stack中,依次把C,D,A 壓入stack中

image.png

  1. 步驟5 thread1進行CAS(A,B),結果就會變成下圖所示

image.png

我們期望的結果是thread1線程應該是要執行失敗的,應該A早就不是當初的A了,對於這種比較不充分情況,可以添加版本號進行比較。

java 鎖

  • 自旋鎖:是指當一個線程在獲取鎖的時候,如果鎖已經被其它線程獲取,那麼該線程將循環等待,然後不斷的判斷鎖是否能夠被被成功獲取,值到獲取到鎖纔會退出循環。
  • 樂觀鎖:假定沒有衝突,在修改數據時如果沒有發現數據和之前獲取的不一致,則讀最新數據,修改後重試修改
  • 悲觀鎖:假定會發生併發衝突,同步所有對數據的相關操作,從讀數據就開始上鎖。
    獨享鎖(寫):給資源加上鎖,線程可以修改資源,其他線程不能再加鎖;(單寫)
  • 獨享鎖(寫):給資源加上鎖,線程可以修改資源,其他線程不能再加鎖;(單寫)
  • 共享鎖(讀):給資源加上讀鎖後只能讀不能改,其他線程也只能加讀鎖,不能加寫鎖;(多讀)
  • 可重入鎖,不可重入鎖:線程拿到一把鎖之後,可以自由進入同一把鎖所同步的其他代碼。
  • 公平鎖,非公平鎖:爭搶鎖的順序,如果是按先來後到,則爲公平。
synchronized關鍵字
  1. 用於實例方法,靜態方法時,隱式指定鎖對象。
  2. 用於代碼塊時,顯示指定鎖對象
  3. 鎖的作用域:對象鎖,類鎖,分佈式鎖

**特性:**可重入,獨享,悲觀鎖
**鎖優化:**鎖消除(開啓鎖消除的參數:-XX:+DoescapeAnalysis -XX:+Eliminacatelocks)
鎖粗化 JDK做了鎖粗化的優化,但我們自己可從代碼層面優化
NOTE:synchronized關鍵字,不僅實現同步,JMM中規定,synchronized要保證可見性(不能夠被緩存)。

Mark Word
image.png

鎖升級過程

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-DoORmt9Q-1591524544688)(https://upload-images.jianshu.io/upload_images/22932958-94ad4b51b6738f80.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

synchronized是JVM提供的鎖的一種方式,JVM對其進行一些優化。

J.U.C 併發編程包

  • AtomicBoolean:原子更新布爾類型
  • AtomicInteger:原子更新整型
  • AtomicLong:原子更新長整型
  • AtomicIntegerArray:原子更新整型數組裏的元素
  • AtomicLongArray:原子更新長整型數組裏的元素
  • AtoimcReferenceArray:原子更新引用類型數組裏的元素
  • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器
  • AtomicLongFieldUpdater:原子更新長整型字段的更新器
  • AtomicReferenceFieldUpdater:原子更新引用類型裏的字段
  • AtomicReference:原子更新引用類型
  • AtomicStampedReference:原子更新帶有版本號的引用類型(可用於解決ABA問題)
  • AtomicMarkableReference:原子更新帶有標記的引用類型
    1.8更新
    計數器增強版,高併發下性能更好
    更新器:DoubleAccumulator,LongAccumulator
    計數器:DoubleAdder,LongAdder
    原理:分成多個操作單元,不同線程更新不同的單元只有需要彙總的時候才計算所有單元的操作
    場景:高併發頻繁更新,不太頻繁讀的取的

Lock接口

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-yeggBHRd-1591524544695)(https://upload-images.jianshu.io/upload_images/22932958-83004b047249bc4a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

synchronized VS Lock

synchronized

  • 優點 :
    使用簡單,語義清晰,哪裏需要點哪裏。
    有JVM提供,提供了多種優化方案(鎖粗化,鎖消除,偏向鎖,輕量級鎖)
    鎖的釋放由虛擬機來完成,不用人工干預,也降低了死鎖的可能性
  • 缺點:
    無法實現一些鎖的高級功能如:公平鎖,中斷鎖,超時鎖,讀寫鎖,共享鎖等
    Lock
  • 優點:
    所有synchronized的缺點
    可以實現更多的功能,讓synchronized缺點更多
  • 缺點:
    需手動釋放鎖unlock,新手使用不當可能死鎖
    結論:syschronized是卡片機,Lock是單反

ReadWriteLock
概念:維護一對關聯鎖,一個只用於讀操作,一個只用於寫操作,讀鎖可以由多個讀線程同時持有,寫鎖是排他的。同一時間,兩把鎖不能被不同線程持有。
適用場景:適合讀取操作多於寫入操作的場景,改進互斥鎖的性能,比如:集合的併發線程安全性改造,緩存組件。
鎖降級:指的是寫鎖降級成爲讀鎖。持有寫鎖的同時,再獲取讀鎖,隨後釋放寫鎖的過程。寫鎖是線程獨佔,讀鎖是共享,所以寫->讀是降級。(讀->寫,是不能實現的)

FutureTask

package com.xbin.juc;

import java.util.concurrent.Callable;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;

public class MyFutureTask<T>  implements Runnable{

    private static final int NEW=0;

    private static final int RUNABLE=1;

    private static final int END=2;

    private  int state;

    private AtomicReference<Thread> ower=new AtomicReference<>(null);

    private LinkedBlockingDeque<Thread> water=new LinkedBlockingDeque<>();

    T result;

    private final Callable<T> callabll;

    public MyFutureTask(Callable<T> callabll) {
        this.callabll = callabll;
        state=NEW;
    }

    @Override
    public void run() {
        while (true){
            if(state!=NEW||!ower.compareAndSet(null,Thread.currentThread())) return;

            try {
                state=RUNABLE;
                result= callabll.call();
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                state=END;
                ower.set(null);
            }
            while (true) {
                Thread thread = water.poll();
                if (thread == null) return;
                LockSupport.unpark(thread);

            }

        }


    }
    public T get(){
        if(state!=END){
            //等待
            water.add(Thread.currentThread());
            LockSupport.park();
        }

        return result;
    }
}

package com.xbin.juc;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class FutureTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CallableImpl callable=new CallableImpl();
        MyFutureTask<String> stringFutureTask = new MyFutureTask<>(callable);

        new Thread(stringFutureTask).start();
//        new Thread(stringFutureTask).start();


        System.out.println(stringFutureTask.get());
        System.out.println(stringFutureTask.get());


    }

    static class CallableImpl implements Callable<String> {

        @Override
        public String call() throws Exception {
            System.out.println("執行callable");
            return "操作成功";
        }
    }
}

Semaphore

/**
 *
 */
public class SemaphoreDemo {
    // Semaphore 信號量 限流 共享鎖
    private static Semaphore semaphore=new Semaphore(6);

    public static void main(String[] args) {
        for (int i = 0; i <1000 ; i++) {
            new Thread(()->{
                try {
                    semaphore.acquire(1);
                    System.out.println("線程正在執行===============================");
                    Thread.sleep(2000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release(1);
                }
            }).start();
        }

    }
}


手寫Semaphore

package cn.tk.myproject.lock;

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class MySemaphore {

    private  Sync sync;

    public MySemaphore(int permit ) {
        this.sync=new Sync(permit);
    }

    public void acquire(int arg){
        sync.acquireShared(arg);
    }

    public void release(int arg){
        sync.releaseShared(arg);
    }

    class Sync extends AbstractQueuedSynchronizer {

        private int permit;

        public Sync(int permit) {
            this.permit = permit;
        }

        @Override
        protected int tryAcquireShared(int arg) {
            for (;;) {
                int state = getState();
                int next = state + arg;
                if (next <= permit) {
                    //可以獲取鎖
                    if (compareAndSetState(state, next)) {
                        return arg;
                    }

                }
            }
        }

        @Override
        protected boolean tryReleaseShared(int arg) {
            for (;;){
                int state = getState();
                if(state==0){
                    return  false;
                }
                if(compareAndSetState(state,state-arg)){
                    return  true;
                }
                return false;
            }

        }
    }


}

CountDownLatch

用法1:

import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {

    private  static  CountDownLatch countDownLatch=new CountDownLatch(4);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            new Thread(()->{
               countDownLatch.countDown();
                System.out.println("開始執行代碼");
            }).start();
        }
        Thread.sleep(2000L);
        countDownLatch.await();
        System.err.println("成功執行完畢");

    }
}

用法2:

import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {

    private  static  CountDownLatch countDownLatch=new CountDownLatch(1);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 6; i++) {
            new Thread(()->{
                try {
                    System.out.println("準備執行!!");
                    countDownLatch.await();
                    System.out.println("開始執行");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        Thread.sleep(3000L);
        countDownLatch.countDown();
    }
}

手寫CountDownLatch

package cn.tk.myproject.lock;

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

/**
 * 共享鎖,從指定值開始下降
 */
public class MyCountDownLatch {
    private final Sync sync;

    public MyCountDownLatch(int count) {
        this.sync=new Sync(count);
    }
    public void await(){
        sync.acquireShared(1);
    }
    public void countDown(){
        sync.releaseShared(1);
    }

    /**
     *     CountDownLatch就可以看做是一個共享鎖
     *     初始狀態,這個共享鎖被獲取了n次,
     *     每次countdown,相當於釋放一次鎖
     *     當鎖釋放完後,其他線程才能再次獲得鎖
     */
    class Sync extends AbstractQueuedSynchronizer{


        public Sync(int count) {
            setState(count);
        }

        @Override
        protected int tryAcquireShared(int arg) {
            return (getState() == 0) ? 1 : -1;
        }

        @Override
        protected boolean tryReleaseShared(int arg) {
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }
}

CyclicBarrier

/**
 * 需要線程數達到要求才會被執行
 */
public class CyclicBarrierDemo {

    private static CyclicBarrier cyclicBarrier=new CyclicBarrier(4);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            new Thread(()->{

                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println("正在執行============================");

            }).start();
            Thread.sleep(300L);
        }
    }
}

CyclicBarrier源碼分析

package cn.tk.myproject.lock;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class MyCyclecbarrier {
    private ReentrantLock reentrantLock=new ReentrantLock();

    private Condition condition=reentrantLock.newCondition();

    private  int count ;

    private final int parties;

    private Object gentation=new Object();

    public MyCyclecbarrier(int parties) {
        this.parties = parties;
        this.count=this.parties;
    }

    public void await(){
       final  ReentrantLock lock =this.reentrantLock;
       lock.lock();
       try{
           final Object g=gentation;
           int index=--count;
           if(index==0){
               //結束等待
               //喚醒等待隊列
               nextGention();
               return;
           }
           //否則進入等待
           for (;;){//防止僞喚醒
               try {
                   condition.await();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               if(g!=gentation){
                   return;
               }
           }
       }finally {
           lock.unlock();
       }



    }

    private void nextGention() {
        condition.signalAll();
        count=parties;//重置數量
        gentation=new Object();
    }


}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章