高性能编程——线程安全问题之Java锁相关(Synchronized深度解析)

Java中锁的概念

其实在上一章原子性的讲解中已经提到并写过一个锁了,但是这还远远不够,Java中关于锁还是有很多东西需要学习,这里先介绍几个与锁相关的概念。

自旋锁

指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
在这里插入图片描述
上图就是一个典型的场景,循环获取锁的过程就像是在旋转一样。

乐观锁

假定没有冲突,在修改数据时如果发现数据和之前获取的不一致,则读最新数据,修改后重试修改。也就是说读取的时候以及其他时候都不加锁,只有在修改前才加锁。

悲观锁

假定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁。也就是说从读取数据开始就加锁了。

独享锁(写锁)

给资源加上写锁,线程可以修改资源,其他线程不能再加锁;(单写)

共享锁(读锁)

给资源加上读锁后只能读不能改,其他线程也只能加读锁,不能加写锁;(多读)

可重入锁、不可重入锁

所谓可重入值的是线程拿到一把锁之后,可以自由进入由同一把锁所同步的其他代码。

公平锁、非公平锁

抢锁是有顺序的,保证抢到锁的顺序就是抢锁的顺序这就是公平锁。

同步关键字synchronized

  1. 用于实例方法、静态方法时,隐式指定锁对象
  2. 用于代码块时,显示指定锁对象
  3. 锁的作用域:对象锁、类锁、分布式锁
  4. 引申:如果是多个进程,怎么办?

认识synchronized

class Counter{

    private static int i=0;

    //synchronized(this){}
    public synchronized void update(){
        //访问数据库
    }

    public void updateBlock(){
        synchronized (this){
            //访问数据库
        }
    }

    //synchronized(Counter.class)
    public static synchronized void staticUpdate(){
        //访问数据库
    }
    
    public static void staticUpdateBlock(){
        synchronized(Counter.class){
            //访问数据库
        }
    }
}

从上面的伪代码简述了synchronized的用法,我们需要明确一点那就是synchronized是一个加锁的过程,但是锁并不是this或者Counter.class,而是JVM内部经过一系列的处理得出来的。

synchronized的特性

synchronized是一个可重入、独享、悲观锁。它还有锁消除、锁粗化等锁优化功能。

锁消除

锁消除这个概念就是在特定的情况下,可以把锁消除了的意思,该过程发生在JIT即时编译的时候。来看一段代码:

public void test1(Object arg){

        //StringBuilder线程不安全,StringBuffer用了synchronized,是线程安全的
        // jit 优化,消除了锁
        StringBuffer stringBuffer = new StringBuffer(); //局部变量,没有在其他线程中使用
        stringBuffer.append("a");
        stringBuffer.append("b");
        stringBuffer.append("c");

        stringBuffer.append("a");
        stringBuffer.append("b");
        stringBuffer.append("c");

        stringBuffer.append("a");
        stringBuffer.append("b");
        stringBuffer.append("c");
//        System.out.println(stringBuffer.toString());;
    }

因为上述代码刚开始执行的时候还会循规蹈矩的执行,但是当该方法执行多次的时候就会触发jit编译,到时候就会消除掉synchronized关键字了,这就是锁消除。但是要注意,这种情况只存在于单线程情况。

锁粗化

用于提高锁的细粒度,把很多细粒度很低的代码放在一个synchronized之中,提高性能。举个例子:
在这里插入图片描述
上面这些小操作最后会被JVM优化成下面这个框的样子。

synchronized原理学习

想要明白它的原理就必须先去知道这个锁是如何锁住this这个对象的,它的加锁的状态是如何记录的?状态会被记录到this对象中吗?如果锁被占用了,那么申请锁的线程将会被挂起,当释放锁的时候,又会唤醒挂起线程队列中的头队列。想要了解上面的机制,就必须先去深入学习对象相关的知识:

Java对象存储原理

先来看一段代码以及Java的内存模型:
在这里插入图片描述
在这里插入图片描述
首先int a = 1;基本类型的局部变量会直接存储在main线程的局部变量表,引用james也会存在该局部变量表。

而类的成员变量都会存在堆内存中该对象之中:
在这里插入图片描述
静态变量、方法(静态和非静态)都存储在方法区中,而对象想取到自己想要的信息就要靠对象头去指向方法区中的类。就能获得初始变量和方法了。要注意在该对象中连引用都是存储的,直接指向另一个对象,而对象再去通过对象头找到指定的类:
在这里插入图片描述

对象头详解

我们抢锁需要修改一个标记,这个标记其实就存储在对象头之中,我们先来看一个对象头的详解图:
在这里插入图片描述

Mark Word

关于锁的标记都存储在这个Mark Word(标记字段)之中。它本质上就是一段堆内存中的内存区域。大小为64位(32位机器为32位)。
在这里插入图片描述
上面是Mark Word的5种状态。
如果Mark Word像第一种那就是没有加上锁。第二段则是偏向锁。第三段为轻量级锁。第四段则是重量级锁。

锁的升级过程

在这里插入图片描述

偏向锁

偏向锁其实就是把0改成thread ID,其实就是加锁之后不再解锁,针对只有对一个线程有用的场景,出现过之后就没有用了。可以在JVM中用参数-XX: -UseBiasedLocking来禁用偏置锁定。

偏向锁,本质就是无锁,如果没有发生过任何多线程争抢锁的情况,JVM认为就是单线程,无需做同步

轻量级锁

加锁的时候首先会把Mark Word复制到有同步代码块的栈帧之中,然后所有线程申请CAS操作,抢到锁的线程把自己的Lock record address写入,这样其他线程的CAS就无法成功了,没有成功的线程则自旋,自选到一定程度就会锁升级。

重量级锁

如果轻量级锁会一直自旋去申请锁,消耗CPU性能,所以升级为重量级锁,就是把申请锁的线程挂起存储在一个entryList,当解锁的时候再去唤醒。

Class meta address

是指方法区中类的地址,在对象初始化的时候用到,相当于一个指针。

Array Length

数组长度,就是假如这个对象是个数组对象的话,记录它的长度。

发布了38 篇原创文章 · 获赞 10 · 访问量 1127

猜你喜欢

转载自blog.csdn.net/weixin_41746577/article/details/103956823