Java中的锁及其工作原理
Java中锁的类型及基本概念
- 乐观锁:假定没有冲突,在修改数据如果发现数据和之前获取的不一致,则读取最新的数据,再次尝试修改。
- 悲观锁:假定会发生冲突,同步所有的数据的相关操作,从读数据就开始上锁。
- 独占锁:给资源加上写锁,当前线程可以修改资源,其它线程不能再加锁(单写)
- 共享锁:给资源加上读锁后只能读不能改,不能加写锁(多读)
- 可重入锁:同时加两次锁,不会出现死锁(再次拿到锁)
- 不可重入锁:同时加两次锁,会出现死锁(阻塞)
- 公平锁:抢到锁的顺序和抢锁顺序相同则为公平锁
- 非公平锁:抢到锁的顺序和抢锁顺序无关。
拓展:Linux中的锁类型
- 互斥锁:如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。
- 条件锁:互斥锁是用来解决互斥问题,而条件锁用来解决等待问题。
- 读写锁:在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。
- 自旋锁:只要没有锁上,就不断重试。
乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
悲观锁
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。
独占锁
独占锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排他锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和 JUC中Lock的实现类就是互斥锁。
共享锁
共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。 独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
可重入锁
所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。synchronized 和 ReentrantLock 都是可重入锁。可重入锁的意义在于防止死锁。
不可重入锁
所谓不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。
公平锁
加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得。
换言之,每个线程抢占锁的顺序为先后调用lock方法的顺序依次获取锁,类似于排队吃饭。
非公平锁
加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待。
换言之,每个线程抢占锁的顺序不定,谁运气好,谁就获取到锁,和调用lock方法的先后顺序无关,类似于堵车时,加塞的那些XXXX。
Java中的ReentrantLock 默认的lock()方法采用的是非公平锁。也就是不用考虑其他在排队的线程的感受,lock()的时候直接询问是否可以获取锁,而不用在队尾排队。
读写锁-ReadWriteLock
背景:现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。
针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁。
与传统锁不同的是读写锁的规则是可以共享读,但只能一个写,总结起来为: 读读不互斥,读写互斥,写写互斥 ,而一般的独占锁是: 读读互斥,读写互斥,写写互斥 ,而场景中往往 读远远大于写 ,读写锁就是为了这种优化而创建出来的一种机制。
读写锁的三个重要特征
在Java中 ReadWriteLock
的主要实现为 ReentrantReadWriteLock
,其提供了以下特性:
- 公平性选择:支持公平与非公平(默认)的锁获取方式,吞吐量非公平优先于公平。
- 可重入:读线程获取读锁之后可以再次获取读锁,写线程获取写锁之后可以再次获取写锁
- 可降级:写线程获取写锁之后,其还可以再次获取读锁,然后释放掉写锁,那么此时该线程是读锁状态,也就是降级操作。
ReentrantReadWriteLock的结构
可重入读写锁ReentrantReadWriteLock
的核心是由一个基于AQS的同步器 Sync
构成,然后由其扩展出 ReadLock
(共享锁), WriteLock
(排它锁)所组成。
-
线程进入读锁的前提条件:
没有其他线程的写锁。
没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。 -
线程进入写锁的前提条件:
没有其他线程的读锁。
没有其他线程的写锁。