1. 程式人生 > 其它 >高併發及執行緒安全

高併發及執行緒安全

一.高併發及執行緒安全

1.高併發及執行緒安全的概念

1.高併發:在某個時間點上,有多個執行緒同時訪問某一個資源。例如:雙十一,12306 , 秒殺

2.執行緒安全性問題:當多個執行緒無序的訪問同一個資源(例如:同一個變數、同一資料庫、同一個檔案……),而且訪問同一資源的程式碼不具有“原子性”,這時對這一資源的方法就會產生安全性問題——導致此資源最終的結果是錯誤。

3.高併發所產生的安全性問題主要表現:

1).可見性:指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。

2).有序性:即程式執行的順序按照程式碼的先後順序執行。

3).原子性:即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行

2.高併發問題一_不可見性(重點)

產生的原因:定義一個變數。一個執行緒修改變數的值,另一個執行緒由於訪問頻率太快,導致一直使用本執行緒區內的變數副本,而沒有實時的到主記憶體中獲取變數的新值。

點選檢視程式碼
/*
    演示多執行緒安全問題的_不可見性
 */
public class MyThread extends Thread{
    //定義一個供多個執行緒共享使用的靜態變數
    public static int a = 0;

    @Override
    public void run() {
        System.out.println("Thread-0執行緒開始執行執行緒任務,睡眠2秒鐘,等待main執行緒先執行!");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Thread-0過了2秒鐘,睡醒了,修改變數a的值為1");
        a=1;
        System.out.println("Thread-0執行執行緒任務結束了!"+a);
    }
}
點選檢視程式碼
public class Demo01Visible {
    public static void main(String[] args) {
        //建立MyThread物件
        MyThread mt = new MyThread();
        //呼叫start方法,開啟一個新的執行緒,執行run方法
        mt.start();

        //main執行緒在開啟新的執行緒之後,會繼續執行main方法中的程式碼
        System.out.println("main執行緒執行一個死迴圈");
        while (true){
            //判斷變數a的值是否為1
            if(MyThread.a==1){
                System.out.println("main執行緒判斷變數a的值為1,結束死迴圈!");
                break;
            }
        }
    }
}
程式的執行結果:**死迴圈並沒有結束
點選檢視程式碼
main執行緒執行一個死迴圈
Thread-0執行緒開始執行執行緒任務,睡眠2秒鐘,等待main執行緒先執行!
Thread-0過了2秒鐘,睡醒了,修改變數a的值為1
Thread-0執行執行緒任務結束了!1

3.Java記憶體模型JMM(瞭解)

概述:JMM(Java Memory Model)Java記憶體模型,是java虛擬機器規範中所定義的一種記憶體模型。

Java記憶體模型(Java Memory Model)描述了Java程式中各種變數(執行緒共享變數)的訪問規則,以及在JVM中將變數儲存到記憶體和從記憶體中讀取變數這樣的底層細節。

所有的共享變數都儲存於主記憶體。這裡所說的變數指的是例項變數(成員變數)和類變數(靜態成員變數)。不包含區域性變數,因為區域性變數是執行緒私有的,因此不存在競爭問題。每一個執行緒還存在自己的工作記憶體,執行緒的工作記憶體,保留了被執行緒使用的變數的工作副本。執行緒對變數的所有的操作(讀,取)都必須在工作記憶體中完成,而不能直接讀寫主記憶體中的變數,不同執行緒之間也不能直接訪問對方工作記憶體中的變數,執行緒間變數的值的傳遞需要通過主記憶體完成。

4.高併發問題二_無序性

1).有序性:多行程式碼的編寫(.java)順序和編譯(.class)順序。
有些時候,編譯器在編譯程式碼時,為了提高效率,會對程式碼“重排”:

點選檢視程式碼
.java檔案
int a = 10;		//第一行
int b = 20;		//第二行
int c = a * b;	//第三行

在執行第三行之前,由於第一行和第二行的先後順序無所謂,所以編譯器可能會對“第一行”和“第二行”進行程式碼重排:

點選檢視程式碼
.class
int b = 20;
int a = 10;
int c = a * b;
2).但在多執行緒環境下,這種重排可能是我們不希望發生的,因為:重排,可能會影響另一個執行緒的結果,所以我們不需要程式碼進行重排

5.高併發問題三_非原子性(重點)

原子:不可分割 100行程式碼是一個原子,執行緒執行100行程式碼不可以分開執行,要麼都執行,要都不執行

需求:
1.定義多執行緒共享的靜態變數money
2.Thread-0執行緒把money的值增加1000
3.main執行緒把money的值增加1000
4.檢視money的最終結果

點選檢視程式碼
/*
    高併發問題三_非原子性(重點)
 */
public class MyThread extends Thread{
    //1.定義多執行緒共享的靜態變數money
    public static int money = 0;

    @Override
    public void run() {
        //2.Thread-0執行緒把money的值增加1000
        System.out.println("Thread-0執行緒開始執行執行緒任務,把money的值增加1000");
        for (int i = 0; i < 1000; i++) {
            money++;
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Thread-0執行緒增加money的值完畢,結束執行緒任務!");
    }
}
點選檢視程式碼
public class Demo01Atomic {
    public static void main(String[] args) throws InterruptedException {
        //建立MyThread物件
        MyThread mt = new MyThread();
        //呼叫start方法,開啟一個新的執行緒,執行run方法
        mt.start();

        //main執行緒在開啟新的執行緒之後,繼續執行main方法中的程式碼
        //3.main執行緒把money的值增加1000
        System.out.println("main執行緒開啟把money的值增加1000");
        for (int i = 0; i < 1000; i++) {
            MyThread.money++;
            //增加睡醒1毫秒,為了提高執行緒安全問題出現的機率(讓兩個執行緒搶奪cpu的執行權次數變多)
            Thread.sleep(1);
        }
        System.out.println("main先增加moeny的值完畢,睡眠2秒鐘,等待Thread-0執行緒增加結束");
        Thread.sleep(3000);

        //4.檢視money的最終結果
        System.out.println("變數moeny最終的值為:"+MyThread.money);
    }
}
點選檢視程式碼
main執行緒開啟把money的值增加1000
main先增加moeny的值完畢,睡眠2秒鐘,等待Thread-0執行緒增加結束
Thread-0執行緒開始執行執行緒任務,把money的值增加1000
Thread-0執行緒增加money的值完畢,結束執行緒任務!
變數moeny最終的值為:2000
    
main執行緒開啟把money的值增加1000
Thread-0執行緒開始執行執行緒任務,把money的值增加1000
main先增加moeny的值完畢,睡眠2秒鐘,等待Thread-0執行緒增加結束
Thread-0執行緒增加money的值完畢,結束執行緒任務!
變數moeny最終的值為:1997
    
main執行緒開啟把money的值增加1000
Thread-0執行緒開始執行執行緒任務,把money的值增加1000
main先增加moeny的值完畢,睡眠2秒鐘,等待Thread-0執行緒增加結束
Thread-0執行緒增加money的值完畢,結束執行緒任務!
變數moeny最終的值為:1995 

原子性原理


每個執行緒訪問money變數,都需要三步:
1).取money的值;
2).將money++
3).將money寫回
這三步就不具有“原子性”——執行某一步時,很可能會被暫停(失去了cpu的執行權),執行另外一個執行緒,就會導致變數的最終結果錯誤!!!!

二.volatile關鍵字

1.volatile解決可見性

點選檢視程式碼
/*
    使用volatile關鍵字解決不可見性
 */
public class MyThread extends Thread{
    //定義一個供多個執行緒共享使用的靜態變數
    /*
        變數被volatile關鍵字修飾,當變數的值被改變,volatile會讓變數所有的變數副本都失效
        執行緒想要再次使用變數,必須重新獲取
     */
    public static volatile int a = 0;

    @Override
    public void run() {
        System.out.println("Thread-0執行緒開始執行執行緒任務,睡眠2秒鐘,等待main執行緒先執行!");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Thread-0過了2秒鐘,睡醒了,修改變數a的值為1");
        a=1;
        System.out.println("Thread-0執行執行緒任務結束了!"+a);
    }
}
點選檢視程式碼
public class Demo01Visible {
    public static void main(String[] args) throws InterruptedException {
        //建立MyThread物件
        MyThread mt = new MyThread();
        //呼叫start方法,開啟一個新的執行緒,執行run方法
        mt.start();

        //main執行緒在開啟新的執行緒之後,會繼續執行main方法中的程式碼
        System.out.println("main執行緒執行一個死迴圈");
        while (true){
            //判斷變數a的值是否為1
            if(MyThread.a==1){
                System.out.println("main執行緒判斷變數a的值為1,結束死迴圈!");
                break;
            }
        }
    }
}
點選檢視程式碼
main執行緒執行一個死迴圈
Thread-0執行緒開始執行執行緒任務,睡眠2秒鐘,等待main執行緒先執行!
Thread-0過了2秒鐘,睡醒了,修改變數a的值為1
main執行緒判斷變數a的值為1,結束死迴圈!
Thread-0執行執行緒任務結束了!1

2.volatile解決有序性

注意:變數添加了volatile關鍵字,就不會在進行重排了

3.volatile不能解決原子性

點選檢視程式碼
/*
    高併發問題三_非原子性(重點)
 */
public class MyThread extends Thread{
    //1.定義多執行緒共享的靜態變數money
    public static volatile int money = 0;

    @Override
    public void run() {
        //2.Thread-0執行緒把money的值增加1000
        System.out.println("Thread-0執行緒開始執行執行緒任務,把money的值增加1000");
        for (int i = 0; i < 1000; i++) {
            money++;
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Thread-0執行緒增加money的值完畢,結束執行緒任務!");
    }
}
點選檢視程式碼
public class Demo01Atomic {
    public static void main(String[] args) throws InterruptedException {
        //建立MyThread物件
        MyThread mt = new MyThread();
        //呼叫start方法,開啟一個新的執行緒,執行run方法
        mt.start();

        //main執行緒在開啟新的執行緒之後,繼續執行main方法中的程式碼
        //3.main執行緒把money的值增加1000
        System.out.println("main執行緒開啟把money的值增加1000");
        for (int i = 0; i < 1000; i++) {
            MyThread.money++;
            //增加睡醒1毫秒,為了提高執行緒安全問題出現的機率(讓兩個執行緒搶奪cpu的執行權次數變多)
            Thread.sleep(1);
        }
        System.out.println("main先增加moeny的值完畢,睡眠2秒鐘,等待Thread-0執行緒增加結束");
        Thread.sleep(3000);

        //4.檢視money的最終結果
        System.out.println("變數moeny最終的值為:"+ MyThread.money);
    }
}
程式執行的結果: volatile不能把原子性的三步變成一個原子執行

​ 1).取money的值;
​ 2).將money++
​ 3).將money寫回

這三步在執行的過程中,還是有可能把其他執行緒打斷的

點選檢視程式碼
main執行緒開啟把money的值增加1000
Thread-0執行緒開始執行執行緒任務,把money的值增加1000
main先增加moeny的值完畢,睡眠2秒鐘,等待Thread-0執行緒增加結束
Thread-0執行緒增加money的值完畢,結束執行緒任務!
變數moeny最終的值為:1984

原子類

概述:所謂的原子性是指在一次操作或者多次操作中,要麼所有的操作全部都得到了執行並且不會受到任何因素的干擾而中斷,要麼所有的操作都不執行,

多個操作是一個不可以分割的整體(原子)。

比如:從張三的賬戶給李四的賬戶轉1000元,這個動作將包含兩個基本的操作:從張三的賬戶扣除1000元,給李四的賬戶增加1000元。這兩個操作必須符合原子性的要求,

要麼都成功要麼都失敗。

1.原子類概述

概述:java從JDK1.5開始提供了java.util.concurrent.atomic包(簡稱Atomic包),這個包中的原子操作類提供了一種用法簡單,效能高效,執行緒安全地更新一個變數的方式。

1). java.util.concurrent.atomic.AtomicInteger:對int變數進行原子操作的類。

2). java.util.concurrent.atomic.AtomicLong:對long變數進行原子操作的類。

3). java.util.concurrent.atomic.AtomicBoolean:對boolean變數進行原子操作的類。

這些類可以保證對“某種型別的變數”原子操作,多執行緒、高併發的環境下,就可以保證對變數訪問的有序性,從而保證最終的結果是正確的。

AtomicInteger原子型Integer,可以實現原子更新操作

點選檢視程式碼
構造方法:
public AtomicInteger():	   				初始化一個預設值為0的原子型Integer
public AtomicInteger(int initialValue): 初始化一個指定值的原子型Integer
成員方法:
int get():   			 				 獲取AtomicInteger物件中儲存的int值
int getAndIncrement(): i++     			 以原子方式將當前值加1,注意,這裡返回的是自增前的值。
int incrementAndGet(): ++i    		     以原子方式將當前值加1,注意,這裡返回的是自增後的值。
####2.AtomicInteger類的基本使用 AtomicInteger也是一個原子的包裝類:裡邊包含了一個int型別的整數值
點選檢視程式碼
import java.util.concurrent.atomic.AtomicInteger;

/*
    AtomicInteger類的基本使用
 */
public class Demo01AtomicInteger {
    public static void main(String[] args) {
        //使用空引數構造方法,建立AtomicInteger物件
        AtomicInteger ai1 = new AtomicInteger();
        System.out.println(ai1.get());//0

        //使用帶引數構造方法,建立AtomicInteger物件
        AtomicInteger ai2 = new AtomicInteger(10);
        System.out.println(ai2.get());//10

        //int getAndIncrement(): i++ 以原子方式將當前值加1,注意,這裡返回的是自增前的值。
        int a = ai2.getAndIncrement();
        System.out.println("a:"+a);//a:10
        System.out.println(ai2.get());//11

        //int incrementAndGet(): ++i  以原子方式將當前值加1,注意,這裡返回的是自增後的值。
        int b = ai2.incrementAndGet();
        System.out.println("b:"+b);//b:12
        System.out.println(ai2.get());//12
    }
}

3.AtomicInteger解決變數的原子性(可見性、有序性)

點選檢視程式碼
import java.util.concurrent.atomic.AtomicInteger;

/*
    AtomicInteger解決變數的原子性
 */
public class MyThread extends Thread{
    //1.定義多執行緒共享的靜態變數money,使用AtomicInteger型別
    public static AtomicInteger money = new AtomicInteger(0);

    @Override
    public void run() {
        //2.Thread-0執行緒把money的值增加1000
        System.out.println("Thread-0執行緒開始執行執行緒任務,把money的值增加1000");
        for (int i = 0; i < 1000; i++) {
            money.getAndIncrement();//money++;
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Thread-0執行緒增加money的值完畢,結束執行緒任務!");
    }
}
點選檢視程式碼
public class Demo01Atomic {
    public static void main(String[] args) throws InterruptedException {
        //建立MyThread物件
        MyThread mt = new MyThread();
        //呼叫start方法,開啟一個新的執行緒,執行run方法
        mt.start();

        //main執行緒在開啟新的執行緒之後,繼續執行main方法中的程式碼
        //3.main執行緒把money的值增加1000
        System.out.println("main執行緒開啟把money的值增加1000");
        for (int i = 0; i < 1000; i++) {
            MyThread.money.getAndIncrement();//MyThread.money++;
            //增加睡醒1毫秒,為了提高執行緒安全問題出現的機率(讓兩個執行緒搶奪cpu的執行權次數變多)
            Thread.sleep(1);
        }
        System.out.println("main先增加moeny的值完畢,睡眠2秒鐘,等待Thread-0執行緒增加結束");
        Thread.sleep(3000);

        //4.檢視money的最終結果
        System.out.println("變數moeny最終的值為:"+ MyThread.money);
    }
}
執行結果:
點選檢視程式碼
main執行緒開啟把money的值增加1000
Thread-0執行緒開始執行執行緒任務,把money的值增加1000
main先增加moeny的值完畢,睡眠2秒鐘,等待Thread-0執行緒增加結束
Thread-0執行緒增加money的值完畢,結束執行緒任務!
變數moeny最終的值為:2000
####3.AtomicInteger的原理_CAS機制(樂觀鎖) **CAS:compareAndSwap 比較並交換**

四.synchronized關鍵字

1.售票案例引發的安全性問題

2.售票案例的程式碼實現

點選檢視程式碼
/*
    賣票案例
 */
public class RunnableImpl implements Runnable{
    //定義一個供多個執行緒訪問的票源
    private static int ticket = 100;

    //執行緒任務:賣票
    @Override
    public void run() {
        //增加一個死迴圈,讓賣票操作重複執行
        while (true){
            //對ticket進行判斷
            if(ticket>0){
                //每賣一張票需要10毫秒
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"執行緒正在賣第"+ticket+"張票!");
                ticket--;
            }else{
                break;
            }
        }

    }
}
點選檢視程式碼
/*
    建立3個執行緒,賣同100張票
 */
public class Demo01PayTicket {
    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();
        //建立3個執行緒
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        t0.start();
        t1.start();
        t2.start();
    }
}
程式執行的結果:
點選檢視程式碼
...
Thread-2執行緒正在賣第15張票!
Thread-1執行緒正在賣第13張票!
Thread-2執行緒正在賣第12張票!
Thread-0執行緒正在賣第11張票!
Thread-1執行緒正在賣第10張票!
Thread-0執行緒正在賣第9張票!
Thread-2執行緒正在賣第8張票!
Thread-1執行緒正在賣第7張票!
Thread-2執行緒正在賣第6張票!
Thread-0執行緒正在賣第6張票!
Thread-1執行緒正在賣第4張票!
Thread-2執行緒正在賣第3張票!
Thread-0執行緒正在賣第3張票!
Thread-1執行緒正在賣第1張票!
Thread-2執行緒正在賣第0張票!
Thread-0執行緒正在賣第-1張票! 


繼承方式實現賣票案例

點選檢視程式碼
public class MyThread extends Thread {
    //定義一個供多個執行緒訪問的票源
    private static int ticket = 100;

    //執行緒任務:賣票
    @Override
    public void run() {
        //增加一個死迴圈,讓賣票操作重複執行
        while (true){
            //對ticket進行判斷
            if(ticket>0){
                //每賣一張票需要10毫秒
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"執行緒正在賣第"+ticket+"張票!");
                ticket--;
            }else{
                break;
            }
        }

    }
}
點選檢視程式碼
public class MyThread extends Thread {
    //定義一個供多個執行緒訪問的票源
    private static int ticket = 100;

    //執行緒任務:賣票
    @Override
    public void run() {
        //增加一個死迴圈,讓賣票操作重複執行
        while (true){
            //對ticket進行判斷
            if(ticket>0){
                //每賣一張票需要10毫秒
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"執行緒正在賣第"+ticket+"張票!");
                ticket--;
            }else{
                break;
            }
        }

    }
}
####3.執行緒安全問題的產生原理

4.解決執行緒安全問題的第一種方式使用同步程式碼塊

點選檢視程式碼
/*
    賣票案例出現了執行緒安全問題:賣出了重複的票,和不存在的票
        100 100 100   0,-1
    解決執行緒安全問題的第一種方案:使用同步程式碼塊
        格式:
            synchronized(鎖物件){
                可能產生執行緒安全問題的程式碼(訪問了共享資料的程式碼)
            }
        原理:
            使用同步程式碼塊把一段程式碼鎖住,只讓一個執行緒進入到程式碼塊中執行賣票的程式碼
        注意:
            1.同步程式碼塊使用的鎖物件可以是任意的物件
                   Student s = new Student();
                   Object obj = new Object();
                   String s = "abc"; //底層是一個字元陣列
            2.必須保證所有的執行緒使用的是同一個鎖物件
 */
public class RunnableImpl implements Runnable{
    //定義一個供多個執行緒訪問的票源
    private int ticket = 100;
    //定義一個同步程式碼塊使用的鎖物件
    //Person p = new Person();
    //Object obj = new Object();
    String str = "aaa";

    //執行緒任務:賣票
    @Override
    public void run() {
        //增加一個死迴圈,讓賣票操作重複執行
        while (true){
            //同步程式碼塊
            synchronized (str){
                //對ticket進行判斷
                if(ticket>0){
                    //每賣一張票需要10毫秒
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"執行緒正在賣第"+ticket+"張票!");
                    ticket--;
                }else{
                    break;
                }
            }
        }

    }
}
點選檢視程式碼
/*
    建立3個執行緒,賣同100張票
 */
public class Demo01PayTicket {
    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();
        //建立3個執行緒
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        t0.start();
        t1.start();
        t2.start();


    }
}
####5.同步的原理

6.解決執行緒安全問題的第二種方式:使用同步方法

點選檢視程式碼
/*
    賣票案例出現了執行緒安全問題:賣出了重複的票,和不存在的票
        100 100 100   0,-1
    解決執行緒安全問題的第二種方案:使用同步方法
        步驟:
            1.把訪問了共享資料的程式碼,抽取出來,放到一個方法中
            2.給方法新增一個synchronized關鍵字,這個方法就是一個同步方法
        格式:
            修飾符 synchronized 返回值型別 方法名(引數列表){
                可能產生執行緒安全問題的程式碼(訪問了共享資料的程式碼)
            }
        原理:
            使用一個同步方法把程式碼鎖住,只讓一個執行緒進入到方法執行賣票的程式碼
 */
public class RunnableImpl implements Runnable{
    //定義一個供多個執行緒訪問的票源
    private static int ticket = 100;

    //執行緒任務:賣票
    @Override
    public void run() {
        System.out.println("this:"+this);
        //增加一個死迴圈,讓賣票操作重複執行
        while (true){
            //payTicket();
            payTicketStatic();
            if(ticket<=0){
                break;
            }
        }
    }

    /*
        定義一個靜態的同步方法(瞭解)
        使用一個鎖物件,把方法鎖住,只讓一個執行緒獲取鎖物件進入到方法執行
        靜態方法的鎖物件是誰?
            還是this嗎? 不是this是建立物件之後才出現的
        是本類的class檔案物件(反射): RunnableImpl.class
     */
    public static /*synchronized*/ void payTicketStatic(){
        synchronized (RunnableImpl.class){
            //對ticket進行判斷
            if(ticket>0){
                //每賣一張票需要10毫秒
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"執行緒正在賣第"+ticket+"張票!");
                ticket--;
            }
        }
    }

    /*
        定義一個同步方法
        使用一個鎖物件,把方法鎖住,只讓一個執行緒獲取鎖物件進入到方法執行
        鎖物件是誰? 是this==>本類物件==>  RunnableImpl run = new RunnableImpl();
            run:com.itheima.demo10synchronized.RunnableImpl@4554617c
            this:com.itheima.demo10synchronized.RunnableImpl@4554617c
     */
    public synchronized void payTicket(){
        //對ticket進行判斷
        if(ticket>0){
            //每賣一張票需要10毫秒
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"執行緒正在賣第"+ticket+"張票!");
            ticket--;
        }
    }

    /*public void payTicket(){
        synchronized (this){
            //對ticket進行判斷
            if(ticket>0){
                //每賣一張票需要10毫秒
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"執行緒正在賣第"+ticket+"張票!");
                ticket--;
            }
        }
    }*/
}
點選檢視程式碼

/*
    建立3個執行緒,賣同100張票
 */
public class Demo01PayTicket {
    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();
        System.out.println("run:"+run);
        //建立3個執行緒
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        t0.start();
        t1.start();
        t2.start();

    }
}
####7.解決執行緒安全問題的第三方式:使用Lock鎖
點選檢視程式碼
import java.util.concurrent.locks.ReentrantLock;

/*
    賣票案例出現了執行緒安全問題:賣出了重複的票,和不存在的票
        100 100 100   0,-1
    解決執行緒安全問題的第三種方案:使用Lock鎖
    java.util.concurrent.locks.Lock介面
        Lock鎖比同步程式碼塊和同步方法使用起來更廣泛。
    Lock介面中的成員方法:
        void lock()獲取鎖。
        void unlock() 釋放鎖。
    java.util.concurrent.locks.ReentrantLock類 implements Lock介面
    Lock鎖的使用步驟:
        1.在成員位置建立ReentrantLock物件
        2.在訪問共享資料的程式碼前呼叫lock方法,獲取鎖物件
        3.在訪問共享資料的程式碼後呼叫unlock方法,釋放鎖物件
    原理:
        使用lock方法和unlock方法把一段程式碼鎖住,只讓一個執行緒進入執行
 */
public class RunnableImpl implements Runnable{
    //定義一個供多個執行緒訪問的票源
    private int ticket = 100;
    //1.在成員位置建立ReentrantLock物件
    private ReentrantLock l = new ReentrantLock();

    //執行緒任務:賣票
    @Override
    public void run() {
        //增加一個死迴圈,讓賣票操作重複執行
        while (true){
            //2.在訪問共享資料的程式碼前呼叫lock方法,獲取鎖物件
            l.lock();
                //對ticket進行判斷
                if(ticket>0){
                    //每賣一張票需要10毫秒
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"執行緒正在賣第"+ticket+"張票!");
                    ticket--;
                }/*else{
                    System.exit(0);//終止JVM
                }*/
            //3.在訪問共享資料的程式碼後呼叫unlock方法,釋放鎖物件
            l.unlock();
            if(ticket<=0){
                break;
            }
        }

    }
}
點選檢視程式碼
/*
    建立3個執行緒,賣同100張票
 */
public class Demo01PayTicket {
    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();
        //建立3個執行緒
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        t0.start();
        t1.start();
        t2.start();
    }
}
####8.CAS與Synchronized **AtomicInteger:只能解決一個變數的原子性** **synchronized:可以解決一段程式碼的原子性** CAS和Synchronized都可以保證多執行緒環境下共享資料的安全性。那麼他們兩者有什麼區別?

Synchronized是從悲觀的角度出發:

總是假設最壞的情況,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會阻塞直到它拿到鎖
共享資源每次只給一個執行緒使用,其它執行緒阻塞,用完後再把資源轉讓給其它執行緒)。因此Synchronized我們也將其稱之為悲觀鎖。jdk中的ReentrantLock也是一種悲觀鎖。

CAS是從樂觀的角度出發:

總是假設最好的情況,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料。

CAS這種機制我們也可以將其稱之為樂觀鎖。

五.併發包

在JDK的併發包java.util.concurrent裡提供了幾個非常有用的併發容器和併發工具類。供我們在多執行緒開發中進行使用。這些集合和工具類都可以保證高併發的執行緒安全問題.

1.併發List集合_CopyOnWriteArrayLis

List集合的特點:

​ 1.是一個有序的集合

​ 2.允許儲存重複的元素

​ 3.包含一些帶索引的方法

1).java.util.concurrent.CopyOnWriteArrayList(類):它是一個“執行緒安全”的ArrayList,我們之前學習的java.utils.ArrayList不是執行緒安全的。
2).如果是多個執行緒,併發訪問同一個ArrayList,我們要使用:CopyOnWriteArrayList

需求:

1.建立一個被多個執行緒共享使用靜態的ArrayList集合物件

2.使用Thread-0執行緒往集合中新增1000個元素

3.使用main執行緒往集合中新增1000個元素

4.統計集合的長度

點選檢視程式碼
import java.util.ArrayList;
import java.util.Vector;
import java.util.concurrent.CopyOnWriteArrayList;

public class MyThread extends Thread{
    //1.建立一個被多個執行緒共享使用靜態的ArrayList集合物件
    /*
        ArrayList集合是JDK1.2之後出現的,是一個多執行緒不安全的集合
            1.儲存的元素少了
            2.索引越界異常
     */
    //public static ArrayList<Integer> list = new ArrayList<>();

    /*
        java.util.Vector<E>集合,是JDK1.0時期,最早期的單列集合
            底層也是陣列結構
            與新 collection 實現不同,Vector 是同步的。
            Vector集合使用synchronized同步技術,保證多執行緒安全
            synchronized同步技術是悲觀鎖,效率低
     */
    //public static Vector<Integer> list = new Vector<>();

    /*
        java.util.concurrent.CopyOnWriteArrayList<E>集合,是JDK1.5之後出現的
            CopyOnWriteArrayList集合底層採用的是CAS機制,保證多執行緒安全
            CAS機制是樂觀鎖,效率高
     */
    public static CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();

    @Override
    public void run() {
        //2.使用Thread-0執行緒往集合中新增1000個元素
        System.out.println("Thread-0執行緒開始執行執行緒任務了,往集合中新增1000個元素!");
        for (int i = 0; i < 1000; i++) {
            list.add(i);//[0,1,2,...999]
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Thread-0執行緒,新增完畢,執行完執行緒任務了!");
    }
}
點選檢視程式碼
public class Demo01List {
    public static void main(String[] args) throws InterruptedException {
        //建立MyThread物件,呼叫start方法,開啟一個新的執行緒,執行run方法
        MyThread mt = new MyThread();
        mt.start();

        //main執行緒在開啟新的執行緒之後,會繼續執行main方法中的程式碼
        //3.使用main執行緒往集合中新增1000個元素
        System.out.println("main執行緒往集合中新增1000個元素!");
        for (int i = 0; i < 1000; i++) {
            MyThread.list.add(i);//[0,1,2,...999]
            Thread.sleep(1);
        }
        System.out.println("main執行緒,新增1000個元素完畢,睡眠3秒鐘,等待Thread-0執行緒新增完畢!");
        Thread.sleep(3000);

        //4.統計集合的長度
        System.out.println("兩個執行緒都執行完畢,集合的長度為:"+MyThread.list.size());
    }
}
ArrayList集合併發的問題:

1.儲存的元素個數不對

2.會引發索引越界異常

點選檢視程式碼
main執行緒往集合中新增1000個元素!
Thread-0執行緒開始執行執行緒任務了,往集合中新增1000個元素!
main執行緒,新增1000個元素完畢,睡眠3秒鐘,等待Thread-0執行緒新增完畢!
Thread-0執行緒,新增完畢,執行完執行緒任務了!
兩個執行緒都執行完畢,集合的長度為:1983

main執行緒往集合中新增1000個元素!
Thread-0執行緒開始執行執行緒任務了,往集合中新增1000個元素!
main執行緒,新增1000個元素完畢,睡眠3秒鐘,等待Thread-0執行緒新增完畢!
Thread-0執行緒,新增完畢,執行完執行緒任務了!
兩個執行緒都執行完畢,集合的長度為:1978 
####2.併發Set集合_CopyOnWriteArraySe Set集合的特點:

​ 1.不允許儲存重複元素

​ 2.不包含索引的方法

需求:

1.建立一個被多個執行緒共享使用靜態的HashSet集合物件

2.使用Thread-0執行緒往集合中新增1000個元素

3.使用main執行緒往集合中新增1000個元素

4.統計集合的長度

點選檢視程式碼
import java.util.HashSet;
import java.util.concurrent.CopyOnWriteArraySet;


public class MyThread extends Thread{
    //1.建立一個被多個執行緒共享使用靜態的HashSet集合物件
    /*
        HashSet集合是JDK1.2之後出現的,是一個多執行緒不安全的集合
            1.儲存的元素少了
     */
    //public static HashSet<Integer> set = new HashSet<>();

    /*
        java.util.concurrent.CopyOnWriteArraySet<E>集合,是JDK1.5之後出現的
            CopyOnWriteArraySet集合底層採用的是CAS機制,保證多執行緒安全
            CAS機制是樂觀鎖,效率高
     */
    public static CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet<>();

    @Override
    public void run() {
        //2.使用Thread-0執行緒往集合中新增1000個元素
        System.out.println("Thread-0執行緒開始執行執行緒任務了,往集合中新增1000個元素!");
        for (int i = 0; i < 1000; i++) {
            set.add(i);//[0,1,2,...999]
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Thread-0執行緒,新增完畢,執行完執行緒任務了!");
    }
}
點選檢視程式碼
public class Demo01Set {
    public static void main(String[] args) throws InterruptedException {
        //建立MyThread物件,呼叫start方法,開啟一個新的執行緒,執行run方法
        MyThread mt = new MyThread();
        mt.start();

        //main執行緒在開啟新的執行緒之後,會繼續執行main方法中的程式碼
        //3.使用main執行緒往集合中新增1000個元素
        System.out.println("main執行緒往集合中新增1000個元素!");
        for (int i = 1000; i < 2000; i++) {
            MyThread.set.add(i);//[1000,1001,1002,...1999]
            Thread.sleep(1);
        }
        System.out.println("main執行緒,新增1000個元素完畢,睡眠3秒鐘,等待Thread-0執行緒新增完畢!");
        Thread.sleep(3000);

        //4.統計集合的長度
        System.out.println("兩個執行緒都執行完畢,集合的長度為:"+ MyThread.set.size());
    }
}
HashSet集合存在併發問題:
點選檢視程式碼
main執行緒往集合中新增1000個元素!
Thread-0執行緒開始執行執行緒任務了,往集合中新增1000個元素!
Thread-0執行緒,新增完畢,執行完執行緒任務了!
main執行緒,新增1000個元素完畢,睡眠3秒鐘,等待Thread-0執行緒新增完畢!
兩個執行緒都執行完畢,集合的長度為:1975
####3.併發Map集合_ConcurrentHashMap 需求:

1.建立一個被多個執行緒共享使用靜態的HashMap集合物件

2.使用Thread-0執行緒往集合中新增1000個元素

3.使用main執行緒往集合中新增1000個元素

4.統計集合的長度

點選檢視程式碼
import java.util.HashMap;
import java.util.Hashtable;
import java.util.concurrent.ConcurrentHashMap;

public class MyThread extends Thread{
    //1.建立一個被多個執行緒共享使用靜態的HashMap集合物件
    /*
        HashMap集合是JDK1.2之後出現的,是一個多執行緒不安全的集合
            1.儲存的元素少了
     */
    //public static HashMap<Integer,Integer> map = new HashMap<>();

    /*
        java.util.Hashtable<K,V>集合,是JDK1.0時期,最早期的雙列集合
            底層也是雜湊表結構,和HashMap一樣
            不像新的 collection 實現,Hashtable 是同步的
            Hashtable集合使用synchronized同步技術,保證多執行緒安全
            synchronized同步技術是悲觀鎖,效率低
     */
    //public static Hashtable<Integer,Integer> map = new Hashtable<>();

    /*
        java.util.concurrent.ConcurrentHashMap<K,V>集合,是JDK1.5之後出現的
            ConcurrentHashMap集合底層採用的是CAS機制,保證多執行緒安全
            CAS機制是樂觀鎖,效率高
     */
    public static ConcurrentHashMap<Integer,Integer> map = new ConcurrentHashMap<>();

    @Override
    public void run() {
        //2.使用Thread-0執行緒往集合中新增1000個元素
        System.out.println("Thread-0執行緒開始執行執行緒任務了,往集合中新增1000個元素!");
        for (int i = 0; i < 1000; i++) {
            map.put(i,i);//[0-1,1-1,2-2,...999-999]
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Thread-0執行緒,新增完畢,執行完執行緒任務了!");
    }
}
點選檢視程式碼
public class Demo01Map {
    public static void main(String[] args) throws InterruptedException {
        //建立MyThread物件,呼叫start方法,開啟一個新的執行緒,執行run方法
        MyThread mt = new MyThread();
        mt.start();

        //main執行緒在開啟新的執行緒之後,會繼續執行main方法中的程式碼
        //3.使用main執行緒往集合中新增1000個元素
        System.out.println("main執行緒往集合中新增1000個元素!");
        for (int i = 1000; i < 2000; i++) {
            MyThread.map.put(i,i);//[1000-1000,1001-1001,1002-1002,...1999-1999]
            Thread.sleep(1);
        }
        System.out.println("main執行緒,新增1000個元素完畢,睡眠3秒鐘,等待Thread-0執行緒新增完畢!");
        Thread.sleep(3000);

        //4.統計集合的長度
        System.out.println("兩個執行緒都執行完畢,集合的長度為:"+ MyThread.map.size());
    }
}
hashMap集合存在併發問題:
點選檢視程式碼
main執行緒往集合中新增1000個元素!
Thread-0執行緒開始執行執行緒任務了,往集合中新增1000個元素!
Thread-0執行緒,新增完畢,執行完執行緒任務了!
main執行緒,新增1000個元素完畢,睡眠3秒鐘,等待Thread-0執行緒新增完畢!
兩個執行緒都執行完畢,集合的長度為:1980
####4.比較ConcurrentHashMap和Hashtable的效率 Java類庫中,從1.0版本也提供一個執行緒安全的Map:Hashtable Hashtable和ConcurrentHashMap有什麼區別: Hashtable採用的synchronized——悲觀鎖,效率更低。 ConcurrentHashMap:採用的CAS 機制——樂觀鎖,效率更高。

需求:

1.建立一個被多個執行緒共享使用靜態的Hashtable集合(ConcurrentHashMap集合)物件

2.開啟1000個執行緒,每個執行緒往集合中儲存100000個數據

點選檢視程式碼
import java.util.Hashtable;
import java.util.concurrent.ConcurrentHashMap;

/*
    Java類庫中,從1.0版本也提供一個執行緒安全的Map:Hashtable
    Hashtable和ConcurrentHashMap有什麼區別:
    Hashtable採用的synchronized——悲觀鎖,效率更低。
    ConcurrentHashMap:採用的CAS 機制——樂觀鎖,效率更高。
 */
public class MyThread extends Thread{
    //1.建立一個被多個執行緒共享使用靜態的Hashtable集合(ConcurrentHashMap集合)物件
    //public static Hashtable<Integer,Integer> map = new Hashtable<>();
    public static ConcurrentHashMap<Integer,Integer> map = new ConcurrentHashMap<>();

    @Override
    public void run() {
        long s = System.currentTimeMillis();
        //每個執行緒往集合中儲存100000個數據
        for (int i = 0; i < 100000; i++) {
            map.put(i,i);
        }
        long e = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName()+"執行緒往集合中新增10萬個元素,耗時:"+(e-s)+"毫秒");
    }
}
點選檢視程式碼
public class Demo01Map {
    public static void main(String[] args) {
        //開啟1000個執行緒
        for (int i = 0; i < 1000; i++) {
            new MyThread().start();
        }
    }
}
Hashtable效率低下原因: public synchronized V put(K key, V value) public synchronized V get(Object key) Hashtable容器使用synchronized來保證執行緒安全,但線上程競爭激烈的情況下Hashtable的效率非常低下。因為當一個執行緒訪問Hashtable的同步方法,其他執行緒也訪問Hashtable的同步方法時,會進入阻塞狀態。如執行緒1使用put進行元素新增,執行緒2不但不能使用put方法新增元素,也不能使用get方法來獲取元素,所以競爭越激烈效率越低。