半小时搞定各种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也能返回正常的结果。
可以使用AtomicStampedReference
和AtomicMarkableReference
解决
悲观锁
悲观锁认为所有的资源都是不安全的,随时会被其他线程操作、更改。所以操作资源前一定要加一把锁、防止其他线程访问。
有两大类
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做分布式锁的优势
- Redis是内存数据库:Redis将数据存储在内存中,读写速度非常快,远远高于磁盘操作。这使得基于Redis实现的分布式锁能够在高并发场景下快速响应。
- Redis支持原子操作:Redis提供了一系列原子操作,如SETNX(设置键值,仅在键不存在时设置成功)和EXPIRE(设置键的过期时间),这些操作可以在一个原子性的命令中完成。这使得获取锁和释放锁的操作能够在一个命令中完成,避免了多个网络往返的开销。
- Redis支持分布式:Redis可以作为分布式缓存使用,它提供了多节点部署和数据复制的功能,可以实现高可用性和负载均衡。这使得基于Redis实现的分布式锁可以在集群环境下使用,并提供高可用性和可靠性。
- Redis支持阻塞操作:Redis提供了阻塞操作(如BLPOP和BRPOP),可以在没有数据时自动阻塞等待,避免了轮询的开销。这使得基于Redis实现的分布式锁能够更高效地实现锁的等待和释放。
Valotile
valotile跟synchronized一样,是Java内置的关键字。不过valotile只能修饰变量。valotile主要的作用是保证变量在内存中的可见性、有序性 有序性禁止了指令重排的优化