前言:
多線程開發(fā)中往往需要同步處理了,這是因?yàn)橐粋€(gè)進(jìn)程中的線程是共享JVM中的方法區(qū)和堆區(qū),同時(shí)操作臨界區(qū)資源的時(shí)候會(huì)破壞了原子性,導(dǎo)致數(shù)據(jù)出現(xiàn)錯(cuò)誤。就需要同步操作,也就有了鎖。
先從一個(gè)簡(jiǎn)單的銀行轉(zhuǎn)賬例子開始:
public class Bank{
List<Account> accounts = new ArrayList<>();
// 虛擬創(chuàng)建10個(gè)賬號(hào)
public Bank(){
for(int i=0;i<10;i++){
accounts.add(new Account());
}
}
// 獲取總資金
public int getTotalMoney(){
int total = 0;
for(int i = 0;i<accounts.size();i++){
total+=accounts.get(i).money;
}
return total;
}
// 轉(zhuǎn)賬操作
public void transfers(int from,int to,int money){
if(accounts.get(from).money<money)
return;
accounts.get(from).money -=money;
accounts.get(to).money +=money;
System.out.printf("Bank總共money = %d \n",getTotalMoney());
}
// 測(cè)試兩個(gè)Thread轉(zhuǎn)賬
public void start(){
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while(true){
int money = (int) ((double)50*Math.random());
int from = (int) ((double)9*Math.random());
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
int to = (int) ((double)9*Math.random());
transfers(from, to, money);
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
......省略 與t1一致
}
});
t1.start();
t2.start();
}
// 程序入口
public static void main(String[] args) {
Bank b = new Bank();
b.start();
}
}
// 用戶賬號(hào)
class Account{
int money = 1000; // 原始資金
}

原因:轉(zhuǎn)賬的時(shí)候,轉(zhuǎn)出和轉(zhuǎn)入是個(gè)原子的操作,兩個(gè)線程同時(shí)操作同一個(gè)賬戶的時(shí)候就很容易出錯(cuò)。線程的執(zhí)行是沒有順序可言的,一行代碼的指令會(huì)有多行,沒執(zhí)行完就被剝奪了運(yùn)行權(quán),另一個(gè)Thread再次處理就會(huì)導(dǎo)致數(shù)據(jù)不一致。
一、ReentrantLock鎖對(duì)象
java5.0版本引入了ReentrantLock類,它位于java.util.concurrent包下面。它是一個(gè)可以被用來保護(hù)臨界區(qū)的可重入鎖,只能有一個(gè)線程獲得鎖對(duì)象,其它線程執(zhí)行l(wèi)ock()方法時(shí),會(huì)阻塞在這里,直到當(dāng)前獲得鎖對(duì)象的線程釋放了鎖即unlock(),其它線程才可以競(jìng)爭(zhēng)。
// ReentrantLock使用步驟
myLock.lock();
try {
同步代碼
} finally {
myLock.unlock();
}
在上面的例子中,只要改變給臨界區(qū)加上ReentrantLock就可以了。但是同一個(gè)線程可以多次獲得鎖對(duì)象(即lock.lock()操作),該ReentrantLock會(huì)有一個(gè)計(jì)數(shù)加鎖幾次,必須全部釋放鎖的時(shí)候才是線程真正的釋放當(dāng)前鎖對(duì)象,這時(shí)鎖計(jì)數(shù)為0。
Lock lock = new ReentrantLock();
// 轉(zhuǎn)賬操作
public void transfers(int from,int to,int money){
lock.lock(); // 加鎖
if(accounts.get(from).money<money)
return;
accounts.get(from).money -=money;
accounts.get(to).money +=money;
System.out.printf("Bank總共money = %d \n",getTotalMoney());
lock.unlock(); // 轉(zhuǎn)賬完成后釋放鎖
}
二、條件對(duì)象Condition
條件對(duì)象,是配合ReentrantLock對(duì)象使用的,他也是在java.util.concurrent包下面的。應(yīng)用場(chǎng)景:剛獲得鎖的線程,并不滿足一些必備的條件,如賬號(hào)金額不足。這個(gè)時(shí)候就必須阻塞當(dāng)前線程,釋放當(dāng)前鎖對(duì)象。其它線程獲得鎖對(duì)象,執(zhí)行成功后再通知解除等待線程的阻塞,但不是立即的就能獲得鎖對(duì)象,想要獲得鎖對(duì)象,還是要重新的競(jìng)爭(zhēng)。
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition(); // 增加一個(gè)條件對(duì)象,用ReentrantLock創(chuàng)建條件對(duì)象
// 轉(zhuǎn)賬操作
public void transfers(int from, int to, int money) {
lock.lock();
try {
while (accounts.get(from).money < money) { // 通常都是用循環(huán),防止重新獲得鎖的時(shí)候,條件依舊不能保證是否能滿足條件
condition.await(); // 將線程加入等待集,阻塞當(dāng)前線程
}
accounts.get(from).money -= money;
accounts.get(to).money += money;
System.out.printf("Bank總共money = %d \n", getTotalMoney());
condition.signalAll(); //必須要通知解除阻塞
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
lock.unlock();
}
}
三、synchronized關(guān)鍵字
有了對(duì)象鎖和條件對(duì)象Condition后,為什么會(huì)有synchronized了。synchronized更加的簡(jiǎn)潔減少出錯(cuò)的概率,鎖的開啟和釋放均有JVM來操作,ReentrantLock則需要手動(dòng)的調(diào)動(dòng)加鎖和釋放鎖。ReentrantLock是可重入鎖,synchronized鎖僅有單一的條件。synchronized只能是非公平鎖,而ReentrantLock可以自己設(shè)置公平和非公平。總的來說java希望兩者最好都不使用,而是用阻塞隊(duì)列等來實(shí)現(xiàn)。
java中存在類鎖和對(duì)象鎖,作用如字面所描述。猜測(cè)java類鎖應(yīng)該作用于方法區(qū)當(dāng)中,對(duì)象鎖則是作用在堆區(qū)中。因?yàn)轭愋畔⒓虞d在方法區(qū),對(duì)象則分配中堆中。
synchronized代碼塊是由一對(duì)monitorenter/monitorexit指令實(shí)現(xiàn)的,Monitor對(duì)象是同步的基本實(shí)現(xiàn)單元。
3.1、synchronized作用在方法中
// 這個(gè)就是對(duì)象鎖
public synchronized void method(){
//同步代碼塊
}
// 這個(gè)就是類鎖
public static synchronized void method(){
//同步代碼塊
}
對(duì)象鎖和類鎖的區(qū)別,簡(jiǎn)單來說就是,類鎖方法怎么調(diào)用都是排斥的,而不同的對(duì)象調(diào)用同一個(gè)對(duì)象鎖方法是不互斥的,不同對(duì)象間沒有任何關(guān)系。如果不同線程,調(diào)用一個(gè)對(duì)象的對(duì)象鎖方法,那么就會(huì)互斥。具體的可以看透徹理解 Java synchronized 對(duì)象鎖和類鎖的區(qū)別,使用了synchronized非常簡(jiǎn)單。
在synchronized 對(duì)象鎖同步代碼塊中,就意味著已經(jīng)獲得了該對(duì)象鎖了,這對(duì)下面的wait()和notifyAll()方法也有用。wait()和notifyAll()方法是Object類的,屬于final不能被修改。需要和synchronized配合使用。
將代碼改成如下就可以了,如果沒有加入synchronized就調(diào)用wait()是會(huì)拋異常的
// 轉(zhuǎn)賬操作
public synchronized void transfers(int from, int to, int money) {
try {
while (accounts.get(from).money < money) {
wait();
}
accounts.get(from).money -= money;
accounts.get(to).money += money;
System.out.printf(Thread.currentThread().getName() + "Bank總共money = %d \n", getTotalMoney());
notifyAll();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
當(dāng)線程執(zhí)行wait()方法時(shí)候,會(huì)釋放當(dāng)前的鎖,然后讓出CPU,進(jìn)入等待狀態(tài)。只有當(dāng) notify/notifyAll() 被執(zhí)行時(shí)候,才會(huì)喚醒一個(gè)或多個(gè)正處于等待狀態(tài)的線程,然后繼續(xù)往下執(zhí)行,直到執(zhí)行完synchronized 代碼塊的代碼或是中途遇到wait() ,再次釋放鎖。
3.2、同步阻塞
格式如下:是對(duì)該obj對(duì)象加入對(duì)象鎖
synchronized (obj){
... 同步代碼塊
}
四、volatile域用法(可見性無原子性)
有了鎖機(jī)制,為什么又有了volatile了,難道volatile有什么更優(yōu)的地方。無論是synchronized 還是 ReentrantLock都是比較重量級(jí)的,有時(shí)只是一個(gè)變量的同步問題,所有java引入了更為精簡(jiǎn)的volatile修飾。
volatile是修飾變量,當(dāng)一個(gè)變量被volatile修飾后會(huì)有以下功能:
- 保證了不同線程對(duì)這個(gè)變量進(jìn)行操作時(shí)的可見性,即一個(gè)線程修改了某個(gè)變量的值,對(duì)其他線程來說是立即可見的。造成不一致的原因在于,電腦是有高速緩存和內(nèi)存的。如果這兩個(gè)內(nèi)存中的數(shù)據(jù)不一致,就會(huì)造成錯(cuò)誤。如果加入volatile后,就會(huì)強(qiáng)制將修改的值立即寫入到內(nèi)存中。
- 禁止進(jìn)行指令重排序。CPU會(huì)優(yōu)化指令,以此增加速度。加入volatile之后的變量,不會(huì)采用優(yōu)化策略。volatile前面的指令全部執(zhí)行完才能執(zhí)行volatile的代碼,同樣volatile代碼沒執(zhí)行完成,不能開始后面的指令執(zhí)行。
五、AtomicInteger
先看下下面這個(gè)例子:
public class Test {
public int num = 0;
public void increase() {
num++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<100;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完
Thread.yield();
System.out.println(test.num);
}
}
結(jié)果不意外的是小于1000,我這個(gè)運(yùn)行結(jié)果是9191。這是因?yàn)閚um++這個(gè)操作不是原子性的,所以這會(huì)導(dǎo)致操作是小于1000,若加入volatile修飾結(jié)果也是一樣,volatile不能保證操作的原子性,只能讓多線程的正確結(jié)果可見。
AtomicInteger就是這個(gè)int原子性操作問題的。得到的結(jié)果才是期望的1000,簡(jiǎn)單用法如下:
public class Test {
public AtomicInteger num = new AtomicInteger(0);
public void increase() {
num.getAndIncrement();
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完
Thread.yield();
System.out.println(test.num);
}
}
六、讀寫鎖
摘自《java核心技術(shù)卷一》第663頁ReentrantReadWriteLock讀寫鎖描述。
- 1、首先構(gòu)造一個(gè)讀寫鎖
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
Lock readLock = rwl.readLock();
Lock writeLock = rwl.writeLock();
- 2、讀數(shù)據(jù)加鎖操作
public double getTotalBalance() {
readLock.lock();
try {
} finally {
readLock.unlock();
}
}
- 3、寫數(shù)據(jù)加鎖操作
public void transfer() {
writeLock.lock();
try {
} finally {
writeLock.unlock();
}
}
小結(jié):如果多線程中,大量的會(huì)用到數(shù)據(jù)的讀取工作,只有少量的寫數(shù)據(jù)操作,這個(gè)時(shí)候可以考慮采用讀寫鎖分離控制。
七、同步器
-
1、CountDownLatch(倒計(jì)時(shí)門栓)
讓一個(gè)線程集等待,直到計(jì)數(shù)變成0。await()之后的線程才停止阻塞。一但計(jì)數(shù)變成0之后,就不能再次利用了。
public static void main(String[] args) {
final int count = 10; // 計(jì)數(shù)次數(shù)
final CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < count; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
// do anything
System.out.println("線程"
+ Thread.currentThread().getName());
} catch (Throwable e) {
// whatever
} finally {
// 很關(guān)鍵, 無論上面程序是否異常必須執(zhí)行countDown,否則await無法釋放
latch.countDown();
}
}
}).start();
}
try {
// 10個(gè)線程countDown()都執(zhí)行之后才會(huì)釋放當(dāng)前線程,程序才能繼續(xù)往后執(zhí)行
latch.await();
} catch (InterruptedException e) {
}
System.out.println("main thread Finish");
}
結(jié)果:
線程Thread-2
線程Thread-1
線程Thread-0
線程Thread-3
線程Thread-4
線程Thread-5
線程Thread-6
線程Thread-7
線程Thread-8
線程Thread-9
main thread Finish
等到前面的全部執(zhí)行完才會(huì)放行。
-
2、CyclicBarrier (障柵)
大量線程運(yùn)行在一次計(jì)算的不同部分的情形,當(dāng)所有的部分都準(zhǔn)備好了,需要把結(jié)果組合在一起。當(dāng)一個(gè)線程完成他的那部分任務(wù)后,就讓他運(yùn)行到障柵處。
CountDownLatch的計(jì)數(shù)器只能使用一次。而CyclicBarrier的計(jì)數(shù)器可以使用reset() 方法重置。所以CyclicBarrier能處理更為復(fù)雜的業(yè)務(wù)場(chǎng)景。
public static void main(String[] args) {
final CyclicBarrier c = new CyclicBarrier(2);
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName()+" start");
Thread.sleep(1000);
c.await();
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName()+" Finish");
}
}).start();
try {
System.out.println(Thread.currentThread().getName()+" start");
Thread.sleep(1000);
c.await();
System.out.println(Thread.currentThread().getName()+" Finish");
} catch (Exception e) {
}
}
一種結(jié)果為:
Thread-0 start
main start
Thread-0 Finish
main Finish
設(shè)置攔截兩個(gè)數(shù)量的障柵,等到兩個(gè)線程都執(zhí)行到await()之前,才允許后續(xù)執(zhí)行。
-
3、semaphore (信號(hào)量)
通常是用來限制訪問資源的總數(shù)
public class SemaphoreTest {
final Semaphore semaphore = new Semaphore(1);
public void start() {
try {
semaphore.acquire(1);
System.out.println(Thread.currentThread().getName() + " start ");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " finash ");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}
public static void main(String[] args) {
SemaphoreTest test = new SemaphoreTest();
for(int i=0;i<5;i++) {
new Thread(new Runnable() {
@Override
public void run() {
test.start();
}
}).start();
}
}
}
因?yàn)槭敲看沃荒茉试S一個(gè)線程訪問臨界資源,所以結(jié)果也是線性執(zhí)行的:
Thread-0 start
Thread-0 finash
Thread-2 start
Thread-2 finash
Thread-1 start
Thread-1 finash
Thread-3 start
Thread-3 finash
Thread-4 start
Thread-4 finash