J.U.C ->java.util.concurrent
-
AQS原理 -> AbstractQueuedSynchronizer
-
抽象队列同步器,阻塞式锁
-
其他相关同步器工具的基础框架
-
特点
- 用state标识资源状态
- 状态分独占与共享两个模式
- 提供FIFO的等待队列
- 等待唤醒支持多条件变量
-
自己基于AQS实现一把不可重入锁
-
/**
* @author 钦尘
* @date 2021/8/7 17:31
* @description 自己以AQS,实现一个不可重入锁
*/
@Slf4j(topic = "TestAqs")
public class TestAqs {
public static void main(String[] args) {
MyLock lock = new MyLock();
new Thread(()->{
lock.lock();
try {
log.info("加锁中");
Sleeper.sleep(1);
} finally {
log.info("解锁中");
lock.unlock();
}
}, "t1").start();
new Thread(()->{
lock.lock();
try {
log.info("加锁中");
} finally {
log.info("解锁中");
lock.unlock();
}
}, "t2").start();
}
}
@Slf4j
class MyLock implements Lock {
/**
* 独占锁
*/
class MySync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
if(compareAndSetState(0, 1)) {
log.info("加锁成功");
// 设置 owner 为当前线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
/**
* 是否持有锁
* @return
*/
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
public Condition newCondition(){
return new ConditionObject();
}
}
private MySync sync = new MySync();
/**
* 加锁
*/
@Override
public void lock() {
sync.acquire(1);
}
/**
* 加锁,可打断
* @throws InterruptedException
*/
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
/**
* 尝试加锁(一次)
* @return
*/
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
/**
* 带超时时间的尝试加锁
* @param time
* @param unit
* @return
* @throws InterruptedException
*/
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
/**
* 解锁
*/
@Override
public void unlock() {
sync.release(1);
}
/**
* 条件变量
* @return
*/
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
-
理解ReentrantLock原理
-
非公平加锁与解锁原理
-
可重入原理
- state++ 与 --
-
可打断原理
- 重置打断标记,抛出异常
-
公平锁原理
- 先检查AQS是否存在前驱节点,没有才去竞争
-
条件变量原理
- 设置Node 状态= -2
- signal逻辑
-
-
读写锁
-
读写锁:ReentrantReadWriteLock
-
数据库数据缓存存在的问题
-
存在缓存失效或缓存数据不一致问题
-
应用读写锁+双检机制进行解决
-
适合读多写少场景,不适合分布式场景
-
核心原理:将state分为高低16位,给对应读锁和写锁
-
读写锁:StampedLock,JDK1.8加入
- 进一步优化读性能
- 的特点是在使用读锁、写锁时都必须配合【戳】使用加解读锁
- 支持乐观读
- 不支持条件变量
- 不支持可重入
-
@Slf4j
class DataContainer {
private int data;
private final StampedLock lock = new StampedLock();
public DataContainer(int data) {
this.data = data;
}
public int read(int readTime){
long stamp = lock.tryOptimisticRead();
if(lock.validate(stamp)) {
// 执行读操作
log.debug("read finish ... {}", stamp);
return data;
}
// 锁升级
log.debug("updating to read lock....");
try {
stamp = lock.readLock();
log.info("读取完毕");
return data;
} finally {
lock.unlockRead(stamp);
}
}
}
-
信号量:Semaphore
-
用来限制能同时访问共享资源的线程上限
-
注意,限制的是线程个数,而非资源个数
-
资源数量限制可以对比Tomcat LimitLatch实现
-
可以考虑使用Semaphore优化之前编写的数库连接池
-
原理
- 类比停车场,permits就是车位数量
- 线程获得permits,停车位空余车位就减一
- 未获得到permits的在车库外面等待
- 唤醒:由前驱节点唤醒后继节点
-
/**
* @author 钦尘
* @date 2021/8/15 22:04
* @description 信号量,用于限制同一资源的线程访问数量上线
*/
@Slf4j
public class TestSemaphore {
public static void main(String[] args) {
// 第二个参数:公平非公平
Semaphore semaphore = new Semaphore(3, true);
// 可以创建10个线程,同时运行
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
// 获得许可
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.info("运行中");
Sleeper.sleep(1);
log.info("运行完毕");
} finally {
// 释放
semaphore.release();
}
}).start();
}
}
}
-
线程同步协作器:CountdownLatch
- 倒计时锁,用来等待所有线程完成倒计时
- await() 用来等待计数归零
- countDown() 用来让计数减一
- 可配合线程池用
- 案例1:模拟10个王者荣耀玩家加载游戏
- 案例2:远程调用多个外部Rest API接口
- 需要拿到多个线程运行的结果集合,使用Future更为合适
private static void try1() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
new Thread(()->{
Sleeper.sleep(1);
latch.countDown();
log.info("end {}", latch.getCount());
}).start();
new Thread(()->{
Sleeper.sleep(2);
latch.countDown();
log.info("end {}", latch.getCount());
}).start();
new Thread(()->{
Sleeper.sleep(1);
latch.countDown();
log.info("end {}", latch.getCount());
}).start();
log.info("主线程等待");
latch.wait();
log.info("主线程等待结束,继续执行");
}
-
循环珊栏:CyclicBarrier
- 用来等待线程满足某个计数
- 与CountdownLatch非常类似,区别在于可重用,计数器可以重置回某个值
- 注意线程数需与计数必须一致
// CyclicBarrier barrier = new CyclicBarrier(2);
// 需要拿到结果?
CyclicBarrier barrier = new CyclicBarrier(2, ()->{
log.info("task1 task2 已运行完毕");
});
barrier.await();
-
线程安全集合类
-
遗留
- Hashtable
- Vector
-
修饰
- 使用 Collections 装饰的线程安全集合
- SynchronizedMap
- SynchronizedList
- SynchronizedSet
- ……
-
JUC
-
Blocking
- 大部分实现基于锁,并提供用来阻塞的方法
-
CopyOnWrite
- 容器修改开销相对较重
- 适合读多写少场景
-
Concurrent
- 内部很多操作使用 cas 优化,一般可以提供较高吞吐量
- 缺点:弱一致性,主要体现在遍历、取size等
- 非线程安全的容器,一般在遍历时被修改,则会抛出异常
-
-
ConcurrentHashMap
- 案例:统计多个文件中单词字母数量
- map.computeIfAbsent(word, (key) -> new LongAdder())
- computeIfAbsent可保证get / put的原子性
- 回顾HashMap原理:桶、链表、扩容,多线程并发扩容,JDK7存在严重的并发死链问题
- JDK8对扩容算法做了调整,不再将头元素加入链表头,而是与扩容前顺序保持一致,解决了死链问题,又会带入扩容丢数据等问题
- 构造器:懒惰初始化(首次使用才创建)
-
LinkedBlockingQueue
- 链表阻塞队列
- 入队核心逻辑:enqueue(Node)
- 出队核心逻辑:dequeue()
- 加锁精髓:用了两把锁+dummy节点实现,对应省消费者和生产者两个线程和并行执行
-
ConcurrentLinkedQueue
- 设计与 LinkedBlockingQueue 非常像
- 两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
- 两把锁住的是不同对象,避免竞争
- 【锁】使用了 cas 来实现
-
CopyOnWriteArrayList
- 底层实现采用写时拷贝思想
- 增删改操作会将底层数组拷贝一份
- 更改操作在新数组上执行
- 读写分离
- get获取元素存在弱一致性问题
- 弱一致性:并不代表不好,MySQL的MVCC就是已弱一致性的表现,高并发与强一致性永远是矛盾的
-
扩展了解:第三方框架
- disruptor
- guava->Ratelimit