天天看點

java并發安全

java并發安全

本次内容主要線程的安全性、死鎖相關知識點。

1、什麼是線程安全性

1.1 線程安全定義

前面使用8個篇幅講到了Java并發程式設計的知識,那麼我們有沒有想過什麼是線程的安全性?在《Java并發程式設計實戰》中定義如下:當多個線程通路某個類時,不管運作時環境采用何種排程方式或者這些線程将如何交替執行,并且在調用代碼中不需要任何額外的同步或者協同,這個類都能表現出正确的行為,那麼就稱這個類是線程安全的。

1.2 無狀态類

沒有任何成員變量的類,就叫無狀态類,這種類一定是線程安全的。但是有一種情況是,這個類方法的參數中用到了對象,看下面的代碼:

public class StatelessClass {

public void test(User user) {
    //do business
}           

}

此時這個類還是線程安全的嗎?那肯定也是,為什麼呢?因為多線程下的使用,固然user這個對象的執行個體會不正常,但是對于StatelessClass這個類的對象執行個體來說,它并不持有User的對象執行個體,它自己并不會有問題,有問題的是User這個類,而非StatelessClass本身。

1.2 volatile

并不能保證類的線程安全性,隻能保證類的可見性,最适合一個線程寫,多個線程讀的情景。

1.3 鎖和CAS

我們最常使用的保證線程安全的手段,使用synchronized關鍵字,使用顯式鎖,使用各種原子變量,修改資料時使用CAS機制等等。

1.4 ThreadLocal

ThreadLocal是實作線程封閉的最好方法。關于ThreadLocal如何保證線程的安全性,請閱讀《java線程間的共享》,裡面有詳細的介紹。

1.5 安全的釋出

1)類中持有的成員變量,如果是基本類型,釋出出去,并沒有關系,因為釋出出去的其實是這個變量的一個副本。看下面的代碼:

public class SafePublish {

private int number;

public SafePublish() {
    number = 2;
}

public int getNumber() {
    return number;
}

public static void main(String[] args) {
    SafePublish safePublish = new SafePublish();
    int result = safePublish.getNumber();
    System.out.println("before modify, result = " + result);
    result = 3;
    System.out.println("before modify, result =" + result);
    System.out.println("getNumber() = " + safePublish.getNumber());
}           

從程式輸出可以看到,number的值并沒被改變,因為result隻是一個副本,這樣的成員變量釋出出去是安全的。

2)如果類中持有的成員變量是對象的引用,如果這個成員對象不是線程安全的,通過get等方法釋出出去,會造成這個成員對象本身持有的資料在多線程下不正确的修改,進而造成整個類線程不安全的問題。看下面代碼:

public class UnSafePublish {

private final User user = new User();

public User getUser() {
    return user;
}

public static void main(String[] args) {
    UnSafePublish unSafePublish = new UnSafePublish();
    User user = unSafePublish.getUser();
    System.out.println("before modify, user = " + unSafePublish.getUser());
    user.setAge(88);
    System.out.println("after modify, user = " + unSafePublish.getUser());
}

static class User {
    private int age;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "UserVo[" +
                "age=" + age +
                ']';
    }
}           

從程式輸出可以看到,user對象的内容發生了改變,如果多個線程同時操作,user對象在堆中的資料是不可預知的。

那麼這個問題應該怎麼處理呢?我們在釋出這對象出去的時候,就應該用線程安全的方式包裝這個對象。對于我們自己使用或者聲明的類,JDK自然沒有提供這種包裝類的辦法,但是我們可以仿造這種模式或者委托給線程安全的類,當然,對這種通過get等方法釋出出去的對象,最根本的解決辦法還是應該在實作上就考慮到線程安全問題。對上面的代碼進行改造:

public class SafePublicUser {

private final User user;

public User getUser() {
    return user;
}

public SafePublicUser(User user) {
    this.user = new SynUser(user);
}

/**
 * 線程安全的類,将内部成員對象進行線程安全包裝
 */
static class SynUser extends User {
    private final User user;
    private final Object lock = new Object();

    public SynUser(User user) {
        this.user = user;
    }

    @Override
    public int getAge() {
        synchronized (lock) {
            return user.getAge();
        }
    }

    @Override
    public void setAge(int age) {
        synchronized (lock) {
            user.setAge(age);
        }
    }
}

static class User {
    private int age;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "UserVo[" +
                "age=" + age +
                ']';
    }
}           

2、死鎖

 2.1 死鎖定義

死鎖的發生必須具備以下四個必要條件:

1)互斥條件:指程序對所配置設定到的資源進行排它性使用,即在一段時間内某資源隻由一個程序占用。如果此時還有其它程序請求資源,則請求者隻能等待,直至占有資源的程序用畢釋放。

2)請求和保持條件:指程序已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它程序占有,此時請求程序阻塞,但又對自己已獲得的其它資源保持不放。

3)不剝奪條件:指程序已獲得的資源,在未使用完之前,不能被剝奪,隻能在使用完時由自己釋放。

4)環路等待條件:指在發生死鎖時,必然存在一個程序——資源的環形鍊,即程序集合{P0,P1,P2,···,Pn}中的P0正在等待一個P1占用的資源;P1正在等待P2占用的資源,……,Pn正在等待已被P0占用的資源。

舉個例子來說明:

老王和老宋去大保健,老王搶到了1号技師,擅長頭部按摩,老宋搶到了2号技師,擅長洗腳。但是老王和老宋都想同時洗腳和頭部按摩,于是互不相讓,老王搶到了1号,還想要2号,老宋搶到了2号,還想要1号。在洗腳和頭部按摩這個事情上老王和老宋就産生了死鎖,怎麼樣可以解決這個問題呢?

方案1:老闆了解到情況,派3号技師過來,3号技師擅長頭部按摩,老王隻有一個頭,是以3号隻能給老宋服務,這個時候死鎖就被打破。

方案2:大保健會所的老闆比較霸道,規定了隻能先頭部按摩,再洗腳。這種情況下,老王和老宋誰先搶到1号,誰就先享受,另一個沒搶到的就等着,這種情況也不會産生死鎖。

對死鎖做一個通俗易懂的總結:

死鎖是必然發生在多個操作者(M>=2)情況下,争奪多個資源(N>=2,且M>=N)才會發生這種情況。很明顯,單線程不會有死鎖,隻有老王一個去,1号2号都歸他,沒人跟他搶。單資源呢?隻有1号,老王和老宋也隻會産生激烈競争,打得不可開交,誰搶到就是誰的,但不會産生死鎖。同時,死鎖還有兩個重要的條件,争奪資源的順序不對,如果争奪資源的順序是一樣的,也不會産生死鎖,另一個條件就是,争奪者拿到資源後不放手。

2.2 死鎖的危害

一旦程式中出現了死鎖,危害是非常緻命的,大緻有以下幾個原因:

1)線程不工作了,但是整個程式還是活着的。

2)沒有任何的異常資訊可以供我們檢查。

3)程式發生了發生了死鎖,是沒有任何的辦法恢複的,隻能重新開機程式,對生産平台的程式來說,這是個很嚴重的問題。

2.3 死鎖的例子

上面講了那麼多關于死鎖的概念,現在直接撸一段死鎖代碼看看。

public class DeadLockDemo {

private static Object No1 = new Object();
private static Object No2 = new Object();

/***
 * 老王搶到了1号,還想要2号
 * @throws InterruptedException
 */
private static void laowang() throws InterruptedException {
    String threadName = Thread.currentThread().getName();
    synchronized (No1) {
        System.out.println(threadName + " get NO1");
        Thread.sleep(100);
        synchronized (No2) {
            System.out.println(threadName + " get NO2");
        }
    }
}

/***
 * 老宋搶到了2号,還想要1号
 * @throws InterruptedException
 */
private static void laosong() throws InterruptedException {
    String threadName = Thread.currentThread().getName();
    synchronized (No2) {
        System.out.println(threadName + " get NO2");
        Thread.sleep(100);
        synchronized (No1) {
            System.out.println(threadName + " get NO1");
        }
    }
}

private static class Laowang extends Thread {
    private String name;

    public Laowang(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        Thread.currentThread().setName(name);
        try {
            laowang();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

private static class Laosong extends Thread {
    private String name;

    public Laosong(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        Thread.currentThread().setName(name);
        try {
            laosong();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

public static void main(String[] args) throws InterruptedException {
    Laosong laosong = new Laosong("laosong");
    laosong.start();

    Laowang laowang = new Laowang("laowang");
    laowang.start();

    Thread.sleep(10);
}           

程式輸出可以看到,老宋搶到了2号,老王搶到了1号,因為産生了死鎖,程式沒有結束,但是并沒有往下執行。

2.4 死鎖的定位

通過JDK的jps檢視應用的id,再使用jstack檢視應用持有鎖的情況。

可以看到"laowang"這個線程持有了<0x000000076b393b78>鎖,還想獲得<0x000000076b393b88>鎖;"laosong"這個線程持有了<0x000000076b393b88>鎖,還想擷取<0x000000076b393b78>鎖。

 2.5 死鎖的解決方案

1)保證拿鎖的順序一緻,内部通過順序比較,确定拿鎖的順序。

2)采用嘗試拿鎖的機制。

我們分别用這2種解決方案來改造上面死鎖的代碼,先看方案1:

public class NormalLockDemo {

private static Object No1 = new Object();
private static Object No2 = new Object();

/**
 * 按照No1、No2順序加鎖
 * @throws InterruptedException
 */
private static void laowang() throws InterruptedException {
    String threadName = Thread.currentThread().getName();
    synchronized (No1) {
        System.out.println(threadName + " get NO1");
        Thread.sleep(100);
        synchronized (No2) {
            System.out.println(threadName + " get NO2");
        }
    }
}

/**
 * 按照No1、No2順序加鎖
 * @throws InterruptedException
 */
private static void laosong() throws InterruptedException {
    String threadName = Thread.currentThread().getName();
    synchronized (No1) {
        System.out.println(threadName + " get NO1");
        Thread.sleep(100);
        synchronized (No2) {
            System.out.println(threadName + " get NO2");
        }
    }
}

static class Laowang extends Thread {
    private String name;

    public Laowang(String name) {
        this.name = name;
    }

    @Override
    public void run() {

        Thread.currentThread().setName(name);
        try {
            laowang();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

static class Laosong extends Thread {
    private String name;

    public Laosong(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        Thread.currentThread().setName(name);
        try {
            laosong();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

public static void main(String[] args) throws InterruptedException {
    Laosong laosong = new Laosong("laosong");
    laosong.start();

    Laowang laowang = new Laowang("laowang");
    laowang.start();

    Thread.sleep(1000);
    System.out.println("2個人都完成了大保健");
}           

從程式輸出可以看到,通過順序拿鎖的方式,2個人都完成了大保健,解決了死鎖問題。

再看方案2,使用ReentrantLock采用嘗試擷取鎖的方式,如果對ReentrantLock不熟悉,歡迎閱讀《java之AQS和顯式鎖》。

import java.util.Random;

import java.util.concurrent.locks.Lock;

import java.util.concurrent.locks.ReentrantLock;

public class TryLock {

private static Lock No1 = new ReentrantLock();
private static Lock No2 = new ReentrantLock();

/***
 * 先嘗試拿No1鎖,再嘗試拿No2鎖,No2鎖沒拿到,連同No1鎖一起釋放掉
 * @throws InterruptedException
 */
private static void laowang() throws InterruptedException {
    String threadName = Thread.currentThread().getName();
    Random r = new Random();
    while (true) {
        if (No1.tryLock()) {
            try {
                System.out.println(threadName + " get NO2");
                if (No2.tryLock()) {
                    try {
                        System.out.println(threadName + " get NO1");
                        break;
                    } finally {
                        No2.unlock();
                    }
                }
            } finally {
                No1.unlock();
            }
        }
        Thread.sleep(r.nextInt(5));
    }
}

/**
 * 先嘗試拿No2鎖,再嘗試拿No1鎖,No1鎖沒拿到,連同No2鎖一起釋放掉
 *
 * @throws InterruptedException
 */
private static void laosong() throws InterruptedException {
    String threadName = Thread.currentThread().getName();
    Random r = new Random();
    while (true) {
        if (No2.tryLock()) {
            try {
                System.out.println(threadName + " get NO2");
                if (No1.tryLock()) {
                    try {
                        System.out.println(threadName + " get NO1");
                        break;
                    } finally {
                        No1.unlock();
                    }
                }
            } finally {
                No2.unlock();
            }
        }
    }
    Thread.sleep(r.nextInt(5));

}

static class Laowang extends Thread {
    private String name;

    public Laowang(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        Thread.currentThread().setName(name);
        try {
            laowang();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

static class Laosong extends Thread {
    private String name;

    public Laosong(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        Thread.currentThread().setName(name);
        try {
            laosong();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
           
public static void main(String[] args) throws InterruptedException {
    Laosong laosong = new Laosong("laosong");
    laosong.start();

    Laowang laowang = new Laowang("laowang");
    laowang.start();

    Thread.sleep(1000);
    System.out.println("2個人都完成了大保健");
}           

從程式輸出可以看到,laowang線程搶到了NO2這把鎖,但是在擷取NO1的時候失敗了,是以把NO2也釋放了。這樣做就使得2個線程都可以擷取到鎖,不會有死鎖問題産生。

 3、結語

本篇幅就介紹這麼多内容,希望大家看了有收獲。Java并發程式設計專題要分享的内容到此就結束了,下一個專題将介紹Java性能優化和JVM相關内容,閱讀過程中如發現描述有誤,請指出,謝謝。

原文位址

https://www.cnblogs.com/hongshaodian/p/12452188.html