Synchronized关键字解析

引言

在Java中实现多线程同步访问共享变量的最广泛实现方法就是使用关键字synchronized
在这里插入图片描述
上图是JVM内存模型。
我们知道JVM内存结构主要分为三大块:堆、栈和方法区。每个线程都有自己的一个虚拟机栈,栈保存着局部变量以及所有调用的方法的参数和返回值。其他线程无法访问该线程的栈中数据。栈仅能保存基本类型和对象引用,对象是存放在堆中的。堆内存和方法区中的静态变量等数据可以被线程共享,而同步处理针对的正是共享数据。
所以这里的虚拟机栈就可以被认为是每个线程的工作内存,方法区和堆内存可以被认为是主内存

synchronized使用场景

synchronized关键字可以作用于方法或者代码块,最主要有以下几种使用方式,如图:
在这里插入图片描述
synchronized不同的修饰方式,锁的对象也不同:

  1. 修饰实例方法:锁是当前实例对象,进入同步代码前要获得当前实例的锁
public class SynchronizedTest implements Runnable {
    static SynchronizedTest synchronizedTest = new SynchronizedTest();
    //共享资源
    static int i = 0;

    public synchronized void increase() {
        System.out.println(Thread.currentThread().getName()+"已经进入同步方法中");
        i++;
        System.out.println(Thread.currentThread().getName()+"操作后i的值为:"+i);
        System.out.println(Thread.currentThread().getName()+"离开同步方法中");
    }

    @Override
    public void run() {
        increase();
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(synchronizedTest,"线程A");
        Thread thread2 = new Thread(synchronizedTest,"线程B");
        thread1.start();
        thread2.start();

    }
}

这里synchronized修饰的是同步方法,锁住的是实例对象(这里是synchronizedTest对象),所以线程A和线程B会争夺这一个对象,结果如下
在这里插入图片描述
结果分析:线程A先拿到该对象的锁,并进入到同步方法中,当执行完毕后,释放对象的锁,然后线程B才能获得对象锁。

下面的代码稍加改动,将上面争用同一把对象锁,改为不同两个对象锁

public class SynchronizedTest implements Runnable {
    //共享资源
    static int i = 0;

    public synchronized void increase() {
        System.out.println(Thread.currentThread().getName()+"已经进入同步方法中");
        i++;
        System.out.println(Thread.currentThread().getName()+"操作后i的值为:"+i);
        System.out.println(Thread.currentThread().getName()+"离开同步方法中");
    }

    @Override
    public void run() {
        for(int i=0;i<5;i++)
            increase();
    }
    public static void main(String[] args) throws InterruptedException {

        Thread thread1 = new Thread(new SynchronizedTest(),"线程A");
        Thread thread2 = new Thread(new SynchronizedTest(),"线程B");
        thread1.start();
        thread2.start();

    }
}

这里ThreadA访问实例对象obj1的synchronized方法,ThreadB访问实例对象obj2的synchronized方法,这样是允许的,因为两个实例对象锁并不相同。结果两个线程交替执行,不会因争夺同一把锁而发生阻塞。
在这里插入图片描述

  1. 修饰静态方法:锁是当前类的class对象,进入同步代码前要获得当前类对象的锁

这里将同步方法改为静态后,还是new两个不同的实例对象,按照第一种方式,应该不会发生阻塞,但是这里的synchronized修饰静态方法,后锁住的是当前类的class对象(这里为SynchronizedTest.class)

public class SynchronizedTest implements Runnable {
    //共享资源
    static int i = 0;

    public static synchronized void increase() {
        System.out.println(Thread.currentThread().getName()+"已经进入同步方法中");
        i++;
        System.out.println(Thread.currentThread().getName()+"操作后i的值为:"+i);
        System.out.println(Thread.currentThread().getName()+"离开同步方法中");
    }

    @Override
    public void run() {

            increase();
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new SynchronizedTest(),"线程A");
        Thread thread2 = new Thread(new SynchronizedTest(),"线程B");
        Thread thread3 = new Thread(new SynchronizedTest(),"线程C");
        Thread thread4 = new Thread(new SynchronizedTest(),"线程D");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }

在这里插入图片描述
所以不管你new多少个实例对象,而当前类对象就只有一个,所以多线程争用一个类对象的话是会发生阻塞的。

  1. 修饰代码块
    synchronized(this):锁是当前实例对象,
    synchronized(AccountingSync.class): 锁是class对象

monitorenter、monitorexit、ACC_SYNCHRONIZED

这三个单词是代表着synchronize底层实现的核心。我们使用jdk自带工具javap -verbose xxx.class,将字节码文件反汇编
一、synchronized修饰代码块

public void dec1(){
     synchronized (this){
        i--;
     }
 }

反汇编:
在这里插入图片描述
二、synchronized修饰方法

public synchronized void dec(){
        i--;
}

反汇编:
在这里插入图片描述
我们看到synchronized修饰的方法在字节码中添加了一个ACC_SYNCHRONIZED的flags,同步代码块则是在同步代码块前插入monitorenter,在同步代码块结束后插入monitorexit

  • 同步方法:方法级别的同步是隐式的,作为方法调用的一部分。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。
    当调用一个设置了ACC_SYNCHRONIZED标志的方法,执行线程需要先获得monitor锁,然后开始执行方法,方法执行之后再释放monitor锁,当方法不管是正常return还是抛出异常都会释放对应的monitor锁
    在这期间,如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。
    如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。
    在这里插入图片描述
  • 同步块:
    每个对象都与一个monitor相关联。当且仅当拥有所有者时(被拥有),monitor才会被锁定。执行到monitorenter指令的线程,会尝试去获得对应的monitor,如下:
    每个对象维护着一个记录着被锁次数的计数器, 对象未被锁定时,该计数器为0。线程获得monitor(执行monitorenter指令)时,会把计数器设置为1.
    当同一个线程再次获得该对象的锁的时候,计数器再次自增。(这里可以解释的通synchromized为啥是可重入锁)
    当其他线程想获得该monitor的时候,就会阻塞,直到计数器为0才能成功。
    线程执行monitorexit指令,就会让monitor的计数器减一。如果计数器为0,表明该线程不再拥有monitor。其他线程就允许尝试去获得该monitor了。
    在这里插入图片描述
  1. 同步代码块是通过monitorenter和monitorexit来实现,当线程执行到monitorenter的时候要先获得monitor锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。
  2. 同步方法是通过中设置ACC_SYNCHRONIZED标志来实现,当线程执行有ACC_SYNCHRONI标志的方法,需要获得monitor锁。
  3. 每个对象维护一个加锁计数器,为0表示可以被其他线程获得锁,不为0时,只有当前锁的线程才能再次获得锁。
  4. 当monitor的计数器不为0时,synchronized可实现可重入锁。
  5. 同步方法和同步代码块底层都是通过monitor来实现同步的。
    每个对象都与一个monitor相关联,线程可以占有或者释放monitor。

monitor监视器

到目前为止我们还是不知道monitor到底是什么呢?它可以理解为一种同步工具,或者说是同步机制,它通常被描述成一个对象。操作系统的管程是概念原理,ObjectMonitor是它的原理实现。(操作系统中管程是同步机制的封装,可以理解为将各种信号量封装成程序员无需了解底层的API接口).
在Java虚拟机(HotSpot)中,Monitor(管程)是由ObjectMonitor实现的,其主要数据结构如下:

public class ObjectMonitor extends VMObject {
    private static ObjectHeap heap;
    private static long headerFieldOffset;
    private static long objectFieldOffset;
    private static long ownerFieldOffset;
    private static long FreeNextFieldOffset;
    private static CIntegerField countField;
    private static CIntegerField waitersField;
    private static CIntegerField recursionsField;

其中最重要的是:
在这里插入图片描述
下图为获取/释放monitor示意图
在这里插入图片描述

  1. 想要获取monitor的线程,首先会进入_EntryList队列)。
  2. 当某个线程获取到对象的monitor后,进入_Owner区域,设置为当前线程,同时计数器_count加1。
  3. 如果线程调用了wait()方法,则会进入_WaitSet队列。它会释放monitor锁,即将_owner赋值为null,_count自减1,进入_WaitSet队列阻塞等待。
  4. 如果其他线程调用 notify() / notifyAll(),会唤醒_WaitSet中的某个线程,该线程再次尝试获取monitor锁,成功即进入_Owner区域。
  5. 同步方法执行完毕了,线程退出临界区,会将monitor的owner设为null,并释放监视锁。

下面我们还用弄清对象如何与monitor发生关系的?
我们知道Java中一切都是对象,Hotpo将对象模型拆为oop和klass,下面表示JVM内存中如何创建一个实例对象的。

class Model
{
    public static int a = 1;
    public int b;

    public Model(int b) {
        this.b = b;
    }
}

public static void main(String[] args) {
    int c = 10;
    Model modelA = new Model(2);
    Model modelB = new Model(3);
}

在这里插入图片描述在这里插入图片描述
通过这两张图,就可以理解到实例对象的对象头是存储在堆空间的,对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)。
Mark Word 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。
我们知道synchronized是重量级锁,那么根据上图所示,MarkWord的前28位表示monitor的地址

java虚拟机对Synchronized的优化

JVM优化synchronized运行的机制,当JVM检测到不同的竞争情况时,会自动切换到适合的锁实现

  1. 当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS操作,在对象头上的Mark Word部分位设置线程ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
  2. 有竞争出现时,当有另外的线程试图锁定某个已经被偏斜锁锁定的对象,jvm就会撤销revoke偏斜锁,并切换到轻量级锁。轻量级锁依赖CAS操作Mark Word来试图获取锁,如果成功,就使用轻量级锁,否则继续升级未重量级锁

参考文献

1. java中synchronized的底层实现
2. Java面试之Synchronized解析

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值