關於線程安全問題,這一篇應該是全網講的最明白的了!

線程安全與不安全

線程安全:當多線程訪問時,採用了加鎖的機制;即當一個線程訪問該類的某一個數據時,會對這個數據進行保護,其餘線程不能對其訪問,直到該線程讀取結束以後,其餘線程纔可使用。防止出現數據不一致或者數據被污染的狀況。
線程不安全:多個線程同時操做某個數據,出現數據不一致或者被污染的狀況。java

代碼示例:面試

package thread_5_10;

public class Demo26 {
    static  int  a = 0;
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100_0000; i++) {
                    a++;
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100_0000; i++) {
                    a--;
                }
            }
        });

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

        //等待線程完成
        //t1.join();
        //t2.join();
        while(t1.isAlive() || t2.isAlive()){

        }
        System.out.println(a);
    }
}

運行結果:安全

493612

結果分析:多線程



我的整理了一些資料,有須要的朋友能夠直接點擊領取。架構

[Java基礎知識大全](https://jq.qq.com/?_wv=1027&k...
)jvm

[22本Java架構師核心書籍](https://jq.qq.com/?_wv=1027&k...
)ide

[從0到1Java學習路線和資料](https://jq.qq.com/?_wv=1027&k...
)工具

[1000+道2021年最新面試題](https://jq.qq.com/?_wv=1027&k...學習

線程不安全的因素:

CPU是搶佔式執行的(搶佔資源)
多個線程操做的是同一個變量
可見性
非原子性
編譯期優化(指令重排)優化

volatile

volatile是指令關鍵字,做用是確保本指令不會因編譯期優化而省略,且每次要求直接讀值。能夠解決內存不可見和指令重排序的問題,可是不能解決原子性問題

解決線程不安全

有兩種加鎖方式:

synchronized(jvm層的解決方案)
Lock手動鎖

synchronized

操做鎖的流程

嘗試獲取鎖a
使用鎖(這一步驟是具體的業務代碼)
釋放鎖
synchronized是JVM層面鎖的解決方案,它幫咱們實現了加鎖和釋放鎖的過程

代碼示例

package thread_5_10;

public class Demo31 {
    //循環的最大次數
    private final static int maxSize = 100_0000;

    //定義全局變量
    private static int number = 0;

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

        //聲明鎖對象
        Object obj = new Object();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < maxSize; i++) {
                    //實現加鎖
                    synchronized (obj){
                        number++;
                    }

                }
            }
        });

        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < maxSize; i++) {
                    synchronized (obj){
                        number--;
                    }

                }
            }
        });

        t2.start();

        //等待兩個線程執行完成
        t1.join();
        t2.join();

        System.out.println(number);
    }
}

運行結果:

0

解析:

注意

synchronized實現分爲:

操做系統層面,它是依靠互斥鎖mutex
針對JVM,monitor實現
針對Java語言來講,是將鎖信息存放在對象頭中

三種使用場景

使用synchronized修飾代碼塊,(能夠對任意對象加鎖)
使用synchronized修飾靜態方法(對當前類進行加鎖)
使用synchronized修飾普通方法(對當前類實例進行加鎖)
修飾靜態方法:

package thread_5_10;

public class Demo32 {

    private static int number = 0;
    private static final int maxSize = 100_0000;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                increment();
            }
        });
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                decrement();
            }
        });
        t2.start();

        t1.join();
        t2.join();
        System.out.println("最終結果爲:"+number);
    }

    public synchronized static void increment(){
        for (int i = 0; i < maxSize; i++) {
            number++;
        }
    }

    public synchronized static void decrement(){
        for (int i = 0; i < maxSize; i++) {
            number--;
        }
    }
}

修飾實例方法:

package thread_5_10;

public class Demo33 {

    private static int number = 0;
    private static final int maxSize = 100_0000;

    public static void main(String[] args) throws InterruptedException {
        Demo33 demo = new Demo33();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                demo.increment();
            }
        });
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                demo.decrement();
            }
        });
        t2.start();

        t1.join();
        t2.join();
        System.out.println("最終結果:"+number);
    }

    public synchronized  void increment(){
        for (int i = 0; i < maxSize; i++) {
            number++;
        }
    }

    public synchronized  void decrement(){
        for (int i = 0; i < maxSize; i++) {
            number--;
        }
    }
}

Lock手動鎖

代碼示例:

package thread_5_10;

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

public class Demo34 {

    private static int number = 0;
    private static final int maxSize = 100_0000;

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

        //建立lock實例
        Lock lock = new ReentrantLock();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < maxSize; i++) {
                    lock.lock();
                    try{
                        number++;
                    }finally {
                        lock.unlock();
                    }
                }
            }
        });
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < maxSize; i++) {
                    lock.lock();
                    try{
                        number--;
                    }finally {
                        lock.unlock();
                    }
                }
            }
        });
        t2.start();

        t1.join();
        t2.join();
        System.out.println("最終結果爲-->   "+number);
    }
}

運行結果:

最終結果爲-->   0

注意事項:

lock()必定要放在try外面

若是放在try裏面,若是try裏面出現異常,尚未加鎖成功就執行finally裏面的釋放鎖的代碼,就會出現異常
若是放在try裏面,若是沒有鎖的狀況下釋放鎖,這個時候產生的異常就會把業務代碼裏面的異常給吞噬掉,增長代碼調試的難度

公平鎖與非公平鎖

公平鎖:當一個線程釋放鎖以後,須要主動喚醒「須要獲得鎖」的隊列來獲得鎖
非公平鎖:當一個線程釋放鎖以後,另外一個線程恰好執行到獲取鎖的代碼就能夠直接獲取鎖
java語言中,全部鎖的默認實現方式都是非公平鎖

1.synchronized是非公平鎖
2.reentrantLock默認是非公平鎖,但也能夠顯示地聲明爲公平鎖

顯示聲明公平鎖格式:

ReentrantLock源碼:

示例一:

package thread_5_10;

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

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

        Lock lock = new ReentrantLock(true);

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    lock.lock();
                    try{
                        System.out.println("線程1");
                    }finally {
                        lock.unlock();
                    }
                }
            }
        });



        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    lock.lock();
                    try{
                        System.out.println("線程2");
                    }finally {
                        lock.unlock();
                    }
                }
            }
        });

        Thread.sleep(1000);
        t1.start();
        t2.start();

    }
}

運行結果:

示例二:

package test;

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

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

        Lock lock = new ReentrantLock(true);

        Runnable r = new Runnable() {
            @Override
            public void run() {
                for(char ch: "ABCD".toCharArray()){
                    lock.lock();
                    try{
                        System.out.print(ch);
                    }finally {
                        lock.unlock();
                    }
                }
            }
        };

        Thread.sleep(100);
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        t1.start();
        t2.start();
    }
}

運行結果:

AABBCCDD

兩種鎖區別

synchronized和lock的區別

關鍵字不一樣
synchronized自動進行加鎖和釋放鎖,而Lock須要手動加鎖和釋放鎖
synchronized是JVM層面上的實現,而Lock是Java層面鎖的實現
修飾範圍不一樣,synchronized能夠修飾代碼塊,靜態方法,實例方法,而Lock只能修飾代碼塊
synchronized鎖的模式是非公平鎖,而lock鎖的模式是公平鎖和非公平鎖
Lock的靈活性更高

死鎖

死鎖定義

在兩個或兩個以上的線程運行中,由於資源搶佔而形成線程一直等待的問題

當線程1擁有資源並1且試圖獲取資源2和線程2擁有了資源2,而且試圖獲取資源1的時候,就發了死鎖

死鎖示例

package thread_5_11;

public class Demo36 {
    public static void main(String[] args) {
        //聲明加鎖的資源
        Object lock1 = new Object();
        Object lock2 = new Object();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //獲取線程名稱
                String threadName = Thread.currentThread().getName();

                //1.獲取資源1
                synchronized (lock1){
                    System.out.println(threadName+" 獲取到了lock1");
                    try {

                        //2.等待1ms,讓線程t1和線程t2都獲取到相應的資源
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(threadName+" waiting lock2");

                    //3.獲取資源2
                    synchronized (lock2){
                        System.out.println(threadName+" 獲取到了lock2");
                    }
                }
            }
        },"t1");
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                String threadName = Thread.currentThread().getName();
                synchronized (lock2){
                    System.out.println(threadName+" 獲取到了lock2");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(threadName+" waiting lock1");
                    synchronized (lock1){
                        System.out.println(threadName+" 獲取到了lock1");
                    }
                }
            }
        },"t2");

        t2.start();
    }
}

運行結果:

經過工具來查看死鎖:
(1)jdk–>bin–>jconsole.exe


(2)jdk–>bin–>jvisualvm.exe

(3)jdk–>bin–>jmc.exe

死鎖的4個必要條件

1.互斥條件:當資源被一個線程擁有以後,就不能被其餘的線程擁有了
2.佔有且等待:當一個線程擁有了一個資源以後又試圖請求另外一個資源
3.不可搶佔:當一個資源被一個線程被擁有以後,若是不是這個線程主動釋放此資源的狀況下,其餘線程不能擁有此資源
4.循環等待:兩個或兩個以上的線程在擁有了資源以後,試圖獲取對方資源的時候造成了一個環路

線程通信

所謂的線程通信就是在一個線程中的操做能夠影響另外一個線程,wait(休眠線程),notify(喚醒一個線程),notifyall(喚醒全部線程)

wait方法

注意事項:
1.wait方法在執行以前必須先加鎖。也就是wait方法必須配合synchronized配合使用
2.wait和notify在配合synchronized使用時,必定要使用同一把鎖

運行結果:

wait以前
主線程喚醒t1
wait以後

多線程

package thread_5_13;

public class demo40 {
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //調用wait方法以前必須先加鎖
                synchronized (lock){
                    try {
                        System.out.println("t1 wait以前");
                        lock.wait();
                        System.out.println("t1 wait以後");

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

            }
        },"t1");


        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                //調用wait方法以前必須先加鎖
                synchronized (lock){
                    try {
                        System.out.println("t2 wait以前");
                        lock.wait();
                        System.out.println("t2 wait以後");

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

            }
        },"t2");

        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                //調用wait方法以前必須先加鎖
                synchronized (lock){
                    try {
                        System.out.println("t3 wait以前");
                        lock.wait();
                        System.out.println("t3 wait以後");

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

            }
        },"t3");

        t1.start();
        t2.start();
        t3.start();
        Thread.sleep(1000);
        System.out.println("主線程調用喚醒操做");

        //在主線程中喚醒
        synchronized (lock){
            lock.notify();
        }
    }
}

運行結果:

t1 wait以前
t2 wait以前
t3 wait以前
主線程調用喚醒操做
t1 wait以後

注意事項:

將lock.notify()修改成lock.notifyAll(),則三個線程都能被喚醒
wait在不傳遞任何參數的狀況下會進入waiting狀態(參數爲0也是waiting狀態);當wait裏面有一個大於0的整數時,它就會進入timed_waiting狀態
關於wait和sleep釋放鎖的代碼:

wait在等待的時候能夠釋放鎖,sleep在等待的時候不會釋放鎖

wait方法與sleep方法對比

相同點:
(1)wait和sleep均可以使線程休眠
(2)wait和sleep在執行的過程當中均可以接收到終止線程執行的通知

不一樣點:
(1)wait必須synchronized一塊兒使用,而sleep不用
(2)wait會釋放鎖,sleep不會釋放鎖
(3)wait是Object的方法,而sleep是Thread的方法
(4)默認狀況下,wait不傳遞參數或者參數爲0的狀況下,它會進入waiting狀態,而sleep會進入timed_waiting狀態
(5)使用wait能夠主動喚醒線程,而使用sleep不能主動喚醒線程

面試題

1.問:sleep(0)和wait(0)有什麼區別
答:(1)sleep(0)表示過0毫秒後繼續執行,而wait(0)會一直等待
(2)sleep(0)表示從新觸發一次CPU競爭

2.爲何wait會釋放鎖,而sleep不會釋放鎖
答:sleep必需要傳遞一個最大等待時間的,也就是說sleep是可控的(對於時間層面來說),而wait是能夠不傳遞時間,從設計層面來說,若是讓wait這個沒有超時等待時間的機制下釋放鎖的話,那麼線程可能會一直阻塞,而sleep不會存在這個問題

3.爲何wait是Object的方法,而sleep是Thread的方法
答:wait須要操做鎖,而鎖是對象級別(全部的鎖都在對象頭當中),它不是線程級別,一個線程能夠有多把鎖,爲了靈活起見,全部把wait放在Object當中

4.解決wait/notify隨機喚醒的問題
答:可使用LockSupport中的park,unpark方法,注意:locksupport雖然不會報interrupted的異常,可是能夠監聽到線程終止的指令

最後

都看到這裏了,記得點個贊哦!