JavaSE# 筆記【高併發和執行緒安全 volatile關鍵字 原子類 多行程式碼的執行緒安全問題 併發包】@Gray

小趙同學麼發表於2020-10-27

一.高併發和執行緒安全

1.高併發和執行緒安全

高併發:就是在一段時間內有大量的執行緒要執行. 雙11,春運12306,大學選選修課

​執行緒安全:在高併發的情況下,多個執行緒之間有互相影響的效果。

2.多執行緒記憶體執行機制

當一個執行緒啟動後,JVM會為其分配一個獨立的"執行緒棧區",這個執行緒會在這個獨立的棧區中執行。

  • 看一下簡單的執行緒的程式碼:

1.一個執行緒類:

public class MyThread extends Thread {
 @Override 
 public void run() { 
 	for (int i = 0; i < 100; i++) { 
	 	System.out.println("i = " + i);
  		} 
	 } 
 }
  1. 測試類:
public class Demo {
	 public static void main(String[] args) { 
		 	//1.建立兩個執行緒物件
		 	 MyThread t1 = new MyThread(); 
			 MyThread t2 = new MyThread(); 
			 
			 //2.啟動兩個執行緒 
			 t1.start(); 
			 t2.start(); 
		} 
 }
  • 啟動後,記憶體的執行機制
    在這裡插入圖片描述

3.安全性問題-可見性

  • 介紹
    多個執行緒在執行的過程中,一個執行緒可能看不到另一個執行緒對變數的改變,這個叫執行緒的可見性問題.
  • 程式碼演示
public class AAA extends Thread {
    //定義變數
    static int num = 0;
    @Override
    public void run() {
        System.out.println("AAA執行緒開始執行了");

        //睡眠2秒鐘
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        num = 10;
        System.out.println("AAA執行結束了");
    }
}

public class BBB extends Thread{
    @Override
    public void run() {
        //迴圈
        while(true){
            //判斷
            if(AAA.num == 10){
                System.out.println("BBB執行緒知道數字是10了");
                break;
            }
        }
    }
}

public class Test01 {
    public static void main(String[] args) {
        AAA a = new AAA();
        a.start();

        BBB b = new BBB();
        b.start();
    }
}

圖解:
在這裡插入圖片描述

4.安全性問題-有序性

  • 介紹

    • 在程式的編譯期間,程式可能會把沒有上下邏輯關係的程式碼上下打亂順序這個叫程式碼重排,一個執行緒程式碼的重排可能會對別的執行緒造成影響。

    • 這是一個小概率事件所以無法通過程式碼演示。

  • 圖解
    在這裡插入圖片描述

5.安全性問題-原子性

  • 介紹

    ​ 一個不可分割的語句,在多執行緒的情況下被分割成了多個步驟,導致執行緒之間互相有影響。

  • 程式碼演示

public class CCC extends Thread {
    static int num = 0;

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            num++;
        }
    }
}

public class Test02 {
    public static void main(String[] args) throws InterruptedException {
        //開啟執行緒
        CCC c1 = new CCC();
        c1.start();
        CCC c2 = new CCC();
        c2.start();

        //讓主執行緒睡眠,為了讓迴圈先執行
        Thread.sleep(2000);

        //列印num
        System.out.println(CCC.num);

    }
}

  • 圖解
    在這裡插入圖片描述

二.volatile關鍵字

​ 可以解決可見性有序性問題。用關鍵字修飾變數即可。

​ 關鍵字可以讓執行緒每次都獲取最新值,並且不允許程式碼重排。

//定義變數
static volatile int num = 0;

三.原子類

1.AtomicInteger演示

​ 原子類可以解決可見性 有序性原子性

建立執行緒:

import java.util.concurrent.atomic.AtomicInteger;
public class CCC extends Thread {
    //static int num = 0;
    static AtomicInteger num = new AtomicInteger(0);

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            //num++;
            num.getAndIncrement();
        }
    }
}

測試類:

public class Test02 {
    public static void main(String[] args) throws InterruptedException {
        //開啟執行緒
        CCC c1 = new CCC();
        c1.start();

        CCC c2 = new CCC();
        c2.start();

        //讓主執行緒睡眠,為了讓迴圈先執行
        Thread.sleep(2000);
        //列印num
        System.out.println(CCC.num);

    }
}

2.AtomicInteger工作機制-CAS機制

在這裡插入圖片描述

3.AtomicIntegerArray類示例[瞭解]

陣列儲存的元素在多執行緒的情況下也會有執行緒安全問題。

執行緒類:

import java.util.concurrent.atomic.AtomicIntegerArray;
public class DDD extends Thread {
    //static int[] arr = new int[1000];

    //原子類陣列代替普通數字,小括號裡面寫的也是陣列長度
    static AtomicIntegerArray arr = new AtomicIntegerArray(1000);

    @Override
    public void run() {
        //1000迴圈
        //0 0 0 0 0 0 0 0 0 0 0 0 0
        //1 1 1 1 1 1 1 1 1 1 1 1 1

        //1000 1000 1000 1000 1000 1000
        for (int i = 0; i < 1000; i++) {
            //arr[i]++;
            //給i索引的元素加一
            arr.addAndGet(i,1);
        }
    }
}

測試類

import java.util.Arrays;
public class Test03 {
    public static void main(String[] args) throws InterruptedException {

        //開啟了100個執行緒
        for (int i = 0; i < 1000; i++) {
            DDD d = new DDD();
            d.start();
        }

        //睡眠2秒鐘
        Thread.sleep(2000);

        //列印陣列
        //System.out.println(Arrays.toString(DDD.arr));
        System.out.println(DDD.arr);
    }
}

四.多行程式碼的執行緒安全問題【重點】

1.執行順序的問題

  • 火車賣票問題

執行緒類:

public class AAA implements Runnable {

    //定義火車站一共有100張票
    int ticket = 100;
    @Override
    public void run() {
        //迴圈
        while(true){
            //判斷有沒有票
            if(ticket <= 0){
                break;
            }

            //如果有票就賣票
            System.out.println(Thread.currentThread().getName() +  "賣出了" + ticket + "號票");
            //給票減一
            ticket--;
        }
    }
}

測試類:

public class Test01 {
    public static void main(String[] args) {
        AAA a = new AAA();
        //開啟執行緒
        Thread t1 = new Thread(a);
        t1.setName("視窗一");
        t1.start();

        //開啟執行緒
        Thread t2 = new Thread(a);
        t2.setName("視窗二");
        t2.start();


    }
}
  • 圖解
    在這裡插入圖片描述

2.synchronized關鍵字

表示同步,同步的意思是一個執行緒在執行的時候,別的執行緒只能等待。一個執行緒執行結束之後別的執行緒才能執行。

3.同步程式碼塊

  • 格式

    synchronized(鎖物件){
        同步程式碼
    }
    
  • 同步鎖

    • 鎖物件可以是任意型別的物件
    • 多個執行緒如果要同步必須使用同一個物件作為鎖
  • 程式碼演示

執行緒實現類:

public class AAA implements Runnable {

    //定義火車站一共有100張票
    int ticket = 100;

    @Override
    public void run() {
        //迴圈
        while(true){
            //同步程式碼塊
            synchronized ("abc") {
                //判斷有沒有票
                if (ticket <= 0) {
                    break;
                }

                //先獲取當前執行緒物件,再獲取執行緒名字
                String name = Thread.currentThread().getName();
                //如果有票就賣票
                System.out.println(name + "賣出了" + ticket + "號票");

                //給票減一
                ticket--;
            }
        }
    }
}

測試類:

public class Test01 {
    public static void main(String[] args) {
        AAA a = new AAA();
        //開啟執行緒
        Thread t1 = new Thread(a);
        t1.setName("視窗一");
        t1.start();

        //開啟執行緒
        Thread t2 = new Thread(a);
        t2.setName("視窗二");
        t2.start();
    }
}

4.同步方法

  • 格式

    public synchronized void method(){
        同步程式碼
    }
    
  • 同步方法的鎖物件

    • 同步方法裡面也是有鎖物件的,但是鎖物件不需要我們指定,同步方法的鎖物件是固定的。
    • 非靜態同步方法:this (代表當前類的物件)
    • 靜態同步方法: 類的位元組碼物件(每一個類只有一個.class物件,所以這個物件一定是唯一的)
  • 程式碼演示

執行緒實現類:

public class AAA implements Runnable {

    //定義火車站一共有100張票
    int ticket = 100;

    @Override
    public void run() {
        //迴圈
        while(true){
            //判斷有沒有票
            if(ticket <= 0){
                break;
            }
            //呼叫方法
            method();
        }
    }
    //定義同步方法
    public  synchronized void method(){
        //再次判斷
        if(ticket > 0) {
            //先獲取當前執行緒物件,再獲取執行緒名字
            String name = Thread.currentThread().getName();
            //如果有票就賣票
            System.out.println(name + "賣出了" + ticket + "號票");

            //給票減一
            ticket--;
        }
    } 
}

測試類:

public class Test01 {
    public static void main(String[] args) {

        AAA a = new AAA();
        //開啟執行緒
        Thread t1 = new Thread(a);
        t1.setName("視窗一");
        t1.start();

        //開啟執行緒
        Thread t2 = new Thread(a);
        t2.setName("視窗二");
        t2.start();

    }
}

5.Lock鎖

​ Lock鎖的方式更符合物件導向的呼叫方式。更符合程式設計師的寫程式碼習慣。

​ Lock是一個介面,有一個子類ReentrantLock

  • 兩個方法
 public void lock()   :加同步鎖 
 public void unlock() :釋放同步鎖
  • 程式碼演示

執行緒實現類:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class AAA implements Runnable {
    //定義火車站一共有100張票
    int ticket = 100;
    //建立鎖物件
    Lock lock = new ReentrantLock();
    
    public void run() {
        //迴圈
        while(true){
            //加鎖
            lock.lock();
            //判斷有沒有票
            if(ticket <= 0){
                lock.unlock();
                break;
            }
            //先獲取當前執行緒物件,再獲取執行緒名字
            String name = Thread.currentThread().getName();
            //如果有票就賣票
            System.out.println(name +  "賣出了" + ticket + "號票");
            //給票減一
            ticket--;
            //解鎖
            lock.unlock();
        }
    }
}

測試類:

public class Test01 {
    public static void main(String[] args) {

        AAA a = new AAA();
        //開啟執行緒
        Thread t1 = new Thread(a);
        t1.setName("視窗一");
        t1.start();

        //開啟執行緒
        Thread t2 = new Thread(a);
        t2.setName("視窗二");
        t2.start();

    }
}

五.併發包

之前學習的集合型別都是會有執行緒安全問題的,如果遇到了多執行緒情況的,需要用併發包解決問題。

1.CopyOnWriteArrayList[瞭解]

  • ArrayList和CopyOnWriteArrayList效果演示

執行緒實現類:

import java.util.ArrayList;
import java.util.concurrent.CopyOnWriteArrayList;

public class AAA extends Thread {

    //定義集合
    //static ArrayList<Integer> list = new ArrayList<>();
    //使用併發包集合
    static CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();

    @Override
    public void run() {
        //給集合新增10000個元素
        for (int i = 0; i < 10000; i++) {
            list.add(i);
        }
    }
}

測試類:

public class Test01 {
    public static void main(String[] args) throws InterruptedException {

        //開啟執行緒
        new AAA().start();
        new AAA().start();

        //讓主執行緒睡眠讓迴圈先執行
        Thread.sleep(2000);

        //列印集合長度
        System.out.println(AAA.list.size());

        //出現問題一:可能長度小於20000
        //出現問題二:可能出現索引越界異常

    }
}

2.CopyOnWriteArraySet[瞭解]

  • HashSet和CopyOnWriteArraySet效果演示

執行緒實現類:

import java.util.HashSet;
import java.util.concurrent.CopyOnWriteArraySet;
public class BBB extends Thread {
    //定義集合
    //static HashSet<Integer> set = new HashSet<>();
    //定義併發包集合
    static CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet<>();
    @Override
    public void run() {
        //給集合新增10000個元素
        for (int i = 0; i < 10000; i++) {
            set.add(i);
        }
    }
}

測試類:

public class Test02 {
    public static void main(String[] args) throws InterruptedException {
        //開啟執行緒
        new BBB().start();
        new BBB().start();

        //讓主執行緒睡眠讓迴圈先執行
        Thread.sleep(2000);
        //列印集合長度
        System.out.println(BBB.set.size());

        //出現問題:集合的長度大於10000
    }
}

3.ConcurrentHashMap

1. HashMap和Hashtable和ConcurrentHashMap效果演示

執行緒實現類:

import java.util.HashMap;
import java.util.Hashtable;
import java.util.concurrent.ConcurrentHashMap;

public class CCC extends Thread {

    //建立集合
    //static HashMap<Integer,Integer> map = new HashMap<>();

    //使用Hashtable集合
    //static Hashtable<Integer,Integer> map = new Hashtable<>();

    //使用ConcurrentHashMap集合
    static ConcurrentHashMap<Integer,Integer> map = new ConcurrentHashMap<>();

    @Override
    public void run() {
        //給集合新增10000個元素
        for (int i = 0; i < 10000; i++) {
            map.put(i,i);
        }
    }
}

測試類:

public class Test03 {
    public static void main(String[] args) throws InterruptedException {
        //開啟執行緒
        new CCC().start();
        new CCC().start();

        //讓主執行緒睡眠
        Thread.sleep(2000);

        //列印集合長度
        System.out.println(CCC.map.size());

        //出現問題:集合的長度大於10000
    }
}

2.Hashtable和ConcurrentHashMap的速度區別【重點】

  • Hashtable執行速度慢
  • ConcurrentHashMap執行速度快

執行緒實現類:

import java.util.HashMap;
import java.util.Hashtable;
import java.util.concurrent.ConcurrentHashMap;
public class CCC extends Thread {
    //建立集合
    //static HashMap<Integer,Integer> map = new HashMap<>();
    //使用Hashtable集合
//    static Hashtable<Integer,Integer> map = new Hashtable<>();
    //使用ConcurrentHashMap集合
    static ConcurrentHashMap<Integer,Integer> map = new ConcurrentHashMap<>();

    @Override
    public void run() {
        //獲取系統當前時間
        long time1 = System.currentTimeMillis();

        //給集合新增10000個元素
        for (int i = 0; i < 10000; i++) {
            map.put(i,i);
        }
        //獲取系統當前時間
        long time2 = System.currentTimeMillis();

        System.out.println( (time2-time1)  + "毫秒");
    }
}

測試類:

public class Test04 {
    public static void main(String[] args) {
        //建立1000個執行緒
        for (int i = 0; i < 1000; i++) {
            new CCC().start();
        }
    }
}

3.速度區別的原因

  • Hashtable方法都是同步的,一個執行緒在執行的時候,別的執行緒只能等待。
public synchronized V put(K key, V value) {
  • ConcurrentHashMap用到了CAS機制部分同步程式碼塊.

4.悲觀鎖和樂觀鎖

  • CAS機制稱為樂觀鎖,執行效率
  • 同步機制稱為悲觀鎖,執行效率

相關文章