多线程安全-- synchronized底层原理、对JMM(Java内存模型)、CAS、volatile、AQS的理解、ReentrantLock实现原理、synchronized和Lock区别

多线程 – synchronized关键字的底层原理

如下抢票的代码,如果不加锁,就会出现超卖或者一张票卖给多个人
Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住

在这里插入代码片public class TicketDemo {

    static Object lock = new Object();
    int ticketNum = 10;


    public synchronized void getTicket() {
        synchronized (this) {
            if (ticketNum <= 0) {
                return;
            }
            System.out.println(Thread.currentThread().getName() + "抢到一张票,剩余:" + ticketNum);
            // 非原子性操作
            ticketNum--;
        }
    }

    public static void main(String[] args) {
        TicketDemo ticketDemo = new TicketDemo();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                ticketDemo.getTicket();
            }).start();
        }
    }


}

Monitor 被翻译为监视器,是由jvm提供,c++语言实现

在代码中想要体现monitor需要借助javap命令查看clsss的字节码,比如以下代码:

public class SyncTest {

    static final Object lock = new Object();
    static int counter = 0;
    public static void main(String[] args) {
        synchronized (lock) {
            counter++;
        }
    }
}

找到这个类的class文件,在class文件目录下执行javap -v SyncTest.class,反编译效果如下:
在这里插入图片描述

  • monitorenter 上锁开始的地方
  • monitorexit 解锁的地方
  • 其中被monitorenter和monitorexit包围住的指令就是上锁的代码
  • 有两个monitorexit的原因,第二个monitorexit是为了防止锁住的代码抛异常后不能及时释放锁

在使用了synchornized代码块时需要指定一个对象,所以synchornized也被称为对象锁

monitor主要就是跟这个对象产生关联,如下图:
在这里插入图片描述
Owner: 存储当前获取锁的线程的,只能有一个线程可以获取
EntryList: 关联没有抢到锁的线程,处于Blocked状态的线程
WaitSet: 关联调用了wait方法的线程,处于Waiting状态的线程

synchronized关键字的底层原理

  • Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】

  • 它的底层由monitor实现的,monitor是jvm级别的对象( C++实现),线程获得锁需要使用对象(锁)关联monitor

  • 在monitor内部有三个属性,分别是owner、entrylist、waitset

  • 其中owner是关联的获得锁的线程,并且只能关联一个线程entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程

JMM (Java内存模型)

1.JMM(Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。
2.JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
3.线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存

在这里插入图片描述

对CAS的理解

1.CAS的全称是: Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性
2.CAS使用到的地方很多:AQS框架、AtomicKXX类
3.在操作共享变量的时候使用的自旋锁效率上更高一些
4.CAS的底层是调用的Unsafe类中的方法,都是操作系统提供的,其他语言实现

例子:

  • 线程1与线程2都从主内存中获取变量int a = 100,同时放到各个线程的工作内存中
    在这里插入图片描述
    一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当旧的预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功。
    线程1操作:V:int a = 100,A:int a = 100,B:修改后的值:int a = 101 (a++)

  • 线程1拿A的值与主内存V的值进行比较,判断是否相等

  • 如果相等,则把B的值101更新到主内存中
    在这里插入图片描述
    线程2操作:V:int a = 100,A:int a = 100,B:修改后的值:int a = 99(a–)

  • 线程2拿A的值与主内存V的值进行比较,判断是否相等(目前不相等,因为线程1已更新V的值99)

  • 不相等,则线程2更新失败
    在这里插入图片描述
    自旋锁操作

  • 因为没有加锁,所以线程不会陷入阻塞效率较高

  • 如果竞争激烈,重试频繁发生,效率会受影响,需要不断尝试获取共享内存V中最新的值,然后再在新的值的基础上进行更新操作,如果失败就继续尝试获取新的值,直到更新成功

乐观锁和悲观锁
CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
synchronized是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

对volatile的理解

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,就具备了两层语义:

  • 保证线程间的可见性
  • 禁止进行指令重排序

1. 保证线程间的可见性

// 可见性例子
// -Xint
public class ForeverLoop {
    static boolean stop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stop = true;
            System.out.println("modify stop to true...");
        }).start();
        foo();
    }

    static void foo() {
        int i = 0;
        while (!stop) {
            i++;
        }
        System.out.println("stopped... c:"+ i);
    }
}

当执行上述代码的时候,发现foo()方法中的循环是结束不了的,也就说读取不到共享变量的值结束循环,主要是因为在JVM虚拟机中有一个JIT(即时编辑器)给代码做了优化。

while (!stop) {
	i++;
}

在很短的时间内,这个代码执行的次数太多了,当达到了一个阈值,JIT就会优化此代码,如下:

while (true) {
	i++;
}

当把代码优化成这样子以后,及时stop变量改变为了false也依然停止不了循环

解决方案一︰在程序运行的时候加入vm参数-Xint表示禁用即时编译器,不推荐,得不偿失(其他程序还要使用)
解决方案二: 在修饰stop变量的时候加上volatile,当前告诉jit,不要对volatile修饰的变量做优化

2. 禁止进行指令重排序

用volatile修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果。
在这里插入图片描述
在变量上添加volatile,禁止指令重排序,则可以解决问题
在这里插入图片描述
volatile使用技巧:

  • 写变量让volatile修饰的变量的在代码最后位置。
  • 读变量让volatile修饰的变量的在代码最开始位置

对AQS的理解

全称是AbstractQueuedSynchronizer,即抽象队列同步器。它是构建锁或者其他同步组件的基础框架,像ReentrantLock、Semaphore都是基于AQS实现的。

AQS与Synchronized的区别
在这里插入图片描述
AQS-多个线程共同去抢这个资源是如何保证原子性的呢
在这里插入图片描述
AQS是公平锁吗,还是非公平锁?(都可以实现)

  • 新的线程与队列中的线程共同来抢资源,是非公平锁
  • 新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁

ReentrantLock的实现原理

ReentrantLock翻译过来是可重入锁,相对于synchronized它具备以下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置公平锁
  • 支持多个条件变量
  • 与synchronized一样,都支持重入

ReentrantLock 主要利用CAS+AQS 队列来实现。它支持公平锁和非公平锁,两者的实现类似
构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。
在这里插入图片描述

  • 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功
  • 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部
  • 当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程
  • 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁

synchronized和Lock的区别

  • 语法层面
    • synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现
    • Lock 是接口,源码由 jdk 提供,用 java 语言实现
    • 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁
  • 功能层面
    • 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
    • Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
    • Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock
  • 性能层面
    • 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
    • 在竞争激烈时,Lock 的实现通常会提供更好的性能
  • 9
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值