首页 > Java > java教程 > 正文

Java中的synchronized关键字是用来实现线程同步的

WBOY
发布: 2023-04-27 14:01:07
转载
950人浏览过

一、synchronized实现锁的表现形式

  1. 修饰实例方法,对于普通同步方法,锁是当前的实例对象

  2. 修饰静态方法,对于静态同步方法,锁是当前的Class对象

  3. 修饰方法代码块,对于同步方法块,锁是synchronized括号里面配置的对象!

当一个线程试图访问同步代码块的时候,就必须得到锁,完成后(或者出现异常),就必须释放锁。那么锁究竟存在什么地方呢?我们一块来探究!

不过,相信,既然大家能够找到这篇文章,相信大家对他的使用早已了熟于心,我们对于使用,以及为什么多线程情况下,数据会出现错乱情况,不做详细的解释!只把他的几种使用方式列出,供参考!

立即学习Java免费学习笔记(深入)”;

①修饰实例方法

修饰实例方法,对于普通同步方法,锁是当前的实例对象

这个没得说,使用的同一个实例,添加上synchronized后,线程需要排队,完成一个原子操作,但是注意前提是使用的同一个实例,他才会生效!

正例:

/**<br/> * @author huangfu<br/> */<br/>public class ExploringSynchronized implements Runnable {<br/>    /**<br/>     * 共享资源(临界资源)<br/>     */<br/>    static int i=0;<br/>    public synchronized void add(){<br/>        i++;<br/>    }<br/><br/>    @Override<br/>    public void run() {<br/>        for (int j = 0; j < 100000; j++) {<br/>            add();<br/>        }<br/>    }<br/><br/>    public static void main(String[] args) throws InterruptedException {<br/>        ExploringSynchronized exploringSynchronized = new ExploringSynchronized();<br/>        Thread t1 = new Thread(exploringSynchronized);<br/>        Thread t2 = new Thread(exploringSynchronized);<br/>        t1.start();<br/>        t2.start();<br/>        //join 主线程需要等待子线程完成后在结束<br/>        t1.join();<br/>        t2.join();<br/>        System.out.println(i);<br/><br/>    }<br/>}
登录后复制

反例:

/**<br/> * @author huangfu<br/> */<br/>public class ExploringSynchronized implements Runnable {<br/>    /**<br/>     * 共享资源(临界资源)<br/>     */<br/>    static int i=0;<br/>    public synchronized void add(){<br/>        i++;<br/>    }<br/><br/>    @Override<br/>    public void run() {<br/>        for (int j = 0; j < 100000; j++) {<br/>            add();<br/>        }<br/>    }<br/><br/>    public static void main(String[] args) throws InterruptedException {<br/>        Thread t1 = new Thread(new ExploringSynchronized());<br/>        Thread t2 = new Thread(new ExploringSynchronized());<br/>        t1.start();<br/>        t2.start();<br/>        //join 主线程需要等待子线程完成后在结束<br/>        t1.join();<br/>        t2.join();<br/>        System.out.println(i);<br/><br/>    }<br/>}
登录后复制

这种,即使你在方法上加上了synchronized也无济于事,因为,对于普通同步方法,锁是当前的实例对象!实例对象都不一样了,那么他们之间的锁自然也就不是同一个!

②修饰静态方法

修饰静态方法,对于静态同步方法,锁是当前的Class对象

从定义上可以看出来,他的锁是类对象,那么也就是说,以上面那个类为例:普通方法的锁对象是 new ExploringSynchronized()而静态方法对应的锁对象是ExploringSynchronized.class所以对于静态方法添加同步锁,即使你重新创建一个实例,它拿到的锁还是同一个!

package com.byit.test;<br/><br/>/**<br/> * @author huangfu<br/> */<br/>public class ExploringSynchronized implements Runnable {<br/>    /**<br/>     * 共享资源(临界资源)<br/>     */<br/>    static int i=0;<br/>    public synchronized static void add(){<br/>        i++;<br/>    }<br/><br/>    @Override<br/>    public void run() {<br/>        for (int j = 0; j < 100000; j++) {<br/>            add();<br/>        }<br/>    }<br/><br/>    public static void main(String[] args) throws InterruptedException {<br/>        Thread t1 = new Thread(new ExploringSynchronized());<br/>        Thread t2 = new Thread(new ExploringSynchronized());<br/>        t1.start();<br/>        t2.start();<br/>        //join 主线程需要等待子线程完成后在结束<br/>        t1.join();<br/>        t2.join();<br/>        System.out.println(i);<br/><br/>    }<br/>}
登录后复制

当然,结果是我们期待的  200000

③修饰方法代码块

修饰方法代码块,对于同步方法块,锁是synchronized括号里面配置的对象!

package com.byit.test;<br/><br/>/**<br/> * @author huangfu<br/> */<br/>public class ExploringSynchronized implements Runnable {<br/>    /**<br/>     * 锁标记<br/>     */<br/>    private static final String LOCK_MARK = "LOCK_MARK";<br/>    /**<br/>     * 共享资源(临界资源)<br/>     */<br/>    static int i=0;<br/>    public void add(){<br/>        synchronized (LOCK_MARK){<br/>            i++;<br/>        }<br/>    }<br/><br/>    @Override<br/>    public void run() {<br/>        for (int j = 0; j < 100000; j++) {<br/>            add();<br/>        }<br/>    }<br/><br/>    public static void main(String[] args) throws InterruptedException {<br/>        Thread t1 = new Thread(new ExploringSynchronized());<br/>        Thread t2 = new Thread(new ExploringSynchronized());<br/>        t1.start();<br/>        t2.start();<br/>        //join 主线程需要等待子线程完成后在结束<br/>        t1.join();<br/>        t2.join();<br/>        System.out.println(i);<br/><br/>    }<br/>}
登录后复制

对于同步代码块,括号里面是什么,锁对象就是什么,里面可以使用this  字符串  对象等等!

二、synchronized的底层实现

java中synchronized的实现是基于进入和退出的 Monitor对象实现的,无论是显式同步(修饰代码块,有明确的monitorenter和 monitorexit指令)还是隐式同步(修饰方法体)!

需要注意的是,只有修饰代码块的时候,才是基于monitorenter和 monitorexit指令来实现的;修饰方法的时候,是通过另一种方式实现的!我会放到后面去说!

在了解整个实现底层之前,我还是希望你能够大致了解一下对象在内存中的结构详情!

java中的synchronized是什么

  • 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

  • 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。

这两个概念,我们简单理解就好!我们今天并不去探究对象的构成原理!我们着重探究一下对象头,他对我们理解锁尤为重要!

一般而言,synchronized使用的锁存在于对象头里面!如果是数组对象,则虚拟机使用3个字宽存储对象,如果是非数组对象,则使用两个字宽存储对象头!字虚拟机里面1字宽等于4字节!主要结构是 Mark Word 和 Class Metadata Address组成,结构如下:

虚拟机位数 头对象结构 说明
32/64bit Mark Word 存储对象的hashCode、锁信息或分代年龄或GC标志等信息
32/64bit Class Metadata Address 存储到队形类型数据的指针
32/64bit(数组) Aarray length 数组的长度

通过上述表格能够看出  锁信息 存在于 Mark Word  内,那么 Mark Word 内又是如何组成的呢?

锁状态 25bit 4bit 1bit是否是偏向锁 2bit锁标志位
无锁状态 对象的hashcode 对象的分代年龄 0 01

在运行起见,mark Word 里存储的数据会随着锁的标志位的变化而变化。mark Word可能变化为存储一下四种数据

java中的synchronized是什么

Java SE 1.6为了减少获得锁和释放锁带来的消耗,引入了偏向锁和轻量级锁,从之前上来就是重量级锁到1.6之后,锁膨胀升级的优化,极大地提高了synchronized的效率;

锁一共有4中状态,级别从低到高:

java中的synchronized是什么

这几个状态会随着锁的竞争,逐渐升级。锁可以升级,但是不能降级,其根本的原因就是为了提高获取锁和释放锁的效率!

那么,synchronized是又如何保证的线程安全的呢?或许我们需要从字节码寻找答案!

package com.byit.test;<br/><br/>/**<br/> * @author Administrator<br/> */<br/>public class SynText {<br/>    private static String A = "a";<br/>    public int i ;<br/><br/>    public void add(){<br/>        synchronized (A){<br/>            i++;<br/>        }<br/><br/>    }<br/>}
登录后复制

反编译的字节码

Compiled from "SynText.java"<br/>public class com.byit.test.SynText {<br/>  public int i;<br/><br/>  public com.byit.test.SynText();<br/>    Code:<br/>       0: aload_0<br/>       1: invokespecial #1                  // Method java/lang/Object."<init>":()V<br/>       4: return<br/><br/>  public void add();<br/>    Code:<br/>       0: getstatic     #2                  // Field A:Ljava/lang/String;<br/>       3: dup<br/>       4: astore_1<br/>       5: monitorenter<br/>       6: aload_0<br/>       7: dup<br/>       8: getfield      #3                  // Field i:I<br/>      11: iconst_1<br/>      12: iadd<br/>      13: putfield      #3                  // Field i:I<br/>      16: aload_1<br/>      17: monitorexit<br/>      18: goto          26<br/>      21: astore_2<br/>      22: aload_1<br/>      23: monitorexit<br/>      24: aload_2<br/>      25: athrow<br/>      26: return<br/>    Exception table:<br/>       from    to  target type<br/>           6    18    21   any<br/>          21    24    21   any<br/><br/>  static {};<br/>    Code:<br/>       0: ldc           #4                  // String a<br/>       2: putstatic     #2                  // Field A:Ljava/lang/String;<br/>       5: return<br/>}<br/>
登录后复制

省去不必要的,简化在简化

   5: monitorenter<br/>      ...<br/>      17: monitorexit<br/>      ...<br/>      23: monitorexit
登录后复制

从字节码中可知同步语句块的实现使用的是monitorenter和 monitorexit指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令的时候,线程将试图获取对象所所对应的monitor特权,当monitor的的计数器为0的时候,线程就可以获取monitor,并将计数器设置为1.去锁成功!如果当前线程已经拥有monitor特权,则可以直接进入方法(可重入锁),计数器+1;如果其他线程已经拥有了monitor特权,那么本县城将会阻塞!

拥有monitor特权的线程执行完成后释放monitor,并将计数器设置为0;同时执行monitorexit指令;不要担心出现异常无法执行monitorexit指令;为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

同步代码块的原理了解了,那么同步方法如何解释?不急,我们不妨来反编译一下同步方法的状态!

javap -verbose -p SynText > 3.txt
登录后复制

代码

package com.byit.test;<br/><br/>/**<br/> * @author huangfu<br/> */<br/>public class SynText {<br/>    public int i ;<br/><br/>    public synchronized void add(){<br/>        i++;<br/><br/>    }<br/>}
登录后复制

字节码

Classfile /D:/2020project/byit-myth-job/demo-client/byit-demo-client/target/classes/com/byit/test/SynText.class<br/>  Last modified 2020-1-6; size 382 bytes<br/>  MD5 checksum e06926a20f28772b8377a940b0a4984f<br/>  Compiled from "SynText.java"<br/>public class com.byit.test.SynText<br/>  minor version: 0<br/>  major version: 52<br/>  flags: ACC_PUBLIC, ACC_SUPER<br/>Constant pool:<br/>   #1 = Methodref          #4.#17         // java/lang/Object."<init>":()V<br/>   #2 = Fieldref           #3.#18         // com/byit/test/SynText.i:I<br/>   #3 = Class              #19            // com/byit/test/SynText<br/>   #4 = Class              #20            // java/lang/Object<br/>   #5 = Utf8               i<br/>   #6 = Utf8               I<br/>   #7 = Utf8               <init><br/>   #8 = Utf8               ()V<br/>   #9 = Utf8               Code<br/>  #10 = Utf8               LineNumberTable<br/>  #11 = Utf8               LocalVariableTable<br/>  #12 = Utf8               this<br/>  #13 = Utf8               Lcom/byit/test/SynText;<br/>  #14 = Utf8               syncTask<br/>  #15 = Utf8               SourceFile<br/>  #16 = Utf8               SynText.java<br/>  #17 = NameAndType        #7:#8          // "<init>":()V<br/>  #18 = NameAndType        #5:#6          // i:I<br/>  #19 = Utf8               com/byit/test/SynText<br/>  #20 = Utf8               java/lang/Object<br/>{<br/>  public int i;<br/>    descriptor: I<br/>    flags: ACC_PUBLIC<br/><br/>  public com.byit.test.SynText();<br/>    descriptor: ()V<br/>    flags: ACC_PUBLIC<br/>    Code:<br/>      stack=1, locals=1, args_size=1<br/>         0: aload_0<br/>         1: invokespecial #1                  // Method java/lang/Object."<init>":()V<br/>         4: return<br/>      LineNumberTable:<br/>        line 6: 0<br/>      LocalVariableTable:<br/>        Start  Length  Slot  Name   Signature<br/>            0       5     0  this   Lcom/byit/test/SynText;<br/><br/>  public synchronized void syncTask();<br/>    descriptor: ()V<br/>    flags: ACC_PUBLIC, ACC_SYNCHRONIZED<br/>    Code:<br/>      stack=3, locals=1, args_size=1<br/>         0: aload_0<br/>         1: dup<br/>         2: getfield      #2                  // Field i:I<br/>         5: iconst_1<br/>         6: iadd<br/>         7: putfield      #2                  // Field i:I<br/>        10: return<br/>      LineNumberTable:<br/>        line 10: 0<br/>        line 11: 10<br/>      LocalVariableTable:<br/>        Start  Length  Slot  Name   Signature<br/>            0      11     0  this   Lcom/byit/test/SynText;<br/>}<br/>SourceFile: "SynText.java"<br/>
登录后复制

简化,在简化

 public synchronized void syncTask();<br/>    descriptor: ()V<br/>    flags: ACC_PUBLIC, ACC_SYNCHRONIZED<br/>    Code:<br/>      stack=3, locals=1, args_size=1<br/>         0: aload_0<br/>         1: dup
登录后复制

我们能够看到 flags: ACC_PUBLIC, ACC_SYNCHRONIZED这样的一句话

从字节码中可以看出,synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理。

那么在JAVA6之前,为什么synchronized会如此的慢?

那是因为,操作系统实现线程之间的切换需要系统内核从用户态切换到核心态!这个状态之间的转换,需要较长的时间,时间成本高!所以这也就是synchronized慢的原因!

三、锁膨胀的过程

在这之前,你需要知道什么是锁膨胀!他是JAVA6之后新增的一个概念!是一种针对之前重量级锁的一种性能的优化!他的优化,大部分是基于经验上的一些感官,对锁来进行优化!

①偏向锁

研究发现,大多数情况下,锁不仅不存在多线程竞争,而且还总是由一条线程获得!因为为了减少锁申请的次数!引进了偏向锁!在没有锁竞争的情况下,如果一个线程获取到了锁,那么锁就进入偏向锁的模式!当线程再一次请求锁时,无需申请,直接获取锁,进入方法!但是前提是没有锁竞争的情况,存在锁竞争,锁会立即膨胀,膨胀为轻量级锁!

②轻量级锁

偏向锁失败,那么锁膨胀为轻量级锁!此时锁机构变为轻量级锁结构!他的经验依据是:“绝大多数情况下,在整个同步周期内,不会存在锁的竞争”,故而,轻量级锁适合,线程交替进行的场景!如果在同一时间出现两条线程对同一把锁的竞争,那么此时轻量级锁就不会生效了!但是,jdk官方为了是锁的优化性能更好,轻量级锁失效后,并不会立即膨胀为重量级锁!而是将锁转换为自旋锁状态!

③自旋锁

轻量级锁失败后,为了是避免线程挂起,引起内核态的切换!为了优化,此时线程会进入自选状态!他可能会进行几十次,上百次的空轮训!为什么呢?又是经验之谈!他们认为,大多数情况下,线程持有锁的时间都不会太长!做几次空轮训,就能大概率的等待到锁!事实证明,这种优化方式确实有效!最后如果实在等不到锁!没办法,才会彻底升级为重量级锁!

④锁消除

jvm在进行代码编译时,会基于上下文扫描;将一些不可能存在资源竞争的的锁给消除掉!这也是JVM对于锁的一种优化方式!不得不感叹,jdk官方的脑子!举个例子!在方法体类的局部变量对象,他永远也不可能会发生锁竞争,例如:

/**<br/> * @author huangfu<br/> */<br/>public class SynText {<br/>    public static void add(String name1 ,String name2){<br/>        StringBuffer sb = new StringBuffer();<br/>        sb.append(name1).append(name2);<br/>    }<br/><br/>    public static void main(String[] args) {<br/>        for (int i = 0; i < 10000000; i++) {<br/>            add("w"+i,"q"+i);<br/>        }<br/>    }<br/>}
登录后复制

不能否认,StringBuffer是线程安全的!但是他永远也不会被其他线程引用!故而,锁失效!故而,被消除掉!

以上就是Java中的synchronized关键字是用来实现线程同步的的详细内容,更多请关注php中文网其它相关文章!

java速学教程(入门到精通)
java速学教程(入门到精通)

java怎么学习?java怎么入门?java在哪学?java怎么学才快?不用担心,这里为大家提供了java速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载
来源:亿速云网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习
PHP中文网抖音号
发现有趣的

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号