虛假喚醒:
由于莫名其妙的原因,線程有可能在沒有調(diào)用過notify()和notifyAll()的情況下醒來。這就是所謂的假喚醒(spurious wakeups)
我的demo測試,確實存在虛假喚醒,導(dǎo)致結(jié)果不一致,如圖將while自旋換成if判斷后 輸出的count值可能到不了10000,需要多測試幾遍。
我的理解,假如一個線程進入if條件后進行wait()釋放鎖,此時有別的線程在執(zhí)行++count,此時剛好發(fā)生虛喚醒(別問我怎么會發(fā)生,高并發(fā)就是這么巧,自己測試)那么就會執(zhí)行下面的語句,也進行++count,其實相當于兩個線程看到的比如都是77 ++后只變到了78,所以就導(dǎo)致了錯誤的結(jié)果發(fā)生,自旋鎖while怎么避免呢,由于是while循環(huán),即使被虛喚醒,那么該線程的代碼還是得執(zhí)行條件判斷,就又進入了wait狀態(tài)(因為即使發(fā)生虛喚醒事件,條件變量isLocked不可能變成false)所以解決了這個問題。但是自旋鎖是while循環(huán),需要耗費cpu資源的。
完整測試代碼
package com.alibaba.otter.canal.common;
import java.util.concurrent.CountDownLatch;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@SuppressWarnings("restriction")
public class LockTest extends AbstractZkTest {
private Object obj = new Object();
private int count = 0;
@Before
public void setUp() {
}
@After
public void tearDown() {
}
@Test
public void testUnsafe() {
CountDownLatch latch=new CountDownLatch(10000);
for (int i = 0; i < 10000; i++) {
Worker worker = new Worker(latch);
worker.start();
}
try {
latch.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public int inc(){
synchronized(this){
System.out.println(count);
return ++count;
}
}
private Lock lock = new Lock();
public int lockInc() {
try {
lock.lock();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
int newCount = ++count;
System.out.println(count);
lock.unlock();
return newCount;
}
public class Lock{
private boolean isLocked = false;
public synchronized void lock()
throws InterruptedException{
// while(isLocked){
if(isLocked){
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
class Worker extends Thread {
private CountDownLatch latch;
public Worker(CountDownLatch latch) {
this.latch = latch;
}
public void run() {
// inc();
lockInc();
latch.countDown();
}
}
}
實現(xiàn)可重入鎖
概念:可重入其實是同步代碼段中 發(fā)現(xiàn)是本線程 則不需要再wait,直接可以執(zhí)行 另一個同步代碼塊。java的synchronized同步是可重入的 例如
public synchronized outer(){
inner();
}
public synchronized inner(){
//do something
}
當線程獲得鎖 進入outer同步塊后 需要執(zhí)行inner 另一個同步塊,按理說此時是所有線程都去搶占inner代碼塊的鎖,但是可重入的話 獲得鎖的線程直接可以執(zhí)行inner語句
如果用lock代替synchronized的話一定要注意處理可重入性,避免死鎖。主要就是通過記錄是不是自己獲得了鎖,并且鎖了幾次,釋放鎖的時候?qū)?yīng)的將次數(shù)減少即可。這里附上完整的測試代碼
package com.alibaba.otter.canal.common;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@SuppressWarnings("restriction")
public class LockTest extends AbstractZkTest {
private Object obj = new Object();
private int count = 0;
@Before
public void setUp() {
}
@After
public void tearDown() {
}
@Test
public void testUnsafe() {
CountDownLatch latch=new CountDownLatch(10000);
for (int i = 0; i < 10000; i++) {
Worker worker = new Worker(latch);
worker.start();
}
try {
latch.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public int inc(){
synchronized(this){
System.out.println(count);
return ++count;
}
}
private Lock lock = new Lock();
public int lockInc() {
int newCount=count;
try {
lock.lock();
newCount = ++count;
System.out.println(count);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
lock.unlock();
}
return newCount;
}
public class Lock{
private boolean isLocked = false;
public synchronized void lock()
throws InterruptedException{
while(isLocked){
// if(isLocked){
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
class Worker extends Thread {
private CountDownLatch latch;
public Worker(CountDownLatch latch) {
this.latch = latch;
}
public void run() {
// inc();
// lockInc();
try {
// reentrantOuter();//可重入 synchronized方式
// unReentrantOuter(); //lock未處理是否自己鎖 產(chǎn)生的是不可重入鎖,導(dǎo)致死鎖
reentrantLockOuter();//lock方式實現(xiàn)可重入鎖
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
latch.countDown();
}
}
/**
*
* 可重入鎖的測試
*/
@Test
public void testReentrant() {
CountDownLatch latch=new CountDownLatch(2);
for (int i = 0; i < 2; i++) {
Worker worker = new Worker(latch);
worker.start();
}
try {
latch.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public synchronized void reentrantOuter() throws InterruptedException{
System.out.println("reentrantOuter1");
reentrantInner();
}
public synchronized void reentrantInner() throws InterruptedException{
Thread.currentThread().sleep(10);
System.out.println("reentrantInner2");
}
public void unReentrantOuter() throws InterruptedException{
try {
lock.lock();
System.out.println("unReentrantouter1");
unReentrantInner();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public synchronized void unReentrantInner() {
try {
lock.lock();
System.out.println("unReentrantInner2");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void reentrantLockOuter() {
try {
reentrantLock.lock();
System.out.println("unReentrantouter1");
reentrantLockInner();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
public synchronized void reentrantLockInner() throws InterruptedException{
try {
reentrantLock.lock();
System.out.println("unReentrantInner2");
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
ReentrantLock reentrantLock = new ReentrantLock();
class ReentrantLock{
boolean isLocked = false;
Thread lockedBy = null;
int lockedCount = 0;
public synchronized void lock()
throws InterruptedException{
Thread callingThread = Thread.currentThread();
while(isLocked && lockedBy != callingThread){
wait();
}
isLocked = true;
lockedCount++;
lockedBy = callingThread;
}
public synchronized void unlock(){
if(Thread.currentThread() ==this.lockedBy){
lockedCount--;
if(lockedCount == 0){
isLocked = false;
notify();
}
}
}
}
}
上述鎖都是非公平的鎖,即先來的請求不一定是先處理,這樣的話就會導(dǎo)致有的線程可能很久得不到鎖(不要問為什么,并發(fā)大的話就是可能發(fā)生),這樣的話有些問題。我們基于此實現(xiàn)各公平的鎖。主要思路是 來的請求線程放到列表中,然后 notify的時候調(diào)用列表第一個的notify,即通知喚醒先來的請求線程即可。附上完整測試代碼。
package com.alibaba.otter.canal.common;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@SuppressWarnings("restriction")
public class FairLockTest extends AbstractZkTest {
private Object obj = new Object();
private int count = 0;
@Before
public void setUp() {
}
@After
public void tearDown() {
}
@Test
public void testUnsafe() {
CountDownLatch latch = new CountDownLatch(10000);
for (int i = 0; i < 10000; i++) {
Worker worker = new Worker(latch, i);
worker.start();
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
FairLock fairLock = new FairLock();
public int fairLockInc() {
int newCount = count;
try {
fairLock.lock();
newCount = ++count;
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
fairLock.unlock();
}
return newCount;
}
class Worker extends Thread {
private CountDownLatch latch;
public Worker(CountDownLatch latch, int i) {
this.latch = latch;
this.setName(i + " thread");
}
public void run() {
try {
fairLockInc();
} catch (Exception e) {
e.printStackTrace();
}
latch.countDown();
}
}
class FairLock {
private boolean isLocked = false;
private Thread lockingThread = null;
private List<QueueObject> waitingThreads = new ArrayList<QueueObject>();
public void lock() throws InterruptedException {
QueueObject queueObject = new QueueObject();
boolean isLockedForThisThread = true;
synchronized (this) {
System.out.println(Thread.currentThread().getName() + " in");
waitingThreads.add(queueObject);
}
while (isLockedForThisThread) {
synchronized (this) {
isLockedForThisThread = isLocked || waitingThreads.get(0) != queueObject;
if (!isLockedForThisThread) {
isLocked = true;
System.out.println(Thread.currentThread().getName()
+ " out");
waitingThreads.remove(queueObject);
lockingThread = Thread.currentThread();
return;
}
}
try {
queueObject.doWait();
} catch (InterruptedException e) {
synchronized (this) {
System.out.println(Thread.currentThread().getName()
+ " out");
waitingThreads.remove(queueObject);
}
throw e;
}
}
}
public synchronized void unlock() {
if (this.lockingThread != Thread.currentThread()) {
throw new IllegalMonitorStateException(
"Calling thread has not locked this lock");
}
isLocked = false;
lockingThread = null;
if (waitingThreads.size() > 0) {
waitingThreads.get(0).doNotify();
}
}
}
class QueueObject {
private boolean isNotified = false;
public synchronized void doWait() throws InterruptedException {
while (!isNotified) {
this.wait();
}
this.isNotified = false;
}
public synchronized void doNotify() {
this.isNotified = true;
this.notify();
}
public boolean equals(Object o) {
return this == o;
}
}
}
其實我們基于此還可以實現(xiàn)更多的鎖,可以實現(xiàn)基于優(yōu)先級的鎖,主要實現(xiàn)思路就是創(chuàng)建線程的時候傳入優(yōu)先級參數(shù),然后我們可以在入等待列表的時候?qū)Ρ葌魅氲膬?yōu)先級參數(shù)進行比較大小,找到插入的位置即可,當然方法不止這一個,也可以是通知的時候選取優(yōu)先級最大的通知。我覺得基于此我們可以把juc的所有類都可以實現(xiàn)。
上面都是基于wait/notify/notifyAll來同步的。wait/notify機制有個很蛋疼的地方是,比如線程B要用notify通知線程A,那么線程B要確保線程A已經(jīng)在wait調(diào)用上等待了,否則線程A可能永遠都在等待。編程的時候就會很蛋疼。另外,是調(diào)用notify,還是notifyAll?notify只會喚醒一個線程,如果錯誤地有兩個線程在同一個對象上wait等待,那么又悲劇了。為了安全起見,貌似只能調(diào)用notifyAll了
看一看 java.util.concurrent.locks對wait/notify/notifyAll的代替 怎么實現(xiàn)的各種鎖
這里涉及到一個基礎(chǔ)類 也是基于Unsafe 類實現(xiàn)的。
給出官方api的翻譯版
用來創(chuàng)建鎖和其他同步類的基本線程阻塞原語。
此類以及每個使用它的線程與一個許可關(guān)聯(lián)(從 Semaphore 類的意義上說)。如果該許可可用,并且可在進程中使用,則調(diào)用 park 將立即返回;否則可能 阻塞。如果許可尚不可用,則可以調(diào)用 unpark 使其可用。(但與 Semaphore 不同的是,許可不能累積,并且最多只能有一個許可。)
park 和 unpark 方法提供了阻塞和解除阻塞線程的有效方法,并且不會遇到導(dǎo)致過時方法 Thread.suspend 和 Thread.resume 因為以下目的變得不可用的問題:由于許可的存在,調(diào)用 park 的線程和另一個試圖將其 unpark 的線程之間的競爭將保持活性。此外,如果調(diào)用者線程被中斷,并且支持超時,則 park 將返回。park 方法還可以在其他任何時間“毫無理由”地返回,因此通常必須在重新檢查返回條件的循環(huán)里調(diào)用此方法。從這個意義上說,park 是“忙碌等待”的一種優(yōu)化,它不會浪費這么多的時間進行自旋,但是必須將它與 unpark 配對使用才更高效。
三種形式的 park 還各自支持一個 blocker 對象參數(shù)。此對象在線程受阻塞時被記錄,以允許監(jiān)視工具和診斷工具確定線程受阻塞的原因。(這樣的工具可以使用方法 getBlocker(java.lang.Thread) 訪問 blocker。)建議最好使用這些形式,而不是不帶此參數(shù)的原始形式。在鎖實現(xiàn)中提供的作為 blocker 的普通參數(shù)是 this。
這些方法被設(shè)計用來作為創(chuàng)建高級同步實用工具的工具,對于大多數(shù)并發(fā)控制應(yīng)用程序而言,它們本身并不是很有用。park 方法僅設(shè)計用于以下形式的構(gòu)造:
while (!canProceed()) { ... LockSupport.park(this); }在這里,在調(diào)用 park 之前,canProceed 和其他任何動作都不會鎖定或阻塞。因為每個線程只與一個許可關(guān)聯(lián),park 的任何中間使用都可能干擾其預(yù)期效果。
示例用法。 以下是一個先進先出 (first-in-first-out) 非重入鎖類的框架。
class FIFOMutex {
private final AtomicBoolean locked = new AtomicBoolean(false);
private final Queue<Thread> waiters
= new ConcurrentLinkedQueue<Thread>();
public void lock() {
boolean wasInterrupted = false;
Thread current = Thread.currentThread();
waiters.add(current);
// Block while not first in queue or cannot acquire lock
while (waiters.peek() != current ||
!locked.compareAndSet(false, true)) {
LockSupport.park(this);
if (Thread.interrupted()) // ignore interrupts while waiting
wasInterrupted = true;
}
waiters.remove();
if (wasInterrupted) // reassert interrupt status on exit
current.interrupt();
}
public void unlock() {
locked.set(false);
LockSupport.unpark(waiters.peek());
}
}
這里寫了個測試類,附上源碼(有大概注釋)
package com.alibaba.otter.canal.common;
import java.util.concurrent.locks.LockSupport;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class LockSupportTest extends AbstractZkTest {
@Before
public void setUp() {
}
@After
public void tearDown() {
}
@Test
public void testLockSupport() {
LockSupport.park();
System.out.println("block.");//阻塞在這里證明 默認許可時不可用的
}
@Test
public void testUnpark() {
Thread thread = Thread.currentThread();
LockSupport.unpark(thread);//釋放許可
LockSupport.park();// 獲取許可
System.out.println("b");//正常執(zhí)行 一對一使用
}
@Test
public void testReentrantUnpark() {
Thread thread = Thread.currentThread();
LockSupport.unpark(thread);
System.out.println("a");
LockSupport.park();
System.out.println("b");
LockSupport.park();
System.out.println("c");//阻塞在這里 ,說明非可重入的
}
@Test
public void testInterrupt() throws Exception {
Thread t = new Thread(new Runnable()
{
private int count = 0;
@Override
public void run()
{
long start = System.currentTimeMillis();
long end = 0;
while ((end - start) <= 1000)
{
count++;
end = System.currentTimeMillis();
}
System.out.println("after 1 second.count=" + count);
//等待或許許可
LockSupport.park();
System.out.println("thread over." + Thread.currentThread().isInterrupted());
}
});
t.start();
Thread.sleep(2000);
// 中斷線程
t.interrupt(); //不會拋出InterruptException 不影響主線程
System.out.println("main over");
}
}