前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从JVM角度解析Java是如何保证线程安全的

从JVM角度解析Java是如何保证线程安全的

作者头像
程序员阿杜
发布2021-08-03 15:15:25
5550
发布2021-08-03 15:15:25
举报
文章被收录于专栏:开源君开源君

文章已同步至GitHub开源项目: JVM底层原理解析 从JVM角度解析Java是如何保证线程安全的

线程安全

​ 当多个线程同时访问一个对象,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要考虑额外的同步,或者在调用方法时进行一些其他的协作,调用这个对象的行为都可以获得正确的结果。那么就称这个对象是线程安全的。

​ 这个定义是严谨并且有可操作性的,他要求线程安全的代码都必须具备一个共同的特性。代码本身封装了所有必要的正确性保障手段(如互斥同步等)。令调用者无需关心多线程下的调用问题。更无需自己实现任何措施来保证安全。

Java中的线程安全

​ 在Java语言中,从JVM底层来看的话,线程安全并不是一个非黑即白的二元排他选项,按照安全程度来划分,我们可以将Java中各种操作共享的数据分为五类:不可变绝对线程安全相对线程安全线程兼容线程对立

​ 接下来,我们一一介绍。

线程安全的实现方案

在Java中,实现线程安全,主要有三种方案, 互斥同步非阻塞同步无同步方案

互斥同步(悲观锁)

synchronized的实现

​ 此关键字经过javac编译之后,会生成两条字节码指令.monitorentermonitorexit

比如以下代码

代码语言:javascript
复制
public synchronized void dosomething(){
        synchronized (SynchronizedTest.class){
            System.out.println("do something");
        }
    }

​ 反编译之后

代码语言:javascript
复制
0 ldc #2 <cn/shaoxiongdu/chapter6/SynchronizedTest>
 2 dup
 3 astore_1
 4 monitorenter
 5 aload_1
 6 monitorexit
 7 goto 15 (+8)
10 astore_2
11 aload_1
12 monitorexit
13 aload_2
14 athrow
15 return

​ 可以看到,在偏移址为4的地方,有一条字节码指令monitorenter,表示synchronized开始的地方,也就是表示开启同步的位置,在偏移址为12的地方,有一条monitorexit表示同步结束的地方。

​ 这两个指令都需要一个引用类型的参数来指明需要锁住的对象。如果代码中指定了,则使用指定的对象锁,如果出现在方法声明位置,那么虚拟机会判断,如果是实例方法则锁实例对象,如果是静态方法则锁类对象。

​ 在执行monitorenter时,首先虚拟机会尝试获取对象锁

  • 如果获取到对象锁,或者当前线程已经有了此对象锁
    • 则将对象锁中对象头位置的锁计数器+1,
    • 在执行到monitorexit时,会将其-1。一旦当前锁对象的锁计数器为0,则当前线程就会释放对象的对象锁。
  • 如果获取不到,则当前线程进入阻塞状态。直到对象锁的值变为0。也就是持有对象锁的线程释放该锁。

特征:

  • 可重入的,同一条线程进入同步块多次也不会被锁死。
  • 在同步块中执行的线程会无条件的阻塞其他线程的进入。这意味着无法像处理数据库那样强制让已获取锁的线程释放锁,也无法让正在的等待锁的进程退出。

从执行的成本来看,synchronized是一个重量级的操作。主流的Java虚拟机实现中,Java的线程是映射到操作系统的内核线程中的,如果要唤醒或者阻塞一个线程,需要从用户态切换到内核态。这种转化是很耗时的。所以synchronized是一个重量级的操作。在有必要的情况下,再去使用其。

lock的实现

​ 在JDK1.5之后,Java类库中新提供了java.util.concurrent包,其中的locks.Lock接口便成为Java另外一种互斥同步的手段。

​ 该接口的定义如下

代码语言:javascript
复制
public interface Lock {

    //获取锁。如果锁已被其他线程获取,则进行等待。
    void lock();

    //如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。
    void lockInterruptibly() throws InterruptedException;

    //尝试获取锁,如果获取成功,则返回true 否则返回false 立即返回 不会和lock一样等待
    boolean tryLock();

    //拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unlock();

    Condition newCondition();
}

​ 使用Lock接口的实现类,用户可以以非块结构来实现互斥同步,从而摆脱了语言的束缚,改为在类库层面去实现同步,这也为日后扩展出不同的调度算法,不同的特性,不同性能的各种锁提供了空间。

​ 重入锁(ReentrantLock)是Lock接口中最常见的一种实现方式。故名思意,他和synchronized一样是可以重入的。写法如下

代码语言:javascript
复制
public static void main(String[] args) {

        Lock lock = new ReentrantLock();
        
        lock.lock();
        try{
            //处理任务
        }catch(Exception ex){

        }finally{
            lock.unlock();  //释放锁 不在finally处释放可能会发生死锁 
        }
    }

相比synchronizedReentrantLock增加了如下的功能。

  • 等待可中断 ​ 当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待。对于处理执行时间比较长的同步块很有帮助
  • 公平锁 ​ 当多个线程在等待同一个锁的时候,必须按照申请锁的顺序来依次获得锁。syn 是非公平的,reentrantLock默认也是非公平的,需要在构造函数中传入true指定使用公平锁。(使用公平锁会导致性能急剧下降)
  • 锁绑定多个条件 ​ 一个ReentrantLock对象可以同时绑定多个Condition对象。只需要多次调用newCondition方法即可。

这种互斥同步的放方案主要问题是在线程阻塞和唤醒的时候会带来性能开销问题。从解决问题的方式上看,互斥同步(阻塞同步)属于一种悲观的并发策略,认为只要是别的线程过来,就一定会修改数据。无论是否真的会修改,他都会进行加锁(此处讨论的是概念模型,实际虚拟机会优化一些不必要的加锁)。这会导致用户态和内核态频繁切换,并且需要维护锁的计数器。比较繁琐。

非阻塞同步(乐观锁)

基于冲突检测的乐观并发策略。

​ 通俗的说,就是不管风险,先进行操作。如果数据没有被修改,则修改成功。如果数据被修改,则不断重试。直到出现没有竞争的共享数据为止。

​ 此种方案需要硬件的发展,因为进行检测是否修改最终写入这两个操作必须保证原子性。如果这里用前边的互斥同步来解决,就没有什么意义了,所以需要硬件层面的支持。确保在语义上看起来有多个操作的行为只需要一条处理器指令就可以完成。常见的这种指令有

  • 测试并设置 TestAndSet
  • 获取并增加 FetchAndIncrement
  • 交换 Swap
  • 比较和交换: CompareAndSwap 在Java中完成乐观锁用的是比较和交换CAS指令。 CAS指令需要有三个操作数,一个是旧的预期值A,一个是内存位置V,还有一个新值B。 ​ 当旧的预期值与内存中真正的值相同的时候,就将旧值替换为新值。否则就不更新。 在JDK1.5之后,Java类库中才开始使用CAS操作,该操作由 sun.misc.Unsafe类中的方法包装提供。虚拟机会对这些方法进行特殊处理,保证编译之后是一条平台相关的处理器CAS指令。 比如AtomicInteger就是包装了CAS指令之后的线程安全类,他的方法都设置在一个死循环中,不断尝试将一个新值赋给内存位置的值,如果失败,说明被其他线程改了,于是再次循环进行下一次操作,直到修改成功位置。 尽管CAS看起来很美好,但是它存在一个逻辑漏洞,当别的线程将值从A改为B,然后又改回A的时候,当前线程是不会发现的。这个漏洞叫做CAS的ABA问题,JUC为了解决这个问题,提供了一个带有标记的原子引用类AtomicStampedReference。它通过控制变量值的版本来解决。

文章已同步至GitHub开源项目: JVM底层原理解析

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-08-02 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 线程安全
  • Java中的线程安全
  • 线程安全的实现方案
  • 互斥同步(悲观锁)
    • synchronized的实现
      • lock的实现
      • 非阻塞同步(乐观锁)
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档