一、JUC中锁的机制
(1)AQS:AQS全名AbstractQueuedSynchronizer,是并发容器JUC(java.util.concurrent)下locks包内的一个类。它实现了一个FIFO(FirstIn、FisrtOut先进先出)的队列。底层实现的数据结构是一个双向链表。
工作原理:
AQS使用一个int state成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。
AQS使用CAS对该同步状态进行原子操作实现对其值的修改,当state大于0的时候表示锁被占用,如果state等于0时表示没有占用锁。
CAS:https://blog.csdn.net/weixin_53455615/article/details/126535949?spm=1001.2014.3001.5501
理解:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用, 获取不到锁的线程加入到队列中。然后排队获取资源。
二、JUC中的锁
JUC中锁的底层使用的就是AQS
-
ReentrantLock: Lock接口的实现类, 可重入锁。相当于synchronized同步锁
-
ReentrantReadWriteLock:ReadWriteLock接口的实现类。类中包含两个静态内部类,ReadLock读锁、WriteLock写锁。
-
Condition:是一个接口,都是通过lock.newCondition()实例化。属于wait和notify的替代品。提供了await()、signal()、singnalAll()与之对应
-
LockSupport:和Thread中suspend()和resume()相似
三、ReentrantLock重入锁
可重入锁:https://blog.csdn.net/weixin_53455615/article/details/126483112?spm=1001.2014.3001.5501
提供了2种类型的构造方法。
1. ReentrantLock(): 创建非公平锁的重入锁。
2. ReentrantLock(boolean): 创建创建锁。取值为true表示公平锁,取值为false表示非公平锁。
公平锁:多线程操作共一个资源时, 严格按照顺序执行。
非公平锁:新添加的线程先尝试获取获取资源,若获取到直接执行此线程,若没有获取到就添加到队列中去排队。
注意:
1. ReentrantLock出现异常时, 不会自动解锁
2. 多线程的情况下, 一个线程出现异常, 并没有释放锁, 其他线程也获取不到锁, 容易出现死锁
3. 建议把解锁方法finally{}代码块中
4. synchronized加锁与释放锁不需要手动的设置, 遇到异常时, 会自动的解锁
5.避免死锁,需要将解锁放到finally{}中
package com.java.test;
import java.util.concurrent.locks.ReentrantLock;
public class Test02 {
static int a = 0;
public static void main(String[] args) throws InterruptedException {
ReentrantLock rt = new ReentrantLock();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i1 = 0; i1 < 1000; i1++) {
rt.lock();//加锁
a++;
rt.unlock();//解锁
}
}
}).start();
}
Thread.sleep(3000);
System.out.println(a);//5000
}
}
四、Condition等待 | 唤醒(线程通信)
condition.await(); //线程等待
condition.signal(); //唤醒一个线程
condition.signalAll(); //唤醒所有线程
注意:等待 | 唤醒,会释放锁
package com.java.test;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/*
* 两个线程配合输出《静夜思》
* 两个线程一个线程一句话
* */
public class Test03 {
public static void main(String[] args) {
//创建Condition实现类
ReentrantLock rt = new ReentrantLock();
Condition condition = rt.newCondition();
new Thread(new Runnable() {
@Override
public void run() {
try {
//加锁
rt.lock();
System.out.println("床前明月光");
//线程等待
condition.await();
System.out.println("举头望明月");
//唤醒线程
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//解锁
rt.unlock();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
rt.lock();
System.out.println("疑是地上霜");
condition.signal();
condition.await();
System.out.println("低头思故乡");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rt.unlock();
}
}
}).start();
}
}
五、ReentrantReadWriteLock读写锁
ReadLock 读锁,又称为共享锁。允许多个线程同时获取该读锁
WriteLock 写锁,又称为独占锁。只有一个线程能获取,其他写的线程等待, 避免死锁。
package com.java.test;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Test04 {
public static void main(String[] args) {
//创建读写锁
ReentrantReadWriteLock rrw = new ReentrantReadWriteLock();
//获取读锁 多个线程可以同时持有 , 共享锁
ReentrantReadWriteLock.ReadLock readLock = rrw.readLock();
//获取写锁 只能一个线程持有 , 独占性
ReentrantReadWriteLock.WriteLock writeLock = rrw.writeLock();
new Thread(new Runnable() {
@Override
public void run() {
//readLock.lock();//加读锁
writeLock.lock();//加写锁
System.out.println(Thread.currentThread().getName());
}
}).start();
}
}
六、LockSupport 暂停 | 恢复
LockSupport是Lock中实现线程暂停和线程恢复。suspend()和resume()是synchronized中的暂停和恢复。
注意: 暂停恢复不会释放锁, 避免死锁问题。
package com.java.test;
import java.util.concurrent.locks.LockSupport;
public class Test05 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("5秒后继续执行");
//暂停线程
LockSupport.park();
System.out.println("执行结束");
}
});
thread.start();
Thread.sleep(5000);
LockSupport.unpark(thread);
}
}
七、CountDownLatch计数器
在开发中经常遇到在主线程中开启多个线程去并行执行任务,并且主线程需要等待所有子线程执行完毕后再进行汇总的场景。之前是使用join() | 主线程休眠实现的,但是不够灵活,某些场合和还无法实现,所以开发了CountDownLatch这个类。底层基于AQS。
CountDown是计数递减的意思,Latch是门闩的意思。内部维持一个递减的计数器。可以理解为初始有n个Latch,等Latch数量递减到0的时候,结束阻塞, 执行后续操作。
package com.java.test;
import java.util.concurrent.CountDownLatch;
public class Test06 {
static int a = 0;
public static void main(String[] args) throws InterruptedException {
//创建计数器
CountDownLatch count = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
test();
count.countDown();//计数器 -1
}
}).start();
}
count.await();//当计数器为0时继续执行
System.out.println(a);
}
private synchronized static void test() {
for (int i = 0; i < 1000; i++) {
a++;
}
}
}
八、CyclicBarrier回环屏障
CountDownLatch优化了join()在解决多个线程同步时的能力,但CountDownLatch的计数器是一次性的。计数递减为0之后,再调用countDown()、await()将不起作用。为了满足计数器可以重置的目的,JDK推出了CyclicBarrier类。
使用原理:await()方法表示当前线程执行时计数器值不为0则等待。如果计数器为0则继续执行。每次await()之后计算器会减少一次。当减少到0下次await从初始值重新递减。九、
九、Semaphore 信号量
CountDownLatch和CyclicBarrier的计数器递减的,而Semaphore的计数器是可加可减的,并可指定计数器的初始值,并且不需要事先确定同步线程的个数,等到需要同步的地方指定个数即可。且Semaphore也具有回环重置的功能,这一点和CyclicBarrier很像。底层也是基于AQS。
package com.java.test;
import java.util.concurrent.Semaphore;
public class Test07 {
public static void main(String[] args) throws InterruptedException {
//创建信号量
Semaphore semaphore = new Semaphore(3);
//信号量+1
semaphore.release();
//信号量+n
semaphore.release(5);
//信号量-1
semaphore.acquire();
//信号量-n 信号量的值小于0 , 线程阻塞执行
semaphore.acquire(10);
//获取信号量中的值
int i = semaphore.availablePermits();
System.out.println(i);
}
}
十、并发集合
并发集合类:主要是提供线程安全的集合
比如:
1. ArrayList对应的并发类是CopyOnWriteArrayList
2. HashSet对应的并发类是 CopyOnWriteArraySet
3. HashMap对应的并发类是ConcurrentHashMap
十一、CopyOnWriteArrayList
使用方式和ArrayList相同, 当然CopyOnWriteArrayList线程为安全的。
Vector:也是ArrayList的子类,它也是线程安全的。
原理:写时复制
通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后往新的容器里添加元素。
添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
十二、CopyOnWriteArraySet
CopyOnWriteArraySet在CopyOnWriteArrayList 的基础上使用了Java的装饰模式,所以底层是相同的。而CopyOnWriteArrayList本质是个动态数组队列,所以CopyOnWriteArraySet相当于通过动态数组实现的Set, CopyOnWriteArrayList中允许有重复的元素;但CopyOnWriteArraySet是一个Set集合,所以它不能有重复数据。因此, CopyOnWriteArrayList额外提供了addIfAbsent()和addAllAbsent()这两个添加元素的API,通过这些API来添加元素时,只有当元素不存在时才执行添加操作!
十三、ConcurrentHashMap
(1)Segment段锁:Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,使用多个锁来控制对hash表的不同部分(段segment)进行的修改,如果多个修改操作发生在不同的段上,他们就可以并发进行,从而提高了效率。
(2)ConcurrentHashMap在JDK8中进行了巨大改动。它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现, 利用synchronized + CAS, 如果没有出现hash冲突, 使用CAS直接添加数据, 只有出现hash冲突的时候才会使用同步锁添加数据, 又提升了效率, 它底层由"数组"+链表+红黑树的方式思想(JDK8中HashMap的实现), 为了做到并发,又增加了很多辅助的类,例如TreeBin,Traverser等对象内部类。
(3)辅助类
Node节点: 默认数组上的结点就是Node结点。Node只有一个next指针,是一个单链表,提供find方法实现链表查询, 当出现hash冲突时,Node结点会首先以链表的形式链接到table上,当结点数量大于等于8并且数组长度大于64,链表会转化为红黑树。
TreeNode节点: TreeNode就是红黑树的结点,TreeNode不会直接链接到table[i]——桶上面,而是由TreeBin链接,TreeBin会指向红黑树的根结点。
TreeBin节点: TreeBin会直接链接到table[i]——桶上面,该结点提供了一系列红黑树相关的操作,以及加锁、解锁操作。
ForwardingNode: ForwardingNode 在table扩容时使用,内部记录了扩容后的table,即nexttable。
ReservationNode: 在并发场景下、在从 Key不存在 到 插入 的 时间间隔内,为了防止哈希槽被其他线程抢占,当前线程会使用一个reservationNode节点放到槽中并加锁,从而保证线程安全。