
文章目录
一、常见锁策略
以下集中锁策略与编程语言无关,属于是通用的那种
- 乐观锁&悲观锁:在加锁的时候我们就去预测这把锁的竞争是大还是小,如果是大并且实现原理复杂,那就是悲观锁,反之就是乐观锁
- 重量级锁&轻量级锁:对于重量级锁来说,加锁这个操作开销大并且它容易触发线程调度,反之就是轻量级锁
- 自旋锁&挂起等待锁:挂起等待锁是重量级锁的典型实现,当遇到锁冲突的时候,线程会进入阻塞状态,等待未来的某个时间唤醒,由于操作系统内部线程调度是随机的,开销比较大;自旋锁是轻量级锁的典型实现,遇到锁冲突时,线程会先重试获取锁,等到其他线程把锁释放了我们当前线程就可以得到锁了,并不会涉及到线程调度和CPU内核
- 公平锁&非公平锁:我们把先来先得到锁称为公平锁,反之如果是各凭本事得到就是非公平锁
- 可重入锁&不可重入锁:一个线程针对同一把锁,连续加锁多次不会发生死锁,就称之为可重入锁,会发生死锁就叫不可重入锁
- 读写锁&普通互斥锁:读操作的锁叫做读锁,写入操作的锁叫做写锁,读锁与读锁之间并不会冲突,反观读锁和写锁或者是写锁和写锁之间会产生冲突
二、synchronized锁
我们这把锁是根据当前锁竞争程度来自动调整锁策略的,我们感知不到也不能干预,因此它又被称之为智能锁
底层大致实现过程
进入代码块 进入代码块 阻塞线程数量到一定程度
无锁 --> 偏向锁 --> 自旋锁 --> 重量级锁
我们来说说什么是偏向锁
其实偏向锁并不是真的上锁了,只是去做个标记,开销比较小
如果使用的时候其他线程没有来竞争这把锁,就始终是一种标记状态,一直到锁释放了就解除标记了,这种就是锁消除
反之如果有其他线程竞争这把锁,在这个线程竞争这把锁想要得到这把锁之前,就把这把锁升级,因此这个竞争这把锁的线程只能进入阻塞状态,如果有更多的线程来竞争,就升级为重量级锁
我们之前说过锁的粒度,越大其实锁的粗度也就越大
三、CAS
我们在实现线程安全的时候,是通过加锁去解决的,但是现在,我们有另一种解决思路
CAS全称为compare and swap
将内存中的某个变量当前值与我期望它现在是多少进行比较
如果相等,说明从我读取这个值到现在,没有其他线程修改过它,那我就安全地更新为新值
如果不相等,说明值已经被其他线程改过,那我这次 CAS 操作就失败,可以选择重试(自旋)或放弃

上述我们的一系列操作属于是原子性操作,内部实现了自旋锁
1. 原子类
我们在开发中使用,比如我们上一篇文章最开始的时候,对于int变量count++问题上会产生线程安全问题,因此此时我们使用原子类就可以很大程度上避免这个问题
public class Demo1 {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 100; i++) {
count.incrementAndGet();//相当于count++
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 100; i++) {
count.incrementAndGet();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
除了使用incrementAndGet()或者是getAndIncrement()是变量自增
还可以使用getAndDecrement()或者是decrementAndGet()是变量自减
2. 伪代码解释
private int value;
getAndIncrement(){
//把内存中的值读取到寄存器中
int oldValue = value;
//判断value值和oldValue值是否相同,相同就触发交换,判断为true,循环进不来
//执行CAS操作,把寄存器value+1位置的值和内存中的值交换
//反之当其他线程穿插执行的时候,我们先不赋予值
//把其他线程穿插的值再重新加载到寄存器上,
while(CAS(value,oldValue,oldValue+1) != true){
//再次加载值,再进行CAS判断
oldValue = value;
}
}
3. 自旋锁
我们之前讲得锁升级的过程,其实synchronized锁内部实现了CAS,其本质是轻量级锁
因此比起阻塞状态等待,我们还不如循环等待,开销就会小很多,下面我写个循环等待的伪代码
private Thread owner;//用于记录是哪个线程持有这把锁
public void lock(){
//如果我当前锁持有对象是空的,我就把这把锁赋予到当前线程
while(!CAS(this.owner,null,Thread.currentThread())){
//如果当前锁对象被另一个对象持有了,我们就进入循环体内部等待
//直到另一个线程释放了锁,我们再去CAS判断
}
}
public void unlock(){
//解锁就是把持有锁的对象设为空
this.owner = null;
}
4. CAS中的ABA问题
可能存在一种情况,一个线程把一个变量值从A改到了B,但是另一个线程又把同一个变量值从B改回了A,此时在CAS看来,这个值好像没有改过一样,因此触发ABA问题
我们来举一个转钱的例子
int oldMoney = money;
CAS(money,oldmoney,oldmoney-500)

面对这种问题,本质上是不能让其他线程同时修改,我们可以约定一个版本号,余额可以加减,但是版本号只能加不能减,每次改动余额的时候我们都原子性的把版本号+1,我们在用CAS判断版本号是否被修改过就好
四、JUC常见类
就是在Java.Uitl.concurrent包下常见的关于线程的类
1. callable接口
这个接口类似于runnable,但是它的call方法可以直接去接收线程的返回值
public class Demo2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i;
}
return sum;
}
};
//因为对于Thread类来说无法直接接收Callable类的返回值
//需要一个类表示Callable的未来会接收到的结果
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
//此时get方法如果我们Callable中没有执行完就进入阻塞状态
//等到执行完了我们就使用get方法获取值
System.out.println(futureTask.get());
}
}
2. Reentrant可重入锁
这个是上古时期的手动加锁,因为那个时候synchronized还没有那么智能,因此那时候普遍使用这个类表示可重入锁,需要自己手动的加锁和解锁
同时也支持trylock,对比lock加锁不成功就进入阻塞等待状态,trylock就是加锁不成功可以主动返回或者是等待指定时间
而且,我们synchronized中的wait和notify只能随机唤醒一个,但是我们Reentrant可以搭配Condition指定唤醒哪个线程
如果我们在new ReentrantLock()参数中填入true,就表示是公平锁
原则:先来先服务,按照线程请求锁的顺序分配
实现:内部维护一个等待队列,新来的线程排队等待
public class Demo3 {
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
ReentrantLock locker = new ReentrantLock();
//由于我们在lock和unlock之间可能存在throw异常或者是return返回
//因此我们使用try-finally,在try中上锁,为了保证解锁一定会执行
//我们在finally中解锁
Thread t1 = new Thread(()->{
for (int i = 0; i < 1000; i++) {
try {
locker.lock();
count++;
}finally{
locker.unlock();
}
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 1000; i++) {
try {
locker.lock();
count++;
}finally {
locker.unlock();
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
3. 信号量
类名是Semaphore,是Java对操作系统提供的机制进行了进一步的封装,本质上就是一个计数器,描述了当前系统“可用资源”的个数
最多减少到0,如果此时线程进行资源申请(P操作)就会造成阻塞,如果有其他线程释放了资源(U操作)当前线程就会从阻塞状态重新变成就绪状态
public static void main(String[] args) throws InterruptedException {
//我们参数决定其初识资源个数
Semaphore s = new Semaphore(2);
//进行几次p操作再进行u操作
s.acquire();
System.out.println("获取资源");
s.acquire();
System.out.println("获取资源");
s.release();//此时释放了资源,重新进入就绪状态
System.out.println("添加了资源");
s.acquire();//到这里因为资源耗尽,陷入阻塞
System.out.println("再次获取到资源");
}
我们还可以实现类似于“锁”的效果,我们让资源只有一个,当一个线程在执行的时候,把资源获取
此时资源就是空,其他线程只能阻塞,等到当前线程执行完毕后,再把资源添加回去,让其他线程执行
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
Semaphore s = new Semaphore(1);
Thread t1 = new Thread(()->{
for (int i = 0; i < 500; i++) {
try {
s.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count++;
s.release();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 500; i++) {
try {
s.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count++;
s.release();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);//1000
}
因此到目前为止,解决线程安全问题我们有了几个策略
- 避免多线程修改同一个变量
- 使用
synchronized锁 - 使用
ReentrantLock锁 - 使用
Semaphore信号量中资源值特性 - CAS或使用原子类
4. CountDownLatch
比如我们要执行一个大任务,我们可以把大任务分成小任务,然后让每一个线程去执行这个小任务
只有当所有线程执行完自己小任务之后,这个大任务才算完成
public class Demo5 {
public static void main(String[] args) throws InterruptedException {
//参数表示任务数量,我们把主线程的大任务拆分成小任务
CountDownLatch c = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
Thread t = new Thread(()->{
//假设我们每个任务都要执行两秒钟
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//两秒后完成任务,并且向c提交
c.countDown();
});
t.start();
}
//我们主线程要等所有线程的小任务执行完毕
//可以使用join,我们使用CountDownLatch中的方法
c.await();
}
}
这种我们的场景就是多线程下载内容,把一个大内容拆分成许多个小内容,我们再让子线程分别去执行一个小内容,当所有子线程内容都下载完毕后,主线程把所有内容进行整合即可
5. 线程安全集合类
我们很多类型都是线程不安全的,天生线程安全的有Vector,HashTable,Stack,String等待
但是对于线程不安全的类我们可以通过几种方式使其线程安全
- 添加
synchroinzed关键字,之前讲过 - 通过
Collections.synchroinzed(new ArrayList),但是不常用
接下来就是重量级嘉宾,使用copyOnWriteArrayList,即写时拷贝
在对一个变量进行修改的时候,我们先复制一份,然后在这个复制后的副本内修改值,再把修改后的副本值覆盖原来的值,避免了那种修改了但是只修改了一半的情况,即脏数据
6. 多线程哈希表
我们之前说过HashMap线程不安全,虽然HashTable天生线程安全,但是它的加锁策略有很大问题

因此我们使用concurrentHashMap就可以避免这种情况,它是有很多把锁,针对每一个下标(即每一个下标的链表头节点)进行加锁
这样就可以避免上述那种情况,并且对每个链表头节点加锁开销不是很大,我们把这种结构称之为锁通,因此这个类又叫做哈希桶
而且对于哈希表中的size表示的键值对总数,我们使用CAS让其线程安全,避免了加锁
如果后续我们想扩容concurrentHashMap不会一次性把所有数据都拷贝到新的哈希数组
因为如果哈希数组很大的话并且每个链表长度比较长,一次性复制开销会非常大,因此concurrentHashMap同时维护新数组和旧数组,分成几个部分进行拷贝
1万+

被折叠的 条评论
为什么被折叠?



