乐观锁:在乐观锁中会假设共享资源并没有加锁,对其进行访问时,直接访问,如果有其他线程对其访问,则会放弃此次访问,等待一定再次访问(乐观锁虽然没有加锁,但是可以识别出数据冲突,下面讲)。一般情况下,每次访问时间间隔会加大,适用于并发冲突较小或不发生的环境中。
悲观锁:悲观锁中访问共享资源,每次都会加锁,其他线程访问时,会进行堵塞挂起。适合用于并发冲突大的环境中。
两者并没有谁优谁劣,在不同情境下有不同优势,如图:
如果老师不忙,显然乐观锁效率高;
如果老师忙,悲观锁效率高;
而synchronized初始使用的就是乐观锁,当锁竞争比较激烈时,就会转换为悲观锁。
乐观锁并没有直接加锁,而是引入了一个版本号或者时间戳的东西,用来检测数据冲突。当一个线程访问共享数据后,会生成一个版本号的东西,如果期间又其他线程对其访问,版本号会发生改变,则第一个线程再运行时,版本号会对不上,那么,第一个线程会通过重新读取数据、合并更改或者放弃修改来解决。
乐观锁执行顺序:
- 读取数据:线程或进程读取要修改的数据,生成版本号或时间戳;
- 修改数据:对数据进行修改;
- 检查冲突:在修改完成后,乐观地认为没有并发冲突,然后尝试提交数据修改。在这个阶段,系统会比较当前数据的版本号或时间戳与线程最初读取的版本号或时间戳是否一致。
- 处理冲突:如果在提交时发现其他线程已经修改了数据(版本号或时间戳不一致),则需要处理冲突。通常,这可以通过重新读取数据、合并更改或者放弃修改来解决。
多线程之间,读(访问)数据与读数据之间并不会有线程安全,但是读数据与写(修改)数据之间却会,我们如果把两者放入一个锁中,对性能便会造成损耗,这个时候读写锁便孕育而生了。
读写锁(readers-writer lock),在执行加锁操作时会额外表名读写意图,那么读与读之间便不会互斥,写与任何操作都互斥。适用于对数据频繁的读而不是改的情景中。
读写锁就是把读操作和写操作区分对待. Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写
锁.
ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行
加锁解锁.
ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进
行加锁解锁.
而synchronized不是读写锁。
加锁的核心就时“原子性”,这样的机制其实时CPU这样的设备提供的。
轻量级锁如其名是一种轻量级的锁,是减小锁开销的一种机制,加锁机制一般都在代码层面完成,尽量不使用mutex。当一个线程在访问共享资源的时候,可以更容易的获得锁,不需要等待堵塞。
轻量级锁主要包括两种状态:
- 自旋锁:当多个线程竞争同一把锁时,不会立即堵塞,而会通过在一定次数的自旋中尝试获得锁,减少线程转换的成本。减少线程调度。
- 偏向锁:偏向锁是一种锁的优化机制,在单线程的场景下,会为偏向锁线程的打上一个偏向锁的标记,当它进入同步块时,不需要竞争直接获得锁,从而减少获得锁的开销。如果后面有其他线程进入争锁,那么将会解除偏向锁,转换为轻量级锁或者重量级锁,会有一定开销。所以在锁竞争激烈的环境中,偏向锁会没有优势。
重量级锁用到了OS提供的mutex,里面涉及了大量的内核态用户态切换,很容易引起线程的调度。在多个线程竞争同一把锁时,会造成线程堵塞。
两者也不分谁优谁劣,轻量级锁适用于多线程竞争不激烈的情况,可以减小锁的开销,而重量级锁适用于需要确保强一致性的高并发场景,但性能开销较大。不同的编程语言和运行时环境可能会采用不同的锁机制,以满足不同应用场景的需求。
公平锁就是当多个线程访问共同资源时,当锁释放后,会按照先后等待的先后顺序获得锁,先来后到,锁里需要含特定的等待队列顺序,所以需要更大的开销。
不公平锁就是,当锁释放后,并不知道哪个线程会获得锁,是随机的。
两者并没有优劣之分。
synchronized就是一种非公平锁。
可重入锁就是在一个锁中,可以再进行一次加锁,不会发生死锁,这时只有两个锁都释放,其他线程才能获得此锁。
不可重入锁,就是当一个锁中再加一个锁就会发生死锁,线程无休止的堵塞。
synchronized就是一个可重入锁。
死锁是在多线程或多进程并发程序中的一种常见问题,它发生在两个或多个线程(或进程)相互等待对方释放所需资源的情况下,导致它们都无法继续执行,从而陷入无限等待的状态。
死锁产生的四个必要条件:
- 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用;
- 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放;
- 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有;
- 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
- 刚开始是乐观锁,锁竞争激烈化为悲观锁;
- 刚开始是轻量级锁,如果持有锁时间较长,转化为重量级锁;
- 实现轻量级锁的时候大概率用到的自旋锁策略
- 非公平锁
- 可重入锁
- 不是读写锁
无锁——偏向锁——自旋锁——重量级锁
锁消除:编译器+JVM会自动判断是否消除锁。
即代码一直处于单线程。
锁粗化:如果一段代码中出现多次加锁,那么编译器+JVM会自动判断是否可以将它合并为一把锁。