目录
一、常见锁策略
1、乐观锁 VS 悲观锁
2、读写锁 VS 普通的互斥锁
3、重量级锁 VS 轻量级锁
4、自旋锁 VS 挂起等待锁
5、公平锁 VS 非公平锁
6、可重入锁 VS 不可重入锁
二、CAS
什么是CAS
CAS的实现与应用
CAS的ABA问题
三、Synchronized原理
synchronized的锁策略
synchronized原理
四、JUC常见类
ReentrantLock:
Callable接口:
信号量(Semaphore):
CountDownLatch:
五、多线程环境使用哈希表
synchronized 初始使用的是乐观锁策略,当发现锁竞争比较频繁的时候,就会自动切换成悲观锁策略。
CAS:全称 Compare and swap ,字面意思就是“比较并交换”,实际CAS做的事情就是:拿着寄存器/某个内存 的值 和 另一个寄存器/内存 的值进行比较,如果值相等,就将两个值交换
一个CAS操作:
- 把内存中的某个值,和CPU寄存器A中的值,进行比较
- 如果两个值相同,就把另一个寄存器B中的值和内存的值进行交换(把内存的值放到寄存器B,同时把寄存器B的值写给内存)
- 返回操作是否成功
深入的的说,也就是:
- 比较旧的预期值A,和原数据V是否相等
- 如果比较相等,就需要将原数据V修改为新的值B
- 返回操作是否成功
需要注意的是,这组操作是通过CPU指令完成的,是原子的,线程安全的且高效
当多个线程同时对某个资源进行CAS操作,只有一个线程操作成功,但并不会阻塞其他线程,其他线程只会收到操作失败的信号(此处可将CAS看成乐观锁的一种实现方式)
CPU提供了一个单独的CAS指令,通过这一条指令,就能完成上述操作,你们上述操作 是 原子的,线程安全的。
1.Java标准库里提供了一组原子类,针对一些常用的(int/long ...)进行封装,可以基于CAS的方式进行修改值,并且是线程安全的。
使用AtomicInterger类对象自增 相当于count++(线程安全)
AtomicInteger num = new AtomicInteger(0);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50; i++) {
// 相当于num++,且是原子的
num.getAndIncrement();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50; i++) {
// 相当于num++,且是原子的
num.getAndIncrement();
}
});
t1.start();// 启动 t1线程
t2.start();// 启动 t2线程
t1.join();
t2.join();
System.out.println(num);// 输出100
纯用户态的轻量级锁,当发现锁被其他进程占有时,另外的线程不挂起等待,而是反复询问,看当前的锁是否被释放
自旋锁时 属于消耗CPU资源,但换来的是第一时间获取到锁,如果当时预期锁竞争不太激烈,就非常适合自旋锁
自旋锁是轻量级锁,也属于乐观锁
当锁对象被其他线程占用时,当前线程就无法获取到锁对象,一直自旋等待(忙等),直到锁对象一为null时,该线程立即就能占用该锁对象,所以自旋锁不适用于锁冲突激烈的场景。
ABA属于CAS的缺陷
在CAS中进行比较时,寄存器A和内存的值相同
我们无法判断是M始终没变,还是M变了一会又变回来了
CAS在比较时,当两次比较的值相等,则认为是没有发生改变,但是实际上却不一样,具体如下:
引入 ABA 问题: 假设在线程一第一次取完款的一瞬间,账户里收到转账50元,存款金额修改为500,线程二阻塞等待中,在线程二准备第二次取款时,朋友又转账我50,此时账户余额又变成了100
当第二次进行CAS时,余额从 100 —> 50 —> 100,而系统却确认为余额没有变过,又进行了一次扣钱,导致多扣了一次钱
解决问题: 引入一个 " 版本号 ",括号里为版本号,每次针对余额修改时, 版本号+1
100(1) —> 50(2) —> 100(3),当版本号不一致时,不扣钱
synchronized是自适应锁,既是乐观锁,也是悲观锁;既是轻量级锁,也是重量级锁;
轻量级锁部分是基于自旋锁实现,重量级锁是基于挂起等待锁实现,不是读写锁;
是非公平锁,是可重入锁。
synchronized的作用就是“加锁”,当两个线程针对一个锁对象加锁时,就会出现锁竞争。后面尝试加锁的线程就会阻塞等待,直到前一个线程释放锁。
锁膨胀/锁升级
对于偏向锁:
锁粗化
此处的粗细指的是“锁的粒度”(锁的涉及范围)
锁消除
有些代码明明不需要加锁,你加锁了,编译器会直接把锁去掉。例如单线程中使用StringBuffer
可重入锁
描述一个任务,方便返回计算结果,Runnable不方便返回结果。
基于 Callable 实现 1+2+3+...+1000
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class 基于Callable实现 {
//基于 Callable 实现 1+2+3+...+1000
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable callable = new Callable() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 1000; i++) {
sum += i;
}
return sum;
}
};
//Thread 的构造方法,不能直接传callable 还需要一个中间变量
FutureTask futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();;
//get方法产生阻塞,直到call方法计算完成,get方法才会返回
System.out.println(futureTask.get());
}
}
信号量,用来表示“可用资源的个数”,本质上就是一个计数器。“锁”就是一个二元信号量。
例如停车场:有车进去,车位-1;有车出来,车位+1.
import java.util.concurrent.Semaphore;
public class Semaphore01 {
public static void main(String[] args) throws InterruptedException {
//可用资源有 4 个
Semaphore semaphore = new Semaphore(4);
//连续 5 个申请资源,第五个申请就会阻塞,直到有人释放资源
semaphore.acquire();
System.out.println("申请成功");
semaphore.acquire();
System.out.println("申请成功");
semaphore.acquire();
System.out.println("申请成功");
semaphore.acquire();
System.out.println("申请成功");
//阻塞 直到有其他线程释放资源
semaphore.acquire();
System.out.println("申请成功");
//释放资源
semaphore.release();
}
}
同时等待N个任务执行结束
就像跑步比赛中的终点线,等所有比赛选手跑完后,再公布结果
import java.util.concurrent.CountDownLatch;
//同时等待N个任务执行结束
public class 基于CountDownLatch模拟跑步比赛的过程 {
public static void main(String[] args) throws InterruptedException {
//1.创建CountDownLatch 实例(参加跑步选手有多少人)
CountDownLatch countDownLatch = new CountDownLatch(10);
//2.Thread 创建线程任务(for循环10次 = 10个线程 = 10个参数选手)
for (int i = 0; i < 10; i++) {
Thread t = new Thread(() -> {
//currentThread()方法返回正在被执行的线程的信息。System.out.println("选手出发" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("选手到达" + Thread.currentThread().getName());
//3.在Thread线程任务中设置,线程结束的终点//countDown()设置线程结束的终点
countDownLatch.countDown();
});
t.start();
}
//4.设置await 阻塞等待,只有当所有任务执行完后才会解锁
countDownLatch.await();
System.out.println("比赛结束");
}
}
HashMap 本身不是线程安全的,在多线程环境下使用哈希表可以使用 Hashtable、ConcurrentHashMap
- Hashtable:在关键方法上加synchronized (对this加锁,即整个Hashtable加锁),锁冲突的概率很大,效率会很低
- ConcurrentHashMap:针对每个链表的头结点加锁
- 减少了锁冲突,针对每个链表的头结点加锁
- 只是针对写操作加锁,读操作没加锁,只是使用了 volatile
- 更广泛的使用了CAS,进一步提高效率
- 当触发扩容操作时,同时会维护一个新的HashMap,一点点将旧的数据搬运到新的上面去,当其他线程读的时候,读旧的表,插入时,插入到新表。