悲观锁和乐观锁的区别
什么是悲观锁?
基本上我们理解的操作前对资源加锁,操作完后释放锁。说的都是悲观锁。悲观锁认为所有的资源都是不安全的,随时会被其他线程操作、更改。所以操作资源前一定要加一把锁、防止其他线程访问。
什么是乐观锁?
乐观锁是一种特殊的锁,它认为所有的资源都是安全的,每个线程对资源的操作都是符合预期的,所以它不需要对资源加锁。
乐观锁在操作资源时,会采用一种确认机制来保证所操作资源未被其他线程更改过。这种机制叫做CAS(Compare And Set)机制。
悲观锁的实现
- synchronized关键字
- 基于Java同步器AQS的各种实现类
synchronized
Java中的关键字、底层由Jvm虚拟机实现的同步机制,通过两条监听器指令:MONITORENTER(进入)、MONITOREXIT(退出)来实现同步效果(代码编译成字节码文件后可看到指令)
synchronized有三种使用方式:
修饰静态方法:锁住的是类,该类下创建的所有对象都被锁住
修饰实例方法:锁住的是当前对象,当前对象所属类创建的其他对象不受影响
修饰代码块(静态代码块、实例代码块):根据代码块所出区域来区别,如代码块在静态方法中,那锁的是整个类、如代码块在实例方法中,那锁住的是当前实例对象。
基于AQS的实现类
AQS全称(AbstractQueuedSynchronizer)。基于Java程序实现的一种抽象队列同步器框架。AQS定义了一个volatile修饰的int类型变量state来控制是否同步,提供一个unsafe实现的原子方法来更新state(也就是更新锁状态,是否上锁)。
基于AQS,Java本身实现了一些同步类。它们都位于java.util.concurrent包下。例如:
ReentrantLock(可重入锁,AQS体系下用户使用的最多的一个锁)
ReentrantReadWriteLock(基于ReentrantLock的读写锁,读锁之间共享资源、读写、写写之间互斥资源,读写锁相较于普通的互斥锁并发能力要稍微好些,但使用起来需要考虑锁的切入点)
StampedLock(基于读写锁优化,对读锁更加细化了一层,但同时使用也更加复杂,用的不多)
Semaphore(信号量,可用于限流)
CountDownLatch(可用于计数,一般用于在多线程环境下需要执行固定次数逻辑的地方)。
乐观锁的实现
- volatile+CAS
- 版本号机制
Java没有提供可直接使用的乐观锁,不过内置了一些由底层由乐观锁实现的类。例如:java.util.concurrent.atomic下的几个原子类。
实现原理:volatile+CAS 的方式实现。
使用场景
悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
Java内存模型
java中的内存模型定义,将内存划分为:主内存和工作内存。Java线程在操作资源时,会将其用到的资源复制一份到线程的私有工作内存中,线程在自己的工作内存中对资源完成操作后,再把资源同步回主内存当中。完成一次资源的操作。
CAS原理
CAS可以理解为比较后赋值。举例:两个线程A、B。修改一个共享资源变量Y、根据java内存模型定义,两个线程分别会复制一份资源的副本到各自的工作内存中。AY1、BY1。两个线程修改完后会将AY1、BY1同步回主内存中。
然而,在CAS机制下,两个线程除了复制AY1、BY1到工作内存之外,还会另存一个资源副本AY2、BY2。当线程各自修改完AY1、BY1之后,同步主内存之前,会用AY2、BY2与主内存中的资源Y对比,如果对比一致,则立即更新主内存,如果不一致,则重复上面操作,重新从主内存获取资源、修改、同步。
由于CAS在Java底层是一个原子操作,所以可以保证同步数据回主内存时是线程安全的。这点可以参考sun.misc.Unsafe类。这个类提供了原生的CAS能力,直接调native方法于系统底层交互。
乐观锁实现原理
volatile和CAS。
volatile保证有序可见
CAS保证原子
了解到Valotile是为了保证资源的可见性,任何一个线程修改了资源后。其他线程都能立刻感知并重新获取资源。CAS是保证资源的安全性,由于是原子操作,任何一个线程在修改资源时,都是一体的。其他线程是不可操作的。所以volatile的特性+CAS的机制就组成了一个完美的乐观锁,既保证了线程安全,对性能影响也不大。volatile的特性+CAS的机制这种组合也可以叫做:volatile+原子操作。