一、AbstractQueuedSynchronizer(AQS)介绍
AQS底层使用的是双向列表(Sync queue)
Condition queue使用的是单向列表。
在AQS中有一个int类型表示状态,states为0表示没有线程获得锁,1表示有现场获得锁,大于1表示重入锁的数量。
二、AQS同步组建(基于AQS)
1、CountDownLatch
一个线程或者多个线程一直等待,直到其他线程执行的操作完成。
CountDownLatch给定一个参数进行初始化,该计数器的操作是一个原子操作,同时只能有一个线程操作该计数器。调用await方法会一直处于阻塞状态,直到其他线程将计数器的值变成0,每次调用countDown() 计数器的值就会减去1。计数器不能重置。
如果业务上需要一个可以重置计数次数的类,可以考虑下面介绍的CyclicBarrier
代码案例
package com.mmall.concurrency.example.aqs;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Slf4j
public class CountDownLatchExample1
{
private final static int threadCount = 200;
public static void main(String[] args) throws Exception
{
ExecutorService exec = Executors.newCachedThreadPool();
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++)
{
final int threadNum = i;
exec.execute(() -> {
try
{
test(threadNum);
}
catch (Exception e)
{
log.error("exception", e);
}
finally
{
countDownLatch.countDown();
}
});
}
countDownLatch.await();
log.info("finish");
exec.shutdown();
}
private static void test(int threadNum) throws Exception {
Thread.sleep(100);
log.info("{}", threadNum);
Thread.sleep(100);
}
}
任务在指定时间完成,超过时间没做完就不管了 await(num,单位)
package com.mmall.concurrency.example.aqs;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@Slf4j
public class CountDownLatchExample2 {
private final static int threadCount = 200;
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
exec.execute(() -> {
try {
test(threadNum);
} catch (Exception e) {
log.error("exception", e);
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await(10, TimeUnit.MILLISECONDS);
log.info("finish");
exec.shutdown();
}
private static void test(int threadNum) throws Exception {
Thread.sleep(100);
log.info("{}", threadNum);
}
}
上面的代码先输出finish,再输出其他线程执行的test方法。
线程池调用shutdown方法,不是第一时间把全部线程销毁掉,而是让当前已有的线程全部执行完之后再把线程池销毁掉。
2、Semaphore信号量
控制某个资源可被同时访问的个数。
acquire获取一个许可,如果没有则等待,release则在操作完成之后放出一个许可。
Semaphore控制当前访问的个数,提供同步机制来控制同时访问的个数。
Semaphore semaphore = new Semaphore(3); 参数是几表示控制同时访问的资源数为几。
tryAcquire方法表示尝试获取许可。
1、acquire()获取一个许可,acquire(n)获取多个许可
package com.mmall.concurrency.example.aqs;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
@Slf4j
public class SemaphoreExample1 {
private final static int threadCount = 20;
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
exec.execute(() -> {
try {
semaphore.acquire(); // 获取一个许可
test(threadNum);
semaphore.release(); // 释放一个许可
} catch (Exception e) {
log.error("exception", e);
}
});
}
exec.shutdown();
}
private static void test(int threadNum) throws Exception {
log.info("{}", threadNum);
Thread.sleep(1000);
}
}
2、tryAcquire尝试获取一个许可,也可以一次获得多许可,参数int
3、semaphore.tryAcquire(5000, TimeUnit.MILLISECONDS))一定时间内尝试获取许可
package com.mmall.concurrency.example.aqs;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
@Slf4j
public class SemaphoreExample3 {
private final static int threadCount = 20;
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
exec.execute(() -> {
try {
if (semaphore.tryAcquire()) { // 尝试获取一个许可
test(threadNum);
semaphore.release(); // 释放一个许可
}
} catch (Exception e) {
log.error("exception", e);
}
});
}
exec.shutdown();
}
private static void test(int threadNum) throws Exception {
log.info("{}", threadNum);
Thread.sleep(1000);
}
}
3、CyclicBarrier
允许一组线程相互等待,直到到达某个公共屏障点,通过它可以完成多个线程之间相互等待,只有当每个线程都准备就绪后,才能继续往下执行,它和CountDownLatch有些相似的地方,都是通过计数器来实现的,当某个线程调用了await方法之后,线程就进入了等待状态,计数器就执行加一操作,当计数器的值达到了初始值,进入等待的现场就会被唤醒继续执行后续的操作,CyclicBarrier可以重用,所以又称为循环屏障。
CyclicBarrier可以用于多线程计算数据,最后合并运算结果的应用场景。
await可以有时间参数。
package com.mmall.concurrency.example.aqs;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Slf4j
public class CyclicBarrierExample1
{
private static CyclicBarrier barrier = new CyclicBarrier(5); //5表示一共有五个线程一起同步等待。
public static void main(String[] args) throws Exception
{
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++)
{
final int threadNum = i;
Thread.sleep(1000);
executor.execute(() -> {
try
{
race(threadNum);
}
catch (Exception e)
{
log.error("exception", e);
}
});
}
executor.shutdown();
}
private static void race(int threadNum) throws Exception
{
Thread.sleep(1000);
log.info("{} is ready", threadNum);
barrier.await();
log.info("{} continue", threadNum);
}
}
CountDownLatch和CyclicBarrier区别
- 1、CountDownLatch的计数器只能使用一次,CyclicBarrier的计数器可以使用方法重置循环使用。
- 2、CountDownLatch主要实现一个或者多个线程需要等待,其他线程完成某个操作之后,这些等待的线程才能继续往下执行。CyclicBarrier主要实现多个线程之间相互等待,直到所有线程都满足条件之后才能继续执行后续的操作。换句话说,CountDownLatch就是等待其他线程完成,我才能继续执行,而CyclicBarrier指的是n个线程之间相互等待
三、ReentrantLock与锁、ReentrantReadWriteLock
1、ReentrantLock(可重入锁)和synchronized区别
- 锁是实现方面的区别:Synchronized关键字是依赖JVM实现的,而ReentrantLock是依赖JDK实现的。区别就在于一个是类似于操作系统操作实现,而另外一个是代码实现的区别。
- 性能的区别:在Synchronized关键字优化之前,Synchronized关键字的性能比ReentrantLock性能要差很多,但是自从synchronized关键字引入了偏向锁、轻量级锁后,两者的性能就差不多了。
- 功能点区别:synchronized的使用比较方便简洁并且是由编译器保证加锁和释放的,而ReentrantLock要我们手工加锁和释放锁,如果忘记手工释放锁会造成死锁。
2、ReentrantLock独有功能
synchronized只能是非公平锁,而ReentrantLock可以指定公平锁和非公平锁。
公平锁是先等待的线程先获得锁。
ReentrantLock实现是一种自旋锁,通过循环调用CAS操作来实现加锁。
ReentrantLock示例代码:
package com.mmall.concurrency.example.lock;
import com.mmall.concurrency.annoations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
@ThreadSafe
public class LockExample2 {
// 请求总数
public static int clientTotal = 5000;
// 同时并发执行的线程数
public static int threadTotal = 200;
public static int count = 0;
private final static Lock lock = new ReentrantLock();
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("count:{}", count);
}
private static void add() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
ReentrantLock有很多函数,如上图。
lockInterruptibly:如果当前线程没有被中断的话,那么就获取锁定,如果已经被中断了,那么就抛出异常。
Condition
package com.mmall.concurrency.example.lock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class LockExample6 {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
Condition condition = reentrantLock.newCondition();
new Thread(() -> {
try {
reentrantLock.lock();
log.info("wait signal"); // 1
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("get signal"); // 4
reentrantLock.unlock();
}).start();
new Thread(() -> {
reentrantLock.lock();
log.info("get lock"); // 2
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
condition.signalAll();
log.info("send signal ~ "); // 3
reentrantLock.unlock();
}).start();
}
}
reentrantLock.lock()之后,线程会被加到AQS的等待队列中,在condition.await()方法之后,线程从AQS队列中移除,对应的操作是锁的释放,然后把线程加入到了Condition的等待队列中,等待的线程需要其他线程给一个信号,如:signalAll。
Condition是多线程之间协调的工具类。
3、ReentrantReadWriteLock
package com.mmall.concurrency.example.lock;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@Slf4j
public class LockExample3 {
private final Map<String, Data> map = new TreeMap<>();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public Data get(String key) {
readLock.lock();
try {
return map.get(key);
} finally {
readLock.unlock();
}
}
public Set<String> getAllKeys()
{
readLock.lock();
try
{
return map.keySet();
}
finally
{
readLock.unlock();
}
}
public Data put(String key, Data value)
{
writeLock.lock();
try
{
return map.put(key, value);
}
finally
{
readLock.unlock();
}
}
class Data {
}
}
在写入的时候不允许有读锁还保持着,这样就要求写入时候所有做的事情做完了,因此存在一个问题,读取情况很多的时候,写入很少的时候,这个类可能会让线程处于饥饿。