1.為什么會出現線程安全問題
計算機系統資源分配的單位為進程,同一個進程中允許多個線程並發執行,並且多個線程會共享進程范圍內的資源:例如內存地址。當多個線程並發訪問同一個內存地址並且內存地址保存的值是可變的時候可能會發生線程安全問題,因此需要內存數據共享機制來保證線程安全問題。
對應到java服務來說,在虛擬中的共享內存地址是java的堆內存,比如以下程序中線程安全問題:
public class ThreadUnsafeDemo {
private static final ExecutorService EXECUTOR_SERVICE;
static {
EXECUTOR_SERVICE = new ThreadPoolExecutor(100, 100, 1000 * 10,
TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(100), new ThreadFactory() {
private AtomicLong atomicLong = new AtomicLong(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "Thread-Safe-Thread-" + atomicLong.getAndIncrement());
}
});
}
public static void main(String[] args) throws Exception {
Map<String, Integer> params = new HashMap<>();
List<Future> futureList = new ArrayList<>(100);
for (int i = 0; i < 100; i++) {
futureList.add(EXECUTOR_SERVICE.submit(new CacheOpTask(params)));
}
for (Future future : futureList) {
System.out.println("Future result:" + future.get());
}
System.out.println(params);
}
private static class CacheOpTask implements Callable<Integer> {
private Map<String, Integer> params;
CacheOpTask(Map<String, Integer> params) {
this.params = params;
}
@Override
public Integer call() {
for (int i = 0; i < 100; i++) {
int count = params.getOrDefault("count", 0);
params.put("count", ++count);
}
return params.get("count");
}
}
}
創建100個task,每個task對map中的元素累加100此,程序執行結果為:
{count=9846}
而預期的正確結果為:
{count=10000}
至於出現這種問題的原因,下面會具體分析。
判斷是否有線程安全性的一個原則是:
是否有多線程訪問可變的共享變量
2.多線程的優勢
發揮多處理器的強大能力,提高效率和程序吞吐量
3.並發帶來的風險
使用並發程序帶來的主要風險有以下三種:
3.1.安全性問題:
競態條件:由於不恰當的執行時序而出現不正確的結果
對於1中的線程安全的例子就是由於競態條件導致的最終結果與預期結果不一致。關鍵代碼塊如下:
int count = params.getOrDefault("count", 0);
params.put("count", ++count);
當多個線程同時取的count的值的時候,每個線程計算之后,在寫入到count,這時候會出現多個線程值被覆蓋的情況,最終導致結果不正確。
如下圖所示:
3.2解決此類問題的幾種方法
1.使用同步機制限制變量的訪問:鎖
比如:
synchronized (LOCK) {
int count = params.getOrDefault("count", 0);
params.put("count", ++count);
}
2.將變量設置為不可變
即將共享變量設置為final
3.不在線程之間共享此變量ThreadLocal
編程的原則:首先編寫正確的代碼,然后在實現性能的提升
無狀態的類一定是線程安全的
3.3 內置鎖
內置鎖:同步代碼塊( synchronized (this) {})
進入代碼塊前需要獲取鎖,會有性能問題。內置鎖是可重入鎖,之所以每個對象都有一個內置鎖,是為了避免顯示的創建鎖對象
常見的加鎖約定:將所有的可變狀態都封裝在對象內部,並使用內置鎖對所有訪問可變狀態的代碼進行同步。例如:Vector等
同步的另一個功能:內存可見性,類似於volatile
非volatile的64位變量double、long:
JVM允許對64位的操作分解為兩次32位的兩次操作,可變64位變量必須用volatile或者鎖來保護
加鎖的含義不僅在於互斥行為,還包括內存可見性,為了所有線程都可以看到共享變量的最新值,所有線程應該使用同一個鎖
原則:
除非需要跟高的可見性,否則應該將所有的域都聲明為私有的,除非需要某個域是可變的,否則應該講所有的域生命為final的
2.活躍性問題
線程活躍性問題主要是由於加鎖不正確導致的線程一直處於等待獲取鎖的狀態,比如以下程序:
public class DeadLock {
private static final Object[] LOCK_ARRAY;
static {
LOCK_ARRAY = new Object[2];
LOCK_ARRAY[0] = new Object();
LOCK_ARRAY[1] = new Object();
}
public static void main(String[] args) throws Exception {
TaskOne taskOne = new TaskOne();
taskOne.start();
TaskTwo taskTwo = new TaskTwo();
taskTwo.start();
System.out.println("finished");
}
private static class TaskOne extends Thread {
@Override
public void run(){
synchronized (LOCK_ARRAY[0]) {
try {
Thread.sleep(3000);
} catch (Exception e) {
}
System.out.println("Get LOCK-0");
synchronized (LOCK_ARRAY[1]) {
System.out.println("Get LOCK-1");
}
}
}
}
private static class TaskTwo extends Thread {
@Override
public void run() {
synchronized (LOCK_ARRAY[1]) {
try {
Thread.sleep(1000 * 3);
} catch (Exception e) {
}
System.out.println("Get LOCK-1");
synchronized (LOCK_ARRAY[0]) {
System.out.println("Get LOCK-0");
}
}
}
}
}
在兩個線程持有一個鎖,並在在鎖沒有釋放之前,互相等待對方持有的鎖,這時候會造成兩個線程會一直等待,從而產生死鎖。在我們使用鎖的時候應該考慮持有鎖的時長,特別是在網絡I/O的時候。
在使用鎖的時候要盡量避免以上情況,從而避免產生死鎖
3.性能問題
在使用多線程執行程序的時候,在線程間的切換以及線程的調度也會消耗CPU的性能。