小白的JUC学习12_锁


锁就是用来解决并发编程下的安全问题的,锁有很多种类型,下面来瞅瞅

一、锁介绍

Java提供了两种加锁的方式

1、Synchronized关键字

2、Lock接口

学习各种锁之前,要明白我们在Java中使用的锁并不是确切地只为一个特定的锁,下面具体解释

1.1、悲观&乐观锁


一、悲观锁

  • 悲观锁就像互斥锁一样,多个线程争抢该锁时,只有一个线程能使用,其他都会阻塞
  • 所以这也是悲观的含义,因为该锁总是认为在某个线程对齐共享变量进行修改时,总会有其他线程也会对其修改
  • 所以某个线程持有后只有释放该锁,其他线程才可以对其争抢
  • Synchronized就是一个悲观锁

二、乐观锁

  • 乐观锁在Java并没有具体实现类,它更像一种实现方式
  • 乐观锁认为,当当前线程持有该锁并对其修改时,其他线程并不会进来捣乱(修改),所以说它不会上锁
  • 但是这样也还会存在并发安全问题,所以它有一种自旋的实现
    • 每次要对其修改时,会进行判断,是否为该线程初次读取时的值,如果是则对其修改
    • 如果不是,则一直循环判断,尝试更新
    • 在操作系统中有个专业术语叫做CAS(Compare-and-Swap 比较并替换)
      • 其中它还会一直去循环判断,所以也称为自旋
  • 根据以上概念,乐观锁也有点像读写锁,但是读写锁的写锁是一种悲观锁,当对其写数据时,其他读写线程的操作都会阻塞

总结:乐观锁更像是一组事务

1.2、CAS


一、CAS介绍

顾名思义:比较并替换(设置,CAS),在CPU的实现中该操作是原子操作,要么执行成功,幺妹失败

重要的是Java也提供了可以对变量进行CAS的操作,待会使用

二、CAS的基本实现思想(简化)

  • 如果符合目标预期结果(标志位),那么将对其修改,从而防止其他线程进入,这个过程中我们就可以安全的修改共享内存数据(原子操作)
  • 其他线程此时或者结束操作
  • 或者一直循环等待(自旋),直到当前线程执行完毕,并对标志位进行复原,其他线程才可以争抢使用【自选锁的实现】

三、自定义CAS

以下案例实现一个简单的“CAS”,但要注意的是直接使用Java代码并无法实现原子操作

  • 标志位需要保证可见性
  • 共享数据并不需要加一个Volatile,因为自旋锁的特性也类似悲观锁,当修改了标志位,这段过程其他线程无法使用该共享内存
package com.migu;

import java.util.concurrent.TimeUnit;

public class Test {
    private volatile static boolean flag = true;  // 标志位,默认可使用
    private static int num = 0; // 共享数据

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                if (flag) { // 比较
                    flag = false; // 后替换,防止其他线程进入
                    num += 1;
                    System.out.println(Thread.currentThread().getName() + ": " + num);

                    flag = true; // 复原
                }
            }
        }, "线程A").start();

        new Thread(() -> {
            while (true) {
                if (flag) {      // 使用Java层面上的比较替换,并不是原子操作
                    flag = false;
                    num += 1;
                    System.out.println(Thread.currentThread().getName() + ": " + num);
                    flag = true;
                }
            }
        }, "线程B").start();
        
        Thread.sleep(1);
        while (Thread.activeCount() > 1)
            System.exit(0);
    }
}

输出:(在Java层面无法实现原子操作)
在这里插入图片描述

四、原子类

上面我们提到过原子类,它的底层实现就是CAS锁,位于java.uti.concurrent.atomic包下

比如AtomicIntegergetAndAddInt方法:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

通过使用Unsafe对象,该类可直接操作内存进行自旋操作

1.3、锁升级:无锁–>偏向锁–>轻量级锁–>重量级锁


一、锁升级介绍

当多个线程争抢同一把锁时,该锁就会升级

JDK1.6之前,Synchronized就是一把重量级锁。而在这之后Synchronized就会存在锁升级的情况,不会一开始就是个重量级锁

重量级锁也如同互斥锁悲观锁),互斥锁会存在大量阻塞的情况,所以在JDK1.6开始就对锁进行了优化

二、锁升级过程

在Java是通过锁对象来进行设置锁的,所以在对象中也保存锁的信息

下图是对象头中的锁状态信息(图取自网上,知道链接的告诉我,我放上去)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jb7Uq0xx-1617425763389)(C:\Users\66432\AppData\Roaming\Typora\typora-user-images\1617281166651.png)]

对象锁的初始状态,为无锁状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H8Tys5Me-1617425763393)(C:\Users\66432\AppData\Roaming\Typora\typora-user-images\1617281255746.png)]

1.3.1、偏向锁


当有线程尝试获取该对象锁时,则会进行CAS判断,成功的话则对该对象头的锁状态的锁标志位设置为01即为偏向锁,并在对象头的线程ID记录该线程

偏向锁

  • 顾名思义,该对象锁将一直属于某个线程所有【不存在竞争】,一直偏向于该线程
  • 除非有其他线程进入争抢,则会升级锁状态
  • 持有偏向锁的线程不会再进行同步判断,如同直接调用方法一样(效率高)

偏向锁设置流程:当锁为无锁状态,则如上操作

偏向锁撤销流程:当多个线程争抢偏向锁时,将会发生撤销

  • 线程B首先进行CAS判断,由于该对象头已保存了线程A(已经历过CAS操作),CAS操作则会失败
  • 那么会暂停该对象锁保存的线程(线程ID记录),并检查该线程是否存活
    • 线程A结束,则重新偏向:线程ID指向线程B
    • 线程A活动中,撤销该锁并准备做升级轻量级锁的操作

1.3.2、轻量级锁(自旋锁)


轻量级锁就真正存在多线程下的加锁解锁操作,自然而然我们就需要对当前线程的栈帧进行处理

具体实现:

加锁过程

  • 首先为想要获取该锁的线程的栈帧中创建一个锁记录
  • 并将对象头信息进行复制操作,放置在所属线程的栈帧中,并尝试堆中MarkWord的轻量级锁引用锁记录
  • 此时开始CAS操作
    • 成功:则修改标志位为00
    • 失败:通过自旋尝试获取该锁,所以轻量级锁也称自旋锁
      • 当自旋获取该锁又失败时(这里可能需要某种条件,具体我也不太懂,不好意思),则会对锁做升级为重量级锁的操作

优缺点

轻量级锁发生在用户态,避免线程频繁在用户态和内核态进行切换

使用自旋获取锁,不会产生阻塞,可提高程序的响应速度

自旋会消耗CPU运算效率

1.3.3、重量级锁


重量级锁依赖于操作系统的互斥量(mutex) 实现 ,此时JVM启动的线程获取该锁需要频繁在用户态和内核态进行切换

优缺点:

不会消耗CPU,但由于线程在获取锁发生在内核态,此时响应时间会慢

适用于追求吞吐量的场景

1.4、可重入锁


一、可重入锁介绍

可重入锁也叫递归锁,这个说白了就很像我们上面讲的偏向锁,但他并不担心其他线程会对其争抢,只要拿到最外面那把锁,里面那些锁都是它的,不需要再做进行争抢锁的操作

类似这样的情况:

public static synchronized void m() {
    synchronized (Test.class) {
        synchronized (Test.class) {
            System.out.println("----");
        }
    }
}

Java提供的锁都是可重入锁,SynchronizedLock

1.5、公平、非公平锁


一、锁介绍

公平锁:顾名思义,对于先申请锁的线程可以优先拿到的锁

非公平锁:后申请的线程可以先获取锁(通过策略,可能是随机,可能是优先级设置)

Synchronized就是一把非公平锁

Lock可以指定,默认是非公平锁,看源码可知

public ReentrantLock() {
    sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

1.6、可中断锁


一、可中断锁

可中断锁并不是指那个抛出中断异常(InterruptedException)让处于等待的线程直接结束的一个机制

而是对获取锁但处于阻塞状态的线程,可以放弃锁的争抢

在Java中Synchronized就是不可中断锁,而Lock则是一把可中断锁,具体看源码

void lock()  // 获取锁。 (阻塞)
void lockInterruptibly()  // 获取该锁除非当前线程 interrupted。 
Condition newCondition()  // 返回一个新的 Condition实例绑定到该 Lock实例。
boolean tryLock()  // 尝试获取锁,如果该锁处于空闲状态,则可直接获取(原子操作) 
boolean tryLock(long time, TimeUnit unit)  // 类似自旋操作(通过给到的时间尝试获取)
void unlock()   // 释放锁。 

所以可以通过tryLock方法决定是否放弃争抢

1.7、读写锁


一、读写锁介绍

拿之前讲过的文章,读写锁其实分两个部分

读锁:也称共享锁

写锁:互斥锁、悲观锁、排它锁、独占锁

读写锁的文章地址:读写锁

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值