半小时了解搞定java的各种锁

半小时搞定各种java的各种锁

锁是为了解决线程安全问题

涉及到 可见性 原子性 有序性

可见性 :一个线程修改共享变量后,其他线程可以立刻看到,这就是可见性。

在多核CPU中,每个CPU都有自己的缓存。对于同一块内存地址,每个CPU都有自己单独的备份,这样就产生了数据一致性问题. 也就是说线程A修改了共享变量x的值后,线程B不能马上知道.

原子性:一个或多个操作在CPU执行的过程中不被中断,这是原子性.

i++的步骤是 1. 内存加载到计算机寄存器中 2.寄存器中自增 3.写回到内存

如果没有加锁 ,两个线程同时++的结果 如果i初始值为0 那么结果 i<=2

有序性 :程序按照代码先后顺序执行,这是有序性

锁的分类

从功能层面来说:可以总的分为读写两种锁 也就是共享锁和排他锁

读锁 可以多个线程抢占锁

写锁 只允许一个线程抢占锁

加锁会使得并行变串行,影响性能,有些时候为了安全,又必须加锁,所以又出来一个划分的角度 锁的粒度。控制锁的粒度

无锁编程(乐观锁)

基于 CAS ->原子性/可见性

认为所有的资源都是安全的,每个线程对资源的操作都是符合预期的,所以它不需要对资源加锁。

只是在对象头改变了标记位,没有通过内核给对象上锁,对比较的操作要进行上锁 保证原子性

会存在ABA的问题 (一个线程先读取共享内存数据值A,随后因某种原因,线程暂时挂起,同时另一个线程临时将共享内存数据值先改为B,随后又改回为A。随后挂起线程恢复,并通过CAS比较,最终比较结果将会无变化。这样会通过检查,这就是ABA问题。)

对值类型的问题不大,对引用类型影响很大

可以使用版本号的一个方式解决

在原有类的基础上,除了比较与修改期待的值外,增加了一个时间戳。对时间戳也进行CAS操作。这也称为双重CAS。从上例中看到。每次修改一个结点,其时间戳都发生变化。这样即使共享一个复用结点,最终CAS也能返回正常的结果。

可以使用AtomicStampedReferenceAtomicMarkableReference解决

悲观锁

悲观锁认为所有的资源都是不安全的,随时会被其他线程操作、更改。所以操作资源前一定要加一把锁、防止其他线程访问。

有两大类

synchronized关键字

基于Java同步器AQS的各种实现

  • ReentrantLock(可重入锁,AQS体系下用户使用的最多的一个锁)
  • ReentrantReadWriteLock(基于ReentrantLock的读写锁,读锁之间共享资源、读写、写写之间互斥资源,读写锁相较于普通的互斥锁并发能力要稍微好些,但使用起来需要考虑锁的切入点)
  • StampedLock(基于读写锁优化,对读锁更加细化了一层,但同时使用也更加复杂,用的不多)
  • Semaphore(信号量,可用于限流)
  • CountDownLatch(可用于计数,一般用于在多线程环境下需要执行固定次数逻辑的地方)。
按线程竞争的严重性分:可以分为偏向锁/轻量级锁/重量级锁

锁的实现是通过操作系统的Mutex机制来实现的

1 涉及到用户态和内核态的切换(上下文的保存)

2涉及到线程的阻塞唤醒

3 并行到串行的改变

轻量级锁(自旋锁):

加锁之前进行重试

锁的实现是给一个占位符一样的东西 例如0,1让对方知道我当前正在占用

1和3为了线程的安全改变不了 但是我们可以减少2

当一个线程占用到锁之后 另一个线程先不阻塞 ,而是通过一定的尝试 在一定的时间内 去重复多次的尝试去获得锁。

偏向锁

加锁的代码 可能并不存在竞争,当线程1进入到锁里面 ,如果当前不存在竞争,那么 我们可以让锁偏向线程1,使得线程1下次进入的时候,不在需要竞争。

重量级锁

当自旋锁尝试一定次数后仍然无法获取锁,锁将升级为重量级锁。重量级锁会将请求锁的线程阻塞,并且使用操作系统提供的底层互斥量来实现锁的同步。

编译器层面的优化

锁消除和锁膨胀

锁膨胀是指 synchronized 从无锁升级到偏向锁,再到轻量级锁,最后到重量级锁的过程,它叫锁膨胀也叫锁升级。

锁消除指的是在某些情况下,JVM 虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而到底提高程序性能的目的。

锁消除的依据是逃逸分析的数据支持,如 StringBuffer 的 append() 方法,或 Vector 的 add() 方法,在很多情况下是可以进行锁消除的,比如以下这段代码:线程安全的加锁的 StringBuffer 对象,在生成字节码之后就被替换成了不加锁不安全的 StringBuilder 对象了,原因是 StringBuffer 的变量属于一个局部变量,并且不会从该方法中逃逸出去,所以此时我们就可以使用锁消除(不加锁)来加速程序的运行。

jvm编译的时候帮你做的事情

锁粗化

如果检测到同一个对象执行了连续的加锁和解锁的操作,则会将这一系列操作合并成一个更大的锁,从而提升程序的执行效率。

自适应自旋锁

自适应自旋锁是指,线程自旋的次数不再是固定的值,而是一个动态改变的值,这个值会根据前一次自旋获取锁的状态来决定此次自旋的次数。比如上一次通过自旋成功获取到了锁,那么这次通过自旋也有可能会获取到锁,所以这次自旋的次数就会增多一些,而如果上一次通过自旋没有成功获取到锁,那么这次自旋可能也获取不到锁,所以为了避免资源的浪费,就会少循环或者不循环,以提高程序的执行效率。简单来说,如果线程自旋成功了,则下次自旋的次数会增多,如果失败,下次自旋的次数会减少。

读写锁

在不使用读写锁的时候,一般情况下我们需要使用synchronized搭配等待通知机制完成并发控制(写操作开始的时候,所有晚于写操作的读操作都会进入等待状态),只有写操作完成并通知后才会将等待的线程唤醒继续执行。

如果改用读写锁实现,只需要在读操作的时候获取读锁,写操作的时候获取写锁。当写锁被获取到的时候,后续操作(读写)都会被阻塞,只有在写锁释放之后才会执行后续操作。并发包中对ReadWriteLock接口的实现类是ReentrantReadWriteLock,这个实现类具有下面三个特点

①具有与ReentrantLock类似的公平锁和非公平锁的实现:默认的支持非公平锁,对于二者而言,非公平锁的吞吐量由于公平锁;

②支持重入:读线程获取读锁之后能够再次获取读锁,写线程获取写锁之后能再次获取写锁,也可以获取读锁。

③锁能降级:遵循获取写锁、获取读锁在释放写锁的顺序,即写锁能够降级为读锁

锁的特性 ,
可重入锁

一个线程抢占到锁,在释放锁之前,再次去竞争同一把锁,这个时候不需要进行阻塞等待,直接进入重试次数。防止死锁。

公平锁和非公平锁

公平锁:每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的,最前面的线程总是最先获取到锁。

非公平锁:每个线程获取锁的顺序是随机的,并不会遵循先来先得的规则,所有线程会竞争获取锁。举个例子,公平锁就像开车经过收费站一样,所有的车都会排队等待通过,先来的车先通过。

公平锁的优点是按序平均分配锁资源,不会出现线程饿死的情况,它的缺点是按序唤醒线程的开销大,执行性能不高。非公平锁的优点是执行效率高,谁先获取到锁,锁就属于谁,不会“按资排辈”以及顺序唤醒,但缺点是资源分配随机性强,可能会出现线程饿死的情况。

分布式锁

上面的锁都是保证了进程内 线程的安全性,那如果是分布式系统,我们就需要去保证多个进程的安全性了。

实现方式

基于MySql数据库的主键约束

可以使用数据库中的唯一索引或者悲观锁来实现分布式锁。当需要获取锁时,在数据库中插入一条唯一索引的记录,如果插入成功,则获取锁;否则,说明锁已经被占用,等待一段时间后重试或者抛出异常。缺点是效率较低,数据库成为了性能瓶颈。

基于Redis的setNX 或redisson

Redis是一款高性能、内存存储的NoSQL数据库,具有快速、可扩展、高可用性等特点,常用于缓存、消息队列、分布式锁等场景。可以使用Redis的setnx命令实现分布式锁。当需要获取锁时,使用setnx命令将一个唯一的随机字符串设置为键,设置成功则获取锁,否则说明锁已经被占用,等待一段时间后重试或者抛出异常。获取锁后,需要设置锁的过期时间,防止因为某些异常情况导致死锁。

基于Zookeeper实现分布式锁

Zookeeper是一款分布式协调服务框架,提供了分布式锁的实现方式。可以使用Zookeeper的临时节点来实现分布式锁。当需要获取锁时,在Zookeeper上创建一个临时节点,创建成功则获取锁,否则说明锁已经被占用,等待一段时间后重试或者抛出异常。获取锁后,当客户端断开连接时,临时节点会自动删除,释放锁。也可以在ZooKeeper上创建一个临时有序节点,通过判断自己创建的节点是否是最小的节点来判断是否获取到锁。

基于spring cloud

Spring Cloud提供了基于Redis和Zookeeper的分布式锁实现方式。使用方式类似于Redis和Zookeeper的实现方式,只是使用了Spring Cloud提供的封装和管理。

总的来说,基于Redis和Zookeeper的实现方式较为常用,其中Redis的实现方式性能较好,使用较为广泛。当选择实现分布式锁时,需要根据实际业务场景和需求,选择适合的实现方式,并考虑异常情况的处理和锁的释放方式,以保证分布式锁的正确性和性能。

基于消息中间件实现

可以使用消息中间件(如RabbitMQ、Kafka)来实现分布式锁。通过发送一条特定的消息到消息队列,只有一个客户端能够成功消费到消息,其他客户端无法消费到消息从而实现锁的竞争。

redis做分布式锁的优势

  1. Redis是内存数据库:Redis将数据存储在内存中,读写速度非常快,远远高于磁盘操作。这使得基于Redis实现的分布式锁能够在高并发场景下快速响应。
  2. Redis支持原子操作:Redis提供了一系列原子操作,如SETNX(设置键值,仅在键不存在时设置成功)和EXPIRE(设置键的过期时间),这些操作可以在一个原子性的命令中完成。这使得获取锁和释放锁的操作能够在一个命令中完成,避免了多个网络往返的开销。
  3. Redis支持分布式:Redis可以作为分布式缓存使用,它提供了多节点部署和数据复制的功能,可以实现高可用性和负载均衡。这使得基于Redis实现的分布式锁可以在集群环境下使用,并提供高可用性和可靠性。
  4. Redis支持阻塞操作:Redis提供了阻塞操作(如BLPOP和BRPOP),可以在没有数据时自动阻塞等待,避免了轮询的开销。这使得基于Redis实现的分布式锁能够更高效地实现锁的等待和释放。

Valotile

valotile跟synchronized一样,是Java内置的关键字。不过valotile只能修饰变量。valotile主要的作用是保证变量在内存中的可见性有序性 有序性禁止了指令重排的优化

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值