Java併發編程 併發安全 線程安全性 保證線程安全的幾種方法

什麼是線程安全性

在《Java 併發編程實戰》中,定義如下:

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

線程封閉

實現好的併發是一件困難的事情,所以很多時候我們都想躲避併發。避免併發最簡單的方法就是線程封閉。什麼是線程封閉呢?
就是把對象封裝到一個線程裏,只有這一個線程能看到此對象。那麼這個對象就算不是線程安全的也不會出現任何安全問題。實現線程封閉有哪些方法呢?

ad-hoc線程封閉
這是完全靠實現者控制的線程封閉,他的線程封閉完全靠實現者實現。 Ad-hoc 線程封閉非常脆弱,應該儘量避免使用。

棧封閉
棧封閉是我們編程當中遇到的最多的線程封閉。
什麼是棧封閉呢?
簡單的說 就是局部變量。多個線程訪問一個方法,此方法中的局部變量都會被拷貝一份到 線程棧中。所以局部變量是不被多個線程所共享的,也就不會出現併發問題。所以能用局部變量就別用全局的變量,全局變量容易引起併發問題。

無狀態的類

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

/**
 1. 無狀態的類
 */
public class StatelessClass {
	public int service(int a,int b){
	    return a+b;
    }

    public void serviceUser(UserVo user){
        //do sth user
    }
}

如果這個類的方法參數中使用了對象,也是線程安全的嗎?比如:
在這裏插入圖片描述
當然也是,爲何?因爲多線程下的使用,固然 user 這個對象的實例會不正 常,但是對於 StatelessClass 這個類的對象實例來說,它並不持有 UserVo 的對象 實例,它自己並不會有問題,有問題的是 UserVo 這個類,而非 StatelessClass 本身。

讓類不可變

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

  1. 加 final 關鍵字,對於一個類,所有的成員變量應該是私有的,同樣的只 要有可能,所有的成員變量應該加上 final 關鍵字,但是加上 final,要注意如果 成員變量又是一個對象時,這個對象所對應的類也要是不可變,才能保證整個類 是不可變的。
    參見代碼:
/**
 1. 類不可變
 */
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;
		}
    }
}
  1. 根本就不提供任何可供修改成員變量的地方,同時成員變量也不作爲方 法的返回值。
    參見代碼:
/**
 * 類不可變--事實不可變
 */
public class ImmutableClassToo {
    private final List<Integer> list = new ArrayList<>(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<>(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 發佈出去後,是可以被外部線程之間修改,那麼在多個線程同時修 改的情況下不安全問題是肯定存在的,怎麼修正這個問題呢?我們在發佈這對象 出去的時候,就應該用線程安全的方式包裝這個對象。
參見代碼:

/**
 1. 安全的發佈
 */
public class SafePublishToo {
    private List<Integer> list
            = Collections.synchronizedList(new ArrayList<>(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,就都是線 程安全的了。
在這裏插入圖片描述
對於我們自己使用或者聲明的類,JDK 自然沒有提供這種包裝類的辦法,但 是我們可以仿造這種模式或者委託給線程安全的類,當然,對這種通過 get 等方 法發佈出去的對象,最根本的解決辦法還是應該在實現上就考慮到線程安全問題

另外對容器的包裝

/**
 * 仿Collections對容器的包裝,將內部成員對象進行線程安全包裝
 */
public class SoftPublicUser {
    private final UserVo user;

    public UserVo getUser() {
        return user;
    }

    public SoftPublicUser(UserVo user) {
        this.user = new SynUser(user);
    }

    private static class SynUser extends UserVo{
        private final UserVo userVo;
        private final Object lock = new Object();

        public SynUser(UserVo userVo) {
            this.userVo = userVo;
        }

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

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

}
/**
 * 類說明:委託給線程安全的類來做
 */
public class SafePublicFinalUser {
    private final SynFinalUser user;

    public SynFinalUser getUser() {
        return user;
    }

    public SafePublicFinalUser(FinalUserVo user) {
        this.user = new SynFinalUser(user);
    }

    public static class SynFinalUser{
        private final FinalUserVo userVo;
        private final Object lock = new Object();

        public SynFinalUser(FinalUserVo userVo) {
            this.userVo = userVo;
        }

        public int getAge() {
            synchronized (lock){
                return userVo.getAge();
            }
        }

        public void setAge(int age) {
            synchronized (lock){
                userVo.setAge(age);
            }
        }
    }

}

TheadLocal

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

Servlet辨析

不是線程安全的類,爲什麼我們平時沒感覺到:

  1. 在需求上,很少有共享的需求
  2. 接收到了請求,返回應答的時候,一 般都是由一個線程來負責的。 但是隻要 Servlet 中有成員變量,一旦有多線程下的寫,就很容易產生線程安全問題。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章