java中的syncronized


目录
  • 1、为什么会需要synchronized?什么场景下使用synchronized?
  • 2、synchronized 作用范围
    • 3.1、三种作用范围加锁的区别
    • 3.2、 JVM 是怎么通过synchronized 在对象上实现加锁,保证多线程访问竞态资源安全的
    • 3.3、对象头的理解
      • 3.3.1、synchronized 是公平锁还是非公平锁吗?
    • 3.4、JDK 6 以来 synchronized 锁状态怎么从无锁状态到偏向锁的
    • 3.5、轻量级锁什么时候会升级为重量级锁
  • 3、应用
    • 3.1、实例1
    • 3.2、示例2

参考连接:https://angela.blog.csdn.net/article/details/105141484?spm=1001.2014.3001.5502

1、为什么会需要synchronized?什么场景下使用synchronized?

线程访问共享资源了,当一个资源有可能被多个线程同时访问并修改的话,需要用到,还是画个图给您看一下,

如上图所示,比如在王者荣耀程序中,我们队有二个线程分别统计后裔和安琪拉的经济,A线程从内存中read 当前队伍总经济加载到线程的本地栈,进行 +100 操作之后,这时候B线程也从内存中取出经济值 + 200,将200写回内存,B线程前脚刚写完,后脚A线程将100 写回到内存中,就出问题了,我们队的经济应该是300, 但是内存中存的却是100,你说糟不糟心。

那你跟我讲讲用 synchronized 怎么解决这个问题的?

在访问竞态资源时加锁,因为多个线程会修改经济值,因此经济值就是静态资源,给您show 一下吧?下图是不加锁的代码和控制台的输出,请您过目:

public class ThreadTestOne {

    private static int money = 0;

    private static void increseMoney(int n){
        money+=1;
    }

    public static void main(String[] args) {
       Thread a =  new Thread(()->{
            for (int i = 0; i < 1000; i++) {
                increseMoney(1);
            }
        });

        Thread b = new Thread(()->{
            for (int i = 0; i < 1000; i++) {
                increseMoney(2);
            }
        });

        a.start();
        b.start();
        try {
            a.join();
            b.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("当前总经济是:"+money);
    }
}

查看控制台输出:

当前总经济是:1796

不加锁,结果就不对。

下面看下加锁之后的代码和控制台的输出

public class ThreadTestOne {

    private static int money = 0;
    private static final Object obj = new Object();
    private static void increseMoney(int n){
        money+=1;
    }

    public static void main(String[] args) {
       Thread a =  new Thread(()->{
           synchronized (obj){
               for (int i = 0; i < 1000; i++) {
                   increseMoney(1);
               }
           }
        });

        Thread b = new Thread(()->{
            synchronized (obj){
                for (int i = 0; i < 1000; i++) {
                    increseMoney(2);
                }
            }
        });

        a.start();
        b.start();
        try {
            a.join();
            b.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("当前总经济是:"+money);
    }
}

2、synchronized 作用范围

  1. 在静态方法上加锁;
  2. 在非静态方法上加锁;
  3. 在代码块上加锁;
public class SynchronizedSample {

    private final Object lock = new Object();

    private static int money = 0;
		//非静态方法
    public synchronized void noStaticMethod(){
        money++;
    }
		//静态方法
    public static synchronized void staticMethod(){
        money++;
    }
		
    public void codeBlock(){
      	//代码块
        synchronized (lock){
            money++;
        }
    }
}

3.1、三种作用范围加锁的区别

synchronized 这三种作用范围的加锁方式的区别:首先要明确一点:锁是加在对象上面的,我们是在对象上加锁。

重要事情说三遍:在对象上加锁 ?? 3 (这也是为什么wait / notify 需要在锁定对象后执行,只有先拿到锁才能释放锁)

这三种作用范围的区别实际是被加锁的对象的区别,请看下表:

作用范围 锁对象
非静态方法 当前对象 => this
静态方法 类对象 => SynchronizedSample.class (一切皆对象,这个是类对象)
代码块 指定对象 => lock (以上面的代码为例)

3.2、 JVM 是怎么通过synchronized 在对象上实现加锁,保证多线程访问竞态资源安全的

先说在JDK6 以前,synchronized 那时还属于重量级锁,相当于关二爷手中的青龙偃月刀,每次加锁都依赖操作系统Mutex Lock实现,涉及到操作系统让线程从用户态切换到内核态,切换成本很高;
到了JDK6,研究人员引入了偏向锁和轻量级锁,因为Sun 程序员发现大部分程序大多数时间都不会发生多个线程同时访问竞态资源的情况,每次线程都加锁解锁,每次这么搞都要操作系统在用户态和内核态之间来回切,太耗性能了。

JDK 6 以前 synchronized为什么这么重? JDK6 之后的偏向锁和轻量级锁是怎么回事?

3.3、对象头的理解

首先要了解 synchronized 的实现原理,需要理解二个预备知识:

  1. 第一个预备知识:需要知道 Java 对象头,锁的类型和状态和对象头的Mark Word息息相关;

    synchronized 锁 和 对象头息息相关。我们来看下对象的结构:

对象存储在堆中,主要分为三部分内容,对象头、对象实例数据和对齐填充(数组对象多一个区域:记录数组长度),下面简单说一下三部分内容,虽然 synchronized 只与对象头中的 Mard Word相关。

1、对象头:

对象头分为二个部分,Mard Word 和 Klass Word,下面出了详细说明:

对象头结构 存储信息-说明
Mard Word 存储对象的hashCode、锁信息或分代年龄或GC标志等信息
Klass Word 存储指向对象所属类(元数据)的指针,JVM通过这个确定这个对象属于哪个类

2、对象实例数据:

如上图所示,类中的 成员变量data 就属于对象实例数据;

3、对齐填充:

JVM要求对象占用的空间必须是8 的倍数,方便内存分配(以字节为最小单位分配),因此这部分就是用于填满不够的空间凑数用的。

2、第二个预备知识点

需要了解 Monitor ,每个对象都有一个与之关联的Monitor 对象;Monitor对象属性如下所示( Hospot 1.7 代码)

//??图详细介绍重要变量的作用
ObjectMonitor() {
    _header       = NULL;
    _count        = 0;   // 重入次数
    _waiters      = 0,   // 等待线程数
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;  // 当前持有锁的线程
    _WaitSet      = NULL;  // 调用了 wait 方法的线程被阻塞 放置在这里
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 等待锁 处于block的线程 有资格成为候选资源的线程
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
}

对象关联的 ObjectMonitor 对象有一个线程内部竞争锁的机制,如下图所示:

JDK 6 以前 synchronized具体实现逻辑

1、当有二个线程A、线程B都要开始给我们队的经济 money变量 + 钱,要进行操作的时候 ,发现方法上加了synchronized锁,这时线程调度到A线程执行,A线程就抢先拿到了锁。拿到锁的步骤为:

  • 1.1 将 MonitorObject 中的 _owner设置成 A线程;
  • 1.2 将 mark word 设置为 Monitor 对象地址,锁标志位改为10;
  • 1.3 将B 线程阻塞放到 ContentionList 队列;

2、JVM 每次从Waiting Queue 的尾部取出一个线程放到OnDeck作为候选者,但是如果并发比较高,Waiting Queue会被大量线程执行CAS操作,为了降低对尾部元素的竞争,将Waiting Queue 拆分成ContentionList 和 EntryList 二个队列, JVM将一部分线程移到EntryList 作为准备进OnDeck的预备线程。另外说明几点:

所有请求锁的线程首先被放在ContentionList这个竞争队列中;

? 2.1、Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;

? 2.2、任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;

? 2.3、当前已经获取到所资源的线程被称为 Owner;

3、处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的);

作为Owner 的A 线程执行过程中,可能调用wait 释放锁,这个时候A线程进入 Wait Set , 等待被唤醒。

以上就是我想说的 synchronized 在 JDK 6之前的实现原理

3.3.1、synchronized 是公平锁还是非公平锁吗?

非公平的。主要有以下二点原因:

1、Synchronized 在线程竞争锁时,首先做的不是直接进ContentionList 队列排队,而是尝试自旋获取锁(可能ContentionList 有别的线程在等锁),如果获取不到才进入 ContentionList,这明显对于已经进入队列的线程是不公平的;

2、另一个不公平的是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源。

到 JDK 6 之后synchronized 做了优化

前面说了锁跟对象头的 Mark Word 密切相关,我们把目光放到对象头的 Mark Word 上, Mark Word 存储结构如下图和源代码注释(以32位JVM为例,后面的讨论都基于32位JVM的背景,64位会特殊说明)。
Mard Word会在不同的锁状态下,32位指定区域都有不同的含义,这个是为了节省存储空间,用4 字节就表达了完整的状态信息,当然,对象某一时刻只会是下面5 种状态种的某一种

简化一下:

hash: 保存对象的哈希码
age: 保存对象的分代年龄
biased_lock: 偏向锁标识位
lock: 锁状态标识位
JavaThread*: 保存持有偏向锁的线程ID
epoch: 保存偏向时间戳

由于 synchronized 重量级锁有以下二个问题, 因此JDK 6 之后做了改进,引入了偏向锁和轻量级锁:

1、依赖底层操作系统的 mutex 相关指令实现,加锁解锁需要在用户态和内核态之间切换,性能损耗非常明显。

2、研究人员发现,大多数对象的加锁和解锁都是在特定的线程中完成。也就是出现线程竞争锁的情况概率比较低。他们做了一个实验,找了一些典型的软件,测试同一个线程加锁解锁的重复率,如下图所示,可以看到重复加锁比例非常高。早期JVM 有 19% 的执行时间浪费在锁上。

参考博客,这里不在来说明。

3.4、JDK 6 以来 synchronized 锁状态怎么从无锁状态到偏向锁的

首先A 线程访问同步代码块,使用CAS 操作将 Thread ID 放到 Mark Word 当中;
如果CAS 成功,此时线程A 就获取了锁
如果线程CAS 失败,证明有别的线程持有锁,例如上图的线程B 来CAS 就失败的,这个时候启动偏向锁撤销 (revoke bias);
锁撤销流程:

  • 让 A线程在全局安全点阻塞(类似于GC前线程在安全点阻塞)
  • 遍历线程栈,查看是否有被锁对象的锁记录( Lock Record),如果有Lock Record,需要修复锁记录和Markword,使其变成无锁状态。
  • 恢复A线程
  • 将是否为偏向锁状态置为 0 ,开始进行轻量级加锁流程 (后面讲述)
    下图说明了 Mark Word 在这个过程中的转化

偏向锁撤销怎么到轻量级锁的? 还有轻量级锁什么时候会变成重量级锁?

继续上面的流程,锁撤销之后(偏向锁状态为0),现在无论是A线程还是B线程执行到同步代码块进行加锁,流程如下:
1、线程在自己的栈桢中创建锁记录 LockRecord。
2、线程A 将 Mark Word 拷贝到线程栈的 Lock Record中,这个位置叫 displayced hdr,如下图所示:

  • 将锁记录中的Owner指针指向加锁的对象(存放对象地址)。
  • 将锁对象的对象头的MarkWord替换为指向锁记录的指针。这二步如下图所示:

  1. 这时锁标志位变成 00 ,表示轻量级锁

3.5、轻量级锁什么时候会升级为重量级锁

当锁升级为轻量级锁之后,如果依然有新线程过来竞争锁,首先新线程会自旋尝试获取锁,尝试到一定次数(默认10次)依然没有拿到,锁就会升级成重量级锁。

为什么这么设计?一般来说,同步代码块内的代码应该很快就执行结束,这时候线程B 自旋一段时间是很容易拿到锁的,但是如果不巧,没拿到,自旋其实就是死循环,很耗CPU的,因此就直接转成重量级锁咯,这样就不用了线程一直自旋了。
这就是锁膨胀的过程,下图是Mark Word 和锁状态的转化图

看不下去了,可以自己来看一下安琪拉的博客:

3、应用

3.1、实例1

来举个例子来进行说明问题:

public class TestSyncronized {
    private   int i = 0;
    private  void helloOne(){
        synchronized (this){
        i++;
        System.out.println("当前线程是-----"+Thread.currentThread().getName()+",对应的i值是:"+i);
        }
    }


    public static void main(String[] args) {
        TestSyncronized testSyncronized = new TestSyncronized();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                testSyncronized.helloOne();
            }).start();
        }
        try {
            Thread.sleep(3000);
            System.out.println(testSyncronized.i);
            System.out.println("main执行结束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

我之前错误的认为,对象调用方法的时候,进入栈结构中之后,对于栈结构来说,syncronized就失效了,一直不敢来这么使用。

今天才懂这里的学问。但是前提是同一个对象,因为同一个对象关联的是同一个对象监视器。所有的线程都将在这个对象监视器中的waitset中等待调度。

3.2、示例2