锁
锁就是用来解决并发编程下的安全问题的,锁有很多种类型,下面来瞅瞅
一、锁介绍
Java提供了两种加锁的方式
1、Synchronized关键字
2、Lock
接口
学习各种锁之前,要明白我们在Java中使用的锁并不是确切地只为一个特定的锁,下面具体解释
1.1、悲观&乐观锁
一、悲观锁
- 悲观锁就像互斥锁一样,多个线程争抢该锁时,只有一个线程能使用,其他都会阻塞
- 所以这也是悲观的含义,因为该锁总是认为在某个线程对齐共享变量进行修改时,总会有其他线程也会对其修改
- 所以某个线程持有后只有释放该锁,其他线程才可以对其争抢
- Synchronized就是一个悲观锁
二、乐观锁
- 乐观锁在
Java
并没有具体实现类,它更像一种实现方式 - 乐观锁认为,当当前线程持有该锁并对其修改时,其他线程并不会进来捣乱(修改),所以说它不会上锁
- 但是这样也还会存在并发安全问题,所以它有一种自旋的实现
- 每次要对其修改时,会进行判断,是否为该线程初次读取时的值,如果是则对其修改
- 如果不是,则一直循环判断,尝试更新
- 在操作系统中有个专业术语叫做CAS(Compare-and-Swap 比较并替换)
- 其中它还会一直去循环判断,所以也称为自旋
- 根据以上概念,乐观锁也有点像读写锁,但是读写锁的写锁是一种悲观锁,当对其写数据时,其他读写线程的操作都会阻塞
总结:乐观锁更像是一组事务
1.2、CAS
一、CAS介绍
顾名思义:比较并替换(设置,CAS),在CPU的实现中该操作是原子操作,要么执行成功,幺妹失败
重要的是Java也提供了可以对变量进行CAS的操作,待会使用
二、CAS的基本实现思想(简化)
- 如果符合目标预期结果(标志位),那么将对其修改,从而防止其他线程进入,这个过程中我们就可以安全的修改共享内存数据(原子操作)
- 其他线程此时或者结束操作
- 或者一直循环等待(自旋),直到当前线程执行完毕,并对标志位进行复原,其他线程才可以争抢使用【自选锁的实现】
三、自定义CAS
以下案例实现一个简单的“CAS”,但要注意的是直接使用Java代码并无法实现原子操作
- 标志位需要保证可见性
- 共享数据并不需要加一个
Volatile
,因为自旋锁的特性也类似悲观锁,当修改了标志位,这段过程其他线程无法使用该共享内存
package com.migu;
import java.util.concurrent.TimeUnit;
public class Test {
private volatile static boolean flag = true; // 标志位,默认可使用
private static int num = 0; // 共享数据
public static void main(String[] args) {
new Thread(() -> {
while (true) {
if (flag) { // 比较
flag = false; // 后替换,防止其他线程进入
num += 1;
System.out.println(Thread.currentThread().getName() + ": " + num);
flag = true; // 复原
}
}
}, "线程A").start();
new Thread(() -> {
while (true) {
if (flag) { // 使用Java层面上的比较替换,并不是原子操作
flag = false;
num += 1;
System.out.println(Thread.currentThread().getName() + ": " + num);
flag = true;
}
}
}, "线程B").start();
Thread.sleep(1);
while (Thread.activeCount() > 1)
System.exit(0);
}
}
输出:(在Java层面无法实现原子操作)
四、原子类
上面我们提到过原子类,它的底层实现就是CAS锁,位于java.uti.concurrent.atomic
包下
比如AtomicInteger的getAndAddInt
方法:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
通过使用Unsafe对象,该类可直接操作内存进行自旋操作
1.3、锁升级:无锁–>偏向锁–>轻量级锁–>重量级锁
一、锁升级介绍
当多个线程争抢同一把锁时,该锁就会升级
在JDK1.6
之前,Synchronized就是一把重量级锁。而在这之后Synchronized就会存在锁升级的情况,不会一开始就是个重量级锁
重量级锁也如同互斥锁(悲观锁),互斥锁会存在大量阻塞的情况,所以在JDK1.6
开始就对锁进行了优化
二、锁升级过程
在Java是通过锁对象来进行设置锁的,所以在对象中也保存锁的信息
下图是对象头中的锁状态信息(图取自网上,知道链接的告诉我,我放上去)
对象锁的初始状态,为无锁状态
1.3.1、偏向锁
当有线程尝试获取该对象锁时,则会进行CAS判断,成功的话则对该对象头的锁状态的锁标志位设置为01
即为偏向锁,并在对象头的线程ID记录该线程
偏向锁:
- 顾名思义,该对象锁将一直属于某个线程所有【不存在竞争】,一直偏向于该线程
- 除非有其他线程进入争抢,则会升级锁状态
- 持有偏向锁的线程不会再进行同步判断,如同直接调用方法一样(效率高)
偏向锁设置流程:当锁为无锁状态,则如上操作
偏向锁撤销流程:当多个线程争抢偏向锁时,将会发生撤销
线程B
首先进行CAS判断,由于该对象头已保存了线程A
(已经历过CAS操作),CAS操作则会失败- 那么会暂停该对象锁保存的线程(线程ID记录),并检查该线程是否存活
线程A
结束,则重新偏向:线程ID指向线程B线程A
活动中,撤销该锁并准备做升级轻量级锁的操作
1.3.2、轻量级锁(自旋锁)
轻量级锁就真正存在多线程下的加锁和解锁操作,自然而然我们就需要对当前线程的栈帧进行处理
具体实现:
加锁过程:
- 首先为想要获取该锁的线程的栈帧中创建一个锁记录
- 并将对象头信息进行复制操作,放置在所属线程的栈帧中,并尝试堆中MarkWord的轻量级锁引用到锁记录。
- 此时开始CAS操作
- 成功:则修改标志位为
00
。 - 失败:通过自旋尝试获取该锁,所以轻量级锁也称自旋锁
- 当自旋获取该锁又失败时(这里可能需要某种条件,具体我也不太懂,不好意思),则会对锁做升级为重量级锁的操作
- 成功:则修改标志位为
优缺点:
轻量级锁发生在用户态,避免线程频繁在用户态和内核态进行切换
使用自旋获取锁,不会产生阻塞,可提高程序的响应速度
自旋会消耗CPU运算效率
1.3.3、重量级锁
重量级锁依赖于操作系统的互斥量(mutex) 实现 ,此时JVM启动的线程获取该锁需要频繁在用户态和内核态进行切换
优缺点:
不会消耗CPU,但由于线程在获取锁发生在内核态,此时响应时间会慢
适用于追求吞吐量的场景
1.4、可重入锁
一、可重入锁介绍
可重入锁也叫递归锁,这个说白了就很像我们上面讲的偏向锁,但他并不担心其他线程会对其争抢,只要拿到最外面那把锁,里面那些锁都是它的,不需要再做进行争抢锁的操作
类似这样的情况:
public static synchronized void m() {
synchronized (Test.class) {
synchronized (Test.class) {
System.out.println("----");
}
}
}
Java提供的锁都是可重入锁,Synchronized
和Lock
1.5、公平、非公平锁
一、锁介绍
公平锁:顾名思义,对于先申请锁的线程可以优先拿到的锁
非公平锁:后申请的线程可以先获取锁(通过策略,可能是随机,可能是优先级设置)
Synchronized就是一把非公平锁
Lock可以指定,默认是非公平锁,看源码可知
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
1.6、可中断锁
一、可中断锁
可中断锁并不是指那个抛出中断异常(InterruptedException
)让处于等待的线程直接结束的一个机制
而是对获取锁但处于阻塞状态的线程,可以放弃锁的争抢
在Java中Synchronized
就是不可中断锁,而Lock
则是一把可中断锁,具体看源码
void lock() // 获取锁。 (阻塞)
void lockInterruptibly() // 获取该锁除非当前线程 interrupted。
Condition newCondition() // 返回一个新的 Condition实例绑定到该 Lock实例。
boolean tryLock() // 尝试获取锁,如果该锁处于空闲状态,则可直接获取(原子操作)
boolean tryLock(long time, TimeUnit unit) // 类似自旋操作(通过给到的时间尝试获取)
void unlock() // 释放锁。
所以可以通过tryLock
方法决定是否放弃争抢
1.7、读写锁
一、读写锁介绍
拿之前讲过的文章,读写锁其实分两个部分
读锁:也称共享锁
写锁:互斥锁、悲观锁、排它锁、独占锁
读写锁的文章地址:读写锁