一文理解Java中的所有锁

并发基础

这些东西是之前自己做笔记写的,有写的不好的地方,包容下。

1.1 线程安全问题

int i = 0;
i++; //两个线程都执行50w 次

i++ 看似就一行代码,但它并不是原子操作;

  1. 首先CPU 从内存中读出 i=0,放到自己的寄存器中;
  2. CPU 控制器执行 +1 操作,
  3. 将 1 写回 i 的内存中
    两个线程同时执行的时候可能就会产生问题,
    i 初始为 0
    线程1 执行到第二步,i=1,线程2 也开始执行 拿到 i =0;
    接着线程1 执行第三步,i=1; 线程2 也执行 2、3 步骤,最终i 还是为1 ;
    这就是所谓的并发问题或者说线程安全问题。

1.2 解决方案

1 悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现

2 乐观锁和cas 理论

乐观锁,总是乐观地假设最好的情况,每次去拿数据的时候都认为别人不会修改这个数据,所以不会上锁,只会要对数据进行更新时判断一下在此期间(拿到数据到更新的期间)别人有没有去更改这个数据,可以使用版本号机制和CAS算法实现。

CAS(Compare AndSwap)是一种常见的“乐观锁”,大部分的CPU都有对应的汇编指令,它有三个操作数:内存地址V,旧值A,新值B。只有当前内存地址V上的值是A,B才会被写到V上,否则操作失败。

3 乐观锁和悲观锁的对比

悲观锁使用于并发量较高的时候;
乐观锁适应于并发量低的场景
因为并发高的时候cas一直失败,自旋cpu空转消耗性能并且没
有任何意义不如让cpu干其他的业务或者说直接等待

cas失败重试的操作=自旋锁 优点:允许同时修改 缺点:可能会失败

4 ABA 问题

如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS操作就会误认为它从来没有被修改过。

这个问题被称为 CAS 操作的 "ABA"问题。
ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。

1.3 synchorized 锁升级过程(无锁,偏向锁,轻量级锁,重量级锁)

Java 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

偏向锁

在锁不存在多线程竞争情况下,为了减小线程获取锁的代价而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储 锁偏向的线程ID,以后该线程再进入同步块时只需简单判断下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
在这里插入图片描述

  • 如果本身是无锁状态(初始状态),只需CAS设置偏向锁指向自己即可;
  • 判断当前对象是否是偏向锁,判断拥有该偏向锁的线程是否还存在,不存在时直接CAS设置偏向锁指向自己线程(拥有偏向锁的线程使用完毕后不会主动释放);
  • 如果拥有该偏向锁的线程还存在,则会暂停拥有偏向锁的线程,升级成轻量级锁。
轻量级锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

轻量级锁在加锁失败进行CAS达到一定次数后(自旋锁默认的次数为 10 次可以通过 -XX:PreBlockSpin 来更改),就会升级为重量级锁;在解锁失败,锁也会升级为重量级锁。

一旦锁升级成重量级锁(就不会再恢复到轻量级锁状态),当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。

轻量级锁什么时候会解锁失败呢?在发生锁竞争时并且占用锁的线程未释放,这时(自旋默认了10次还是未获取到锁)竞争锁的线程就会 将MarkWord 修改为重量级锁,并且将自己阻塞在该锁的monitor对象上。之后占用锁的线程将栈帧中的 Mark Word进行CAS替换回对象头的时候,发现有其它线程竞争该锁(已经由竞争锁的线程更改了锁状态),然后它释放锁并且唤醒在等待的线程,后续的线程操作就全部都是重量级锁了。

重量级锁

重量级锁也就是普通的悲观锁了,加锁时都需要进行系统调用,系统调用会涉及到用户态和内核态的切换,效率比较低。

1.4 公平锁和非公平锁

Synchorized 默认非公平锁,ReentrantLock默认也是非公平锁,不过可以传递参数变成公平锁。
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
● 优点:所有的线程都能得到资源,不会饿死在队列中。
● 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
● 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
● 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

1.5 可重入锁和不可重入锁

在jdk中synchronized和Reentrantlock,都是可重入锁,为了更高效的性能和防止发生死锁。
当某个线程试图获取一个自己已经持有的锁时,那么会立刻获得这个锁,不用等待,可以理解为这个锁可以继承。
同时这个锁的计数器也会加1,但计数器为0时,释放锁。
如果没有可重入锁,那么当已经拿到锁的线程再次获取锁时就会进入死锁状态。可重入锁是很常见的。

如果是不可重入锁,当我这个线程 已经获得了一把锁,再去想要获得者一把锁时就会产生自己等待自己的锁释放的过程,就会产生死锁问题。

可重入锁

public class ReentrantTest {

    public static void main(String[] args) {
        // 第一次获取锁
        synchronized (ReentrantTest.class){
            while (true){
//            第二次获取锁
                synchronized (ReentrantTest.class){
                    System.out.println("ReentrantLock");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

}

在这里插入图片描述
不可重入锁


 class NotReentrantLock{

    private static boolean isLock = false;

    // 加锁
    public synchronized void lock() throws InterruptedException {
//        判断是否加锁
        while (isLock){
            wait();
        }
//        锁住当前线程
        isLock = true;
    }

    // 解锁
    public synchronized void unLock(){
        isLock = false;
        notify();
    }
}
public class 手写不可重入锁 {
    private static NotReentrantLock notReentrantLock = new NotReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        test();
    }
    public static void test() throws InterruptedException {
        notReentrantLock.lock();
        dosomething();
        notReentrantLock.unLock();
    }

    public static void dosomething() throws InterruptedException {
        notReentrantLock.lock();
        System.out.println("dosomething...");
        notReentrantLock.unLock();
    }

}

结果是直接阻塞了,什么都没输出

1.6 共享锁和非共享锁(独享锁)

独享锁:该锁一次只能被一个线程所持有
共享锁:该锁可以被多个线程所持有
举例:
synchronized是独享锁;
可重入锁ReentrantLock是独享锁;
读写锁ReentrantReadWriteLock中的读锁ReadLock是共享锁,写锁WriteLock是独享锁。
独享锁与共享锁通过AQS(AbstractQueuedSynchronizer)来实现的,通过实现不同的方法,来实现独享或者共享。

1.7 单机锁和分布式锁

为了保证一个方法在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLcok或synchronized)进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题

1.8 ReentrantLock的流程

  1. ReentrantLock先通过CAS尝试获取锁,
    a. 如果此时锁已经被占用,该线程加入AQS队列并wait()
    b. 当前驱线程的锁被释放,挂在CLH队列为首的线程就会被notify(),然后继续CAS尝试获取锁,此时:
    ⅰ. 非公平锁,如果有其他线程尝试lock(),有可能被其他刚好申请锁的线程抢占。
    ⅱ. 公平锁,只有在CLH队列头的线程才可以获取锁,新来的线程只能插入到队尾。

简单来说,ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。

这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成

便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。

锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值