文章目录
一. AQS 定义
AQS:AbstractQuenedSynchronizer 抽象的队列式同步器,是除 synchronized 之外的锁机制。ReentrantLock 即是基于AQS实现的独占锁。
AQS基于 CLH 队列,用 volatile 修饰共享变量 state,线程通过 CAS 去改变 state 的值,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
利用 AQS 实现自定义锁:
class MyAQSLock implements Lock {
//内部类,利用AQS实现自定义锁
class AQSLock extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
// CAS 修改状态
if (compareAndSetState(0, 1)) {
//设置锁的所有者为当前线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//这个方法无需加锁,因为每次只有一个线程会访问该方法
@Override
protected boolean tryRelease(int arg) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
//是否锁被独占
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
private AQSLock lock = new AQSLock();
@Override
public void lock() {
lock.acquire(1);
}
//可打断
@Override
public void lockInterruptibly() throws InterruptedException {
lock.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return lock.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return lock.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
lock.release(1);
}
@Override
public Condition newCondition() {
return null;
}
}
二. 读写锁
1. ReentrantReadWriteLock
当读操作的频率远远高于写时,可以考虑使用读写锁 ReentrantReadWriteLock,读锁在 “读-读” 操作时可以实现并发,在 “读-写” 操作时会互斥。即读锁与写锁互斥。
示例:
class Data {
private static final Logger logger = LoggerFactory.getLogger(Data.class);
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock rLock = rwLock.readLock();
private ReentrantReadWriteLock.WriteLock wLock = rwLock.writeLock();
public void getData() throws InterruptedException {
logger.info("获取读锁");
rLock.lock();
try {
logger.info("获取读锁成功");
Thread.sleep(1000);
} finally {
logger.info("释放读锁");
rLock.unlock();
}
}
public void putData() throws InterruptedException {
logger.info("获取写锁");
wLock.lock();
try {
logger.info("获取写锁成功");
Thread.sleep(1000);
} finally {
logger.info("释放写锁");
wLock.unlock();
}
}
}
测试:
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
try {
data.getData();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
data.putData();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
读写互斥:
读读并发:
注意:
- 读锁不支持条件变量;
- 重入时升级不支持,即同一个线程获取了读锁后再去获取写锁会导致获取写锁永久等待;但重入时支持降级,即获取了写锁后可以再获取读锁。
应用:缓存读取、缓存更新
2. StampedLock
由于 ReentrantReadWriteLock 在实现读读并发时还是需要通过CAS的方式不断修改锁的标识位,其性能肯定不如不加锁。StampedLock 在读的过程中采用了乐观读的方式,即配合一个 “戳” 使用。首先获取戳,读完之后验戳,只有验戳失败以后才加读锁重新读取,最大性地提升读的性能。
class MyStampedLock {
private static final Logger logger = LoggerFactory.getLogger(MyStampedLock.class);
private StampedLock stampedLock = new StampedLock();
private int data;
public MyStampedLock(int data) {
this.data = data;
}
public int getData() {
//乐观读,没有加锁,只是获取了戳
long stamp = stampedLock.tryOptimisticRead();
logger.info("乐观读:" + stamp);
try {
Thread.sleep(1000); //模拟读的过程
} catch (InterruptedException e) {
e.printStackTrace();
}
//验戳失败,加读锁
if (stampedLock.validate(stamp)) {
logger.info(stamp + "验证通过");
return data;
}
stamp = stampedLock.readLock();
logger.info("获取读锁:" + stamp);
try {
Thread.sleep(1000); //模拟读的过程,重新读取
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
logger.info("释放读锁:" + stamp);
stampedLock.unlockRead(stamp);
}
return data;
}
public void setData(int data) {
//写的时候直接加写锁
long stamp = stampedLock.writeLock();
logger.info("获取写锁:" + stamp);
try {
Thread.sleep(1000);
this.data = data;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
logger.info("释放写锁:" + stamp);
stampedLock.unlockWrite(stamp);
}
}
}
两个线程并发读:
public static void main(String[] args) throws InterruptedException {
MyStampedLock myStampedLock = new MyStampedLock(1);
new Thread(() -> {
myStampedLock.getData();
}, "t1").start();
Thread.sleep(500);
new Thread(() -> {
myStampedLock.getData();
}, "t2").start();
}
一个线程读,一个线程写:
public static void main(String[] args) throws InterruptedException {
MyStampedLock myStampedLock = new MyStampedLock(1);
new Thread(() -> {
myStampedLock.getData();
}, "t1").start();
Thread.sleep(500);
new Thread(() -> {
myStampedLock.setData(2);
}, "t2").start();
}
三. Semaphore 信号量
用来限制同时访问共享资源的线程数量上限,可用于限流。
Semaphore semaphore = new Semaphore(n);
semaphore.acquire();
semaphore.release();
用法示例:当许可数量为2时,4个线程只有2个能获得许可,其他线程将被阻塞,直到许可被释放后,其他线程才能重新获得许可。(可代替 wait/notify )
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2);
for (int i = 0; i < 4; i++) {
new Thread(() -> {
try {
semaphore.acquire();
logger.info("获得许可");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
logger.info("释放许可……");
}).start();
}
}
四. CountDownLatch
用于倒计时等待所有线程结束。类似于 join,但是 join 属于比较底层的 API,join 必须等到线程完全结束,释放连接才行,无法配合线程池使用。
CountDownLatch 配合线程池使用示例:
public class CountDownLatchTest {
private static final Logger logger = LoggerFactory.getLogger(CountDownLatchTest.class);
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(3);
CountDownLatch countDownLatch = new CountDownLatch(3);
service.execute(() -> {
try {
logger.info("线程0启动");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
logger.info("线程0结束--" + countDownLatch.getCount());
});
service.execute(() -> {
try {
logger.info("线程1启动");
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
logger.info("线程1结束--" + countDownLatch.getCount());
});
service.execute(() -> {
try {
logger.info("线程2启动");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
logger.info("线程2结束--" + countDownLatch.getCount());
});
logger.info("等待子线程结束……");
countDownLatch.await();
logger.info("等待结束!");
}
}
实际上如果有返回结果需要同步,可以考虑使用 Future 接收结果。
五. CyclicBarrier
CountDownLatch 的局限性是不能重复使用,即当计数减为1时,无法重新设置count 值继续计数。CyclicBarrier 也是通过计数等待所有线程结束,但是弥补了这一缺陷。当 count 值减为 0 时,如果继续调用 CyclicBarrier 对象的 await() 方法,该对象的 count 值又会从初始值开始。
public class CyclicBarrierTest {
private static final Logger logger = LoggerFactory.getLogger(CyclicBarrierTest.class);
public static void main(String[] args) {
//CyclicBarrier 可重用,当计数减到0时,下次调用await又会从初始值开始
CyclicBarrier cyclicBarrier = new CyclicBarrier(2, ()->{
logger.info("线程1、2同步完毕"); //参数二为同步完成后执行的代码
});
ExecutorService service = Executors.newFixedThreadPool(2);
for (int i = 0; i < 3; i++) {
service.execute(() -> {
try {
logger.info("线程1开始执行");
Thread.sleep(1000);
cyclicBarrier.await(); //计数减一,进入同步等待,当 count 减为0时退出等待
logger.info("线程1执行完毕");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
service.execute(() -> {
try {
logger.info("线程2开始执行");
Thread.sleep(2000);
cyclicBarrier.await(); //计数减一,进入同步等待,当 count 减为0时退出等待
logger.info("线程2执行完毕");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
}
service.shutdown();
}
}
注意:CyclicBarrier 初始化的 count 值必须与线程数保持一致,否则不能起到同步的效果。