Java并发编程之线程同步
線程安全就是防止某個(gè)對(duì)象或者值在多個(gè)線程中被修改而導(dǎo)致的數(shù)據(jù)不一致問題,因此我們就需要通過同步機(jī)制保證在同一時(shí)刻只有一個(gè)線程能夠訪問到該對(duì)象或數(shù)據(jù),修改數(shù)據(jù)完畢之后,再將最新數(shù)據(jù)同步到主存中,使得其他線程都能夠得到這個(gè)最新數(shù)據(jù)。下面我們就來(lái)了解Java一些基本的同步機(jī)制。
volatile關(guān)鍵字
Java提供了一種稍弱的同步機(jī)制即volatile變量,用來(lái)確保將變量的更新操作通知到其他線程。當(dāng)把變量聲明為volatile類型后,編譯器與運(yùn)行時(shí)都會(huì)注意到這個(gè)變量是共享的。然而,在訪問volatile變量時(shí)不會(huì)執(zhí)行加鎖操作,因此也就不會(huì)使線程阻塞,因此volatile變量是一種比synchronized關(guān)鍵字更輕量級(jí)的同步機(jī)制。
volatile變量對(duì)所有的線程都是可見的,對(duì)volatile變量所有的寫操作都能立即反應(yīng)到其他線程之中,即volatile變量在各個(gè)線程中是一致的。
有一種情況需要注意:volatile的語(yǔ)義不能確保遞增(count++)的原子性,除非你能確保只有一個(gè)線程對(duì)變量執(zhí)行寫操作。
public class VolatileTest{public static volatile int i;public static void increase(){i++;} }查看字節(jié)碼: javap -c -l VolatileTest.classpublic class VolatileTest {public static volatile int i;public VolatileTest();Code:0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V4: return LineNumberTable:line 1: 0public static void increase();Code:0: getstatic #2 // Field i:I, 把i的值取到了操作棧頂,volatile保證了i值此時(shí)是正確的. 3: iconst_1 4: iadd // increase,但其他線程此時(shí)可能已經(jīng)把i值加大了好多5: putstatic #2 // Field i:I ,把這個(gè)已經(jīng)out of date的i值同步回主內(nèi)存中,i值被破壞了.8: return LineNumberTable:line 6: 0line 7: 8 }加鎖機(jī)制即可以確保原子性又可以確保可見性,而volatile變量只能確保可見性。
內(nèi)置鎖-synchronized
Java中最常用的同步機(jī)制就是synchronized關(guān)鍵字,它是一種基于語(yǔ)言的粗略鎖,能夠作用于對(duì)象、函數(shù)、Class。每個(gè)對(duì)象都只有一個(gè)鎖,誰(shuí)能夠拿到這個(gè)鎖誰(shuí)就得到了訪問權(quán)限。當(dāng)synchronized作用于函數(shù)時(shí),實(shí)際上鎖的也是對(duì)象,鎖定的對(duì)象是該函數(shù)所在類的對(duì)象。而synchronized作用于Class時(shí)則鎖的是這個(gè)Class類,并非某個(gè)具體對(duì)象。
synchronized同步方法和同步塊
public class SynchronizedDemo {/*** @param args*/public static void main(String[] args) {// TODO Auto-generated method stubfinal Test test = new Test();new Thread(new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stubtest.syncMethod(Thread.currentThread());}}).start();new Thread(new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stubtest.syncMethod(Thread.currentThread());}}).start();new Thread(new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stubtest.asyncMethod(Thread.currentThread());}}).start();} }class Test {public synchronized void syncMethod(Thread thread) {for(int i = 0;i < 3;i++) {System.out.println(thread.getName());try {Thread.sleep(100);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}public void asyncMethod(Thread thread) {synchronized (this) {for(int i = 0;i < 3;i++) {System.out.println(thread.getName()+2);}}} }syncMethod和asyncMethod代碼塊都加鎖時(shí)結(jié)果:Thread-0 Thread-0 Thread-0 Thread-1 Thread-1 Thread-1 Thread-2 Thread-2 Thread-2 #多個(gè)線程不能同時(shí)訪問同一個(gè)對(duì)象中的synchronized鎖的方法或代碼塊syncMethod加鎖和asyncMethod代碼塊不加鎖時(shí)結(jié)果:class Test {public synchronized void syncMethod(Thread thread) {for(int i = 0;i < 3;i++) {System.out.println(thread.getName());try {Thread.sleep(100);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}public void asyncMethod(Thread thread) {synchronized (this) {for(int i = 0;i < 3;i++) {System.out.println(thread.getName());}}} }Thread-0 Thread-22 Thread-22 Thread-22 Thread-0 Thread-0 Thread-1 Thread-1 Thread-1 #其他線程可以訪問同一個(gè)對(duì)象的非同步方法或代碼塊syncMethod不加鎖和asyncMethod代碼塊不加鎖時(shí)結(jié)果:class Test {public void syncMethod(Thread thread) {for(int i = 0;i < 3;i++) {System.out.println(thread.getName());try {Thread.sleep(100);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}public void asyncMethod(Thread thread) {for(int i = 0;i < 3;i++) {System.out.println(thread.getName()+2);}} }Thread-0 Thread-1 Thread-22 Thread-22 Thread-22 Thread-0 Thread-1 Thread-1 Thread-0synchronized同步方法和同步塊鎖定的是引用對(duì)象,synchronized作用于引用對(duì)象是防止其他線程訪問同一個(gè)對(duì)象的synchronized代碼塊或方法,但可以訪問其他非同步代碼塊或方法。
synchronized同步Class對(duì)象和靜態(tài)方法
public class SynchronizedDemo {/*** @param args*/public static void main(String[] args) {// TODO Auto-generated method stubfinal Test test = new Test();new Thread(new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stubTest.syncStaticMethod(Thread.currentThread());}}).start();new Thread(new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stubTest.syncStaticMethod(Thread.currentThread());}}).start();new Thread(new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stubTest.asyncStaticMethod(Thread.currentThread());}}).start();} }class Test {public synchronized static void syncStaticMethod(Thread thread) {for (int i = 0; i < 3; i++) {System.out.println(thread.getName());try {Thread.sleep(50);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}public static void asyncStaticMethod(Thread thread) {synchronized (Test.class) {for (int i = 0; i < 3; i++) {System.out.println(thread.getName() + 22);try {Thread.sleep(50);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}} } syncStaticMethod和asyncStaticMethod代碼塊都加鎖的結(jié)果:Thread-0 Thread-0 Thread-0 Thread-222 Thread-222 Thread-222 Thread-1 Thread-1 Thread-1 ##多個(gè)線程不能同時(shí)訪問添加了synchronized鎖的代碼塊和方法。syncStaticMethod加鎖和asyncStaticMethod代碼塊不加鎖的結(jié)果:class Test {public synchronized static void syncStaticMethod(Thread thread) {for (int i = 0; i < 3; i++) {System.out.println(thread.getName());try {Thread.sleep(50);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}public static void asyncStaticMethod(Thread thread) {for (int i = 0; i < 3; i++) {System.out.println(thread.getName() + 22);try {Thread.sleep(50);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}} }Thread-0 Thread-222 Thread-222 Thread-0 Thread-0 Thread-222 Thread-1 Thread-1 Thread-1 ##多個(gè)線程可以同時(shí)訪問非同步的代碼塊和方法syncStaticMethod加鎖和asyncStaticMethod代碼塊都不加鎖的結(jié)果:class Test {public static void syncStaticMethod(Thread thread) {for (int i = 0; i < 3; i++) {System.out.println(thread.getName());try {Thread.sleep(50);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}public static void asyncStaticMethod(Thread thread) {for (int i = 0; i < 3; i++) {System.out.println(thread.getName() + 22);try {Thread.sleep(50);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}} }Thread-0 Thread-1 Thread-222 Thread-1 Thread-0 Thread-222 Thread-1 Thread-0 Thread-222synchronized同步Class對(duì)象和靜態(tài)方法鎖的是Class對(duì)象,它的作用是防止多個(gè)線程同時(shí)訪問添加了synchronized鎖的代碼塊和方法。
總結(jié)
當(dāng)一個(gè)線程正在訪問一個(gè)對(duì)象的synchronized方法,那么其他線程不能訪問該對(duì)象的其他synchronized方法,因?yàn)橐粋€(gè)對(duì)象只有一把鎖,當(dāng)一個(gè)線程獲取了該對(duì)象的鎖之后,其他線程無(wú)法獲取該對(duì)象的鎖,所有無(wú)法訪問該對(duì)象的其他synchronized方法。
當(dāng)一個(gè)線程正在訪問一個(gè)對(duì)象的synchronized方法,那么其他線程能訪問該對(duì)象的非synchronized方法。因?yàn)榉莝ynchronized方法不需要獲取該對(duì)象的鎖。
如果一個(gè)線程A需要訪問對(duì)象object1的synchronized方法fun1,另外一個(gè)線程B需要訪問對(duì)象object2的synchronized方法fun1,即使object1和object2是同一類型,也不會(huì)產(chǎn)生線程安全問題,因?yàn)樗麄冊(cè)L問的是不同的對(duì)象,所以不存在互斥問題。
如果一個(gè)線程執(zhí)行一個(gè)對(duì)象的非static synchronized方法,另一個(gè)線程執(zhí)行這個(gè)對(duì)象所屬類的static synchronized方法,此時(shí)不會(huì)發(fā)生互斥現(xiàn)象,因?yàn)樵L問static synchronized方法占用的是類鎖,而訪問非static synchronized方法占用的是對(duì)象鎖,所以不存在互斥現(xiàn)象。
需要注意的是:對(duì)于synchronized方法或者synchronized代碼塊,當(dāng)出現(xiàn)異常時(shí),JVM會(huì)自動(dòng)釋放當(dāng)前線程占用的鎖,因此不會(huì)由于異常導(dǎo)致出現(xiàn)死鎖現(xiàn)象。
顯示鎖-ReentrantLock與Condition
ReentrantLock
在JDk 5.0之前,協(xié)調(diào)共享對(duì)象的訪問時(shí),只有synchronized和volatile。Java 6.0增加了一種新的機(jī)制:ReentrantLock。顯示鎖ReentrantLock和內(nèi)置鎖synchronized相比,實(shí)現(xiàn)了相同的語(yǔ)義,但是具有更高的靈活性。
內(nèi)置鎖synchronized的獲取和釋放都在同一個(gè)代碼塊中,而顯示鎖ReentrantLock則可以將鎖的獲得和釋放分開。同時(shí)顯示鎖可以提供輪訓(xùn)鎖和定時(shí)鎖,同時(shí)可以提供公平鎖或者非公平鎖。
ReentrantLock的基本操作如下:
| lock() | 獲取鎖 |
| tryLock() | 嘗試獲取鎖 |
| tryLock(timeout,Timeunit unit) | 在指定時(shí)間內(nèi)嘗試獲取鎖 |
| unLock() | 釋放鎖 |
| newCondition | 獲取鎖的Condition |
使用ReentrantLock的一般是lock、tryLock與unLock成對(duì)出現(xiàn),需要注意的是,千萬(wàn)不要忘記調(diào)用unLock來(lái)釋放鎖,否則會(huì)引發(fā)死鎖等問題。
ReentrantLock的常用形式如下所示:
Lock lock = new ReentrantLock();public void run() {lock.lock();try {//執(zhí)行任務(wù)} finally {lock.unlock();} }需要注意的是,lock必須在finally塊中釋放,否則,如果受保護(hù)的代碼塊拋出異常,鎖就有可能永遠(yuǎn)得不到釋放。而使用synchronized同步,JVM將確保鎖會(huì)獲得自動(dòng)釋放,這也是Lock沒有完全替代掉synchronized的原因。
當(dāng)JVM用synchronized管理鎖定請(qǐng)求和釋放行為時(shí),JVM在生成線程轉(zhuǎn)儲(chǔ)時(shí)能夠包括鎖定信息,這些對(duì)調(diào)式有非常大的價(jià)值,因?yàn)樗鼈兡軜?biāo)識(shí)死鎖和其他異常行為的來(lái)源。Lock類只是普通的類,JVM不知道具體哪個(gè)線程擁有Lock對(duì)象。
Condition
在ReentrantLock類中有一個(gè)重要的函數(shù)newCondition(),該函數(shù)用于獲取lock上的一個(gè)條件,也就是說(shuō)Condition是和Lock綁定的。Condition用于實(shí)現(xiàn)線程間的通信,它是為了解決Object.wait()、notify()、notifyAll()難以使用的問題。
Condition的基本操作如下所示:
| await() | 線程等待 |
| await(int time,TimeUnit unit) | 線程等待特定的時(shí)間,超過時(shí)間則為超時(shí) |
| signal() | 隨機(jī)喚醒某個(gè)等待線程 |
| signalAll() | 喚醒所有等待中的線程 |
綜合應(yīng)用
下面通過ReentrantLock和Condition類實(shí)現(xiàn)一個(gè)簡(jiǎn)單的阻塞隊(duì)列。如果調(diào)用take方法時(shí)集合中沒有數(shù)據(jù),那么調(diào)用線程阻塞;如果調(diào)用put方法時(shí),集合數(shù)據(jù)已滿則調(diào)用線程阻塞。但是這兩個(gè)阻塞條件是不同的,分別為notFull和notEmpty。MyArrayBlockingQueue的實(shí)現(xiàn)代碼如下:
public class MyArrayBlockingQueue<T> {// 數(shù)據(jù)數(shù)組private final T[] items;// 鎖private final Lock mLock = new ReentrantLock();// 數(shù)組滿的條件private Condition notFull = mLock.newCondition();// 數(shù)組空的條件private Condition notEmpty = mLock.newCondition();// 頭部private int head;// 尾部private int tail;// 數(shù)據(jù)數(shù)量private int count;public MyArrayBlockingQueue(int maxSize) {// TODO Auto-generated constructor stubitems = (T[]) new Object[maxSize];}public MyArrayBlockingQueue() {// TODO Auto-generated constructor stubthis(10);}public void put(T t) {mLock.lock();try {// 如果數(shù)據(jù)已滿,等待while (count == getCapacity()) {System.out.println("數(shù)據(jù)已滿,請(qǐng)等待");notFull.await();}System.out.println("存入數(shù)據(jù)");items[tail] = t;if (++tail == getCapacity()) {tail = 0;}++count;// 喚醒等待數(shù)據(jù)的線程notEmpty.signalAll();} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();} finally {mLock.unlock();}}public T take() {mLock.lock();try {// 如果數(shù)組數(shù)據(jù)為空,則阻塞while (count == 0) {System.out.println("還沒有數(shù)據(jù),等待");notEmpty.await();}System.out.println("取出數(shù)據(jù)");T t = items[head];items[head] = null;if (++head == getCapacity()) {head = 0;}--count;// 喚醒添加數(shù)據(jù)的線程notFull.signalAll();return t;} catch (InterruptedException e) {// TODO: handle exception} finally {mLock.unlock();}return null;}public int getCapacity() {return items.length;}public int size() {mLock.lock();try {return count;} finally {mLock.unlock();}}/*** @param args*/public static void main(String[] args) {// TODO Auto-generated method stubfinal MyArrayBlockingQueue<String> mQueue = new MyArrayBlockingQueue<>(5);new Thread(new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stubwhile (true) {for(int i = 0;i < 3;i++)mQueue.put("just");try {Thread.sleep(50);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}}).start();new Thread(new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stubwhile (true) {mQueue.take();}}}).start();}}結(jié)果打印 存入數(shù)據(jù) 存入數(shù)據(jù) 存入數(shù)據(jù) 取出數(shù)據(jù) 取出數(shù)據(jù) 取出數(shù)據(jù) 還沒有數(shù)據(jù),等待 存入數(shù)據(jù) 存入數(shù)據(jù) 存入數(shù)據(jù) 取出數(shù)據(jù) 取出數(shù)據(jù) 取出數(shù)據(jù) 還沒有數(shù)據(jù),等待信號(hào)量-Semaphore
Semaphore是一個(gè)計(jì)數(shù)信號(hào)量,它的本質(zhì)是一個(gè)“共享鎖”。信號(hào)量維護(hù)一個(gè)信號(hào)許可集合,線程可以通過調(diào)用acquire()來(lái)獲取信號(hào)量的許可。當(dāng)信號(hào)量有可用的許可時(shí),線程能獲取該許可;否則線程必須等到,直到有可用的許可為止。線程可以通過release()來(lái)釋放它所持有的信號(hào)量許可。
Semaphore實(shí)現(xiàn)的功能類似食堂窗口。例如,食堂只有3個(gè)銷售窗口,要吃飯的有5個(gè)人,那么同時(shí)只有3個(gè)人買飯菜,每個(gè)人占用一個(gè)窗口,另外2人只能等待。當(dāng)前3個(gè)人有人離開之后,后續(xù)的人才可以占用窗口進(jìn)行購(gòu)買。這里的窗口就是我們所說(shuō)的許可集,這里為3.一個(gè)人占用窗口時(shí)相當(dāng)于他調(diào)用acquire()獲取了許可,當(dāng)他離開時(shí)也就等于調(diào)用release()釋放了許可,這樣后續(xù)的人才可以得到許可。下面看看具體的示例:
public class SemaphoreTest {/*** @param args*/public static void main(String[] args) {// TODO Auto-generated method stubExecutorService service = Executors.newFixedThreadPool(3);final Semaphore semaphore = new Semaphore(3);for(int i = 0;i < 5;i++) {service.submit(new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stubtry {semaphore.acquire();System.out.println("剩余許可: " + semaphore.availablePermits());Thread.sleep(2000);semaphore.release();} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}});}}}結(jié)果打印:剩余許可: 0 剩余許可: 0 剩余許可: 0剩余許可: 2 剩余許可: 1上述結(jié)果中:前三行是立刻輸出的,后兩行是等待2秒之后才輸出。原因是,信號(hào)量的許可集是3個(gè),而消費(fèi)線程是5個(gè)。前3個(gè)線程獲取了許可之后,信號(hào)量的許可就為0。此時(shí)后面的線程再調(diào)用acquire()就會(huì)阻塞,直到前3個(gè)線程執(zhí)行完之后,釋放了許可(不需要同時(shí)釋放許可)后兩個(gè)線程才能獲取許可并且繼續(xù)執(zhí)行。
循環(huán)柵欄-CyclicBarrier
CyclicBarrier是一個(gè)同步輔助類,允許一組線程互相等待,直到達(dá)到某個(gè)公共屏障點(diǎn)。因?yàn)樵揵arrier在釋放等待線程后可以重用,所有稱為循環(huán)的barrier。
下面看看示例:
public class CyclicBarrierTest {private static final int SIZE = 5;private static CyclicBarrier mCyclicBarrier;/*** @param args*/public static void main(String[] args) {// TODO Auto-generated method stubmCyclicBarrier = new CyclicBarrier(SIZE, new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stubSystem.out.println("--滿足條件執(zhí)行特定操作,參與者: "+ mCyclicBarrier.getParties());}});for(int i = 0;i < SIZE;i++) {new WorkerThread().start();}}static class WorkerThread extends Thread {@Overridepublic void run() {// TODO Auto-generated method stubtry {System.out.println(Thread.currentThread().getName() + "等待CyclicBarrier");//將mCyclicBarrier的參與者數(shù)量加1mCyclicBarrier.await();//mCyclicBarrier的參與者數(shù)量加5時(shí),才繼續(xù)往后執(zhí)行System.out.println(Thread.currentThread().getName()+"繼續(xù)執(zhí)行");} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (BrokenBarrierException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}}結(jié)果打印:Thread-1等待CyclicBarrier Thread-0等待CyclicBarrier Thread-2等待CyclicBarrier Thread-3等待CyclicBarrier Thread-4等待CyclicBarrier --滿足條件執(zhí)行特定操作,參與者: 5 Thread-4繼續(xù)執(zhí)行 Thread-3繼續(xù)執(zhí)行 Thread-2繼續(xù)執(zhí)行 Thread-0繼續(xù)執(zhí)行 Thread-1繼續(xù)執(zhí)行從結(jié)果可以看出,只有當(dāng)有5個(gè)線程調(diào)用了mCyclicBarrier.await()方法后,后續(xù)的任務(wù)才會(huì)繼續(xù)執(zhí)行。上述例子中的5個(gè)WorkThread就位之后首先會(huì)執(zhí)行一個(gè)Runnable,也就是CyclicBarrier構(gòu)造函數(shù)的第二個(gè)參數(shù),該參數(shù)也可以省略。執(zhí)行該Runnable之后才會(huì)繼續(xù)執(zhí)行下面的任務(wù)。CyclicBarrier實(shí)際上相當(dāng)于可以用于多個(gè)線程等待,直到某個(gè)條件被滿足后開始繼續(xù)執(zhí)行后續(xù)的任務(wù)。對(duì)于該示例來(lái)說(shuō),這里的條件也就是有指定個(gè)數(shù)的線程調(diào)用了mCyclicBarrier.await()方法。
閉鎖-CountDownLatch
CountDownLatch是一個(gè)同步輔助類,在完成一組正在其他線程中執(zhí)行的操作之前,它允許一個(gè)或多個(gè)線程一直等待,直到條件被滿足。
示例如下:
public class CountDownLatchTest {private static final int LATCH_SIZE = 5;/*** @param args*/public static void main(String[] args) {// TODO Auto-generated method stubtry {CountDownLatch countDownLatch = new CountDownLatch(LATCH_SIZE);for(int i = 0;i < LATCH_SIZE;i++) {new WorkerThread(countDownLatch).start();}System.out.println("主線程等待");countDownLatch.await();System.out.println("主線程繼續(xù)執(zhí)行");} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}static class WorkerThread extends Thread {private CountDownLatch latch;public WorkerThread(CountDownLatch latch) {this.latch = latch;}@Overridepublic void run() {// TODO Auto-generated method stubtry {Thread.sleep(1000);System.out.println(Thread.currentThread().getName() + "執(zhí)行操作");//將latch的數(shù)量減1latch.countDown();} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();}}}}結(jié)果打印:主線程等待 Thread-3執(zhí)行操作 Thread-1執(zhí)行操作 Thread-0執(zhí)行操作 Thread-4執(zhí)行操作 Thread-2執(zhí)行操作 主線程繼續(xù)執(zhí)行5個(gè)WorkThread對(duì)象在執(zhí)行完操作之后會(huì)調(diào)用CountDownLatch的countDown()函數(shù),當(dāng)5個(gè)WorkThread全都調(diào)用了countDown()之后主線程就會(huì)被喚醒繼續(xù)執(zhí)行任務(wù)。
CountDownLatch與CyclicBarrier區(qū)別
CountDownLatch的作用是允許1或者多個(gè)線程等待其他線程完成執(zhí)行,而CyclicBarrier則是允許N個(gè)線程相互等待。
CountDownLatch的計(jì)數(shù)器無(wú)法被重置,CyclicBarrier的計(jì)數(shù)器可以被重置后使用。
總結(jié)
以上是生活随笔為你收集整理的Java并发编程之线程同步的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 『码蛋』Android 周刊第1期
- 下一篇: 【基础部分】之apache配置与应用