共享锁
共享锁有CountDownLatch, CyclicBarrier, Semaphore, ReentrantReadWriteLock等
ReadWriteLock,顾名思义,是读写锁。它维护了一对相关的锁 — — “读取锁”和“写入锁”,一个用于读取操作,另一个用于写入操作。
“读取锁”用于只读操作,它是“共享锁”,能同时被多个线程获取。
“写入锁”用于写入操作,它是“独占锁”,写入锁只能被一个线程锁获取。
注意:不能同时存在读取锁和写入锁!
独占锁/排他锁
java中的Synchronied和ReentrantLook 都是独占锁
悲观锁
顾名思义,是悲观的,觉得不锁柱的资源会被别人的线程抢走,所以悲观锁每次获取和修改数据都会锁定数据。
典型的悲观锁案例:synchronized关键词和Lock接口。
乐观锁
认为自己在操作资源时不会有其他线程干扰,所以不会锁定对象,只是在更新资源时会去对比一下我修改过的数据之间是否有其他线程修改过的数据。若无修改,此次修改正常,若有其他线程修改,则放弃此次修改,并选择报错或重试。这是一个基于冲突检测的并发策略,这种并发策略的实现不需要线程挂起,因此是非阻塞同步。乐观主义锁一般采用CAS算法实现。
常见的乐观锁实现方式有两种,分别是:1、版本号机制;2、CAS算法。
版本号机制:当然线程A需要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
典型的乐观锁案例:
1、Java并发包Atomic原子类,
2、synchronized锁升级,
3、JDK1.8中的ConcurrentHashMap实现,完全重构了JDK1.7,不再使用分段锁,而是给数组中的每个头节点都加锁,并且用的是synchronized。整体采用CAS+synchronized来保证并发的安全性,
ConcurrentHashMap的put操作:
判断表是否为空,如果为空就初始化表initTable(),只有一个线程可以初始化成功。
如果已经初始化,则找到当前key所在桶是判断是否为空,若为空则通过CAS把新节点插入此位置casTabAt(),只有一个线程可以CAS成功
如果key所在桶不为空,则判断节点的hash值是否为-1,若为-1则说明当前数组正在扩容。
如果如果key所在桶不为空,且没在扩容,则给桶中的第一个节点对象加锁synchronized,然后判断是否是链表或者树,然后插入数据。
判断链表长度是否大于8,如果是链表转为红黑树。
自旋锁的定义
自旋锁指的是当线程获取不到资源时,不是进入阻塞状态,而是让当前的线程不停地执行空循环,直到循环条件被其他线程改变,进入临界区。
自旋锁:竞争锁的失败的线程,并不会真实的在操作系统层面挂起等待,而是JVM会让线程做 几个空循环(基于预测在不久的将来就能获得),在经过若干次循环后,如果可以获得锁,那么进入临界区,如果还不能获得锁,才会真实的将线程在操作系统层面进行挂起。 适用场景:自旋锁可以减少线程的阻塞,这对于锁竞争不激烈,且占用锁时间非常短的代码块 来说,有较大的性能提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗。 如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,就不适合使用自旋锁 了,因为自旋锁在获取锁前一直都是占用cpu做无用功,线程自旋的消耗大于线程阻塞挂起操作的消耗,造成cpu的浪费。
什么是自旋
很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
公平锁和非公平锁:
CPU在调度线程的时候是在等待队列里随机挑选一个线程,由于这种随机性所以是无法保证线程先到先得的(synchronized控制的锁就是这种非公平锁)。但这样就会产生饥饿现象,即有些线程(优先级较低的线程)可能永远也无法获取CPU的执行权,优先级高的线程会不断的强制它的资源。那么如何解决饥饿问题呢,这就需要公平锁了。公平锁可以保证线程按照时间的先后顺序执行,避免饥饿现象的产生。但公平锁的效率比较低,因为要实现顺序执行,需要维护一个有序队列。
synchronized控制的锁就是这种非公平锁;
ReentrantLock便是一种公平锁,通过在构造方法中传入true就是公平锁,传入false,就是非公平锁;
同个线程可以进入之前获得锁的同步代码块,这是可重入锁的核心思想了。
可重入锁:
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁;
其他博客总结:
1、分门别类总结Java中的各种锁,让你彻底记住:
分门别类总结Java中的各种锁,让你彻底记住_Rain仰望的博客-CSDN博客
2、java 中的锁 -- 偏向锁、轻量级锁、自旋锁、重量级锁:
java 中的锁 -- 偏向锁、轻量级锁、自旋锁、重量级锁_朱清震的博客-CSDN博客_java 属性锁
3、下面是可重入和不可重入锁的示例:
因为不可重锁没有内置对象,所以需要我们自己去实现一个不可重入锁。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTest {
/**
* 不可重入锁
* 执行结果:方法1运行
*/
// MyLock lock = new MyLock();
/**
* 可重入锁:同个线程可以进入之前获得锁的同步代码块,这是可重入锁的核心思想
* 执行结果:方法1运行 方法2运行
*/
Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
new ReentrantLockTest().method1();
}
public void method1() throws InterruptedException {
lock.lock();
System.out.println("方法1运行");
method2();
lock.unlock();
}
public void method2() throws InterruptedException {
lock.lock();
System.out.println("方法2执行");
lock.unlock();
}
}
class MyLock{
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
}
public synchronized void unlock() {
isLocked = false;
notify();
}
}