synchronized使用以及原理-透过现象看本质

1. 前言

说到synchronized大部分java开发者一定会脱口而出:保证同步,但是进一步深究其底层原理,每个人的理解就不可同日耳语了,下边我们就来深入了解下synchronized关键字。

2. 使用案例分析

synchronized可以修饰:
#1 普通方法;
#2 代码块;
#3 静态方法;

首先,我们通过简单的代码执行判断顺序:

2.1 synchronized修饰普通方法:

#代码1
	public synchronized void method1(){
        System.out.println("method1 start" + Thread.currentThread().getName());
        try{
            System.out.println("method1 execute" + Thread.currentThread().getName());
            Thread.sleep(3000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("method1 end"+ Thread.currentThread().getName());
    }
    public synchronized void method2(){
        System.out.println("method2 start"+ Thread.currentThread().getName());
        try{
            System.out.println("method2 execute"+ Thread.currentThread().getName());
            Thread.sleep(3000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("method2 end"+ Thread.currentThread().getName());
    }
	//同一个对象2个方法 
    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }

测试结果:
在这里插入图片描述

#代码2
	public synchronized void method1(){
        System.out.println("method1 start" + Thread.currentThread().getName());
        try{
            System.out.println("method1 execute" + Thread.currentThread().getName());
            Thread.sleep(3000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("method1 end"+ Thread.currentThread().getName());
    }
    public synchronized void method2(){
        System.out.println("method2 start"+ Thread.currentThread().getName());
        try{
            System.out.println("method2 execute"+ Thread.currentThread().getName());
            Thread.sleep(3000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("method2 end"+ Thread.currentThread().getName());
    }
	//2个对象2个方法 
    public static void main(String[] args) {
        final SynchronizedTest test1 = new SynchronizedTest();
        final SynchronizedTest test2 = new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test1.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test2.method2();
            }
        }).start();
    }

测试结果:
在这里插入图片描述
结论:synchronized修饰普通方法时,锁的是对象的某个实例,对于同一个实例,线程2需要等待线程1的method1执行完成才能开始执行method2方法;对于不同的实例之间互不影响。

2.2 synchronized修饰普通方法:

# 代码3
	public void method1(){
        synchronized (this){
            System.out.println("method1 start");
            try{
                System.out.println("method1 execute");
                Thread.sleep(3000);
            }catch (Exception e){
                e.printStackTrace();
            }
            System.out.println("method1 end");
        }
    }

    public void method2(){
        synchronized (this){
            System.out.println("method2 start");
            try{
                System.out.println("method2 execute");
                Thread.sleep(3000);
            }catch (Exception e){
                e.printStackTrace();
            }
            System.out.println("method2 end");
        }
    }
	//同一对象2个方法
    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }

测试结果:
在这里插入图片描述

# 代码4
	public void method1(){
        synchronized (this){
            System.out.println("method1 start");
            try{
                System.out.println("method1 execute");
                Thread.sleep(3000);
            }catch (Exception e){
                e.printStackTrace();
            }
            System.out.println("method1 end");
        }
    }

    public void method2(){
        synchronized (this){
            System.out.println("method2 start");
            try{
                System.out.println("method2 execute");
                Thread.sleep(3000);
            }catch (Exception e){
                e.printStackTrace();
            }
            System.out.println("method2 end");
        }
    }
	//不同对象2个方法
	public static void main(String[] args) {
        final SynchronizedTest test1 = new SynchronizedTest();
        final SynchronizedTest test2 = new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test1.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test2.method2();
            }
        }).start();
    }

测试结果:
在这里插入图片描述
结论:synchronized修饰代码块时类似synchronized修饰普通方法时,锁的是对象的某个实例,对于同一个实例,线程2需要等待线程1的method1执行完成才能开始执行method2方法;不同的实例之间互不影响。

2.3 synchronized修饰静态方法:

#代码5
	public static synchronized void method1() {
        System.out.println("method1 start");
        try {
            System.out.println("method1 execute");
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("method1 end");
    }

    public static synchronized void method2() {
        System.out.println("method2 start");
        try {
            System.out.println("method2 execute");
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("method2 end");
    }
	// 同一个类对象锁的2个方法
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                SynchronizedTest.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                SynchronizedTest.method2();
            }
        }).start();
    }

测试结果:
在这里插入图片描述

#代码6
    public static synchronized void method1() {
        System.out.println("method1 start");
        try {
            System.out.println("method1 execute");
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("method1 end");
    }

    public synchronized void method2() {
        System.out.println("method2 start");
        try {
            System.out.println("method2 execute");
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("method2 end");
    }
	// 一个是类对象的锁,一个是类实例对象的锁,2个方法
    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                SynchronizedTest.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }

测试结果:
在这里插入图片描述

#代码7
	public static synchronized void method1() {
        System.out.println("method1 start" + Thread.currentThread().getName());
        try {
            System.out.println("method1 execute" + Thread.currentThread().getName());
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("method1 end" + Thread.currentThread().getName());
    }
    
    // 一个是类对象的锁,一个是类实例对象的锁,2个方法
    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                SynchronizedTest.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();
    }

执行结果:
在这里插入图片描述

结论:synchronized修饰静态方法时,锁的是类对象,跟对象实例的锁是不同的锁,互不冲突,但是如果是访问同一个方法时,只要是这个类产生的对象,都是产生互斥同步执行。

使用总结:

Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。
**Synchronized的作用主要有三个**:
(1)确保线程互斥的访问同步代码
(2)保证共享变量的修改能够及时可见
(3)有效解决重排序问题。
/*
 *  非静态方法:
 *  给对象加锁(可以理解为给这个对象的内存上锁,注意 只是这块内存,其他同类对象都会有各自的内存锁),这时候
 * 在其他一个以上线程中执行该对象的这个同步方法(注意:是该对象)就会产生互斥
 
 *  静态方法: 
 * 相当于在类上加锁(*.class 位于代码区,静态方法位于静态区域,这个类产生的对象公用这个静态方法,所以这块
 * 内存,N个对象来竞争), 这时候,只要是这个类产生的对象,在调用这个静态方法时都会产生互斥
 */
**从语法上讲,Synchronized总共有三种用法**:
(1)修饰普通方法-锁的是实例对象
(2)修饰静态方法-锁的是类对象
(3)修饰代码块-锁的是实例对象

3. 原理解析

下边我们通过反编译了解synchronized的原理
同步代码块源码:

package com.test.yy.base;

/**
 * @author: system
 * @date: 2020-12-16 18:08
 **/
public class TestJavap {
    public void method(){
        synchronized (this){
            System.out.println("sync method");
        }
    }
}

同步代码块反编译结果:
在这里插入图片描述
关于monitorenter、monitorexit两条指令的作用,我们直接参考JVM规范中描述:

monitorenter :

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.

• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.

• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.

这段话的大概意思为:

  1. 每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
  2. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
  3. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
  4. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

monitorexit:
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

这段话的大概意思为:

  1. 执行monitorexit的线程必须是objectref所对应的monitor的所有者。
  2. 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

然后我们再看看同步方法的反编译结果:

package com.test.yy.base;

/**
 * @author: system
 * @date: 2020-12-16 18:08
 **/
public class TestJavap {
    public synchronized void method(){
        System.out.println("sync method");
    }
}

反编译结果:
在这里插入图片描述
从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

4. 总结

Synchronized是Java并发编程中最常用的用于保证线程安全的方式,其使用相对也比较简单。但是如果能够深入了解其原理,对监视器锁等底层知识有所了解,一方面可以帮助我们正确的使用Synchronized关键字,另一方面也能够帮助我们更好的理解并发编程机制,有助我们在不同的情况下选择更优的并发策略来完成任务。对平时遇到的各种并发问题,也能够从容的应对。

5. synchronized的监视器锁了解

ps:synchronized的锁在java6的时候有进行优化:无锁-> 轻量级锁 -> 偏向锁 -> 重量级锁;这里我们主要讨论重量级锁。

偏向锁:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁
轻量级锁:轻量级锁考虑的是,多个线程不会在同一时刻来竞争同一把锁。
重量级锁:多个线程同一时刻竞争一把锁

说到锁我们先来了解下java中的对象内存模型

对象内存模型
对象头
真实数据
内存补齐

如果对象是数组类型,则虚拟机用3个字宽存储对象头(类型指针、对象标记、数组长度),如果对象是非数组类型,则用2字宽存储对象头(类型指针、对象标记)。在32位虚拟机中,1字宽等于4字节,即32bit
在这里插入图片描述

  • 类型指针:JVM根据该指针确定该对象是哪个类的实例化对象。
  • Mark Word:用于存储对象自身运行时的数据,如hashcode,GC分代年龄,锁状态标志位,线程持有的锁,偏向线程ID,等等。
    在这里插入图片描述

Monitor监视器锁,也就是synchronized对象锁,锁标识为10,其中指针指向monitor对象(监视器锁)的起始地址。每个对象都存在一个monitor与之关联,对象与其monitor之间也存在着多种实现方式(monitor可以与对象一起创建或者销毁或当前线程试图获取锁时自动生成),但一个monitor被某线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是有ObjectMonitor实现的,其主要数据结构如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;        // 记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;    // 记录当前持有锁的线程ID
    _WaitSet      = NULL;    // 等待池:记录调用 wait() 方法并还未被通知的线程
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;    // 锁池:记录所有阻塞等待锁的线程(阻塞队列)
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
}

ObjectMonitor 中有两个队列,_WaitSet 和 _EntryList ,用来保存ObjectWaiter 对象列表;每个等待锁的线程都会被封装成ObjectWaiter 对象,_owner 指向持有ObjectMonitor 对象的线程;
当多个线程同时访问同一同步代码块或者同步方法,步骤如下:

  1. 线程首先会进入 _EntryList 队列,
  2. 当线程获取到monitor 后进入_Owner 区域并把 monitor中的 _Owner 变量设置为当前线程,同时monitor 中的计数器count 加1;
  3. 若线程调用wait() 方法,将释放当前持有的monitor,_owner变量恢复为null,count 自减 1 ,同时该线程进入_WaitSet 集合中等待被唤醒;
  4. 若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁);

采用Synchronized给对象加锁会使线程阻塞,因而会造成线程状态的切换,而线程状态的切换必须要操作系统来执行,因此需要将用户态切换为内核态,这个切换的过程是十分耗时的都需要操作系统来帮忙,有可能比用户执行代码的时间还要长。

所以,monitor对象存在于每一个java对象的对象头(存储指针的指向),synchronized锁便是通过这种方式获取的,也是为什么java中任何对象都可以作为锁的原因,同时也是 notify/notifyAll/wait 方法等存在于顶级对象Object中的原因。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值