一、乐观锁 与 悲观锁
对于线程是否需要锁住共享的资源,我们可以将其分为乐观锁与悲观锁,前者不会锁住共享资源后者会将共享资源进行锁住。
1. 乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新时会判断此期间数据是否被更新。
采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
Java 中的乐观锁基本通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
2. 悲观锁
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。
Java 中的悲观锁就是 Synchronized,AQS 框架下的锁则是先尝试 CAS 乐观锁去获取锁,获取不到,
才会转换为悲观锁,如 RetreenLock。
3. 两个锁适用的场景
-
悲观锁适合写多的操作,先加锁可以确保数据写操作时的准确性
-
乐观锁适合读多的操作,不加锁的特点能够使读操作的性能得到提升
二、自旋锁
对于锁住同步资源失败,线程是否要阻塞,我们可以将锁分为自旋锁与非自旋锁。
1. 原理
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需自旋,等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
线程自旋需消耗 CPU 的,如果一直获取不到锁,则线程长时间占用 CPU 自旋,需要设定一个自旋等待最大事件在最大等待时间内仍未获得锁就会停止自旋进入阻塞状态。
2. 自旋锁优缺点
优点
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗(这些操作会导致线程发生两次上下文切换)
缺点
锁竞争激烈或者持有锁的线程需要长时间占用锁执行同步块,不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 CPU 做无用功,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 CPU 的线程又不能获取到 CPU,造成 CPU 的浪费。
3.自旋锁时间阈值
JDK 1.6 引入了适应性自旋锁
自旋锁的目的是为了占着 CPU 的资源不释放,等到获取到锁立即进行处理。
自旋执行时间太长,会有大量的线程处于自旋状态占用 CPU 资源,进而会影响整体系统的性能。
JVM 对于自旋周期的选择,JDK1.5 这个限度是一定的写死的。
在 1.6 引入了适应性自旋锁,自旋的时间不固定,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间。
4. 自旋锁的开启
JDK1.6 中 -XX:+UseSpinning
开。
XX:PreBlockSpin=10
为自旋次数。
JDK1.7
后,去掉此参数,由 jvm 控制。
三、公平锁 与 非公平锁
对于多线程竞争是否排队,我们可以将锁分成公平锁和非公平锁。
1. 公平锁
公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁。加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得。
缺点:
整体的吞吐率相对非公平锁要低,等待队列中除第一个线程以为的其他所有线程都要阻塞,CPU唤醒阻塞线程的开销比较大。
2. 非公平锁
非公平锁是多个线程加锁时,直接获取锁,获取不到才会进入等待队列的队尾等待。如果锁刚好可用,那么这个线程就可以无需阻塞直接获取锁。所以非公平锁可能会出现后申请锁的线程先获取锁的场景。
优点: 可以减少CPU唤醒线程的开销,整体的吞吐率高,因为线程有机率不阻塞直接获取锁。
缺点: 处于等待队列的线程容易出现饿死,或者等很久才会获取锁。
Java中 ReentrantLock 默认的 Lock() 方法,和 synchronized 都是非公平锁。
四、独占锁 与 共享锁
Java 并发包 (JUC) 提供的加锁模式分为独占锁和共享锁,他们的区别是多线程是否可以共享一把锁,前者不可以后者可以。
1. 独占锁
独占锁模式下,每次只能有一个线程能持有锁,独占锁又叫排它锁
独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。
ReentrantLock
就是以独占方式实现的互斥锁。
2. 共享锁
共享锁则允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock
。
共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
Java 的并发包中提供了 ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。
五、可重入锁 (递归锁)
同一线程多个流程能否重复获取同一把锁,可以将锁分为可重入锁与非可重入锁。
可重入锁指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
在 Java 中 ReentrantLock 和 synchronized 都是可重入锁。
可重入锁的优点:可一定程度避免死锁。
六、读写锁
为了提高性能,Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制。
如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。
读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥由 JVM 控制的,程序员只需要上好相应的锁即可。
要求:
- 代码只读数据,可以很多人同时读,但不能同时写,可上读锁。
- 代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。
Java 中 读 写 锁 有 个 接 口 java.util.concurrent.locks.ReadWriteLock , 也有具体的实现ReentrantReadWriteLock
七、Synchronized 同步锁
Synchronized 关键字,用于解决多个线程间访问资源同步性问题,保证其修饰的方法或代码块任意时刻只能有一个线程访问 synchronized 它可以把任非 NULL 的对象当作锁。他属于独占式悲观锁,同时属于可重入锁。
1. Synchronized 作用范围
作用实例方法时。锁住的是对象的实例(this)
作用静态方法时,锁住的是该类,该 Class所有实例,又因为 Class 的相关数据存储在永久带PermGen( JDK 1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程。
线程A调用一个实例对象非静态 Synchronized 方法,允许线程 B 调用该实例对象所属类的静态 s 方法而不会发生互斥,前者锁的是当前实例对象,后者锁的是当前类。
作用于同步代码块 锁住的当前对象,进入同步代码块前需要获得对象的锁
2. Synchronized 实现
Synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。
Java1.6,synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理了优化。引入了偏向锁和轻量级锁,都是在对象头中有标记位,不需要经过操作系统加锁。
3. JDK1.6 后的优化
synchronized
是根据 JVM 实现的,该关键字的优化也是在 JVM 层面实现,而未直接暴露。
JDK1.6后对锁做了大量优化如偏向锁,轻量锁,自旋锁,自适应锁等等。
锁主要有四种状态:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态,他们会随着锁竞争的激烈而逐渐升级且这种升级不可降,利用该策略提高获得锁和释放锁的效率。
八、ReentrantLock
ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完成synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
1. Lock接口主要方法
void lock()
: 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁。lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行。
boolean tryLock()
:如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false。tryLock() 只是"试图"获取锁, 如果锁不可用, 不会导致当前线程阻塞挂起,当前线程仍然继续往下执行代码。
void unlock()
: 解锁isLock()
:此锁是否有任意线程占用
2. tryLock 和 lock 和 lockInterruptibly
tryLock 能获得锁就返回 true,不能就立即返回 false。
tryLock(long timeout,TimeUnitunit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false
lock 能获得锁就返回 true,不能的话一直等待获得锁。
lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两线程,但此时中断这两个线程,lock 不会抛出异常,而 lockInterruptibly 会抛出异常。
3. ReentrantLock 与 synchronized
两者均为可重入锁
Synchronized依赖 JVM 而 Reentrantlock 依赖于API (lock(),trylock() 配合 try/finally 语句块来实现)
ReentrantLock 通过方法 lock() 与 unlock()来进行加锁与解锁操作, synchronized 会被 JVM 自动解锁
ReentrantLock 加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作。
ReentrantLock 相比 synchronized 的优势是可中断、公平锁、可选择通知,多个锁,这种情况下需ReentrantLock。
九、Synchronized 锁状态
1. 无锁
无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一资源,但同时只能有一个线程修改成功。
无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环。如果多个线程修改同一个值,必定会有一个线程修改成功,而其他修改失败的线程会不断的重试直到修改成功。
无锁无法全面代替有锁,但是无锁在某些场合下的性能是非常高的。
2. 偏向锁
-
偏向锁是指一段同步代码块一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
-
翩向锁是在只有一个线程执行同步代码块是进一个提高性能。
-
偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入得开销,看起来让这个线程得到了偏袒。
3. 轻量级锁
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁 (锁的升级是单向的,只能从低到高升级,不会出现锁的降级)
轻量级锁的意义:再没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。
轻量级锁的使用前提:再线程交替执行同步块的情况,如果存在同一个时间访问同一个锁的情况,就会导致轻量级锁膨胀为重量级锁。
4. 重量级锁
锁的升级过程:无锁——>偏向锁——>轻量级锁——>重量级锁
锁的升级是单向的, 也就是说只能从低到高升级,不会出现锁的降级。
-
偏向锁通过比Mark Word解决加锁问题,避免执行CAS操作。
-
轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。
-
重量级锁是将除了拥有锁的线程以外的线程都阻塞。
十、同步锁与死锁
1. 同步锁
当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行的多个线程。
在同一时间内只允许一个线程访问共享数据。 Java 中可以使用 synchronized 关键字来取得一个对象的同步锁。
2. 死锁
就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。
十一、锁优化思路
1. 减少锁持有时间
只用在有线程安全要求的程序上加锁
2. 减小锁粒度
将大对象(这个对象可能会被很多线程访问),拆成小对象,增加并行度,降低锁竞争,降低了锁的竞争,偏向锁,轻量级锁成功率才会提高,最最典型的减小锁粒度的案例就 ConcurrentHashMap。
3. 锁分离
最常见的锁分离就是读写锁 ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能,读写分离思想可以延伸,只要操作互不影响,锁就可以分离,比如 LinkedBlockingQueue 从头部取出,从尾部放数据。
4. 锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短(使用完公共资源后,应该立即释放锁)。文章来源:https://www.toymoban.com/news/detail-438983.html
如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化所以可以锁粗化使得占有锁的时间加长。文章来源地址https://www.toymoban.com/news/detail-438983.html
到了这里,关于Java 中的各种锁的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!