①常⻅的锁策略
(1)乐观锁&悲观锁
顾名思义
乐观锁:加锁的时候,认为锁竞争不会很激烈,不需要做额外工作
悲观锁:加锁的时候,认为锁竞争会很激烈,额外做一些工作(比如:加锁失败就等待 / 阻塞,直到拿到锁)
(2)重量级锁&轻量级锁
重量级锁:实现成本 / 开销维度大的锁,即运行更低效
轻量级锁:实现成本 / 开销难度小的锁,运行更高效
(3)挂起等待锁&自旋锁
挂起等待锁:获取锁失败 → 线程被挂起(进入阻塞队列)→ 锁释放后被唤醒
自选锁:获取锁失败 → 线程不挂起,循环重试(CPU 空转)→ 直到拿到锁 / 放弃
⾃旋锁伪代码:
java
while (抢锁(lock) == 失败) {}
按之前的⽅式(挂起等待锁),线程在抢锁失败后进⼊阻塞状态,放弃CPU,需要过很久才能再次被调度.
但实际上,⼤部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃CPU这个时候就可以使⽤⾃旋锁来处理这样的问题.
6种锁的关系:
一般是:
悲观锁→重量级锁→挂起等待锁
乐观锁→轻量级锁→挂起等待锁
| 设计策略 | 实现成本 | 等待方式 | 典型例子 | 核心特征 |
|---|---|---|---|---|
| 悲观锁 | 轻量级 | 自旋锁 | synchronized 轻量级锁 | 先锁后做、自旋等待、开销小 |
| 悲观锁 | 重量级 | 挂起等待锁 | synchronized 重量级锁、ReentrantLock | 先锁后做、挂起等待、开销大 |
| 乐观锁 | 轻量级 | 自旋锁 | CAS、AtomicInteger、数据库版本号 | 先做后校验、自旋重试、无阻塞 |
而我们之前学的synchronized锁是自适应的,可以自由切换锁的类型1
(4)互斥锁和读写锁:
互斥锁:所有人都互斥,读和写都要抢同一把锁。
读写锁:读和读不互斥,读和写、写和写互斥,提高读多写少场景的效率。
因为读和读不会出现线程安全问题,但是写就会,所以有关写的都要互斥,读不用(从而提高效率)
(5)可重入锁&不可重入锁
可重入锁:同一线程已持有锁,再次获取该锁时不会死锁,而是直接成功(synchronized锁)
不可重入锁:同一线程已持有锁,再次获取该锁时会阻塞 / 死锁
(6)公平锁&非公平锁
公平锁:遵守"先来后到".B⽐C先来的.当A释放锁的之后,B就能先于C获取到锁.
非公平锁:不遵守 "先来后到",B 和 C 都有可能获取到锁。
(7)锁消除:锁消除是 JVM 提供的一种自动优化机制,核心是:JVM 在即时编译(JIT)阶段,检测到某些锁对象不可能被多线程访问,就会自动移除这些锁,避免无意义的加锁 / 解锁开销。
锁加了,但 JVM 发现没必要,就偷偷给你删了
(8)锁粗化:
先说锁粒度:锁保护的范围越大,粒度越粗;保护的范围越小,粒度越细。它直接影响并发效率 ------ 粗粒度锁安全但并发低,细粒度锁并发高但实现复杂、可能有额外开销
所以一个代码中,如果反复多次对锁粒度低的进行加锁,就可能被优化 成更粗粒度的加锁
- 未优化:拿 1 个水果→开门→关门,重复 10 次(对应多次加锁 / 解锁);
- 锁粗化优化:开门→一次性拿 10 个水果→关门(对应一次加锁 / 解锁)。
- 所以虽然写的是未优化,但是运行起来有可能按优化的运行
②CAS
(1)CAS概念:
全称 Compare and swap,字面意思:"比较并交换",一个 CAS 涉及到以下操作:我们假设内存中的原数据 V,旧的预期值 A,需要修改的新值 B。
- 比较 A 与 V 是否相等。(比较)
- 如果比较相等,将 B 写入 V。(交换)
- 返回操作是否成功。
所以CAS是一种原子指令(由 CPU 硬件层面保证原子性),核心逻辑是:通过对比内存值与预期值,仅当二者一致时,才将内存值更新为目标值,整个过程一步完成、不可中断。
CAS 是无锁编程的核心技术,也是乐观锁的典型实现方式,全程无阻塞、无内核态切换,是并发编程中高效保证原子性的关键手段。
CAS伪代码:
java
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
(2)CAS是怎么实现的
针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:
・java 的 CAS 利用的是 unsafe 这个类提供的 CAS 操作;
・unsafe 的 CAS 依赖的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
・Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性。
简而言之,是因为硬件予以了支持,软件层面才能做到。
(3)CAS的应用:
1)实现原子类:
原子类是在不使用同步锁(如
synchronized、ReentrantLock)的前提下,保证单个变量的多线程原子操作
java
public class demo6 {
private static AtomicInteger count=new AtomicInteger(0);
public static void main(String[] args) {
Thread t1=new Thread(()->{
for(int i=0;i<50000;i++){
count.getAndIncrement();
}
});
}
①private只是编程规范, static用于多线程共享使用
②初始定义的()里填这个初始值,然后想使用就使用api方法
③想对count进行操作,就是用已经有的方法即可
java
count.getAndIncrement();//count++
count.incrementAndGet();//++count
count.addAndGet(n)//count+=n
所以这就是原子类,不用加锁也没有线程安全问题。boolean int long 甚至数组等等都有原子类,也有对应写好的方法
2)实现⾃旋锁
自旋锁伪代码:
这里使用CAS比较owner和null,如果相等说明锁没有被占用,那么把现在运行的线程赋值给owner,此时owner的值不再为null,其他的线程无法使用,从而反复执行while循环里的CAS代码进行比较,从而形成自旋锁
java
public class SpinLock {
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就⾃旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
CAS的ABA问题:
ABA 的问题:
假设存在两个线程 t1 和 t2。有一个共享变量 num, 初始值为 A。
接下来,线程 t1 想使用 CAS 把 num 值改成 Z, 那么就需要
・先读取 num 的值,记录到 oldNum 变量中。
・使用 CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z。
但是,在 t1 执行这两个操作之间,t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了 A。
线程 t1 的 CAS 是期望 num 不变就修改。但是 num 的值已经被 t2 给改了。只不过又改成 A 了。这个时候 t1 究竟是否要更新 num 的值为 Z 呢?
到这一步,t1 线程无法区分当前这个变量始终是 A, 还是经历了一个变化过程。
解决方案:
• CAS?操作在读取旧值的同时,也要读取版本号.
• 真正修改的时候,
◦ 如果当前版本号和读到的版本号相同,则修改数据,并把版本号+1.
◦ 如果当前版本号⾼于读到的版本号.就操作失败(认为数据已经被修改过了).
也就是说不能选有可能发生误判的变量来作为参考
③JUC(java.util.concurrent)的常⻅类
JUC 是 Java 提供的并发编程工具包 ,专门解决多线程开发中的线程同步、任务调度、并发容器、原子操作等问题,替代了传统的
synchronized+Thread的低效写法,是高并发开发的核心工具。
(1)Callable接⼝
Callable和Runnable是并列关系,但是Callable有返回值(泛型),Runnable没有(void)
使用方法就是先单独初始化,<>内写类型,然后在{} 里重写call()方法,方法名要指定返回类型,方法里写执行的指令,然后需要返回(当然用lambda也可以)
而且还得引入一个相同储存类型的类FutureTask<>,把Callable传入()中,然后把实例化的FutureTask传入建立的线程里,启动后,结果仍然需要futuretask的方法提取
java
public class demo7 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable=new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int res=0;
for(int i=0;i<100;i++){
res+=i;
}
return res;
}
};//有;号!!
/*lambda表达式
Callable<Integer> callable= () -> {
int res=0;
for(int i=0;i<100;i++){
res+=i;
}
return res;
};//有;号!!
*/
FutureTask<Integer> futureTask=new FutureTask<>(callable);//中继?
Thread t=new Thread(futureTask);
t.start();
System.out.println(futureTask.get()); //要声明抛出异常
}
}
(2)ReentrantLock锁
可重⼊互斥锁,和synchronized定位类似,都是⽤来实现互斥效果,保证线程安全.
但是根synchronized的代码块不一样,这里需要手动上锁解锁
・lock (): 加锁,如果获取不到锁就死等.
・trylock (超时时间): 加锁,如果获取不到锁,等待一定的时间之后就放弃加锁.
・unlock (): 解锁.
・ReentrantLock 和 synchronized 的区别:
・synchronized 是一个关键字,是 JVM 内部实现的 (大概率是基于 C++ 实现). ReentrantLock 是标准库的一个类,在 JVM 外实现的 (基于 Java 实现).
・synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放。使用起来更灵活,但是也容易遗漏 unlock.
・synchronized 在申请锁失败时,会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.
・synchronized 是非公平锁,ReentrantLock 默认是非公平锁。可以通过构造方法传入一个 true 开启公平锁模式.
・更强大的唤醒机制. synchronized 是通过 Object 的 wait /notify 实现等待 - 唤醒。每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待 - 唤醒,可以更精确控制唤醒某个指定的线程.如何选择使用哪个锁?
・锁竞争不激烈的时候,使用 synchronized, 效率更高,自动释放更方便.
・锁竞争激烈的时候,使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为,而不是死等.
・如果需要使用公平锁,使用 ReentrantLock.
(3)Semaphore信号量
信号量,用来表示 "可用资源的个数". 本质上就是一个计数器.
使用:定义里的括号就是初始值
acquire()就是信号量-1,release就是信号量+1,
信号量最小值是0,如果是0了还使用acquire那就阻塞,以下的代码无法运行,release()就是信号值+1,无上限
java
Semaphore semaphore=new Semaphore(4);
semaphore.acquire();
System.out.println("执行");
semaphore.acquire();
System.out.println("执行");
semaphore.acquire();
System.out.println("执行");
semaphore.release();
System.out.println("执行");
semaphore.release();
System.out.println("执行");
semaphore.release();
System.out.println("执行");
(4)CountDownLatch:
让一个或多个线程等待,直到其他线程完成一组操作后,再继续执行
定义方法需要再括号内写数字,代表想运行的次数,相当于计数器
每执行完一个线程在尾部写上 方法名.countDown(); 就是初始值-1
CountDownLatch.await() 会在计数器减到 0 时自动解除阻塞,然后执行main线程里的代码
java
CountDownLatch count=new CountDownLatch(10);
ExecutorService pool= Executors.newFixedThreadPool(4);
for(int i=0;i<10;i++){
int id=i;
pool.submit(()->{
System.out.println("执行"+id);
try {
Thread.sleep(500);
}catch (InterruptedException e){
throw new RuntimeException(e);
}
count.countDown();
});
}
count.await();
System.out.println("执行完10个了");
④多线程环境使⽤哈希表
(1)HashMap : 线程不安全
Hashtable:线程安全,但是效率低(对所有方法全部一律加锁)
(2)ConcurrentHashMap
是上面的代替品,只对写操作进⾏加锁。

每一个头结点都单独有自己的锁,节点用链表向下延伸