什么是線程安全?怎么樣才能做到線程安全?


什么是線程安全?

當多個線程訪問某個類時,不管運行時環境采用何種調度方式或者這些線程將如何交替執行,並且在調用代碼中不需要任何額外的同步或者協同,這個類都能表現出正確的行為,那么就稱這個類是線程安全的。

怎么樣才能做到線程安全?

 

解決線程安全的方案:

1.基於JVM的鎖

  無法解決分布式情況的問題

2.基於數據庫的鎖(分布式)

  耗費資源

3.基於redis的鎖(分布式)

  可能會出現死鎖

4.基於zookeeper的鎖(分布式)

  最優級

 

  實現好的並發是一件困難的事情,所以很多時候我們都想躲避並發。從下面幾點可以避免並發:

  • 線程封閉
  • 無狀態的類
  • 讓類不可變
  • volatile
  • 加鎖和CAS
  • 安全的發布
  • ThreadLocal

線程封閉

什么是線程封閉?

就是把對象封裝到一個線程里,只有這一個線程能看到此對象。那么這個對象就算不是線程安全的也不會出現任何安全問題。

實現線程封閉有哪些方法?

ad-hoc 線程封閉

這是完全靠實現者控制的線程封閉,他的線程封閉完全靠實現者實現。

Ad-hoc 線程封閉非常脆弱,應該盡量避免使用。

棧封閉

棧封閉是我們編程當中遇到的最多的線程封閉。

什么是棧封閉呢?

簡單的說就是局部變量。

多個線程訪問一個方法,此方法中的局部變量都會被拷貝一份到線程棧中。所以局部變量是不被多個線程所共享的,也就不會出現並發問題。所以能用局部變量就別用全局的變量,全局變量容易引起並發問題。

無狀態的類

沒有任何成員變量的類,就叫無狀態的類,這種類一定是線程安全的。

無狀態就是一次操作,不能保存數據。無狀態對象(Stateless Bean),就是沒有實例變量的對象.不能保存數據,是不變類。

如果這個類的方法參數中使用了對象,也是線程安全的嗎?比如:

當然也是,為何?因為多線程下的使用,固然 user 這個對象的實例會不正常,但是對於 StatelessClass 這個類的對象實例來說,它並不持有 UserVo 的對象實例,它自己並不會有問題,有問題的是 UserVo 這個類,而非 StatelessClass 本身。

讓類不可變

讓狀態不可變,兩種方式:

1,加 final 關鍵字,對於一個類,所有的成員變量應該是私有的,同樣的只要有可能,所有的成員變量應該加上 final 關鍵字,但是加上 final,要注意如果成員變量又是一個對象時,這個對象所對應的類也要是不可變,才能保證整個類是不可變的。

參見代碼 

public class ImmutableClass {
    private final int a;
    private final UserVo user = new UserVo();//不安全

    public int getA() {
        return a;
    }

    public UserVo getUser() {
        return user;
    }


    public ImmutableClass(int a) {
        this.a = a;
    }

    public static class User{
        private int age;

        public int getAge() {
            return age;
        }

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

2、根本就不提供任何可供修改成員變量的地方,同時成員變量也不作為方法的返回值

參見代碼

public class ImmutableClassToo {
    private final List<Integer> list = new ArrayList<Integer>(3);

    public ImmutableClassToo() {
        list.add(1);
        list.add(2);
        list.add(3);
    }

    public boolean isContain(int i){
        return list.contains(i);
    }
}

但是要注意,一旦類的成員變量中有對象,上述的 final 關鍵字保證不可變並不能保證類的安全性,為何?因為在多線程下,雖然對象的引用不可變,但是對象在堆上的實例是有可能被多個線程同時修改的,沒有正確處理的情況下,對象實例在堆中的數據是不可預知的。這就牽涉到了如何安全的發布對象這個問題。

volatile

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

加鎖和CAS

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

安全的發布

類中持有的成員變量,如果是基本類型,發布出去,並沒有關系,因為發布出去的其實是這個變量的一個副本.

參見代碼

/**
 * 演示基本類型的發布
 */
public class SafePublish {
    private int i;

    public SafePublish() {
        i = 2;
    }
    
    public int getI() {
        return i;
    }

    public static void main(String[] args) {
        SafePublish safePublish = new SafePublish();
        int j = safePublish.getI();
        System.out.println("before j="+j);
        j = 3;
        System.out.println("after j="+j);
        System.out.println("getI = "+safePublish.getI());
    }
}

但是如果類中持有的成員變量是對象的引用,如果這個成員對象不是線程安全的,通過 get 等方法發布出去,會造成這個成員對象本身持有的數據在多線程下不正確的修改,從而造成整個類線程不安全的問題。

參見代碼

/**
 * 不安全的發布
 */
public class UnSafePublish {
    private List<Integer> list = new ArrayList<Integer>(3);
    
    public UnSafePublish() {
        list.add(1);
        list.add(2);
        list.add(3);
    }
    
    public List getList() {
        return list;
    }

    public static void main(String[] args) {
        UnSafePublish unSafePublish = new UnSafePublish();
        List<Integer> list = unSafePublish.getList();
        System.out.println(list);
        list.add(4);
        System.out.println(list);
        System.out.println(unSafePublish.getList());
    }
}

這個 list 發布出去后,是可以被外部線程之間修改,那么在多個線程同時修改的情況下不安全問題是肯定存在的,怎么修正這個問題呢?

我們在發布這對象出去的時候,就應該用線程安全的方式包裝這個對象。

參見代碼

/**
 * 安全的發布
 */
public class SafePublishToo {
    private List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>(3));

    public SafePublishToo() {
        list.add(1);
        list.add(2);
        list.add(3);
    }

    public List getList() {
        return list;
    }

    public static void main(String[] args) {
        SafePublishToo safePublishToo = new SafePublishToo();
        List<Integer> list = safePublishToo.getList();
        System.out.println(list);
        list.add(4);
        System.out.println(list);
        System.out.println(safePublishToo.getList());
    }
}

我們將 list 用Collections.synchronizedList 進行包裝以后,無論多少線程使用這個 list,就都是線程安全的了。 

private List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>(3));

對於我們自己使用或者聲明的類,JDK 自然沒有提供這種包裝類的辦法,但是我們可以仿造這種模式或者委托給線程安全的類,當然,對這種通過 get 等方法發布出去的對象,最根本的解決辦法還是應該在實現上就考慮到線程安全問題。

參見以上代碼 ↑↑↑

ThreadLocal

ThreadLocal 是實現線程封閉的最好方法。

ThreadLocal 內部維護了一個 Map,Map 的 key 是每個線程的名稱,而 Map 的值就是我們要封閉的對象。每個線程中的對象都對應着 Map 中一個值,也就是 ThreadLocal 利用 Map 實現了對象的線程封閉。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM