对synchronized关键字的一点理解

一、线程安全性问题

多线程技术的引用能够提升程序的处理性能,同时,也带来了很多麻烦,举个简单的例子,如多线程对于共享变量访问带来的安全性问题,看下面的一段代码:

public class SynchronizedTest extends Thread{
    private static int count=0;
    public static void inc(){
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++;
    }
    public static void main(String[] args) throws InterruptedException {
        for(int i=0;i<1000;i++){
            new Thread(()->SynchronizedTest.inc()).start();
        }
        Thread.sleep(3000);
        System.out.println(" 运行结果"+count);
    }
}

打印结果

 运行结果997

Process finished with exit code 0

为什么打印结果不是1000呢?都知道这就是我们平时说的线程安全问题,那怎么解决线程安全问题呢,大家第一时间想到的就是加锁,加synchronized关键字就可以,确实如此,如下代码:

public synchronized static void inc(){
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++;
    }

打印结果

 运行结果1000

Process finished with exit code 0

当我们把inc()方法加上关键字,重新运行,结果真的就1000
那么为什么加锁就可以解决这个线程安全问题呢?
什么是锁?
Synchroinzed是怎么保证线程安全的,他的原理是什么样的?

一系列的问题接踵而来。没错Java 提供的加锁方法就是 Synchroinzed 关键字那么我们就围绕着这个关键字的几个问题研究一下。

二、synchronized 的基本认识

在多线程并发编程中 synchronized 一直是元老级角色,很多人都会称呼它为重量级锁。但是 Java SE 1.6 对synchronized 进行了各种优化之,为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。

三、synchronized 的基本用法

synchronized 有三种方式来加锁,分别是
1. 修饰实例方法,作用于当前实例加锁,进入同步代码前
要获得当前实例的锁

 public synchronized  void inc(){
        System.out.println("synchronized修饰实例方法");
    }

2. 静态方法,作用于当前类对象加锁,进入同步代码前要
获得当前类对象的锁

 public synchronized static void inc(){
        System.out.println("synchronized修饰静态方法");
    }

3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同
步代码库前要获得给定对象的锁。

public void incr(){
        synchronized (SynchronizedDemoCase.class) {
        	System.out.println("synchronized修饰静态方法");
        }
    }

不同的修饰类型,代表锁的控制粒度,一些常见的写法总结如下
类锁:作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁,比如下面两种写法

    public synchronized static void incr01(){
   
    }

    public static void incr02(){
        synchronized (SynchronizedStaticDemo.class) {
           
        }
    }

实例锁,当前实例加锁,进入同步代码前要获得当前实例的锁,比如下面两种写法

public synchronized void incr01(){

}
public void incr02(){
        synchronized (this) {
           
        }
    }

四、锁是如何存储的

我们来分析,synchronized 锁是如何存储的呢,最起码得有个状态来记录加没加锁,加的什么锁,并且这个锁状态对多线程还得是可见的,按这个思路,我们观察synchronized 的整个语法发现,synchronized(lock)是基于对象的生命周期来控制锁粒度的,那是不是锁的存储和这个 lock 对象有关系呢?所以我们以对象为切入点,去jvm研究一下对象。
对象的内存布局
在 Hotspot 虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
我们在 Java 代码中,使用 new 创建一个对象实例的时候,JVM 层面实际上会创建一个instanceOopDesc 对象,代码如下
对synchronized关键字的一点理解_第1张图片
从 instanceOopDesc 代码中可以看到 instanceOopDesc继承自 oopDesc,oopDesc 的定义在oop.hpp 文件中
在普通实例对象中,oopDesc 的定义包含两个成员,分别是 _mark 和 _metadata
_mark 表示对象标记、属于 markOop 类型,也就是接下来要说的的 Mark World,它记录了对象和锁有关的信息
_metadata 表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中 Klass 表示普通指针、_compressed_klass 表示压缩类指针
认识MarkWord
我们直接看他的定代码:
对synchronized关键字的一点理解_第2张图片
在源码中,就定义了一个字段来记录锁标识的字段,他的结果其实如下图表示
对synchronized关键字的一点理解_第3张图片
那为什么每个对象都能实现锁呢?
那是因为java中的对象都默认派生自Object类,而每个Java Object 在 JVM 内部都有一个 native 的 C++对象oop/oopDesc 进行对应

五、synchronized 锁的升级

我么从刚才的MarkWorld可以其状态有偏向锁、轻量级锁、重量级锁的区分。在分析这几种锁的区别时,我们先来思考一个问题
使用锁能够实现数据的安全性,但是会带来性能的下降不使用锁能够基于线程并行提升程序性能,但是却不能保证线程安全性。这两者之间似乎是没有办法达到既能满足性能也能满足安全性的要求
那针对上面的问题,那我们该怎么取舍呢?其实虚拟机的作者就研究发现,对于大多数加锁的代码,其实往往不存在多线程竞争,并且总是由一个线程多次获取执行权,所以JDK1.6 之后做了一些优化,为了减少获得锁和释放锁带来的性能开销,引入了偏向锁、轻量级锁的概念。

偏向锁原理(获取和撤销)

  1. 首先获取锁 对象的 Markword,判断是否处于可偏向状态。(biased_lock=1、且 ThreadId 为空)
  2. 如果是可偏向状态,则通过 CAS 操作,把当前线程的 ID写入到 MarkWord
    a) 如果 cas 成功,那么表示已经获得了锁对象的偏向锁,接着执行同步代码块
    b) 如果 cas 失败,说明有其他线程已经获得了偏向锁,这种情况说明当前锁存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁(这个操作需要等到全局安全点,也就是没有线程在执行字节码)才能执行
  3. 如果是已偏向状态,需要检查 markword 中存储的ThreadID是否等于当前线程的ThreadID
    a) 如果相等,不需要再次获得锁,可直接执行同步代码块
    b) 如果不相等,说明当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁
    注意: 在我们的应用开发中,绝大部分情况下一定会存在 2 个以上的线程竞争,那么如果开启偏向锁,反而会提升获取锁的资源消耗。所以可以通过 jvm 参数UseBiasedLocking 来设置开启或关闭偏向锁

轻量级锁的基本原理
轻量级锁在加锁过程中,用到了自旋锁(可以理解为一个空的for循环)当有另外一个线程来竞争锁时,当前线程没有竞争到锁的话,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁。
自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而会消耗 CPU 资源。默认情况下自旋的次数是 10 次,超过10次后,就会膨胀为重量级锁。所以,轻量级锁适用于那些同步代码块执行的很快的场景

重量级锁的基本原理
这是真正意义上的锁,竞争失败的话,会进行阻塞。
我们先写一段代码

public class SynchronizedTest{
    public static void inc(){
        synchronized(SynchronizedTest.class){
            int a = 0;
        }
    }
}

可以通过命令javap -v SynchronizedTest.class来查看实现的字节码指令
对synchronized关键字的一点理解_第4张图片
字 节 码指令中 中,我们 会 看 到 一 个monitorentermonitorexit的指令,中间是加锁的代码指令。
每一个 JAVA 对象都会与一个监视器 monitor 关联,当一个线程想要执行一段被synchronized 修饰的同步方法或者代码块时,该线程得先
获取到 synchronized 修饰的对象对应的 monitor。
monitorenter 表示去获得一个对象监视器。monitorexit 表示释放 monitor 监视器的所有权,使得其他被阻塞的线程可以尝试去获得这个监视器。
monitor 依赖操作系统的互斥锁来实现的, 我们此处不再多说。
重量级锁的加锁过程,可以参考下图理解
对synchronized关键字的一点理解_第5张图片

本文是综合自己的认识和参考各类资料(书本及网上资料)编写,若有侵权请联系作者,所有内容仅代表个人认知观点,如有错误,欢迎校正; 邮箱:[email protected] 博客地址:https://blog.csdn.net/qq_35576976/

你可能感兴趣的:(多线程,java)