今天長(zhǎng)沙牛耳教育java學(xué)院小編為大家介紹java的線程安全問題原因及解決辦法,希望對(duì)各位java程序員有幫助,下面就隨小編一起看看java的線程安全問題原因及解決辦法吧。
1、為什么會(huì)出現(xiàn)線程安全問題
計(jì)算機(jī)系統(tǒng)資源分配的單位為進(jìn)程,同一個(gè)進(jìn)程中允許多個(gè)線程并發(fā)執(zhí)行,并且多個(gè)線程會(huì)共享進(jìn)程范圍內(nèi)的資源:例如內(nèi)存地址。當(dāng)多個(gè)線程并發(fā)訪問同一個(gè)內(nèi)存地址并且內(nèi)存地址保存的值是可變的時(shí)候可能會(huì)發(fā)生線程安全問題,因此需要內(nèi)存數(shù)據(jù)共享機(jī)制來保證線程安全問題。
對(duì)應(yīng)到j(luò)ava服務(wù)來說,在虛擬中的共享內(nèi)存地址是java的堆內(nèi)存,比如以下程序中線程安全問題:
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");
}
}
}
創(chuàng)建100個(gè)task,每個(gè)task對(duì)map中的元素累加100此,程序執(zhí)行結(jié)果為:
{count=9846}
而預(yù)期的正確結(jié)果為:
{count=10000}
至于出現(xiàn)這種問題的原因,下面會(huì)具體分析。
判斷是否有線程安全性的一個(gè)原則是:
是否有多線程訪問可變的共享變量
2、多線程的優(yōu)勢(shì)
發(fā)揮多處理器的強(qiáng)大能力,提高效率和程序吞吐量。
3、并發(fā)帶來的風(fēng)險(xiǎn)
使用并發(fā)程序帶來的主要風(fēng)險(xiǎn)有以下三種:
(1)安全性問題:
競(jìng)態(tài)條件:由于不恰當(dāng)?shù)膱?zhí)行時(shí)序而出現(xiàn)不正確的結(jié)果。
對(duì)于1中的線程安全的例子就是由于競(jìng)態(tài)條件導(dǎo)致的最終結(jié)果與預(yù)期結(jié)果不一致。關(guān)鍵代碼塊如下:
int count = params.getOrDefault("count", 0);
params.put("count", ++count);
當(dāng)多個(gè)線程同時(shí)取的count的值的時(shí)候,每個(gè)線程計(jì)算之后,在寫入到count,這時(shí)候會(huì)出現(xiàn)多個(gè)線程值被覆蓋的情況,最終導(dǎo)致結(jié)果不正確。如下圖所示:
(2)解決此類問題的幾種方法
1)使用同步機(jī)制限制變量的訪問:鎖
比如:
synchronized (LOCK) {
int count = params.getOrDefault("count", 0);
params.put("count", ++count);
}
2)將變量設(shè)置為不可變
即將共享變量設(shè)置為final
3)不在線程之間共享此變量ThreadLocal
編程的原則:首先編寫正確的代碼,然后在實(shí)現(xiàn)性能的提升。
無狀態(tài)的類一定是線程安全的
?。?)內(nèi)置鎖
內(nèi)置鎖:同步代碼塊( synchronized (this) {})
進(jìn)入代碼塊前需要獲取鎖,會(huì)有性能問題。內(nèi)置鎖是可重入鎖,之所以每個(gè)對(duì)象都有一個(gè)內(nèi)置鎖,是為了避免顯示的創(chuàng)建鎖對(duì)象。
常見的加鎖約定:將所有的可變狀態(tài)都封裝在對(duì)象內(nèi)部,并使用內(nèi)置鎖對(duì)所有訪問可變狀態(tài)的代碼進(jìn)行同步。例如:Vector等
同步的另一個(gè)功能:內(nèi)存可見性,類似于volatile。
非volatile的64位變量double、long:
JVM允許對(duì)64位的操作分解為兩次32位的兩次操作,可變64位變量必須用volatile或者鎖來保護(hù)。
加鎖的含義不僅在于互斥行為,還包括內(nèi)存可見性,為了所有線程都可以看到共享變量的最新值,所有線程應(yīng)該使用同一個(gè)鎖。
原則:除非需要跟高的可見性,否則應(yīng)該將所有的域都聲明為私有的,除非需要某個(gè)域是可變的,否則應(yīng)該講所有的域生命為final的。
活躍性問題:線程活躍性問題主要是由于加鎖不正確導(dǎo)致的線程一直處于等待獲取鎖的狀態(tài),比如以下程序:
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");
}
}
}
}
}
在兩個(gè)線程持有一個(gè)鎖,并在在鎖沒有釋放之前,互相等待對(duì)方持有的鎖,這時(shí)候會(huì)造成兩個(gè)線程會(huì)一直等待,從而產(chǎn)生死鎖。在我們使用鎖的時(shí)候應(yīng)該考慮持有鎖的時(shí)長(zhǎng),特別是在網(wǎng)絡(luò)I/O的時(shí)候。
在使用鎖的時(shí)候要盡量避免以上情況,從而避免產(chǎn)生死鎖。
3、性能問題
在使用多線程執(zhí)行程序的時(shí)候,在線程間的切換以及線程的調(diào)度也會(huì)消耗CPU的性能。
以上就是長(zhǎng)沙牛耳教育java學(xué)院小編介紹的“java線程安全問題原因及解決辦法”的內(nèi)容,希望對(duì)各位java程序員有幫助,如有疑問,請(qǐng)?jiān)诰€咨詢,有專業(yè)老師隨時(shí)為你服務(wù)。