java多线程编程--synchronized

参考:https://blog.csdn.net/sinat_40770656/article/details/113784150
https://www.jianshu.com/p/6d0f135a89cd

synchronized锁锁的是啥?

永远都是对象

是:与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。

synchronized同步原理
数据同步需要依赖锁,那锁的同步又依赖谁?synchronized给出的答案是在软件层面依赖JVM,而j.u.c.Lock给出的答案是在硬件层面依赖特殊的CPU指令。

1、synchronized 同步语句块原理

public class SynchronizedDemo {
	public void method() {
		synchronized (this) {
			System.out.println("synchronized 代码块");
		}
	}
}

通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行javap -c -s -v -l SynchronizedDemo.class。
在这里插入图片描述
从图中可以看出:

synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。

当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。

在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitor实现的。每个对象中都内置了一个 ObjectMonitor对象。

另外,wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。

在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

2、synchronized 修饰方法原理

public class SynchronizedDemo2 {
	public synchronized void method() {
		System.out.println("synchronized 方法");
	}
}

在这里插入图片描述
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

简单总结一下:

synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。

不过两者的本质都是对对象监视器 monitor 的获取。

synchronized同步概念

Java对象头

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。
在这里插入图片描述
对象头这部分在对象的最前端,包含两部分或者三部分:Mark Words、Klass Words,如果对象是一个数组,那么还可能包含第三部分:数组的长度。
在这里插入图片描述
synchronized用的锁是存在Java对象头里的。

Hotspot 有两种对象头:

  1. 数组类型,如果对象是数组类型,则虚拟机用3个字宽 (Word)存储对象头
  2. 非数组类型:如果对象是非数组类型,则用2字宽存储对象头。

对象头由两部分组成

  • Mark Word:存储自身的运行时数据,例如 HashCode、GC 年龄、锁相关信息等内容。
  • Klass Pointer:类型指针指向它的类元数据的指针。

64 位虚拟机 Mark Word 是 64bit,在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。

监视器(Monitor)

任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是 基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。

  1. MonitorEnter指令:插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁;
  2. MonitorExit指令:插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit;

那什么是Monitor?可以把它理解为 一个同步工具,也可以描述为 一种同步机制,它通常被 描述为一个对象。

与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。

也就是通常说Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的。

synchronized优化

从JDK5引入了现代操作系统新增加的CAS原子操作( JDK5中并没有对synchronized关键字做优化,而是体现在J.U.C中,所以在该版本concurrent包有更好的性能 ),从JDK6开始,就对synchronized的实现机制进行了较大调整,包括使用JDK5引进的CAS自旋之外,还增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略。由于此关键字的优化使得性能极大提高,同时语义清晰、操作简单、无需手动关闭,所以推荐在允许的情况下尽量使用此关键字,同时在性能上此关键字还有优化的空间。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。

在这里插入图片描述

得先了解一下:CAS

在计算机科学中,比较和交换(Conmpare And Swap)是用于实现多线程同步的原子指令。它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。这是作为单个原子操作完成的。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成。

核心思想
一个 CAS 涉及到以下操作: 我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。

类似于 CAS 的指令允许算法执行读-修改-写操作,而无需害怕其他线程同时 修改变量,因为如果其他线程修改变量,那么 CAS 会检测它(并失败),算法 可以对该操作重新计算。

比较 A 与 V 是否相等。(比较) 如果比较相等,将 B 写入 V。(交换) 返回操作是否成功。 当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS 其实是一个乐观锁。

在这里插入图片描述
如上图中,主存中保存V值,线程中要使用V值要先从主存中读取V值到线程的工作内存A中,然后计算后变成B值,最后再把B值写回到内存V值中。多个线程共用V值都是如此操作。CAS的核心是在将B值写入到V之前要比较A值和V值是否相同,如果不相同证明此时V值已经被其他线程改变,重新将V值赋给A,并重新计算得到B,如果相同,则将B值赋给V。

如果不使用CAS机制,看看存在什么问题:

假如V=1,现在Thread1要对V进行加1,Thread2也要对V进行加1,首先Thread1读取V=1到自己工作内存A中此时A=1,假设Thread2此时也读取V=1到自己的工作内存A中,分别进行加1操作后,两个线程中B的值都为2,此时写回到V中时发现V的值为2,但是两个线程分别对V进行加处理结果却只加了1有问题。

**ABA问题。**因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。

  1. 在变量前面追加版本号:每次变量更新就把版本号加1,则A-B-A就变成1A-2B-3A。

  2. atomic包下的AtomicStampedReference类:其compareAndSet方法首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用的该标志的值设置为给定的更新值。

synchronized 关键字之锁的升级

synchronized 关键字之锁的升级(偏向锁->轻量级锁->重量级锁)

synchronized 的使用

当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。

 一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

 二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。

 三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。

 四、第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。

 五、以上规则对其它对象锁同样适用.

1、锁方法:

package com.example.dtest.synchronizedtest;

import cn.hutool.core.date.DateUtil;

import java.util.Date;

public class SynchronizedMethod implements Runnable {

    public static void main(String[] args) {

        SynchronizedChunk synchronizedChunk = new SynchronizedChunk();
        Thread thread1 = new Thread(synchronizedChunk,"sync-thread-1");
        Thread thread2 = new Thread(synchronizedChunk,"sync-thread-2");
        thread1.start();
        thread2.start();

    }

    //    全局变量 ,创建一个计数器
    private static int counter = 1;

    @Override
    public synchronized void run() {

        Date startDate = DateUtil.date();
        for(int i = 0; i<5 ;i++){
            try{

                System.out.println("线程:"+ Thread.currentThread().getName() + "当前计数器:" +(counter++));
                System.out.println("开始时间 :"+ startDate + "当前时间:" + DateUtil.date());
                System.out.println();
                Thread.sleep(1000);

            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }

    }
}

在这里插入图片描述

2、锁代码块:

package com.example.dtest.synchronizedtest;


import cn.hutool.core.date.DateUtil;

import java.util.Date;

public class SynchronizedChunk implements Runnable{

    public static void main(String[] args) {

        SynchronizedChunk synchronizedChunk = new SynchronizedChunk();
        Thread thread1 = new Thread(synchronizedChunk,"sync-thread-1");
        Thread thread2 = new Thread(synchronizedChunk,"sync-thread-2");
        thread1.start();
        thread2.start();

    }

//    全局变量 ,创建一个计数器
    private static int counter = 1;


    @Override
    public void run() {

        Date startDate = DateUtil.date();
        synchronized (this){
            for(int i =0; i < 5; i++){

                try{

                    System.out.println("线程 :" + Thread.currentThread().getName() + " 当前计数器 :" + (counter++));
                    System.out.println("开始时间 :" + startDate + " 当前时间 :" + DateUtil.date());
                    System.out.println();
                    Thread.sleep(1000);

                }catch (InterruptedException e){
                    e.printStackTrace();
                }

            }

        }

    }
}

在这里插入图片描述

3、锁静态方法中的代码块:

静态方法所得对象不是在堆里面new的了,而是ClassName.class方法才能调用,所以不能用this ,但还是锁的时对象!

JVM 拿到编译器编译好的 class 文件后,首先会把文件载入到内存中,class 文件当然会有自己的格式,所以需要由 ClassLoader 来解析文件的内容,这个解析出来的内容会用一个 Class 类的实例 - Class object 来表示,这个 object 可以通过 Java 的 ClassName.class来获取。

也就是说,Class object 是一个 Class 类型的实例(instance),而对象是一个 ClassName 的 instance。Class 和 ClassName都是类型,ClassName是由 class 关键字定义的,而Class是内置类型。

因此成员方法的synchronized method 就等价于 synchronized (this) block,即下面两种方式是等价的。

package com.example.dtest.synchronizedtest;

import cn.hutool.core.date.DateUtil;

import java.util.Date;

public class SynchronizedStatic implements Runnable{

    public static void main(String[] args) {

        SynchronizedStatic synchronizedStatic = new SynchronizedStatic();
        Thread thread1 = new Thread(synchronizedStatic,"sync-thread-1");
        Thread thread2 = new Thread(synchronizedStatic,"sync-thread-2");
        thread1.start();
        thread2.start();

    }
    //    全局变量 ,创建一个计数器
    private static int counter = 1;

    @Override
    public void run() {

        runTest();

    }

    public static void runTest(){

        Date startDate = DateUtil.date();
        synchronized (SynchronizedStatic.class){
        for(int i = 0; i<5 ;i++){
            try{

                System.out.println("线程:"+ Thread.currentThread().getName() + "当前计数器:" +(counter++));
                System.out.println("开始时间 :"+ startDate + "当前时间:" + DateUtil.date());
                System.out.println();
                Thread.sleep(1000);

            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        }

    }


}

4、从一个全局不变的对象中拿到锁:

package com.example.dtest.synchronizedtest;

import cn.hutool.core.date.DateUtil;

import java.util.Date;

public class SynchronizedObject implements Runnable{

    //    全局变量 ,创建一个计数器
    private static int counter = 1;
    //    局部唯一变量,提供唯一的锁的对象
    final Object lockObj = new Object();


    public static void main(String[] args) {


        SynchronizedObject synchronizedObject = new SynchronizedObject();
        Thread thread1 = new Thread(synchronizedObject,"sync-thread-1");
        Thread thread2 = new Thread(synchronizedObject,"sync-thread-2");
        thread1.start();
        thread2.start();

    }

    @Override
    public void run() {
        runTest();
    }

    public void runTest(){

        Date startDate = DateUtil.date();
        synchronized (lockObj){
            for(int i = 0; i<10000 ;i++){
//                try{

                    System.out.println("线程:"+ Thread.currentThread().getName() + "当前计数器:" +(counter++));
                    System.out.println("开始时间 :"+ startDate + "当前时间:" + DateUtil.date());
                    System.out.println();
//                    Thread.sleep(1000);

//                }catch (InterruptedException e){
//                    e.printStackTrace();
//                }
            }
        }

    }

}

在这里插入图片描述
这个代码可以充分理解到synchronized代码块底层是在这段代码上得到一个固定对象的监视器,判断是否有资格执行这段代码。
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值