本系列文章:
Java并发编程学习之路(一)并发编程三要素、Thread、Runnable、interrupted、join、sleep、yield
Java并发编程学习之路(二)线程同步机制、synchronized、CAS、volatile、final、Lock、AQS
Java并发编程学习之路(三)ReentrantLock、ReentrantReadWriteLock、死锁、原子类
Java并发编程学习之路(四)线程池、FutureTask
Java并发编程学习之路(五)线程协作、wait/notify/notifyAll、Condition、await/signal/signalAll、生产者–消费者
Java并发编程学习之路(六)ThreadLocal、BlockingQueue、CopyOnWriteArrayList、ConcurrentHashmap
Java并发编程学习之路(七)CountDownLatch、CyclicBarrier、Semaphore、Exchanger、Phaser
Java并发编程学习之路(八)多线程编程例子
一、ReentrantLock
上篇文章介绍完了AQS,接下来就介绍一下AQS的运用:ReentrantLock。ReentrantLock主要利用CAS+AQS队列来实现,支持公平锁和非公平锁。
ReentrantLock使用示例:
private Lock lock = new ReentrantLock();
public void test(){
lock.lock();
try{
doSomeThing();
}catch (Exception e){
}finally {
lock.unlock();
}
}
ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:
- ReentrantLock实例是非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;
- ReentrantLock实例是公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。
1.1 ReentrantLock的特点
- 1、可重入锁
可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。
关于可重入性,看个例子:
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
method1();
}
public static void method1() {
lock.lock();
try {
log.debug("execute method1");
method2();
} finally {
lock.unlock();
}
}
public static void method2() {
lock.lock();
try {
log.debug("execute method2");
method3();
} finally {
lock.unlock();
}
}
public static void method3() {
lock.lock();
try {
log.debug("execute method3");
} finally {
lock.unlock();
}
}
- 2、可中断锁
可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。 - 3、公平锁与非公平锁
公平锁是指多个线程同时尝试获取同一把锁时,锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO;而非公平锁则允许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁。
ReentrantLock提供了两个构造方法:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
默认构造方法初始化为NonfairSync对象,即非公平锁,而带参数的构造器可以指定使用公平锁和非公平锁。
- 4、可以进行超时设置
1.2 重入性
要支持重入性,就要解决两个问题:
- 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;
- 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。
- 1、获取锁
针对第一个问题,来看看ReentrantLock是怎样实现的。以非公平锁为例,要判断当前线程能否获得锁,核心方法为nonfairTryAcquire:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//1. 如果该锁未被任何线程占有,该锁能被当前线程获取
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//2.若被占有,检查占有线程是否是当前线程
else if (current == getExclusiveOwnerThread()) {
// 3. 再次获取,计数加一
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
为了支持重入性,在第二步增加了处理逻辑,如果该锁已经被线程所占有了,会继续检查占有线程是否为当前线程,如果是的话,同步状态加1返回true,表示可以再次获取成功,每次重新获取都会对同步状态进行加一的操作。
- 2、释放锁
还是以非公平锁为例,核心方法为tryRelease:
protected final boolean tryRelease(int releases) {
//1. 同步状态减1
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
//2. 只有当同步状态为0时,锁成功被释放,返回true
free = true;
setExclusiveOwnerThread(null);
}
// 3. 锁未被完全释放,返回false
setState(c);
return free;
}
需要注意的是,重入锁的释放必须得等到同步状态为0时锁才算成功释放,否则锁仍未释放。如果锁被获取n次,释放了n-1次,该锁未完全释放返回false,只有被释放n次才算成功释放,返回true。
- 3、重入锁使用示例
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
for (int i = 1; i <= 3; i++) {
lock.lock();
System.out.println("lock"+i);
}
for(int i=1;i<=3;i++){
try {
} finally {
lock.unlock();
System.out.println("unlock"+i);
}
}
}
结果:
lock1
lock2
lock3
unlock1
unlock2
unlock3
1.3 非公平锁和非公平锁
1.3.1 非公平锁
即NonfairSync。
- 1、获取锁
源码:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
NonfairSync中lock方法的逻辑:用一个CAS操作,判断state是否是0(表示当前锁未被占用),如果是0则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操作只能保证一个线程操作成功,剩下的线程要去排队。
“非公平”即体现在这里,如果占用锁的线程刚释放锁,state置为0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就“插队”了。
如果有三个线程去竞争锁,假设线程A的CAS操作成功了,线程B和C就要执行AQS中的acquire方法。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这个方法的逻辑在AQS章节已介绍,不再赘述。
- 2、释放锁
即unlock()方法:
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
unlock()方法的流程大致为先尝试释放锁,若释放成功,那么查看头结点的状态是否为SIGNAL,如果是则唤醒头结点的下个节点关联的线程,如果释放失败那么返回false表示解锁失败。
tryRelease源码:
/**
* 释放当前线程占用的锁
* @param releases
* @return 是否释放成功
*/
protected final boolean tryRelease(int releases) {
// 计算释放后state值
int c = getState() - releases;
// 如果不是当前线程占用锁,那么抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
// 锁被重入次数为0,表示释放成功
free = true;
// 清空独占线程
setExclusiveOwnerThread(null);
}
// 更新state值
setState(c);
return free;
}
这里入参为1。tryRelease的过程为:当前释放锁的线程若不持有锁,则抛出异常。若持有锁,计算释放后的state值是否为0,若为0表示锁已经被成功释放,并且则清空独占线程,最后更新state值,返回free。
1.3.2 公平锁
即FairSync。公平锁和非公平锁不同之处在于,公平锁在获取锁的时候,不会先去检查state状态,而是直接执行aqcuire(1)。fairSync的lock()源码:
final void lock() {
acquire(1);
}
acquire是AQS中的模板方法,会调用子类(FairSync)的tryAcquire(int acquires)的方法:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
这段代码的逻辑与nonfairTryAcquire基本一致,唯一的不同在于增加了hasQueuedPredecessors的逻辑判断,该方法用来判断当前节点在同步队列中是否有前驱节点的判断,如果有前驱节点说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败。如果当前节点没有前驱节点的话,再才有做后面的逻辑判断的必要性。公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁
。
1.3.3 非公平锁和公平锁的比较
先用一个例子测试一下:
public class TestDemo {
static Lock lock = new ReentrantLock(true);
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<5;i++){
new Thread(new ThreadDemo(i)).start();
}
}
static class ThreadDemo implements Runnable {
Integer id;
public ThreadDemo(Integer id) {
this.id = id;
}
@Override
public void run() {
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<2;i++){
lock.lock();
System.out.println("获得锁的线程:"+id);
lock.unlock();
}
}
}
}
公平锁的测试结果,可以看到线程几乎是轮流的获取到了锁:
再将上述代码改成非公平锁实现,可以看出线程会重复获取锁。如果申请获取锁的线程足够多,那么可能会造成某些线程长时间得不到锁。这就是非公平锁的“饥饿”问题:
公平锁与非公平锁的比较:
公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象
。- 公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。
1.4 响应中断
当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。而ReentrantLock给我们提供了一个可以响应中断的获取锁的方法lockInterruptibly()
,该方法可以用来解决死锁问题。
接下来看个例子:两个子线程,子线程在运行时会分别尝试获取两把锁。其中一个线程先获取锁1在获取锁2,另一个线程正好相反。如果没有外界中断,该程序将处于死锁状态永远无法停止。此时可以使其中一个线程中断,来结束线程间毫无意义的等待。被中断的线程将抛出异常,而另一个线程将能获取锁后正常结束。示例:
public class TestDemo {
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
//该线程先获取锁1,再获取锁2
Thread thread = new Thread(new ThreadDemo(lock1, lock2));
//该线程先获取锁2,再获取锁1
Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));
thread.start();
thread1.start();
thread.interrupt();//是第一个线程中断
}
static class ThreadDemo implements Runnable {
Lock firstLock;
Lock secondLock;
public ThreadDemo(Lock firstLock, Lock secondLock) {
this.firstLock = firstLock;
this.secondLock = secondLock;
}
@Override
public void run() {
try {
firstLock.lockInterruptibly();
TimeUnit.MILLISECONDS.sleep(10);//更好的触发死锁
secondLock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
firstLock.unlock();
secondLock.unlock();
System.out.println(Thread.currentThread().getName()+"正常结束!");
}
}
}
}
结果:
1.5 超时机制
在ReetrantLock中,tryLock(long timeout, TimeUnit unit) 提供了超时获取锁的功能。它的语义是在指定的时间内如果获取到锁就返回true,获取不到则返回false。这种机制避免了线程无限期的等待锁释放。
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
如果线程被中断了,那么直接抛出InterruptedException。如果未中断,先尝试获取锁,获取成功就直接返回,获取失败则进入doAcquireNanos。
/**
* 在有限的时间内去竞争锁
* @return 是否获取成功
*/
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
// 起始时间
long lastTime = System.nanoTime();
// 线程入队
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
// 又是自旋!
for (;;) {
// 获取前驱节点
final Node p = node.predecessor();
// 如果前驱是头节点并且占用锁成功,则将当前节点变成头结点
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 如果已经超时,返回false
if (nanosTimeout <= 0)
return false;
// 超时时间未到,且需要挂起
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
// 阻塞当前线程直到超时时间到期
LockSupport.parkNanos(this, nanosTimeout);
long now = System.nanoTime();
// 更新nanosTimeout
nanosTimeout -= now - lastTime;
lastTime = now;
if (Thread.interrupted())
//相应中断
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
该方法流程简述为:线程先入等待队列,然后开始自旋,尝试获取锁,获取成功就返回,失败则在队列里找一个安全点把自己挂起直到超时时间过期。这里为什么还需要循环呢?因为当前线程节点的前驱状态可能不是SIGNAL,那么在当前这一轮循环中线程不会被挂起,然后更新超时时间,开始新一轮的尝试。
用超时机制解决死锁的例子:
public class TestDemo {
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
//该线程先获取锁1,再获取锁2
Thread thread = new Thread(new ThreadDemo(lock1, lock2));
//该线程先获取锁2,再获取锁1
Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));
thread.start();
thread1.start();
}
static class ThreadDemo implements Runnable {
Lock firstLock;
Lock secondLock;
public ThreadDemo(Lock firstLock, Lock secondLock) {
this.firstLock = firstLock;
this.secondLock = secondLock;
}
@Override
public void run() {
try {
while(!lock1.tryLock()){
TimeUnit.MILLISECONDS.sleep(10);
}
while(!lock2.tryLock()){
lock1.unlock();
TimeUnit.MILLISECONDS.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
firstLock.unlock();
secondLock.unlock();
System.out.println(Thread.currentThread().getName()+"正常结束!");
}
}
}
}
结果:
Thread-0正常结束!
Thread-1正常结束!
二、ReentrantReadWriteLock
2.1 ReentrantReadWriteLock的特点
有这样的场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。
针对这种场景,可以用读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁。读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞
。
读写锁有以下三个重要的特性:
公平和非公平
:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。重进入
:读锁和写锁都支持线程重进入。锁降级
:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁
。
ReentrantReadWriteLock类的大致结构:
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
/** 读锁 */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** 写锁 */
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
/** 使用默认(非公平)的排序属性创建一个新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock() {
this(false);
}
/** 使用给定的公平策略创建一个新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
/** 返回用于写入操作的锁 */
public ReentrantReadWriteLock.WriteLock writeLock() {
return writerLock; }
/** 返回用于读取操作的锁 */
public ReentrantReadWriteLock.ReadLock readLock() {
return readerLock; }
abstract static class Sync extends AbstractQueuedSynchronizer {
...}
static final class NonfairSync extends Sync {
...}
static final class FairSync extends Sync {
...}
public static class ReadLock implements Lock, java.io.Serializable {
...}
public static class WriteLock implements Lock, java.io.Serializable {
...}
}
ReentrantReadWriteLock实现了ReadWriteLock接口,ReadWriteLock接口定义了获取读锁和写锁的规范,具体需要实现类去实现。ReadWriteLock非常简单,只定义了两个接口:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
ReentrantReadWriteLock有五个内部类:
Sync继承自AQS、NonfairSync继承自Sync类、FairSync继承自Sync类;ReadLock实现了Lock接口、WriteLock也实现了Lock接口。
2.2 Sync
Sync类内部存在两个内部类,分别为HoldCounter和ThreadLocalHoldCounter,其中HoldCounter主要与读锁配套使用:
// 计数器
static final class HoldCounter {
//表示某个读线程重入的次数,用来计数
int count = 0;
// Use id, not reference, to avoid garbage retention
// 获取当前线程的TID属性的值
final long tid = getThreadId(Thread.currentThread());
}
ThreadLocalHoldCounter的源码:
// 本地线程计数器
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
// 重写初始化方法,在没有进行set的情况下,获取的都是该HoldCounter值
public HoldCounter initialValue() {
return new HoldCounter();
}
}
Sync类的属性:
abstract static class Sync extends AbstractQueuedSynchronizer {
// 版本序列号
private static final long serialVersionUID = 6317671515068378041L;
// 高16位为读锁,低16位为写锁
static final int SHARED_SHIFT = 16;
// 读锁单位
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// 读锁最大数量
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
// 写锁最大数量
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// 本地线程计数器
private transient ThreadLocalHoldCounter readHolds;
// 缓存的计数器
private transient HoldCounter cachedHoldCounter;
// 第一个读线程
private transient Thread firstReader = null;
// 第一个读线程的计数
private transient int firstReaderHoldCount;
}
Sync类的构造函数:
// 构造函数
Sync() {
// 本地线程计数器
readHolds = new ThreadLocalHoldCounter();
// 设置AQS的状态
setState(getState()); // ensures visibility of readHolds
}
2.3 读写状态的设计
同步状态在重入锁的实现中是表示被同一个线程重复获取的次数,即一个整形变量来维护,在ReentrantLock中的state仅仅表示是否锁定,不用区分是读锁还是写锁。但读写锁需要在同步状态(一个整形变量)上维护多个读线程和一个写线程的状态。
读写锁对于同步状态的实现是在一个整形变量上通过“按位切割使用”:将变量切割成两部分,高16位表示读,低16位表示写。
假设当前同步状态值为S,get和set的操作如下:
- 获取写状态:
S & 0x0000FFFF:将高16位全部抹去。- 获取读状态:
S>>>16:无符号补0,右移16位。- 写状态加1:
S+1。- 读状态加1:
S+(1<<16)即S + 0x00010000。
2.4 写锁的获取与释放
WriteLock类中的lock和unlock方法:
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
2.4.1 写锁的获取
即Sync类中的tryAcquire方法:
protected final boolean tryAcquire(int acquires) {
//当前线程
Thread current = Thread.currentThread();
//获取状态
int c = getState();
//写线程数量(即获取独占锁的重入数)
int w = exclusiveCount(c);
//当前同步状态state != 0,说明已经有其他线程获取了读锁或写锁
if (c != 0) {
// 当前state不为0,此时:如果写锁状态为0说明读锁此时被占用返回false;
// 如果写锁状态不为0且写锁没有被当前线程持有返回false
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//判断同一线程获取写锁是否超过最大次数(65535),支持可重入
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//更新状态
//此时当前线程已持有写锁,现在是重入,所以只需要修改锁的数量即可。
setState(c + acquires);
return true;
}
//到这里说明此时c=0,读锁和写锁都没有被获取
//writerShouldBlock表示是否阻塞
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//设置锁为当前线程所有
setExclusiveOwnerThread(current);
return true;
}
//返回占有写锁的线程数量
static int exclusiveCount(int c) {
//直接将状态state和(2^16 - 1)做与运算,其等效于将state模上2^16。
//这样计算是因为写锁数量由state的低十六位表示。
return c & EXCLUSIVE_MASK;
}
获取写锁的步骤:
- (1)首先获取c、w。c表示当前锁状态;w表示写线程数量。然后判断同步状态state是否为0。如果state!=0,说明已经有其他线程获取了读锁或写锁,执行(2);否则执行(5)。
- (2)如果锁状态不为零(c != 0),而写锁的状态为0(w = 0),说明读锁此时被其他线程占用,所以当前线程不能获取写锁,自然返回false。或者锁状态不为零,而写锁的状态也不为0,但是获取写锁的线程不是当前线程,则当前线程也不能获取写锁。
- (3)判断当前线程获取写锁是否超过最大次数,若超过,抛异常,反之更新同步状态(此时当前线程已获取写锁,更新是线程安全的),返回true。
- (4)如果state为0,此时读锁或写锁都没有被获取,判断是否需要阻塞(公平和非公平方式实现不同),在非公平策略下总是不会被阻塞,在公平策略下会进行判断(判断同步队列中是否有等待时间更长的线程,若存在,则需要被阻塞,否则,无需阻塞),如果不需要阻塞,则CAS更新同步状态,若CAS成功则返回true,失败则说明锁被别的线程抢去了,返回false。如果需要阻塞则也返回false。
- (5)成功获取写锁后,将当前线程设置为占有写锁的线程,返回true。
2.4.2 写锁的释放
即Sync类中的tryRelease方法:
protected final boolean tryRelease(int releases) {
//若锁的持有者不是当前线程,抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//写锁的新线程数
int nextc = getState() - releases;
//如果独占模式重入数为0了,说明独占模式被释放
boolean free = exclusiveCount(nextc) == 0;
if (free)
//若写锁的新线程数为0,则将锁的持有者设置为null
setExclusiveOwnerThread(null);
//设置写锁的新线程数
//不管独占模式是否被释放,更新独占重入数
setState(nextc);
return free;
}
写锁的释放过程:首先查看当前线程是否为写锁的持有者,如果不是抛出异常。然后检查释放后写锁的线程数是否为0,如果为0则表示写锁空闲了,释放锁资源将锁的持有线程设置为null,否则释放仅仅只是一次重入锁而已,并不能将写锁的线程清空。
2.5 读锁的获取与释放
2.5.1 读锁的获取
即Sync类中的tryAcquireShared方法:
protected final int tryAcquireShared(int unused) {
// 获取当前线程
Thread current = Thread.currentThread();
// 获取状态
int c = getState();
//如果写锁线程数 != 0 ,且独占锁不是当前线程则返回失败,因为存在锁降级
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 读锁数量
int r = sharedCount(c);
/*
* readerShouldBlock():读锁是否需要等待(公平锁原则)
* r < MAX_COUNT:持有线程小于最大数(65535)
* compareAndSetState(c, c + SHARED_UNIT):设置读取锁状态
*/
// 读线程是否应该被阻塞、并且小于最大值、并且比较设置成功
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//r == 0,表示第一个读锁线程,第一个读锁firstRead是不会加入到readHolds中
if (r == 0) {
// 读锁数量为0
// 设置第一个读线程
firstReader = current;
// 读线程占用的资源数为1
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 当前线程为第一个读线程,表示第一个读锁线程重入
// 占用资源数加1
firstReaderHoldCount++;
} else {
// 读锁数量不为0并且不为当前线程
// 获取计数器
HoldCounter rh = cachedHoldCounter;
// 计数器为空或者计数器的tid不为当前正在运行的线程的tid
if (rh == null || rh.tid != getThreadId(current))
// 获取当前线程对应的计数器
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0) // 计数为0
//加入到readHolds中
readHolds.set(rh);
//计数+1
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
2.5.2 读锁的释放
即Sync类中的tryReleaseShared方法:
protected final boolean tryReleaseShared(int unused) {
// 获取当前线程
Thread current = Thread.currentThread();
if (firstReader == current) {
// 当前线程为第一个读线程
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1) // 读线程占用的资源数为1
firstReader = null;
else // 减少占用的资源
firstReaderHoldCount--;
} else {
// 当前线程不为第一个读线程
// 获取缓存的计数器
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) // 计数器为空或者计数器的tid不为当前正在运行的线程的tid
// 获取当前线程对应的计数器
rh = readHolds.get();
// 获取计数
int count = rh.count;
if (count <= 1) {
// 计数小于等于1
// 移除
readHolds.remove();
if (count <= 0) // 计数小于等于0,抛出异常
throw unmatchedUnlockException();
}
// 减少计数
--rh.count;
}
for (;;) {
// 无限循环
// 获取状态
int c = getState();
// 获取状态
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc)) // 比较并进行设置
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
此方法表示读锁线程释放锁。首先判断当前线程是否为第一个读线程firstReader,若是,则判断第一个读线程占有的资源数firstReaderHoldCount是否为1,若是,则设置第一个读线程firstReader为空,否则,将第一个读线程占有的资源数firstReaderHoldCount减1;若当前线程不是第一个读线程,那么首先会获取缓存计数器(上一个读锁线程对应的计数器 ),若计数器为空或者tid不等于当前线程的tid值,则获取当前线程的计数器,如果计数器的计数count小于等于1,则移除当前线程对应的计数器,如果计数器的计数count小于等于0,则抛出异常,之后再减少计数即可。无论何种情况,都会进入无限循环,该循环可以确保成功设置状态state。
2.6 读写锁的互斥性测试
- 1、基础代码
public class ReadWriteLockTest {
private ReentrantReadWriteLock rw1 = new ReentrantReadWriteLock();
//获取写锁
public void getW(Thread thread) {
try {
rw1.writeLock().lock();
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start <= 10){
System.out.println(thread.getName() + "正在写操作");
}
System.out.println(thread.getName() + "写操作完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
rw1.writeLock().unlock();
}
}
//获取读锁
public void getR(Thread thread) {
try {
rw1.readLock().lock();
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start <= 10){
System.out.println(thread.getName() + "正在读操作");
}
System.out.println(thread.getName() + "读操作完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
rw1.readLock().unlock();
}
}
}
- 2、并发读
public static void main(String[] args) {
final ReadWriteLockTest test = new ReadWriteLockTest();
new Thread(){
@Override
public void run() {
test.getR(Thread.currentThread());
}
}.start();
new Thread(){
@Override
public void run() {
test.getR(Thread.currentThread());
}
}.start();
}
结果:
Thread-1正在读操作
Thread-0正在读操作
Thread-1读操作完成
Thread-0读操作完成
可以看到读线程间是不用排队的。
- 3、并发写
public static void main(String[] args) {
final ReadWriteLockTest test = new ReadWriteLockTest();
new Thread(){
@Override
public void run() {
test.getW(Thread.currentThread());
}
}.start();
new Thread(){
@Override
public void run() {
test.getW(Thread.currentThread());
}
}.start();
}
结果:
可以看出写线程获取锁是互斥的。
- 3、并发读写
public static void main(String[] args) {
final ReadWriteLockTest test = new ReadWriteLockTest();
new Thread(){
@Override
public void run() {
test.getR(Thread.currentThread());
}
}.start();
new Thread(){
@Override
public void run() {
test.getW(Thread.currentThread());
}
}.start();
}
结果:
可以看出读写线程获取锁也是互斥的。
2.7 读写锁的使用
示例:
public class ReadWriteLockTest {
static Lock lock = new ReentrantLock();
private static int value;
static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
static Lock readLock = readWriteLock.readLock();
static Lock writeLock = readWriteLock.writeLock();
public static void read(Lock lock) {
try {
lock.lock();
Thread.sleep(1000);
System.out.println("read over!");
//模拟读取操作
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void write(Lock lock, int v) {
try {
lock.lock();
Thread.sleep(1000);
value = v;
System.out.println("write over!");
//模拟写操作
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
Runnable readR = ()-> read(readLock);
Runnable writeR = ()->write(writeLock, new Random().nextInt());
for(int i=0; i<2; i++)
new Thread(readR).start();
for(int i=0; i<2; i++)
new Thread(writeR).start();
}
}
结果:
read over!
read over!
write over!
write over!
三、Lock相关的一些问题
到此,显式锁和隐式锁的基本使用已经介绍了,接下来就看一些Lock相关的问题。
3.1 synchronized 和 Lock有什么区别
因为Lock的最常见实现是ReentrantLock,所以下面会穿插着ReentrantLock来对比。
synchronized和ReentrantLock相同点:都是用来协调多线程,对共享对象、变量的访问都是可重入锁,同一线程可以多次获得同一个锁,都保证了可见性和互斥性。
具体的不同点:
- 1、底层实现
synchronized是关键字,属于JVM层面,底层是由一对monitorenter和monitorexit指令实现的。
ReentrantLock是一个具体类,是API层面的锁。
synchronized隐式获得释放锁,ReentrantLock 显式的获得、释放锁
。 - 2、是否自动释放锁
synchronized不需要用户手动释放锁
,当synchronized代码块执行完成后,系统会自动让线程释放对锁的占用。线程执行发生异常,JVM会让线程释放锁。
ReentrantLock需要在finally块中手动释放锁
,若没有手动释放可能导致死锁现象。 - 3、是否能判断是否获得了锁
synchronized不能判断。
Lock可以判断。 - 4、加锁是否公平
synchronized非公平锁
。
ReentrantLock两者都可以,默认是非公平锁
。 - 5、是否可以有多个唤醒条件
synchronized不能。
ReentrantLock可用来分组唤醒需要唤醒的线程
。而不是像synchronized要么随机唤醒一个线程,要么唤醒所有线程。 - 6、是否可中断
synchronized不可中断
,除非抛出异常或者正常运行完成。
ReentrantLock可中断
。 - 7、是否可设置超时
synchronized 获取锁无法设置超时;
ReentrantLock 可以设置获取锁的超时时间。 - 8、悲观与乐观策略
synchronized是同步阻塞,使用的是悲观并发
策略;
Lock是同步非阻塞,采用的是乐观并发
策略。
3.2 乐观锁和悲观锁有哪些实现方式
- 悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁
。synchronized 关键字的实现就是悲观锁。 - 乐观锁
每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制
。乐观锁适用于多读的应用类型,这样可以提高吞吐量。在 Java中JUC 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。 - 悲观锁的实现方式
如synchronized通过行monitorenter和monitorexit指令来实现。 - 乐观锁的实现方式
使用版本标识来确定读到的数据与提交时的数据是否一致
。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。- CAS。
3.3 自旋锁一定比重量级锁效率高吗
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块
来说,性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗。
如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用CPU做无用功,线程自旋的消耗大于线程阻塞挂起操作的消耗
,其它需要CPU的线程又不能获取到CPU,造成CPU的浪费。
四、线程的活性故障
线程活性故障是由于资源稀缺性或程序自身的问题和缺陷导致线程一直处于非RUNNABLE状态,或者线程虽然处于RUNNABLE状态,但是其要执行的任务却一直无法进展的故障现象。
4.1 死锁
死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去
。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态:
4.1.1 死锁的产生条件
当线程产生死锁时,这些线程及相关的资源将满足如下全部条件:
- 1、互斥
一个资源只能被一个线程(进程)占用
,直到被该线程(进程)释放。 - 2、请求与保持(不主动释放)条件
一个线程(进程)因请求被占用资源(锁)而发生阻塞时,对已获得的资源保持不放
。 - 3、不剥夺(不能被强占)条件
线程(进程)已获得的资源
,在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源
。 - 4、循环等待(互相等待)条件
当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞
。
用一句话该概括:
两个或多个线程持有并且不释放独有的锁,并且还需要竞争别的线程所持有的锁,导致这些线程都一直阻塞下去。
这些条件是死锁产生的必要条件而非充分条件,也就是说只要产生了死锁,那么上面的这些条件一定同时成立
,但是上述条件即便同时成立也不一定产生死锁。
如果把锁看作一种资源,这种资源正好符合“资源互斥”和“资源不可抢夺”的要求。那么,可能产生死锁的特征就是在持有一个锁的情况下去申请另外一个锁,通常是锁的嵌套,示例:
//内部锁
public void deadLockMethod1(){
synchronized(lockA){
//...
synchronized(lockB){
//...
}
}
}
//显式锁
public void deadLockMethod2(){
lockA.lock();
try{
//...
lockB.lock();
try{
//...
}finally{
lockB.unlock();
}
}finally{
lockA.unlock();
}
}
写个demo:
private static Object lockObject1 = new Object();
private static Object lockObject2 = new Object();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
test1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
test2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
public static void test1() throws InterruptedException{
synchronized (lockObject1) {
System.out.println(Thread.currentThread().getName()
+"获取到lockObject1,正在获取lockObject2");
Thread.sleep(1000);
synchronized (lockObject2) {
System.out.println(Thread.currentThread().getName()
+"获取到lockObject2");
}
}
}
public static void test2() throws InterruptedException{
synchronized (lockObject2) {
System.out.println(Thread.currentThread().getName()
+"获取到lockObject2,正在获取lockObject1");
Thread.sleep(1000);
synchronized (lockObject1) {
System.out.println(Thread.currentThread().getName()
+"获取到lockObject1");
}
}
}
以下结果表明已经出现了死锁:
Thread-1获取到lockObject2,正在获取lockObject1
Thread-0获取到lockObject1,正在获取lockObject2
4.1.2 死锁的规避
由上文可知,要产生死锁需要同时满足四个条件,所以,只要打破其中一个条件就可以避免死锁的产生。常用的规避方法有如下几种:
- 1、粗锁法
用一个粒度较粗的锁
替代原来的多个粒度较细的锁,这样涉及的线程都只需要申请一个锁从而避免了死锁。粗锁法的缺点是它明显降低了并发性并可能导致资源浪费。 - 2、锁排序法
相关线程使用全局统一的顺序申请锁。假设有多个线程需要申请锁(资源),那么只需要让这些线程依照一个全局(相对于使用这种资源的所有线程而言)统一的顺序去申请这些资源,就可以消除“循环等待资源”这个条件,从而规避死锁。一般,可以使用对象的hashcode作为资源的排序依据。 - 3、使用ReentrantLock.tryLock(long timeout, TimeUnit unit) 申请锁
ReentrantLock.tryLock(long timeout, TimeUnit unit) 允许为申请锁这个操作加上一个超时时间。在超时事件内,如果相应的锁申请成功,该方法返回true。如果在tryLock执行的那一刻相应的锁正在被其他线程持有,那么该方法会使当前线程暂停,直到这个锁申请成功(此时该方法返回true)或者等待时间超过指定的超时时间(此时该问题返回false)。因此,使用ReentrantLock.tryLock(long timeout, TimeUnit unit) 来申请锁可以避免一个线程无限制地等待另外一个线程持有的资源
,从而最终能够消除死锁产生的必要条件中的“占用并等待资源”。示例:
boolean locked = false;
try {
locked = lock.tryLock(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(locked) lock.unlock();
}
- 4、使用开放调用----在调用外部方法时不加锁
开放调用是一个方法在调用方法(包括其他类的方法以及当前类的可覆盖方法)的时候不持有任何锁。显然,开放调用能够消除死锁产生的必要条件中的“持有并等待资源”。 - 5、使用锁的替代品
使用一些锁的替代品
(无状态对象、线程特有对象以及volatile关键字等
),在条件允许的情况下,使用这些替代品在保障线程安全的前提下不仅能够避免锁的开销,还能够直接避免死锁。
4.2 线程饥饿
线程饥饿是指一直无法获得其所需的资源而导致任务一直无法进展的一种活性故障。
线程饥饿的一个典型例子是在争用的情况下使用非公平模式的读写锁。此种情况下,可能会导致某些线程一直无法获取其所需的资源(锁),即导致线程饥饿。
把锁看作一种资源的话,其实死锁也是一种线程饥饿。死锁的结果是故障线程都无法获得其所需的全部锁中的一个锁,从而使其任务一直无法进展,这相当于线程无法获得其所需的全部资源(锁)而使得其任务一直无法进展,即产生了线程饥饿。由于线程饥饿的产生条件是一个(或多个)线程始终无法获得其所需的资源,显然这个条件的满足并不意味着死锁的必要条件(而不是充分条件)的满足,因此线程饥饿并不会导致死锁。
线程饥饿涉及的线程,其生命周期不一定就是WAITING或BLOCKED状态,其状态也可能是RUNNING(说明涉及的线程一直在申请宁其所需的资源),这时饥饿就会演变成活锁。
4.3 活锁
活锁指线程一直处于运行状态,但是其任务却一直无法进展的一种活性故障。
4.4 死锁与活锁的区别,死锁与饥饿的区别
- 死锁
是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。 - 活锁
任务或者执行者没有被阻塞,由于某些条件没有满足
,导致一直重复尝试,却一直获得不了锁。 - 饥饿
一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。
Java中导致饥饿的原因:
- 高优先级线程抢占了所有的低优先级线程的 CPU 时间。
- 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
- 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。
- 1、活锁与死锁的区别
活锁和死锁的区别在于:处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能
。活锁可以认为是一种特殊的饥饿。 - 2、死锁活锁与饥饿的区别
进程会处于饥饿状态是因为持续地有其它优先级更高的进程请求相同的资源。不像死锁或者活锁,饥饿能够被解开。例如,当其它高优先级的进程都终止时并且没有更高优先级的进程强占资源。
五、原子类
原子操作指不可被中断的一个或一系列操作
。
Java从JDK 1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),Atomic包里面提供了一组原子类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。
Atomic包大致可以属于4种类型的原子更新方式,分别是:
原子更新基本类型
原子更新数组
原子更新引用
原子更新属性
5.1 原子更新基本类型
Atomic包提高原子更新基本类型的工具类,主要有:
AtomicBoolean:以原子更新的方式更新 boolean;
AtomicInteger:以原子更新的方式更新 Integer;
AtomicLong:以原子更新的方式更新 Long;
5.1.1 AtomicInteger
- 1、常用API
以AtomicInteger为例,介绍一些常用的方法。
- AtomicInteger的创建和get()方法
AtomicInteger的创建分为两种:1、无参的,默认值0;有参的,指定默认值。
get():用于获取当前值,该方法是不需要锁。
示例:
AtomicInteger i = new AtomicInteger();
System.out.println(i.get()); //0
AtomicInteger j = new AtomicInteger(10);
System.out.println(j.get()); //10
- 设置值的方法,示例:
AtomicInteger i = new AtomicInteger();
i.set(12);
System.out.println(i.get()); //12
- 先取值,再设置值,示例:
AtomicInteger i = new AtomicInteger();
int result = i.getAndSet(10);
System.out.println(result); //0
System.out.println(i.get()); //10
- 先取值并且后加上指定的值,示例:
AtomicInteger i = new AtomicInteger(10);
int result = i.getAndAdd(10);
System.out.println(result); //10
System.out.println(i.get()); //20
- 先加上指定的值再取值,示例:
AtomicInteger i = new AtomicInteger(10);
int result = i.addAndGet(10);
System.out.println(result);//输出20
System.out.println(i.get());//输出20
- 先取值然后再+1,示例:
AtomicInteger i = new AtomicInteger();
int result = i.getAndIncrement();
System.out.println(result); //0
System.out.println(i.get()); //1
- 先+1再取值,示例:
AtomicInteger i = new AtomicInteger();
int result = i.incrementAndGet();
System.out.println(result); //1
System.out.println(i.get()); //1
- 先取值再-1,示例:
AtomicInteger i = new AtomicInteger(10);
int result = i.getAndDecrement();
System.out.println(result); //10
System.out.println(i.get()); //9
- 先-1再取值,示例:
AtomicInteger i = new AtomicInteger();
int result = i.decrementAndGet();
System.out.println(result); //9
System.out.println(i.get()); //9
- 快速失败策略,是用于判断期望值是否与变量实际值相等,如果相等则将update赋值给变量,否则失败。示例:
//成功案例
AtomicInteger atomicInteger = new AtomicInteger(10);
boolean result = atomicInteger.compareAndSet(10, 12);
System.out.println(result); //true
System.out.println(atomicInteger.get()); //12
//失败案例
AtomicInteger atomicInteger1 = new AtomicInteger(10);
boolean result1 = atomicInteger1.compareAndSet(11, 12);
System.out.println(result1); //false
System.out.println(atomicInteger1.get()); //10
- 2、实现原理
上面几个类的用法基本一致,以getAndIncrement方法为例,其源码:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
unsafe实例是通过UnSafe类的静态方法getUnsafe获取:
private static final Unsafe unsafe = Unsafe.getUnsafe();
valueOffset是由AtomicInteger类中的变量转化而来,源码:
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex); }
}
private volatile int value;
Unsafe类提供了一些底层操作,Atomic包下的原子操作类的也主要是通过 Unsafe类提供的compareAndSwapInt、compareAndSwapLong等一系列提供CAS操作的方法来进行实现。CAS操作能够保证数据更新的时候是线程安全的,并且由于CAS是采用乐观锁策略,因此,这种数据更新的方法也具有高效性。
看个例子:
private static AtomicInteger atomicInteger = new AtomicInteger(1);
public static void main(String[] args) {
System.out.println(atomicInteger.getAndIncrement()); //1
System.out.println(atomicInteger.get()); //2
}
AtomicLong 的实现原理和 AtomicInteger 一致,只不过一个针对的是 long 变量,一个针对的是 int 变量。AtomicBoolean稍有不同,看下AtomicBoolean的compareAndSet方法:
public final boolean compareAndSet(boolean expect, boolean update) {
int e = expect ? 1 : 0;
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}
上面的方法的实际上也是先转换成 0,1 的整型变量,然后是通过针对 int 型变量的原子更新方法compareAndSwapInt来实现的。
5.1.2 AtomicBoolean
看一下AtomicBoolean的常用方法。
- AtomicBoolean的创建和get()方法
AtomicBoolean的创建分为两种:1、无参的,默认值0;有参的,指定默认值。
示例:
AtomicBoolean bool = new AtomicBoolean();
System.out.println(bool.get()); //false
AtomicBoolean bool2 = new AtomicBoolean(true);
System.out.println(bool2.get()); //true
- 设置值
示例:
AtomicBoolean bool = new AtomicBoolean();
bool.set(true);
System.out.println(bool.get()); //true
- 先取值,再设置值
示例:
AtomicBoolean bool = new AtomicBoolean(true);
boolean result = bool.getAndSet(false);
System.out.println(result); //true
System.out.println(bool.get()); //false
- 快速失败策略,是用于判断期望值是否与变量实际值相等,如果相等则将update赋值给变量,否则失败
示例:
//成功案例
AtomicBoolean bool = new AtomicBoolean(true);
boolean result = bool.compareAndSet(true, false);
System.out.println(result); //true
System.out.println(bool.get()); //false
//失败案例
AtomicBoolean bool1 = new AtomicBoolean(true);
boolean result1 = bool1.compareAndSet(false, true);
System.out.println(result1); //false
System.out.println(bool1.get()); //true
AtomicBoolean可以当做多线程中的开关flag,从而来代替synchronized这样比较重的锁。
示例(下面的代码可以正常停止):
private static AtomicBoolean flag = new AtomicBoolean(true);
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
int i = 0;
while (flag.get()) {
i++;
}
System.out.println("i="+i);
}
}).start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag.set(false);
}
结果示例:
i=147049097
5.2 原子更新数组类型
Atomic包下提供能原子更新数组中元素的类有:
AtomicIntegerArray:原子更新整型数组中的元素;
AtomicLongArray:原子更新长整型数组中的元素;
AtomicReferenceArray:原子更新引用类型数组中的元素。
他们的用法基本一致,以AtomicIntegerArray来介绍下常用的方法:
//以原子更新的方式将数组中索引为 i 的元素与输入值相加
public final int addAndGet(int i, int delta)
//以原子更新的方式将数组中索引为 i 的元素自增加 1
public final int getAndIncrement(int i)
//将数组中索引为 i 的位置的元素进行更新
public final boolean compareAndSet(int i, int expect, int update)
AtomicIntegerArray与AtomicInteger的方法基本一致,只不过在 AtomicIntegerArray的方法中会多一个指定数组索引位 i。示例:
private static int[] value = new int[]{
1, 2, 3};
private static AtomicIntegerArray integerArray = new AtomicIntegerArray(value);
public static void main(String[] args) {
//对数组中索引为1的位置的元素加5
int result = integerArray.getAndAdd(1, 5);
System.out.println(integerArray.get(1)); //7
System.out.println(result); //2
}
5.3 原子更新引用类型
Atomic包下相关的原子引用类:
AtomicReference:原子更新引用类型;
AtomicReferenceFieldUpdater:原子更新引用类型里的字段;
AtomicMarkableReference:原子更新带有标记位的引用类型。
这几个类的使用方法也是基本一样的,以AtomicReference为例,看个例子:
private static AtomicReference reference = new AtomicReference();
public static void main(String[] args) {
User user1 = new User("a", 1);
reference.set(user1);
User user2 = new User("b",2);
User user = (User) reference.getAndSet(user2);
System.out.println(user); //User{userName='a', age=1}
System.out.println(reference.get()); //User{userName='b', age=2}
}
static class User {
private String userName;
private int age;
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
@Override
public String toString() {
return "User{" + "userName='" + userName + '\'' + ", age=" + age + '}';
}
}
5.4 原子更新字段类型
Atomic包下相关的原子字段类:
AtomicIntegeFieldUpdater:原子更新整型字段类;
AtomicLongFieldUpdater:原子更新长整型字段类;
AtomicStampedReference:原子更新引用类型,这种更新方式会带有版本号,是为了解决 CAS 的 ABA 问题。
使用原子更新字段需要两步操作:
- 原子更新字段类都是抽象类,只能通过静态方法newUpdater来创建一个更新器,并且需要设置想要更新的类和属性;
- 更新类的属性必须使用public volatile进行修饰。
这几个类提供的方法基本一致,以AtomicIntegerFieldUpdater为例,看下其使用:
private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(User.class,"age");
public static void main(String[] args) {
User user = new User("a", 1);
int oldValue = updater.getAndAdd(user, 5);
System.out.println(oldValue); //1
System.out.println(updater.get(user)); //6
}
static class User {
private String userName;
public volatile int age;
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
@Override
public String toString() {
return "User{" + "userName='" + userName + '\'' + ", age=" + age + '}';
}
}
5.5 Atomic的原理
Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
AtomicInteger 类主要利用 CAS + volatile
和 native 方法来保证原子操作,从而避免synchronized的高开销,提升执行效率。
CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
5.6 volatile 变量和Atomic变量有什么不同?
volatile常见的功能是保证其修饰的变量在不同线程之间的可见性和禁止重排序, 但它并不能保证原子性
。例如用volatile修饰 count 变量,那么count++操作就不是原子性的。
Atomic变量提供的方法可以让类似count++的操作具有原子性
,如AtomicInteger类中的getAndIncrement()方法会原子性的进行增量操作把当前值加1,其它数据类型和引用变量也可以进行相似操作。