上一篇講到多線程如何使用淑倾,多線程使用時(shí)特別應(yīng)該注意的是線程安全問(wèn)題,本篇將專(zhuān)門(mén)講述問(wèn)題原因和解決方案
為什么:
為什么出現(xiàn)多線程安全問(wèn)題
- 多線程安全問(wèn)題原因要從jmm(java內(nèi)存模型砖织,非jvm模型)講起款侵。jmm簡(jiǎn)易模型如下圖,
jmm模型.jpg
- 分為主內(nèi)存和線程工作內(nèi)存侧纯,多個(gè)線程使用共享變量時(shí)新锈,都是先從主內(nèi)存中拷貝到工作內(nèi)存,使用完成之后如果有寫(xiě)入操作則再寫(xiě)入主內(nèi)存眶熬。即線程A與線程B要使用共享變量c妹笆,都是從主內(nèi)存中拷貝一份副本到自己的工作內(nèi)存中,改后再將變更修改回主內(nèi)存娜氏,存在A線程修改變量C后拳缠,B線程不清楚C的修改,用的仍是C的副本贸弥,導(dǎo)致B完成后再次改變變量C窟坐,把A線程對(duì)變量的改動(dòng)覆蓋了∶嗥#或者B沒(méi)讀到A對(duì)C的修改哲鸳。這就存在數(shù)據(jù)不一致的事,A完成的任務(wù)又被B覆蓋了最岗。這就是多線程并發(fā)使用共享變量時(shí)的不安全問(wèn)題帕胆。
什么時(shí)候出現(xiàn)多線程安全問(wèn)題
- 多線程并發(fā)訪問(wèn)共享資源引起,即多個(gè)線程同時(shí)讀寫(xiě)相同的資源或者共享變量
怎么辦:如何解決多線程安全的問(wèn)題
多線程安全的三個(gè)特性
- 原子性:即在執(zhí)行一個(gè)或者多個(gè)操作的過(guò)程中般渡,要么一起成功要么一起失敗。典型的i++就是非原子操作,分三步驯用,取出i脸秽,i+1,再把結(jié)果賦給i。中途存在i在取出后再次賦值前蝴乔,存在被其他線程修改的可能性记餐。示例見(jiàn)下面demo,每次運(yùn)行結(jié)果都不同薇正,存在線程安全問(wèn)題片酝。
@Slf4j
public class ThreadNotSafeDemo {
public static void main(String[] args) throws Exception{
JobAdd jobAdd=new JobAdd();
for(int i=0;i<10;i++){
new Thread(jobAdd,"thread "+i).start();
}
Thread.sleep(1000);
log.debug("thread is {} ,total is {}",Thread.currentThread().getName(),jobAdd.getTotal());
}
}
@Slf4j
@Data
class JobAdd implements Runnable{
private int total=0;
@Override
public void run() {
for (int i=0;i<100;i++){
try {
// 必須加簡(jiǎn)單的sleep,否則可能當(dāng)前線程在下一個(gè)線程啟動(dòng)前就跑完了挖腰,演示不出效果
Thread.sleep(10l);
} catch (InterruptedException e) {
e.printStackTrace();
}
total++;
}
log.debug("thread is {} ,total is {}",Thread.currentThread().getName(),total);
}
}
打印結(jié)果如下:
05:56:18.109 [thread 1] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 1 ,total is 912
05:56:18.119 [thread 4] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 4 ,total is 919
05:56:18.119 [thread 9] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 9 ,total is 915
05:56:18.119 [thread 8] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 8 ,total is 917
05:56:18.119 [thread 0] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 0 ,total is 919
05:56:18.132 [thread 5] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 5 ,total is 923
05:56:18.132 [thread 3] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 3 ,total is 924
05:56:18.132 [thread 6] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 6 ,total is 924
05:56:18.132 [thread 2] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 2 ,total is 925
05:56:18.132 [thread 7] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 7 ,total is 923
05:56:18.993 [main] DEBUG com.dz.demo.multiThread.ThreadNotSafeDemo - thread is main ,total is 925
- 可見(jiàn)性:可見(jiàn)性是指當(dāng)一個(gè)線程對(duì)共享變量進(jìn)行修改后雕沿,能立刻被其他正在使用該變量的線程感知,包含兩步:對(duì)變量修改后立馬同步回主內(nèi)存猴仑;使其他線程的該共享變量的副本值失效审轮,必須重新從主內(nèi)存中獲取。
- 有序性:一般情況下辽俗,處理器為了提高運(yùn)行效率疾渣,在不影響本線程的前提下會(huì)對(duì)指令的執(zhí)行順序進(jìn)行重排序,代碼運(yùn)行順序可能與編寫(xiě)的順序不一致崖飘。但是對(duì)于多線程情況下榴捡,就容易出現(xiàn)問(wèn)題。當(dāng)前線程指令執(zhí)行先后對(duì)其他線程產(chǎn)生影響朱浴,這就是無(wú)序性吊圾。
要實(shí)現(xiàn)線程安全的幾個(gè)方案
- 最簡(jiǎn)單的不使用或者慎重使用共享變量或者共享狀態(tài):不使用就不存在多線程并發(fā)訪問(wèn)共享變量安全問(wèn)題
- 使用jdk已有的線程安全的api,包括以下幾種:java.util.concurrent.atomic包下的原子類(lèi)如AtomicBoolean赊琳、AtomicInteger街夭、AtomicIntegerArray、AtomicLong躏筏、DoubleAdder等板丽;可變字符串StringBuffer;java并發(fā)包下線程安全的集合趁尼,如ConcurrentHashMap埃碱、CopyOnWriteArrayList、CopyOnWriteArraySet酥泞、ConcurrentSkipListMap砚殿、BlockingQueue的實(shí)現(xiàn)類(lèi);一些老的線程安全集合如Hashtable芝囤,不過(guò)不推薦似炎,性能較低
- 使用jdk自帶的同步關(guān)鍵字synchronized,它可以用在方法和代碼塊上辛萍。使用在方法上是取該對(duì)象的監(jiān)視器為同步對(duì)象。使用在代碼塊上則是取synchronized括號(hào)里的對(duì)象的監(jiān)視器為同步對(duì)象羡藐,如果使用靜態(tài)方法上贩毕,則是取該類(lèi)對(duì)象的監(jiān)視器為同步對(duì)象。synchronized同時(shí)是可重入的同步仆嗦,即在同一線程中可以在釋放鎖前多次獲取鎖辉阶。將上面例子改進(jìn)下為:
@Slf4j
public class SynchronizedUseDemo {
public static void main(String[] args) throws Exception{
SafeJobAdd jobAdd=new SafeJobAdd();
for(int i=0;i<10;i++){
new Thread(jobAdd,"thread "+i).start();
}
Thread.sleep(3000l);
log.debug("thread is {} ,total is {}",Thread.currentThread().getName(),jobAdd.getTotal());
}
}
@Slf4j
@Data
class SafeJobAdd implements Runnable{
private int total=0;
@Override
public void run() {
// 使用了同步關(guān)鍵字synchronized
synchronized (this){
for (int i=0;i<100;i++){
try {
Thread.sleep(2l);
} catch (InterruptedException e) {
e.printStackTrace();
}
total++;
}
log.debug("thread is {} ,total is {}",Thread.currentThread().getName(),total);
}
}
}
打印結(jié)果是:
06:36:35.800 [thread 0] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 0 ,total is 100
06:36:36.042 [thread 9] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 9 ,total is 200
06:36:36.290 [thread 8] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 8 ,total is 300
06:36:36.539 [thread 7] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 7 ,total is 400
06:36:36.787 [thread 6] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 6 ,total is 500
06:36:37.040 [thread 5] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 5 ,total is 600
06:36:37.292 [thread 4] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 4 ,total is 700
06:36:37.541 [thread 3] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 3 ,total is 800
06:36:37.793 [thread 2] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 2 ,total is 900
06:36:38.043 [thread 1] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 1 ,total is 1000
06:36:38.554 [main] DEBUG com.dz.demo.multiThread.SynchronizedUseDemo - thread is main ,total is 1000
- 使用jdk的自帶的鎖相關(guān)api;如ReentrantLock(可重入鎖)瘩扼、ReentrantReadWriteLock.ReadLock(可重入的讀寫(xiě)鎖之讀鎖)谆甜、ReentrantReadWriteLock.WriteLock(可重入鎖之寫(xiě)鎖)。這幾個(gè)鎖都是基于jdk的同步框架AbstractQueuedSynchronizer實(shí)現(xiàn)的集绰,具體可查看jdk源碼规辱。也可用AbstractQueuedSynchronizer實(shí)現(xiàn)自定義的同步鎖。在代碼塊中使用lock倒慧,注意在finnaly中釋放鎖按摘。實(shí)例如下:
@Slf4j
public class LockUseDemo {
public static void main(String[] args) throws Exception{
LockJobAdd jobAdd=new LockJobAdd();
for(int i=0;i<10;i++){
new Thread(jobAdd,"thread "+i).start();
}
Thread.sleep(3000l);
log.debug("thread is {} ,total is {}",Thread.currentThread().getName(),jobAdd.getTotal());
}
}
@Slf4j
@Data
class LockJobAdd implements Runnable{
private int total=0;
private ReentrantLock lock=new ReentrantLock();
@Override
public void run() {
for (int i=0;i<100;i++){
try {
Thread.sleep(2l);
lock.lock();
total++;
} catch (InterruptedException e) {
e.printStackTrace();
}
finally {
lock.unlock();
}
}
log.debug("thread is {} ,total is {}",Thread.currentThread().getName(),total);
}
}
打印如下:
06:54:22.861 [thread 7] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 7 ,total is 994
06:54:22.862 [thread 4] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 4 ,total is 997
06:54:22.861 [thread 9] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 9 ,total is 994
06:54:22.861 [thread 8] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 8 ,total is 994
06:54:22.862 [thread 3] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 3 ,total is 999
06:54:22.861 [thread 5] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 5 ,total is 994
06:54:22.862 [thread 0] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 0 ,total is 997
06:54:22.861 [thread 1] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 1 ,total is 998
06:54:22.861 [thread 2] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 2 ,total is 994
06:54:22.862 [thread 6] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 6 ,total is 1000
06:54:25.633 [main] DEBUG com.dz.demo.multiThread.LockUseDemo - thread is main ,total is 1000
使用volatile關(guān)鍵字修飾共享變量。volatile關(guān)鍵字并沒(méi)有實(shí)現(xiàn)lock或者synchronized關(guān)鍵字的完整的同步作用纫谅,只是保證了可見(jiàn)性與有序性炫贤。在寫(xiě)入是原子性操作或者寫(xiě)入時(shí)線程安全時(shí),用volatile關(guān)鍵字付秕,實(shí)現(xiàn)比synchronized性能更高一點(diǎn)的線程安全兰珍。這個(gè)文章講的比較詳細(xì)volatile,特此引用询吴。正確使用Volatile變量
分布式環(huán)境下的共享資源的安全問(wèn)題不屬于多線程概念內(nèi)的掠河,是多實(shí)例多線程共同使用共享資源引起的,但是也是存在類(lèi)似的問(wèn)題猛计。一般的解決方案唠摹,是用鎖的辦法來(lái)實(shí)現(xiàn),使用數(shù)據(jù)庫(kù)實(shí)現(xiàn)樂(lè)觀鎖比較版本號(hào)奉瘤,或者使用redis 的setnx操作或者使用zookeeper實(shí)現(xiàn)勾拉。具體使用將在后面單獨(dú)講一篇。