关于Java中的锁,看这一篇就够了(总结篇)
基本上我们理解的操作前对资源加锁,操作完后释放锁。说的都是悲观锁。悲观锁认为所有的资源都是不安全的,随时会被其他线程操作、更改。所以操作资源前一定要加一把锁、防止其他线程访问。乐观锁是一种特殊的锁,它认为所有的资源都是安全的,每个线程对资源的操作都是符合预期的,所以它不需要对资源加锁。那么问题来了,既然乐观锁不会对资源加锁,那它能不能保证线程安全呢?答案是:可以保证线程安全,且性能比悲观锁还要好。
文章目录
锁的概念
java当中的锁、是在多线程环境下为保证共享资源健康、线程安全的一种手段。
线程操作某个共享资源之前,先对资源加一层锁,保证操作期间没有其他线程访问资源,当操作完成后,再释放锁。
锁的分类
锁有哪些?我们经常会听到比如:读写锁、公平锁、非公平锁、乐观锁、悲观锁、自旋锁、可重入锁……等等
看一张图(来源网络):
以上都属于是一些锁的名称、属性。以Java来说,关于锁的大分类,就只有:悲观锁、乐观锁这两种。其余说的各种锁都是基于这两大分类下的细节实现。
一、什么是悲观锁?
基本上我们理解的操作前对资源加锁,操作完后释放锁。说的都是悲观锁。悲观锁认为所有的资源都是不安全的,随时会被其他线程操作、更改。所以操作资源前一定要加一把锁、防止其他线程访问。
Java中的悲观锁有哪些
- 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(可用于计数,一般用于在多线程环境下需要执行固定次数逻辑的地方)。
二、什么是乐观锁?
乐观锁是一种特殊的锁,它认为所有的资源都是安全的,每个线程对资源的操作都是符合预期的,所以它不需要对资源加锁。
那么问题来了,既然乐观锁不会对资源加锁,那它能不能保证线程安全呢?
答案是:可以保证线程安全,且性能比悲观锁还要好。因为乐观锁在操作资源时,会采用一种确认机制来保证所操作资源未被其他线程更改过。这种机制叫做CAS(Compare And Set)机制。
Java中的乐观锁有哪些
Java没有提供可直接使用的乐观锁,不过内置了一些由底层由乐观锁实现的类。例如:java.util.concurrent.atomic下的几个原子类。
如果我们想自己实现乐观锁的话,可以参考上面那些原子类,使用valotile+CAS 的方式实现。
那么valotile是什么呢?CAS又是什么呢?
Valotile
valotile跟synchronized一样,是Java内置的关键字。不过valotile只能修饰变量。valotile主要的作用是保证变量在内存中的可见性、有序性
- 可见性:valotile修饰的变量被修改后,对内存中是立即可见的。举个例子:有两个线程A、B。有一个valotile修饰的变量Y。当线程A对变量Y进行修改后,线程B能够立刻感知。感知到后会重新读取变量Y的最新值放到自己工作内存中进行操作。
- 有序性:我们都知道,Java代码是一行一行根据编写顺序去运行的。看着是顺序执行没错。但是实际底层JVM在执行字节码文件指令时,有些指令并不会依照代码顺序一样执行。因为Java编译器会在编译成字节码的时候为了提高性能会对你的代码进行指令优化,这个优化叫做指令重排。这个指令重排在单线程环境下不会有问题,因为不管你怎么重排指令,最后的结果都是期望的。但是如果在多线程环境下,就会有线程安全问题。所以valotile帮我们禁止了指令重排的优化,保证了编译器是严格按照代码编写顺序去编译指令。
Java内存模型
了解CAS之前先来了解一下Java内存模型。Java中的内存模型定义,将内存划分为:主内存和工作内存。Java线程在操作资源时,会将其用到的资源复制一份到线程的私有工作内存中,线程在自己的工作内存中对资源完成操作后,再把资源同步回主内存当中。完成一次资源的操作
CAS(Compare And Set)
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方法于系统底层交互
Valotile+CAS
上面分别介绍了Valotile和CAS。了解到Valotile是为了保证资源的可见性,任何一个线程修改了资源后。其他线程都能立刻感知并重新获取资源。CAS是保证资源的安全性,由于是原子操作,任何一个线程在修改资源时,都是一体的。其他线程是不可操作的。所以Valotile的特性+CAS的机制就组成了一个完美的乐观锁,既保证了线程安全,对性能影响也不大。Valotile的特性+CAS的机制这种组合也可以叫做:Valotile+原子操作
总结
Java当中锁总共分为悲观锁、乐观锁两大分类,悲观锁使用简单、但对并发性能稍微有影响。乐观锁对并发性能影响不大,但是使用较为复杂。
以上就是关于Java中的锁的一个基本总结!
更加详细的内容期待后续更新……
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)