并发编程---3、Synchronized原理分析

目录

一、概念

二、用法

2.1 同步方法

2.2 同步代码块

三、synchronized 原理分析

1、线程堆分析(互斥)

2、指令分析

3、使用synchronized 注意的问题

四、JVM对synchronized 的优化


一、概念

Synchronized是利用锁的机制来实现同步的。锁的机制有如下两种特性:

1、互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块进行访问。互斥性我们也往往成为操作的原子性。

2、可见性:必须确保锁在释放之前,对共享变量所做的修改对于随后获取锁的另一个线程是可见的(即在获取锁时应获取最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作从而引起不一致。

二、用法

2.1 同步方法

synchronized 修饰方法,该方法就会变成同步的了,同一时刻只能有一个线程去访问该方法。

1、同步非静态方法

public synchronized void methodName(){

........................

}

2、同步静态方法

public synchronized static void methodName(){

........................

}

2.2 同步代码块

同步代码块,就是在你想同步的代码逻辑上面加上synchronized 一般的写法主要是下面2种

1、获取对象锁

锁的是类的实例,堆里面的实例

获取对象的锁的写法如下所示,在java中每个对象都会有一个monitor对象,这个对象其实就是java对象的锁。通常会被称为‘内置锁’ 或 ”对象锁“ 。 类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。该写法应用在非静态方法中

public  void methodName(){
    synchronized (this){
        XXXXXXXXXXXXXX
    }
}

private final Object object = new Object();
public  void methodName(){
    synchronized (object){
        XXXXXXXXXXXXXX
    }
}

 

 

2、获取类锁

锁的是类的字节码

在java中,针对每个类也有一个锁,可以称为“ 类锁 ”,类锁实际上是通过对象锁实现的,即类的Class对象锁。每个类只有一个class对象,所以每个类只有一个类锁。 该写法应用在静态方法中。

public static  void methodName(){
    synchronized (类名.class){
        XXXXXXXXXXXXXX
    }
}

 

对象的monitor是如何进行监视的呢? 当该线程开始synchronized 块时,计数器+1,退出synchronized 块时,计数器-1。具体步骤描述如下:

1) 某一线程占有这个对象时,先查看monitor的计数器是不是0,如果是0表示没有线程占用,这个时候该线程可以占有这个对象,并且对这个对象的monitor+1

2) 如果不为0,表示这个对象已经被其他线程占有,该线程等待。

3) 当其他线程释放对象的占有权时,monitor-1

4) 同一线程可以对同一个对象多次加锁,重入性,重入锁就是可以多次加锁具有重入性。

重入性什么意思呢?下面具体解释一下。

例1: 嵌套synchronized ()

例2 :

public class Demo1 {
    public synchronized void functionA(){
        System.out.println("iAmFunctionA");
        functionB();
    }
    public synchronized void functionB(){
        System.out.println("iAmFunctionB");
    }
}

 

如果synchronized 没有重入性的话。 functionA() 和functionB()都是同步方法,当线程进入A会获得该类的对象锁,A中调用了B,B也是同步的,因此该线程需要再次获得该对象锁。但是JVM会认为这个线程已经获得了此对象锁,不能再次获取了,从而无法调用B方法,进而产生死锁。所以重入性非常重要。

Monitor

Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。

Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

现在话题回到synchronized,synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。

修饰实例方法:对当前实例加锁,进入同步代码前要获得当前实例的锁。

 

public class SynchronizedTest {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            //创建五个实例,会发现是并行的跟没加锁一样。,但是如果这个
            //new在for循环外面,就会是串行的,因为只有一个实例对象
            SynchronizedTest synchronizedTest = new SynchronizedTest();
            Thread thread = new Thread(() -> {
                synchronizedTest.testSynchronizedMethod();
            });
            thread.start();
        }
    }
     public synchronized void testSynchronizedMethod() {
        System.out.println("testSynchronizedMethod-" + Thread.currentThread().getName());
    }
}

修饰静态方法:对当前Class 对象加锁,进入同步代码前要获得当前Class 对象的锁

从锁的粗粒度来对比,锁类 Class 对象的粒度大于锁实例对象。

比如Person p=new Person

这里的对象叫实例对象

比如Class p=Person.class

这里的对象叫类对象

public class SynchronizedTest {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            //创建五个实例,会发现是串行的,
            //因为加锁是对象锁。实例锁和对象锁有区别,别弄混了。
            //加上 static 不是锁实例对象,而是锁 Class 对象。
            SynchronizedTest synchronizedTest = new SynchronizedTest();
            Thread thread = new Thread(() -> {
                synchronizedTest.testSynchronizedStaticMethod();
            });
            thread.start();
        }
    }
     public synchronized static void testSynchronizedStaticMethod() {
        System.out.println("testSynchronizedStaticMethod-" + Thread.currentThread().getName());
    }
}

 

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

给定的对象是:this、object、xxx.class 这三种。

this : 锁住当前对象,和原来使用同步实例方式一样,锁住当前实例对象;

object:创建一个object对象,所有线程去竞争一个资源,跟class锁一样。

xxx.class : 锁类对象,跟静态方法一样,只不过修饰代码块更灵活,可以在代码中小范围加锁。

synchronized 在运行过程中如果遇到异常,会自动释放锁的。

 

三、synchronized 原理分析

1、线程堆分析(互斥)

当我们写一个例子,简单的调用四个线程,每个sleep一段时间,然后在cmd中输入jconsole,就会调用jdk自带的工具来看线程。

 

public class SynDemo {
   //修饰方法
    public synchronized  void accessResource1(){
        try {
            TimeUnit.SECONDS.sleep(30);
            System.out.println(Thread.currentThread().getName()+"is running");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
//修饰代码块
public void accessResource2(){
    //synchronized (SynDemo.class)
    synchronized (this){
        try {
            TimeUnit.SECONDS.sleep(3);
            System.out.println(Thread.currentThread().getName()+"is running");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
    public static void main(String[] args) {
        SynDemo synDemo = new SynDemo();
        for(int i=0;i<4;i++){
            new Thread(synDemo::accessResource1).start();
        }
    }
}

 

通过这个可以看到,当前线程在执行的时候状态是TIMED_WAITING,而其他线程的状态就是BLOCKED状态。如果你不加synchronized 你会看到都是TIMED_WAITING状态。

 

利用jstack命令查看效果是一样的。jstack pid (pid可以从任务管理器里面找下。所以睡眠时间长点,因为你找这个id估计得花段时间)

 

2、指令分析

开始提到的monitor大家可能会问,这个怎么看到呢,下面我们把文件反编译成字节码的形式给大家看下

进入到target下面class的文件目录中,运行javap -v 文件名 (不要带.class)控制台就会输出字节码。

可以看到 当修饰方法的时候,synchronized 是用一个信号量来控制同步的 ACC_SYNCHRIONIZED

 

synchronized 修饰代码块的时候,是使用Monitor的monitorenter 和 monitorexit来控制同步的,aload_n 就是这块代码执行区域

这个大家一定要回去自己去试下。大家或许会发现有2个monitorexit,第一个是正常的退出出口,第二个是异常退出的出口。程序里面不是有个try cache嘛。

到这里有的同学会问synchronized 括号里面的参数到底有什么用啊? 大家可以理解成括号里面的参数就是:锁标记。

Monitorenter的操作就是先lock,当一个线程占有,其他线程请求时就会进入BLOCK状态,直到monitor的计数器为0

Monitorexit 就是让monitor的计数器减1,为0时就可以解锁了。

 

3、使用synchronized 注意的问题

(1)与monitor关联的对象不能为空

(2)synchronized 作用域很大

(3)不同的monitor不要企图锁相同的方法

(4)多个锁的交叉会导致死锁

(5)不公平的

 

四、JVM对synchronized 的优化

Java SE1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。对synchronized 进行了优化。JDK 1.6 之后为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁。

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级为重量级锁,为了提高获得锁和释放锁的效率,锁的升级是单向的,即只有升级而不存在降级。

synchronized是悲观锁,在操作同步资源之前需要给同步资源先加锁,这把锁就是存在Java对象头里的。

 

一个对象实例包括:对象头、实例变量、填充数据。其中对象头就是加锁的基础。对象头包括:

1、Mark word

2、klass word 存的是指向元数据的指针,元数据就是类被JVM加载进内存中方法区的字节码,也可以理解为指向了class文件在jvm中存储的地址。

下一节专门讲下对象头,这个东西也非常重要。

实例数据 是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。

填充数据:它仅仅起着占位符的作用,因为jvm要求对象起始地址必须是8字节的整数倍。

64位虚拟机在不同状态下markword结构如下图所示:synchronized 默认是重量级锁,如果配置的话也可以转换成轻量级锁或者偏向锁。

下面介绍下主要的锁状态:

1、无锁状态:没有加锁

2、偏向锁:很多线程去竞争的时候,第一次占有它的线程更容易占用成功,偏向第一次占有它的线程,也就是说第一次占有过它的线程下一次就更容易成功占有,所以叫偏向锁。

在对象第一次被某一线程占有的时候markdown的锁信息会这样操作:是否偏向锁标识位置1,锁标志01,写入线程ID。 在运行过程中,当其他的线程抢占锁的时候,持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁升级到轻量级锁。

可以使用CAS算法去升级偏向锁,偏向锁与无锁状态的执行时间很接近,所以竞争不激烈的情况可以使用偏向锁。

3、轻量级锁:线程有交替使用,互斥性不是很强的时候可以去使用它,CAS检测失败就将锁标记00

4、重量级锁:强互斥,等待时间长

5、自旋锁:如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(执行几次空循环),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗

资源争夺中锁 从偏向锁升级到轻量级锁,再次升级到重量级锁。

Java 虚拟机在 JIT 编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,这就是锁消除。这个过程就叫做 锁消除。

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值