一、显示锁
在Java 5.0之前,在协调对共享对象的访问时可以使用的机制只有synchronized和volatile。Java 5.0增加了一种新的机制:ReentrantLock。与之前提到过的机制相反,ReentrantLock并不是一种替代内置加锁的方法,而是当内置加锁机制不适用时,作为一种可选择的高级功能。
1.Lock与ReentrantLock
Lock接口:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
ReentrantLock实现了Lock接口,并提供了与synchronized相同的互斥性和内容可见性。在获取ReentrantLock时,有着与进入同步代码块相同的内存语义,在释放ReentrantLock时,同样有着与退出同步代码块相同的内存语义。此外,与synchronized一样,ReentrantLock还提供了可重入的加锁语义。ReentrantLock支持在Lock接口中定义的所有获取锁模式,并且与synchronized相比,它还为处理锁的不可用性问题提供了更高的灵活性。
使用ReentrantLock来保护对象状态:
//LockTest
public class LockTest {
private Lock lock = new ReentrantLock();
public void doSomething() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Log.d("LockTest", "zwm, run start. Thread: " + Thread.currentThread().getName());
lock.lock();
try {
Log.d("LockTest", "zwm, running. Thread: " + Thread.currentThread().getName());
} finally {
lock.unlock(); //一定要使用finally来释放Lock
}
Log.d("LockTest", "zwm, run end. Thread: " + Thread.currentThread().getName());
}
});
thread.start();
}
}
//测试代码
private void testMethod() {
Log.d(TAG, "zwm, testMetod");
LockTest lockTest = new LockTest();
lockTest.doSomething();
lockTest.doSomething();
}
//输出log
2019-07-22 20:04:54.862 zwm, testMetod
2019-07-22 20:04:54.865 zwm, run start. Thread: Thread-10
2019-07-22 20:04:54.865 zwm, running. Thread: Thread-10
2019-07-22 20:04:54.865 zwm, run end. Thread: Thread-10
2019-07-22 20:04:54.867 zwm, run start. Thread: Thread-11
2019-07-22 20:04:54.867 zwm, running. Thread: Thread-11
2019-07-22 20:04:54.867 zwm, run end. Thread: Thread-11
可定时的锁:
//LockTest
public class LockTest {
private Lock lock = new ReentrantLock();
public void doSomething() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Log.d("LockTest", "zwm, run start. Thread: " + Thread.currentThread().getName());
boolean locked = false;
try {
locked = lock.tryLock(3000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Log.d("LockTest", "zwm, InterruptedException. Thread: " + Thread.currentThread().getName());
}
if (!locked) {
Log.d("LockTest", "zwm, time out, not acquire lock. Thread: " + Thread.currentThread().getName());
return;
}
Log.d("LockTest", "zwm, running. Thread: " + Thread.currentThread().getName());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
Log.d("LockTest", "zwm, run end. Thread: " + Thread.currentThread().getName());
}
});
thread.start();
}
}
//测试代码
private void testMethod() {
Log.d(TAG, "zwm, testMetod");
LockTest lockTest = new LockTest();
lockTest.doSomething();
lockTest.doSomething();
}
//输出log
2019-07-22 20:23:46.420 zwm, testMetod
2019-07-22 20:23:46.423 zwm, run start. Thread: Thread-11
2019-07-22 20:23:46.424 zwm, running. Thread: Thread-11
2019-07-22 20:23:46.426 zwm, run start. Thread: Thread-10
2019-07-22 20:23:49.427 zwm, time out, not acquire lock. Thread: Thread-10
2019-07-22 20:23:51.425 zwm, run end. Thread: Thread-11
可中断的锁:
//LockTest
public class LockTest {
private Lock lock = new ReentrantLock();
Thread thread;
public void doSomething() {
thread = new Thread(new Runnable() {
@Override
public void run() {
Log.d("LockTest", "zwm, run start. Thread: " + Thread.currentThread().getName());
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
Log.d("LockTest", "zwm, InterruptedException. Thread: " + Thread.currentThread().getName());
return;
}
Log.d("LockTest", "zwm, running. Thread: " + Thread.currentThread().getName());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
Log.d("LockTest", "zwm, run end. Thread: " + Thread.currentThread().getName());
}
});
thread.start();
}
public void interrupt() {
thread.interrupt();
}
}
//测试代码
private void testMethod() {
Log.d(TAG, "zwm, testMetod");
final LockTest lockTest = new LockTest();
lockTest.doSomething();
lockTest.doSomething();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
lockTest.interrupt();
}
}, 3000);
}
//输出log
2019-07-23 14:04:57.826 zwm, testMetod
2019-07-23 14:04:57.828 zwm, run start. Thread: Thread-10
2019-07-23 14:04:57.828 zwm, running. Thread: Thread-10
2019-07-23 14:04:57.828 zwm, run start. Thread: Thread-11
2019-07-23 14:05:00.829 zwm, InterruptedException. Thread: Thread-11
2019-07-23 14:05:02.828 zwm, run end. Thread: Thread-10
非块结构的加锁:
在内置锁中,锁的获取和释放等操作都是基于代码块的,释放锁的操作总是与获取锁的操作处于同一个代码块,而不考虑控制权如何退出该代码块。自动的锁释放操作简化了对程序的分析,避免了可能的编码错误,但有时候需要更灵活的加锁规则。
在synchronized和ReentrantLock之间进行选择:
在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具,当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的、可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用synchronized。
2.读-写锁
读-写锁:一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
在读取锁和写入锁之间的交互可以采用多种实现方式:
- 释放优先。
- 读线程插队。
- 重入性。
- 降级。
- 升级。
ReentrantReadWriteLock为这两种锁都提供了可重入的加锁语义。与ReentrantLock类似,ReentrantReadWriteLock在构造时也可以选择是一个非公平的锁(默认)还是一个公平的锁。在公平的锁中,等待时间最长的线程将优先获得锁。如果这个锁由读线程持有,而另一个线程请求写入锁,那么其他读线程都不能获得读取锁,直到写线程使用完并且释放了写入锁。在非公平的锁中,线程获得访问许可的顺序是不确定的。写线程降级为读线程是可以的,但从读线程升级为写线程则是不可以的(这样做会导致死锁)。
当锁的持有时间较长并且大部分操作都不会修改被守护的资源时,那么读-写锁能提高并发性。
//LockTest
public class LockTest {
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void read() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Log.d("LockTest", "zwm, run start. Read Thread: " + Thread.currentThread().getName());
Lock readLock = readWriteLock.readLock();
readLock.lock();
Log.d("LockTest", "zwm, running. Read Thread: " + Thread.currentThread().getName());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
Log.d("LockTest", "zwm, run end. Read Thread: " + Thread.currentThread().getName());
}
});
thread.start();
}
public void write() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Log.d("LockTest", "zwm, run start. Write Thread: " + Thread.currentThread().getName());
Lock writeLock = readWriteLock.writeLock();
writeLock.lock();
Log.d("LockTest", "zwm, running. Write Thread: " + Thread.currentThread().getName());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
Log.d("LockTest", "zwm, run end. Write Thread: " + Thread.currentThread().getName());
}
});
thread.start();
}
}
//测试代码
private void testMethod() {
Log.d(TAG, "zwm, testMetod");
final LockTest lockTest = new LockTest();
lockTest.read();
lockTest.read();
lockTest.read();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
lockTest.write();
lockTest.write();
}
}, 3000);
}
//输出log
2019-07-23 14:54:25.075 zwm, testMetod
2019-07-23 14:54:25.081 zwm, run start. Read Thread: Thread-10
2019-07-23 14:54:25.082 zwm, running. Read Thread: Thread-10
2019-07-23 14:54:25.082 zwm, run start. Read Thread: Thread-11
2019-07-23 14:54:25.083 zwm, run start. Read Thread: Thread-12
2019-07-23 14:54:25.083 zwm, running. Read Thread: Thread-11
2019-07-23 14:54:25.083 zwm, running. Read Thread: Thread-12
2019-07-23 14:54:28.085 zwm, run start. Write Thread: Thread-14
2019-07-23 14:54:28.088 zwm, run start. Write Thread: Thread-13
2019-07-23 14:54:30.083 zwm, run end. Read Thread: Thread-10
2019-07-23 14:54:30.084 zwm, run end. Read Thread: Thread-11
2019-07-23 14:54:30.088 zwm, run end. Read Thread: Thread-12
2019-07-23 14:54:30.088 zwm, running. Write Thread: Thread-14
2019-07-23 14:54:35.089 zwm, run end. Write Thread: Thread-14
2019-07-23 14:54:35.089 zwm, running. Write Thread: Thread-13
2019-07-23 14:54:40.091 zwm, run end. Write Thread: Thread-13
二、构建自定义的同步工具
1.条件队列
正如每个Java对象都可以作为一个锁,每个对象同样可以作为一个条件队列,并且Object中的wait、notify和notifyAll方法就构成了内部条件队列的API。对象的内置锁与其内部条件队列是相互关联的,要调用对象X中条件队列的任何一个方法,必须持有对象X上的锁。这是因为"等待由状态构成的条件"与"维护状态一致性"这两种机制必须被紧密地绑定在一起:只有能对状态进行检查时,才能在某个条件上等待,并且只有能修改状态时,才能从条件等待中释放另一个线程。
Object.wait会自动释放锁,并请求操作系统挂起当前线程,从而使其他线程能够获得这个锁并修改对象的状态。当被挂起的线程醒来时,它将在返回之前重新获取锁。从直观上来理解,调用wait意味着"我要去休息了,但当发生特定的事情时唤醒我",而调用通知方法就意味着"特定的事情发生了"。
状态依赖方法的标准形式:
void stateDependentMethod() throws InterruptedException {
//必须通过一个锁来保护条件谓词
synchronized(lock) {
while(!conditionPredicate())
lock.wait();
//现在对象处于合适的状态
}
}
在条件队列API中有两个发出通知的方法,即notify和notifyAll,无论调用哪一个,都必须持有与条件队列对象相关联的锁。在调用notify时,JVM会从这个条件队列上等待的多个线程中选择一个来唤醒,而调用notifyAll则会唤醒所有在这个条件队列上等待的线程。由于在调用notify或notifyAll时必须持有条件队列对象的锁,而如果这些等待中线程此时不能重新获得锁,那么无法从wait返回,因此发出通知的线程应该尽快地释放锁,从而确保正在等待的线程尽可能快地解除阻塞。
由于多个线程可以基于不同的条件谓词在同一个条件队列上等待,因此如果使用notify而不是notifyAll,那么将是一种危险的操作,因为单一的通知很容易导致类似于信号丢失的问题。
只有同时满足以下两个条件时,才能用单一的notify而不是notifyAll:
- 所有等待线程的类型都相同。
只有一个条件谓词与条件队列相关,并且每个线程在从wait返回后执行相同的操作。 - 单进单出。
在条件变量上的每次通知,最多只能唤醒一个线程来执行。
普遍认可的做法是优先使用notifyAll而不是notify。虽然notifyAll可能比notify更低效,但却更容易确保类的行为是正确的。
//LockTest
public class LockTest {
private boolean state = false;
public void waitMethod() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Log.d("LockTest", "zwm, run start. Thread: " + Thread.currentThread().getName());
synchronized (LockTest.this) {
Log.d("LockTest", "zwm, running. Thread: " + Thread.currentThread().getName());
try {
while(!state) {
Log.d("LockTest", "zwm, before wait, state: " + state + ". Thread: " + Thread.currentThread().getName());
LockTest.this.wait();
Log.d("LockTest", "zwm, after wait, state: " + state + ". Thread: " + Thread.currentThread().getName());
}
} catch (InterruptedException e) {
Log.d("LockTest", "zwm, InterruptedException. Thread: " + Thread.currentThread().getName());
}
}
Log.d("LockTest", "zwm, run end. Thread: " + Thread.currentThread().getName());
}
});
thread.start();
}
public void notifyMethod() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Log.d("LockTest", "zwm, run start. Thread: " + Thread.currentThread().getName());
synchronized (LockTest.this) {
Log.d("LockTest", "zwm, running, notifyAll. Thread: " + Thread.currentThread().getName());
state = true;
LockTest.this.notifyAll();
}
Log.d("LockTest", "zwm, run end. Thread: " + Thread.currentThread().getName());
}
});
thread.start();
}
}
//测试代码
private void testMethod() {
Log.d(TAG, "zwm, testMetod");
final LockTest lockTest = new LockTest();
lockTest.waitMethod();
lockTest.waitMethod();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
lockTest.notifyMethod();
}
}, 3000);
}
//输出log
2019-07-23 16:30:58.634 zwm, testMetod
2019-07-23 16:30:58.636 zwm, run start. Thread: Thread-10
2019-07-23 16:30:58.636 zwm, running. Thread: Thread-10
2019-07-23 16:30:58.636 zwm, before wait, state: false. Thread: Thread-10
2019-07-23 16:30:58.636 zwm, run start. Thread: Thread-11
2019-07-23 16:30:58.636 zwm, running. Thread: Thread-11
2019-07-23 16:30:58.636 zwm, before wait, state: false. Thread: Thread-11
2019-07-23 16:31:01.648 zwm, run start. Thread: Thread-12
2019-07-23 16:31:01.649 zwm, running, notifyAll. Thread: Thread-12
2019-07-23 16:31:01.650 zwm, run end. Thread: Thread-12
2019-07-23 16:31:01.650 zwm, after wait, state: true. Thread: Thread-11
2019-07-23 16:31:01.650 zwm, run end. Thread: Thread-11
2019-07-23 16:31:01.651 zwm, after wait, state: true. Thread: Thread-10
2019-07-23 16:31:01.651 zwm, run end. Thread: Thread-10
2.显式的Condition对象
Condition接口:
public interface Condition {
void await() throws InterruptedException;
long awaitNanos(long nanosTimeout) throws InterruptedException;
void awaitUninterruptibly();
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
}
如果想编写一个带有多个条件谓词的并发对象,或者想获得除了条件队列可见性之外的更多控制权,就可以使用显式的Lock和Condition而不是内置锁和条件队列,这是一种更灵活的选择。一个Condition和一个Lock关联在一起,就像一个条件队列和一个内置锁相关联一样。要创建一个Condition,可以在相关联的Lock上调用Lock.newCondition方法。正如Lock比内置加锁提供了更为丰富的功能,Condition同样比内置条件队列提供了更丰富的功能:在每个锁上可存在多个等待、条件等待可以是可中断的或不可中断的、基于时限的等待,以及公平的或非公平的队列操作。
与内置条件队列不同的是,对于每个Lock,可以有任意数量的Condition对象。Condition对象继承了相关的Lock对象的公平性,对于公平的锁,线程会依照FIFO顺序从Condition.await中释放。特别注意:在Condition对象中,与wait、notify和notifyAll方法对应的分别是await、signal和signalAll。但是,Condition对Object进行了扩展,因而它也包含wait和notify方法。一定要确保使用正确的版本——await和signal。
在分析使用多个Condition的类时,比分析一个使用单一内部队列加多个条件谓词的类简单得多。通过将多个条件谓词分开并放到多个等待线程集中,Condition使其更容易满足单次通知的需求。signal比signalAll更高效,它能极大地减少在每次缓存操作中发生的上下文切换与锁请求的次数。与内置锁和条件队列一样,当使用显式的Lock和Condition时,也必须满足锁、条件谓词和条件变量之间的三元关系。在条件谓词中包含的变量必须由Lock来保护,并且在检查条件谓词以及调用await和signal时,必须持有Lock对象。
在使用显式的Condition和内置条件队列之间进行选择时,与在ReentrantLock和synchronized之间进行选择是一样的:如果需要一些高级功能,例如使用公平的队列操作或者在每个锁上对应多个等待线程集,那么应该优先使用Condition而不是内置条件队列。(如果需要ReentrantLock的高级功能,并且已经使用了它,那么就已经做出了选择。)
//LockTest
public class LockTest {
private Lock lock = new ReentrantLock();
private boolean state1 = false;
private boolean state2 = false;
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
public void await1Method() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Log.d("LockTest", "zwm, run start. Thread: " + Thread.currentThread().getName());
lock.lock();
Log.d("LockTest", "zwm, running. Thread: " + Thread.currentThread().getName());
try {
try {
while (!state1) {
Log.d("LockTest", "zwm, before condition1 await, state1: " + state1 + ". Thread: " + Thread.currentThread().getName());
condition1.await();
Log.d("LockTest", "zwm, after condition1 await, state1: " + state1 + ". Thread: " + Thread.currentThread().getName());
}
} catch (InterruptedException e) {
Log.d("LockTest", "zwm, InterruptedException. Thread: " + Thread.currentThread().getName());
}
} finally {
lock.unlock();
}
Log.d("LockTest", "zwm, run end. Thread: " + Thread.currentThread().getName());
}
});
thread.start();
}
public void await2Method() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Log.d("LockTest", "zwm, run start. Thread: " + Thread.currentThread().getName());
lock.lock();
Log.d("LockTest", "zwm, running. Thread: " + Thread.currentThread().getName());
try {
try {
while (!state2) {
Log.d("LockTest", "zwm, before condition2 await, state2: " + state2 + ". Thread: " + Thread.currentThread().getName());
condition2.await();
Log.d("LockTest", "zwm, after condition2 await, state2: " + state2 + ". Thread: " + Thread.currentThread().getName());
}
} catch (InterruptedException e) {
Log.d("LockTest", "zwm, InterruptedException. Thread: " + Thread.currentThread().getName());
}
} finally {
lock.unlock();
}
Log.d("LockTest", "zwm, run end. Thread: " + Thread.currentThread().getName());
}
});
thread.start();
}
public void signal1Method() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Log.d("LockTest", "zwm, run start. Thread: " + Thread.currentThread().getName());
lock.lock();
try {
state1 = true;
condition1.signal();
Log.d("LockTest", "zwm, running, condition1 signal. Thread: " + Thread.currentThread().getName());
} finally {
lock.unlock();
}
Log.d("LockTest", "zwm, run end. Thread: " + Thread.currentThread().getName());
}
});
thread.start();
}
public void signal2Method() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Log.d("LockTest", "zwm, run start. Thread: " + Thread.currentThread().getName());
lock.lock();
try {
state2 = true;
condition2.signal();
Log.d("LockTest", "zwm, running, condition2 signal. Thread: " + Thread.currentThread().getName());
} finally {
lock.unlock();
}
Log.d("LockTest", "zwm, run end. Thread: " + Thread.currentThread().getName());
}
});
thread.start();
}
}
//测试代码
private void testMethod() {
Log.d(TAG, "zwm, testMetod");
final LockTest lockTest = new LockTest();
lockTest.await1Method();
lockTest.await2Method();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
lockTest.signal2Method();
lockTest.signal1Method();
}
}, 3000);
}
//输出log
2019-07-23 17:26:26.142 zwm, testMetod
2019-07-23 17:26:26.166 zwm, run start. Thread: Thread-11
2019-07-23 17:26:26.166 zwm, running. Thread: Thread-11
2019-07-23 17:26:26.167 zwm, before condition2 await, state2: false. Thread: Thread-11
2019-07-23 17:26:26.168 zwm, run start. Thread: Thread-10
2019-07-23 17:26:26.168 zwm, running. Thread: Thread-10
2019-07-23 17:26:26.169 zwm, before condition1 await, state1: false. Thread: Thread-10
2019-07-23 17:26:29.170 zwm, run start. Thread: Thread-12
2019-07-23 17:26:29.170 zwm, running, condition2 signal. Thread: Thread-12
2019-07-23 17:26:29.171 zwm, run end. Thread: Thread-12
2019-07-23 17:26:29.171 zwm, after condition2 await, state2: true. Thread: Thread-11
2019-07-23 17:26:29.171 zwm, run end. Thread: Thread-11
2019-07-23 17:26:29.188 zwm, run start. Thread: Thread-13
2019-07-23 17:26:29.188 zwm, running, condition1 signal. Thread: Thread-13
2019-07-23 17:26:29.189 zwm, run end. Thread: Thread-13
2019-07-23 17:26:29.189 zwm, after condition1 await, state1: true. Thread: Thread-10
2019-07-23 17:26:29.189 zwm, run end. Thread: Thread-10
3.AbstractQueuedSynchronizer
AQS是一个用于构建锁和同步器的框架,许多同步器都可以通过AQS很容易并且高效地构造出来。不仅ReentrantLock和Semaphore是基于AQS构建的,还包括CountDownLatch、ReentrantReadWriteLock、SynchronousQueue和FutureTask。
大多数开发者都不会直接使用AQS,标准同步器类的集合能够满足绝大多数情况的需求。但如果能了解标准同步器类的实现方式,那么对于理解它们的工作原理是非常有帮助的。
AQS中获取操作和释放操作的标准形式:
boolean acquire() throws InterruptedException {
while(当前状态不允许获取操作) {
if(需要阻塞获取请求) {
如果当前线程不在队列中,则将其插入队列
阻塞当前线程
} else {
返回失败
}
}
可能更新同步器的状态
如果线程位于队列中,则将其移出队列
返回成功
}
void release() {
更新同步器的状态
if(新的状态允许某个被阻塞的线程获取成功) {
解除队列中一个或多个线程的阻塞状态
}
}
三、原子变量与非阻塞同步机制
1.锁的劣势
通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有守护变量的锁,都能采用独占方式来访问这些变量,并且对变量的任何修改对随后获得这个锁的其他线程都是可见的。现代的许多JVM都对非竞争锁获取和锁释放等操作进行了极大的优化,但如果有多个线程同时请求锁,那么JVM就需要借助操作系统的功能。如果出现了这种情况,那么一些线程将被挂起并且在稍后恢复运行。当线程恢复执行时,必须等待其他线程执行完它们的时间片以后,才能被调度执行。在挂起和恢复线程等过程中存在着很大的开销,并且通常存在着较长时间的中断。如果在基于锁的类中包含有细粒度的操作(例如同步容器类,在其大多数方法中只包含了少量操作),那么当在锁上存在着激烈的竞争时,调度开销与工作开销的比值会非常高。另外,当一个线程正在等待锁时,它不能做任何其他事情。如果一个线程在持有锁的情况下被延迟执行,那么所有需要这个锁的线程都无法执行下去。
与锁相比,volatile变量是一种更轻量级的同步机制,因为在使用这些变量时不会发生上下文切换或线程调度等操作。然而,volatile变量同样存在一些局限:虽然他们提供了相似的可见性保证,但不能用于构建原子的复合操作。因此,当一个变量依赖其他的变量时,或者当变量的新值依赖于旧值时,就不能使用volatile变量。这些都限制了volatile变量的使用,因此它们不能用来实现一些常见的工具,例如计数器或互斥体。
2.硬件对并发的支持
比较并交换:
在大多数处理器架构中实现了一个比较并交换(CAS)指令。CAS包含了3个操作数——需要读取的内存位置V、进行比较的值A和拟写入的新值B。当且仅当V的值等于A时,CAS才会通过原子方式用新值B来更新V的值,否则不会执行任何操作。
在原子变量类(例如java.util.concurrent.atomic中的AtomicXxx)中使用了这些底层的JVM支持为数字类型和引用类型提供一种高效的CAS操作,而在java.util.concurrent中的大多数类在实现时则直接或间接地使用了这些原子变量类。
3.原子变量类
原子变量比锁的粒度更细,量级更轻,并且对于在多处理器系统上实现高性能的并发代码来说是非常关键的。原子变量将发生竞争的范围缩小到单个变量上,这是你获得的粒度最细的情况。原子变量类相当于一种泛化的volatile变量,能够支持原子的和有条件的读-改-写操作。
共有12个原子变量类,可分为4组:
标量类(Scalar)、更新器类、数组类以及复合变量类。最常见的原子变量就是标量类:AtomicInteger、AtomicLong、AtomicBoolean以及AtomicReference。所有这些类都支持CAS,此外,AtomicInteger和AtomicLong还支持算术运算。
尽管原子的标量类扩展了Number类,但并没有扩展一些基本类型的包装类,例如Integer或Long。事实上,它们也不能进行扩展:基本类型的包装类是不可修改的,而原子变量类是可修改的。在原子变量类中同样没有重新定义hashCode或equals方法,每个实例都是不同的。与其他可变对象相同,它们也不宜用做基于散列的容器中的键值。
四、Java内存模型
1.Happens-Before规则
Java内存模型为程序中所有的操作定义了一个偏序关系,称之为Happens-Before。要想保证执行操作B的线程看到操作A的结果(无论A和B是否在同一个线程中执行),那么在A和B之间必须满足Happens-Before关系。如果两个操作之间缺乏Happens-Before关系,那么JVM可以对它们任意地重排序。
Happens-Before的规则包括:
- 程序顺序规则。
如果程序中操作A在操作B之前,那么在线程中A操作将在B操作之前执行。 - 监视器锁规则。
在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行。 - volatile变量规则。
对volatile变量的写入操作必须在对该变量的读操作之前执行。 - 线程启动规则。
在线程上对Thread.start的调用必须在该线程中执行任何操作之前执行。 - 线程结束规则。
线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或者从Thread.join中成功返回,或者在调用Thread.isAlive时返回false。 - 中断规则。
当一个线程在另一个线程上调用interrupt时,必须在被中断线程检测到interrupt调用之前执行(通过抛出InterruptedException,或者调用isInterrupted和interrupted)。 - 终结器规则。
对象的构造函数必须在启动该对象的终结器之前执行完成。 - 传递性。
如果操作A在操作B之前执行,并且操作B在操作C之前执行,那么操作A必须在操作C之前执行。
当两个线程使用同一个锁进行同步时,在它们之间的Happens-Before关系为:在线程A内部的所有操作都按照它们在源程序中的先后顺序来排序,在线程B内部的操作也是如此。由于A释放了锁M,并且B随后获得了锁M,因此A中所有在释放锁之前的操作,也就位于B中请求锁之后的所有操作之前。如果这两个线程是在不同的锁上进行同步的,那么就不能推断它们之间的动作顺序,因为在这两个线程的操作之间并不存在Happens-Before关系。
2.发布
不安全的发布:
public class JMMTest {
private static Object object;
public static Object getInstance() {
if(object == null) {
object = new Object();
}
return object;
}
}
安全的发布:
线程安全的延迟初始化:
public class JMMTest {
private static Object object;
public synchronized static Object getInstance() {
if(object == null) {
object = new Object();
}
return object;
}
}
提前初始化:
public class JMMTest {
private static Object object = new Object();
public static Object getInstance() {
return object;
}
}
延迟初始化占位类模式:
public class JMMTest {
private static class Holder {
public static Object object = new Object();
}
public static Object getInstance() {
return Holder.object;
}
}
双重检查加锁:
DCL属于"糟糕"的技巧,不建议这么做。
public class JMMTest {
private static Object object;
public static Object getInstance() {
if(object == null) {
synchronized (JMMTest.class) {
if(object == null) {
object = new Object();
}
}
}
return object;
}
}
DCL的真正问题在于:当在没有同步的情况下读取一个共享对象时,可能发生的最糟糕事情只是看到一个失效值(在这种情况下是一个空值),此时DCL方法将通过在持有锁的情况下再次尝试来避免这种风险。然而,实际情况远比这种情况糟糕——线程可能看到引用的当前值,但对象的状态值却是失效的,这意味着线程可以看到对象处于无效或错误的状态。
在JMM的后续版本(Java 5.0以及更高的版本)中,如果把object声明为volatile类型,那么就能启用DCL,并且这种方式对性能的影响很小,因为volatile变量读取操作的性能通常只是略高于非volatile变量读取操作的性能。然而,DCL的这种使用方式已经被广泛地废弃了——促使该模式出现的驱动力(无竞争同步的执行速度很慢,以及JVM启动时很慢)已经不复存在,因而它不是一种高效的优化措施。延迟初始化占位类模式能带来同样的优势,并且更容易理解。
3.初始化过程中的安全性
初始化安全性只能保证通过final域可达的值从构造过程完成时开始的可见性。对于通过非final域可达的值,或者在构造过程完成后可能改变的值,必须采用同步来确保可见性。
网友评论