文章目录
一、什么是锁
java当中的锁、是在多线程环境下为保证共享资源健康、线程安全的一种手段。
线程操作某个共享资源之前,先对资源加一层锁,保证操作期间没有其他线程访问资源,当操作完成后,再释放锁。
二、锁的分类
以上都属于是一些锁的名称、属性。以Java来说,关于锁的大分类,就只有:悲观锁、乐观锁这两种。其余说的各种锁都是基于这两大分类下的细节实现。
2.1 悲观锁
悲观锁总是假设最坏的情况,认为所有的资源都是不安全的,随时会被其他线程操作、更改。所以操作资源前一定要加一把锁、防止其他线程访问,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
Java 中的悲观锁一般有两种实现方式:
- synchronized关键字
- 基于Java同步器AQS的各种实现类
2.1.1 基于synchronized的实现
Java中的关键字、底层由Jvm虚拟机实现的同步机制,通过两条监听器指令:MONITORENTER(进入)、MONITOREXIT(退出) 来实现同步效果(代码编译成字节码文件后可看到指令)
synchronized有三种使用方式:
- 修饰静态方法: 锁住的是类,该类下创建的所有对象都被锁住
- 修饰实例方法: 锁住的是当前对象,当前对象所属类创建的其他对象不受影响
- 修饰代码块(静态代码块、实例代码块): 根据代码块所出区域来区别,如代码块在静态方法中,那锁的是整个类、如代码块在实例方法中,那锁住的是当前实例对象。
2.1.2 基于AQS的实现类
AQS全称(AbstractQueuedSynchronizer)。基于Java程序实现的一种抽象队列同步器框架。AQS定义了一个volatile
修饰的int
类型变量state
来控制是否同步,提供一个unsafe
实现的原子方法来更新state
(也就是更新锁状态,是否上锁)。
基于AQS,Java本身实现了一些同步类。它们都位于java.util.concurrent
包下。例如:
- ReentrantLock(可重入锁,AQS体系下用户使用的最多的一个锁)
- ReentrantReadWriteLock(基于ReentrantLock的读写锁,读锁之间共享资源、读写、写写之间互斥资源,读写锁相较于普通的互斥锁并发能力要稍微好些,但使用起来需要考虑锁的切入点)
- StampedLock(基于读写锁优化,对读锁更加细化了一层,但同时使用也更加复杂,用的不多)
- Semaphore(信号量,可用于限流)
- CountDownLatch(可用于计数,一般用于在多线程环境下需要执行固定次数逻辑的地方)。
2.2 乐观锁
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。
Java没有提供可直接使用的乐观锁,不过内置了一些由底层由乐观锁实现的类。例如:java.util.concurrent.atomic
下的几个原子类。
2.2.1 基于版本号机制实现
一般是在数据表中加上一个数据版本号 version
字段,表示数据被修改的次数。当数据被修改时,version
值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version
值,在提交更新时,若刚才读取到的 version
值为当前数据库中的 version
值相等时才更新,否则重试更新操作,直到更新成功。
举一个简单的例子:假设数据库中帐户信息表中有一个 version
字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。
- 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
- 在操作员 A 操作的过程中,操作员 B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20
)。 - 操作员 A 完成了修改工作,将数据版本号( version=1 ),连同帐户扣除后余额( balance=$50
),提交至数据库更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,数据库记录version
更新为 2 。 - 操作员 B 完成了操作,也将版本号(
version
=1 )试图向数据库提交数据( balance=$80
),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1 ,数据库记录当前版本也为 2 ,不满足 “
提交版本必须等于当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。 这样就避免了操作员 B 用基于version
=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。
2.2.2 基于CAS机制实现
CAS 的全称是 Compare And Swap(比较与交换) ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
具体看CAS详解
2.3 两种锁的区别
高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。但是,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升。不过,大量失败重试的问题也是可以解决的,像我们前面提到的 LongAdder以空间换时间的方式就解决了这个问题。理论上来说:悲观锁通常多用于写比较多的情况(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。不过,如果乐观锁解决了频繁失败和重试这个问题的话(比如LongAdder),也是可以考虑使用乐观锁的,要视实际情况而定。乐观锁通常多用于写比较少的情况(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量(参考java.util.concurrent.atomic
包下面的原子变量类)。
三、synchronized 关键字
3.1 synchronized介绍
synchronized
是 Java 中的一个关键字,翻译成中文是同步的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。在 Java 早期版本中,synchronized
属于 重量级锁,效率低下。这是因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。
不过,在 Java 6 之后,synchronized
引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让 synchronized
锁的效率提升了很多。因此, synchronized
还是可以在实际项目中使用的,像 JDK 源码、很多开源框架都大量使用了 synchronized
。
3.2 synchronized的使用
synchronized
关键字的使用方式主要有下面 3 种:
- 修饰实例方法
- 修饰静态方法
- 修饰代码块
1、**修饰实例方法 (锁当前对象实例)**给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁 。
synchronized void method() {
//业务代码
}
2、**修饰静态方法 (锁当前类)**给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。
synchronized static void method() {
//业务代码
}
3、修饰代码块 (锁指定对象/类)
对括号里指定的对象/类加锁:
synchronized(object)
表示进入同步代码库前要获得 给定对象的锁。synchronized(类.class)
表示进入同步代码前要获得 给定 Class 的锁
synchronized(this) {
//业务代码
}
3.3 synchronized的锁升级过程
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
1.偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程的ID,当该线程下次如果又来获取该锁就可以直接获取到了,也就是支持锁重入
2.轻量级锁:当两个或以上线程交替获取锁,但并没有在对象上并发获取锁时,偏向锁升级为轻量级锁,在此阶段,线程采用CAS的自旋方式尝试获取锁,避免阻塞线程造成的cpu在用户态和内核态转换的消耗
3.两个或以上线程并发的在同一个对象上进行同步时,为了避免无用自旋消耗cpu,轻量级锁会升级成重量级锁
4.自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都需要操作系统帮忙完成,比较消耗时间,自旋锁是线程通过CAS机制实现的
四、ReentrantLock
4.1 ReentrantLock介绍
ReentrantLock
实现了 Lock
接口,是一个可重入且独占式的锁,和 synchronized
关键字类似。不过,ReentrantLock
更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。
ReentrantLock
里面有一个内部类 Sync
,Sync
继承 AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在 Sync
中实现的。Sync
有公平锁 FairSync 和非公平锁 NonfairSync 两个子类。
ReentrantLock
默认使用非公平锁,也可以通过构造器来显式的指定使用公平锁。
4.2 主要方法
4.3 synchronized 和 ReentrantLock 的区别
- synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
- 等待可中断 : ReentrantLock能够中断等待锁的线程,通过
lock.lockInterruptibly()
来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 - 可实现公平锁
- 可实现选择性通知(Condition)
- 获取锁可设置超时时间
五、ReentrantReadWriteLock
ReentrantReadWriteLock
是ReadWriteLock
接口的实现类:
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable{
}
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
ReadWriteLock
维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有writer
,读取锁可以由多个reader
线程同时保持。写入锁是独占的。
使用方式和ReentrantLock较相似不过多赘述