什么是锁?
锁(lock)是一种同步机制,用于在多线程环境中强制对资源的访问限制。锁旨在强制实施互斥排他、并发控制策略。
锁的三个概念:锁有开销、会产生竞争、要避免产生死锁
锁开销 lock overhead:锁占用内存空间、cpu初始化和销毁锁、获取和释放锁的时间。
锁竞争 lock contention:一个进程或线程试图获取另一个进程/线程持有的锁,就会发生竞争。锁粒度越小,发生锁竞争的可能性越小。
死锁 deadlock:至少两个任务中的每一个都在等待另一个任务持有的锁的情况。
锁的种类:
1、公平锁/非公平锁
公平锁,多个线程按照申请锁的顺序来获取锁;以打水为例,所有人排队,管理员不接收插队
ReentrantLock(true)公平锁,默认是非公平锁
非公平锁,不按照申请顺序,有可能造成优先级反转或者饥饿现象;所有人排队,管理员接收插队
ReentrantLock、Synchronized
可重入锁
同一个线程可以重入上锁的代码段,不同的线程则需要进行阻塞。Synchronized具有重入锁的功能,在一个synchronized方法/块内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。
以打水为例,可重入锁相当于一个人有多个水桶,水桶和锁绑定。非重入锁,第一个水桶和锁绑定打完水之后并不会释放锁,导致第二个水桶不能和水桶绑定无法打水。当前线程出现死锁。
通过重入锁ReentrantLock和非可重入锁NonReentrantLock的源码分析:
都继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0。当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status==0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。如果status!=0,则判断当前线程是否获取到这个锁的线程,如果是的话执行status+1,且当前线程再次获取锁。而非可重入锁是直接去获取并尝试更新status的值,如果status!=0的话会导致其获取锁失败,当前线程阻塞。
2、独享锁/共享锁
独享锁,该锁一次只能被一个线程所持有;
共享锁,该锁可以被多个线程所持有;线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
互斥锁/读写锁
互斥锁/读写锁是上面独享锁/共享锁的具体实现
ReentrantLock和ReentrantReadWriteLock:独享锁和共享锁。
3、乐观锁/悲观锁
不是具体类型的锁,而是指看待并发同步的角度。
悲观锁认为对于同一数据的并发操作,一定会发生修改。所以采取先加锁的形式。
乐观锁则认为对于同一数据的并发操作,是不会发生修改的。更新数据的时候,会采用尝试更新,不断更新的方式更新数据。
悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景。悲观锁在java中的使用,显示的锁定之后再操作同步资源;而乐观锁则直接去操作同步资源,就是无锁编程,常采用CAS算法,通过CAS自旋实现原子操作的更新。java.util.concurrent包中的原子类就是通过CAS实现了乐观锁。jdk1.5开始提供了AtomicReference类来保证引用对象之间的原子性。重量级锁是悲观锁的一种,自旋锁、轻量级锁与偏向锁属于乐观锁。
CAS是一种无锁算法,有三个操作,内存值V,旧的预期值A,要修改的新值B。当且仅当旧的预期值A和内存值V相同时,才将内存值V修改为B,否则什么都不做。类似乐观锁
4、分段锁
分段锁是一种锁的设计,并不是具体的一种锁。对于ConcurrentHashMap,其并发实现就是通过分段锁的形式来实现高效的并发操作。当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode计算出要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不在一个分段中,就实现了真正的并行的插入。分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
5、无锁/偏向锁/轻量级锁/重量级锁
这四种是指锁的状态,并且是针对Synchronized的。两个重要的概念:“Java对象头“,”Monitor”
Java对象头:Hotspot虚拟机的对象头主要包括:Mark Word(标记字段)和 Klass Pointer(类型指针)
Mark Word:默认存储对象的HashCode、分代年龄和锁标志信息。被设计成非固定的数据结构。
Klass Pointer:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
Monitor:可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段放拥有该所的线程的唯一标识,表示该锁被这个线程占用。
synchronized通过Monitor来实现线程同步,Monitor是依赖底层的操作系统的Mutex Lock(互斥锁)来实现线程同步。
依赖操作系统Mutex Lock所实现的锁,称之为:重量级锁,JDK6中为了减少获得锁和释放锁带来的性能消耗,引入“偏向锁”和“轻量级锁”。
目前锁一共有4种状态:无锁:偏向锁、轻量级锁和重量锁。锁状态只能升级不能降级。
在Java5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word是否存储着指向当前线程的偏向锁。引入偏向锁的目的是:在无多线程竞争情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁是轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低
锁 | 优点 | 缺点 | 适用场景 |
偏向锁 | 加锁和解锁不需要额外的消耗,与执行非同步方法仅存在纳秒级的差距 | 如果线程间存在竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块的情况 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 始终得不到锁的线程,使用自旋会消耗CPU | 追求响应时间,同步块执行速度非常快,只有两个线程竞争锁 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度比较慢,竞争锁的线程大于2个 |
综上:偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。
线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录(Lock Record)的空间,并将对象头中的Mark Word复制到锁记录中,官方称为:Displaced Mark Word;整个synchronized锁流程如下:
1.检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁
2.如果不是,则使用CAS将当前线程的ID替换Mark Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1
3.如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁
4.当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁
5.如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁
6.如果自旋成功则依然处于轻量级锁状态
7.如果自旋失败,则升级为重量级锁
6、自旋锁/适应性自旋锁
阻塞或者唤醒一个java线程需要操作系统切换CPU来完成,这种状态装换需要耗费处理器时间,如果同步代码块内容太简单执行速度很快,同步资源的锁定时间很短,切换CPU得不偿失。因而引入自旋锁,让当前线程“稍等一下”,让线程进行自旋。
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
自旋锁尽可能的减少线程的阻塞,适用于锁的竞争不激烈,且占用锁的时间非常短的代码块来说性能能大幅度提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗。
自适应意味着自旋的时间(次数)不固定,刚刚自旋获得过锁,虚拟机允许自旋等待持续相对更长的时间;如果自旋很少成功过,可能在尝试获取锁过程中忽略自旋,直接阻塞线程。
我们所熟知的Java锁机制无非就是Synchronized锁和Lock锁
Synchronized是基于JVM来保证数据同步的,而Lock则是在硬件层面,依赖特殊的CPU指令实现数据同步的。
Synchronized:它就是一个:非公平、悲观、独享、互斥、可重入的重量级锁。
ReentrantLock:它就是一个:默认非公平但也实现公平的、悲观、独享、互斥、可重入的重量级锁。
ReentrantReadWriteLock:它是一个:默认非公平但可实现公平的、悲观、写独享、读共享、读写可重入的重量级锁。
线程安全实现方式:
互斥同步(锁机制):
非阻塞同步(使用循环CAS实现原子操作):