Java中的各种锁


前言

在Java中,锁是用来控制多线程访问共享资源的一种机制。在Java中有着各种的锁机制用于支持多线程环境下的并发控制。这些锁机制设计的目的在于保证共享资源在多线程访问时的正确性和一致性。

一、乐观锁

在这里插入图片描述
乐观锁是一种乐观思想,假定当前环境是读多写少,遇到并发写的概率比较低,读数据时认为别的线程不会正在进行修改(所以没有上锁)。写数据时,判断当前值与期望值是否相同,如果相同则进行更新(更新期间加锁,保证是原子性的)。

Java中的乐观锁:CAS,比较并替换,比较当前值(主内存中的值),与预期值(当前线程中的值,主内存中值的一份拷贝)是否一样,一样则更新,否则继续进行CAS操作。
如上图所示,可以同时进行读操作,读的时候其他线程不能进行写操作。

二、悲观锁

在这里插入图片描述
悲观锁是一种悲观思想,即认为写多读少,遇到并发写的可能性高,每次读写数据的时候都认为其他线程会修改,所以每次读写数据时都会上锁。其他线程想要读写这个数据时,会被这个线程block,直到这个线程释放锁然后其他线程获取到锁。

Java中的悲观锁:synchronized修饰的方法和方法块、ReentrantLock。
如上图所示,只能有一个线程进行读操作或者写操作,其他线程的读写操作均不能进行。

三、偏向锁

在这里插入图片描述
偏向锁是JDK6时加入的一种锁优化机制: 在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了。偏是指偏心,它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如加锁、解锁及对Mark Word的更新操作等)。

优点: 把整个同步都消除掉,连CAS操作都不去做了,优于轻量级锁。
缺点: 如果程序中大多数的锁都总是被多个不同的线程访问,那偏向锁就是多余的。

在Java中使用synchronized实现线程之间的同步,以确保共享资源的一致性和完整性。
早期的JDK版本中,synchronized使用重量级锁,这会导致较大的性能开销,尤其是在没有多线程竞争的情况下。
为了提高性能,从JDK 1.6开始,HotSpot虚拟机引入了锁升级机制,即锁可以从无锁状态逐步升级为更重的锁形式。
锁升级的过程大致如下:
无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁

  • 偏向锁:当一个线程访问同步块并获取锁时,如果没有其他线程竞争,则使用偏向锁。偏向锁假设锁持有者不会很快改变,这样可以减少锁的获取和释放操作。
  • 轻量级锁:当存在多个线程竞争同一个锁时,偏向锁会升级为轻量级锁。轻量级锁使用CAS操作来尝试获取锁,如果获取失败,则线程会在一个短时间内自旋等待锁的释放。
  • 重量级锁:如果轻量级锁仍然不能解决锁的竞争问题,比如自旋时间过长或者竞争过于激烈,锁就会升级为重量级锁。重量级锁涉及到操作系统层面的互斥锁,可能会导致线程挂起和恢复,开销较大。

四、轻量级锁

在这里插入图片描述
轻量级锁是JDK6时加入的一种锁优化机制: 轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量。轻量级是相对于使用操作系统互斥量来实现的重量级锁而言的。轻量级锁在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互
斥量产生的性能消耗。如果出现两条以上的线程争用同一个锁的情况,那轻量级锁将不会有效,必须膨胀为重量级锁。

优点: 如果没有竞争,通过CAS操作成功避免了使用互斥量的开销。
缺点: 如果存在竞争,除了互斥量本身的开销外,还额外产生了CAS操作的开销,因此在有竞争的情况下,轻量级锁比传统的重量级锁更慢。

轻量级锁和自旋锁

在轻量级锁阶段,当线程尝试获取锁时,如果锁被其他线程持有时,当前线程不会直接放弃CPU时间片,而是尝试使用自旋的方式等待锁的释放。但这是轻量级锁的一部分行为,而不是单独的“自旋锁”。自旋的目的是为了在锁持有时间较短的情况下提高性能,减少不必要的线程挂起和恢复操作。然而,如果锁的竞争激烈或者锁持有的时间较长,自旋就会变成一种资源浪费,这时轻量级锁就会升级为重量级锁。

五、重量级锁

在这里插入图片描述
重量级锁是一种称谓:synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本身依赖底层的操作系统的 Mutex Lock来实现。操作系统实现线程的切换需要从用户态切换到核心态,成本非常高。这种依赖于操作系统 Mutex Lock来实现的锁称为重量级锁。为了优化synchonized,引入了轻量级锁,偏向锁。
Java中的重量级锁: synchronized

六、自旋锁

在这里插入图片描述
自旋锁是一种技术: 为了让线程等待,我们只须让线程执行一个忙循环(自旋)。
现在绝大多数的个人电脑和服务器都是多路(核)处理器系统,如果物理机器有一个以上的处理器或者处理器核心,能让两个或以上的线程同时并行执行,就可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。

自旋锁的优点: 避免了线程切换的开销。挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给Java虚拟机的并发性能带来了很大的压力。
自旋锁的缺点: 占用处理器的时间,如果占用的时间很长,会白白消耗处理器资源,而不会做任何有价值的工作,带来性能的浪费。因此自旋等待的时间必须有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起
线程。

自适应自旋: 自适应意味着自旋的时间不再是固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。有了自适应自旋,随着程序运行时间的增长及性能监控信息的不断完善,虚拟机对程序锁的状态预测就会越来越精准。
JDK1.6及1.6之后默认开启自旋锁,自旋次数默认值:10次,可以使用参数-XX:PreBlockSpin来自行更改。

七、可重入锁(递归锁)

在这里插入图片描述
可重入锁是一种技术: 任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞。
可重入锁的原理: 通过组合自定义同步器来实现锁的获取与释放。

  • 再次获取锁:识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。获取锁后,进行计数自增,
  • 释放锁:释放锁时,进行计数自减。

Java中的可重入锁: ReentrantLock、synchronized修饰的方法或代码段。
可重入锁的作用: 避免死锁。
面试题1: 可重入锁如果加了两把,但是只释放了一把会出现什么问题?
答:程序卡死,线程不能出来,也就是说我们申请了几把锁,就需要释放几把锁。
面试题2: 如果只加了一把锁,释放两次会出现什么问题?
答:会报错,java.lang.IllegalMonitorStateException。

八、读写锁

读写锁是一种技术: Java中通过ReentrantReadWriteLock类来实现。为了提高性能, Java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。
读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的。
读锁: 允许多个线程获取读锁,同时访问同一个资源。
在这里插入图片描述
写锁: 只允许一个线程获取写锁,不允许同时访问同一个资源。
在这里插入图片描述
如何使用:

/*** 创建一个读写锁
 * 它是一个读写融为一体的锁,在使用的时候,需要转换
*/
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

获取读锁和释放读锁

// 获取读锁
rwLock.readLock().lock();
// 释放读锁
rwLock.readLock().unlock();

获取写锁和释放写锁

// 创建一个写锁
rwLock.writeLock().lock();
// 写锁 释放
rwLock.writeLock().unlock();

九、共享锁

在这里插入图片描述
共享锁是一种思想: 可以有多个线程获取读锁,以共享的方式持有锁。共享锁允许多个事务或线程同时读取锁定的数据项而不相互阻塞。
Java中ReentrantReadWriteLock 类提供了类似的功能,但它是在编程语言层面实现的,并且用于控制对象上的并发访问。在 ReentrantReadWriteLock 中,读取锁类似于共享锁,而写入锁类似于排他锁。它们都是为了协调并发环境中的访问冲突而设计的工具。

十、公平锁

在这里插入图片描述
公平锁是一种思想: 多个线程按照申请锁的顺序来获取锁。在并发环境中,每个线程会先查看此锁维护的等待队列,如果当前等待队列为空,则占有锁,如果等待队列不为空,则加入到等待队列的末尾,按照FIFO的原则从队列中拿到线程,然后占有锁。

十一、非公平锁

在这里插入图片描述
非公平锁是一种思想: 线程尝试获取锁,如果获取不到,则再采用非公平锁的方式。
多个线程获取锁的顺序,不是按照先到先得的顺序,有可能后申请锁的线程比先申请的线程优先获取锁。

  • 优点: 非公平锁的性能高于公平锁。
  • 缺点: 有可能造成线程饥饿(某个线程很长一段时间获取不到锁)

Java中的非公平锁:synchronized是非公平锁,ReentrantLock通过构造函数指定
该锁是公平的还是非公平的,默认是非公平的。

十二、独占锁

在这里插入图片描述
独占锁是一种思想: 只能有一个线程获取锁,以独占的方式持有锁。和悲观锁、互斥锁同义。
Java中用到的独占锁: synchronized,ReentrantLock

十三、分段锁

在这里插入图片描述
分段锁是一种机制: 最好的例子来说明分段锁是ConcurrentHashMap。

ConcurrentHashMap原理:它内部细分了若干个小的 HashMap,称之为段(Segment)。 默认情况下一个 ConcurrentHashMap 被进一步细分为 16 个段,既就是锁的并发度。如果需要在 ConcurrentHashMap 添加一项key-value,并不是将整个 HashMap 加锁,而是首先根据 hashcode 得到该key-value应该存放在哪个段中,然后对该段加锁,并完成 put 操作。在多线程环境中,如果多个线程同时进行put操作,只要被加入的key-value不存放在同一个段中,则线程间可以做到真正的并行。

线程安全:ConcurrentHashMap 是一个 Segment 数组, Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全

十四、互斥锁

在这里插入图片描述
互斥锁与悲观锁、独占锁同义,表示某个资源只能被一个线程访问,其他线程不能访问。

  • 读-读互斥
  • 读-写互斥
  • 写-读互斥
  • 写-写互斥
    Java中的同步锁: synchronized

十五、同步锁

在这里插入图片描述
同步锁与互斥锁同义,表示并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。
Java中的同步锁: synchronized

十六、死锁

在这里插入图片描述
死锁是一种现象:如线程A持有资源x,线程B持有资源y,线程A等待线程B释放资源y,线程B等待线程A释放资源x,两个线程都不释放自己持有的资源,则两个线程都获取不到对方的资源,就会造成死锁。

Java中的死锁不能自行打破,所以线程死锁后,线程不能进行响应。所以一定要注意程序的并发场景,避免造成死锁。

十七、锁粗化

在这里插入图片描述
锁粗化是一种优化技术: 如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作都是出现在循环体之中,就算真的没有线程竞争,频繁地进行互斥同步操作将会导致不必要的性能损耗,所以就采取了一种方案:把加锁的范围扩展(粗化)到整个操作序列的外部,这样加锁解锁的频率就会大大降低,从而减少了性能损耗。

十八、锁消除

在这里插入图片描述
锁消除是一种优化技术: 就是把锁干掉。当Java虚拟机运行时发现有些共享数据不会被线程竞争时就可以进行锁消除。

那如何判断共享数据不会被线程竞争?
利用逃逸分析技术:分析对象的作用域,如果对象在A方法中定义后,被作为参数传递到B方法中,则称为方法逃逸;如果被其他线程访问,则称为线程逃逸。
在堆上的某个数据不会逃逸出去被其他线程访问到,就可以把它当作栈上数据对待,认为它是线程私有的,同步加锁就不需要了。

十九、synchronized

在这里插入图片描述
synchronized是Java中的关键字:用来修饰方法、对象实例。属于独占锁、悲观锁、可重入锁、非公平锁。

  1. 作用于实例方法时,锁住的是对象的实例(this);
  2. 当作用于静态方法时,锁住的是 Class类,相当于类的一个全局锁,会锁所有调用该方法的线程;
  3. synchronized 作用于一个非 NULL的对象实例时,锁住的是所有以该对象为锁的代码块。 它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

每个对象都有个 monitor 对象, 加锁就是在竞争 monitor 对象,代码块加锁是在代码块前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的。

二十、Lock和synchronized的区别

Lock: 是Java中的接口,可重入锁、悲观锁、独占锁、互斥锁、同步锁。

  1. Lock需要手动获取锁和释放锁。
  2. Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现。
  3. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。
  4. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用synchronized 时,等待的线程会一直等待下去,不能够响应中断。
  5. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
  6. Lock 可以通过实现读写锁提高多个线程进行读操作的效率。

synchronized的优势:

  1. 足够清晰简单,只需要基础的同步功能时,用synchronized。
  2. Lock应该确保在finally块中释放锁。如果使用synchronized,JVM确保即使出现异常,锁也能被自动释放。
  3. 使用Lock时,Java虚拟机很难得知哪些锁对象是由特定线程锁持有的。

二十一、ReentrantLock 和synchronized的区别

在这里插入图片描述
ReentrantLock是Java中的类 : 继承了Lock类,可重入锁、悲观锁、独占锁、互斥锁、同步锁。
相同点:

  1. 主要解决共享变量如何安全访问的问题
  2. 都是可重入锁,也叫做递归锁,同一线程可以多次获得同一个锁,
  3. 保证了线程安全的两大特性:可见性、原子性。
    不同点:
  4. ReentrantLock 就像手动汽车,需要显示的调用lock和unlock方法,synchronized 隐式获得释放锁。
  5. ReentrantLock 可响应中断, synchronized 是不可以响应中断的,ReentrantLock 为处理锁的不可用性提供了更高的灵活性
  6. ReentrantLock 是 API 级别的, synchronized 是 JVM 级别的
  7. ReentrantLock 可以实现公平锁、非公平锁,默认非公平锁,synchronized 是非公平锁,且不可更改。
  8. ReentrantLock 通过 Condition 可以绑定多个条件

二十二、分布式锁

分布式锁,即在分布式系统或集群模式中使用的锁。在单体应用中我们通过锁解决的是控制共享资源访问的问题,而分布式锁,就是解决了分布式系统中控制共享资源访问的问题。
在这里插入图片描述

分布式锁的几种实现方案

  1. 基于数据库实现(已淘汰)
  2. 基于Zookeeper
  3. 基于Redis setnx实现
  4. Redis框架 Redisson、RedisLock

详情可以看这篇文章:redis分布式锁


Java有多种,可以根据应用场景的不同选择不同的。 1. synchronized:synchronized是Java最基本的。它是一种互斥,同一时间只能有一个线程持有该,其他线程必须等待该线程释放才能获得。synchronized可以应用于方法或代码块。 2. ReentrantLock:ReentrantLock是Java的另一种互斥,与synchronized相比,它的灵活性更高,可以指定公平或非公平的获取方式。在高并发环境下,ReentrantLock的性能可能比synchronized更好。但是,它需要手动释放,否则可能会导致死。 3. ReadWriteLock:ReadWriteLock是Java的一种读写,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。在读多写少的场景下,使用ReadWriteLock可以提高程序的并发性能。 4. StampedLock:StampedLock是Java8新增的一种,它比ReadWriteLock更加灵活,支持乐观读取和写,可以大幅度提高程序的并发性能。但是,StampedLock的使用要求比较高,需要仔细考虑的使用场景。 5. CAS:CAS(Compare And Swap)是Java的一种无算法,它通过比较内存的值与期望值是否相等来判断是否发生了竞争。如果没有竞争,就将新值写入内存;如果有竞争,则重新尝试。CAS的性能非常高,但是它对程序员的要求也非常高,需要处理好竞争和冲突问题。 6. Semaphore:Semaphore是Java的一种信号量,它可以控制同时访问某个资源的线程数量。Semaphore可以应用于控制并发访问的线程数量、线程池的大小等场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值