Java开发入门到精通之Java并发编程之Synchronized关键字
小职 2021-03-12 来源 : 阅读 489 评论 0

摘要:本文主要介绍了Java开发入门到精通之Java并发编程之Synchronized关键字,通过具体的内容向大家展现,希望对大家Java开发的学习有所帮助。

本文主要介绍了Java开发入门到精通之Java并发编程之Synchronized关键字,通过具体的内容向大家展现,希望对大家Java开发的学习有所帮助。

Java开发入门到精通之Java并发编程之Synchronized关键字

并发编程的重点也是难点是数据同步、线程安全、锁。要编写线程安全的代码,其核心在于对共享和可变的状态的访问进行管理。

 

共享意味着变量可以由多个线程访问,而可变则意味着变量的值在其生命周期内可以发生变化。

 

当多个线程访问某个状态变量且其中有一个线程执行写入操作时,必须采用同步机制来协同这些线程对变量的访问。

 

Java中的主要同步机制是关键字synchronized,它提供了一种独占的加锁方式。

 

勾勾从一下几个方面来学习synchronized:

 Java开发入门到精通之Java并发编程之Synchronized关键字

 

关键字synchronized的特性

synchronized关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么该对象的所有读和写都需通过同步的方式。

 

synchronized的特性:

不可中断:synchronized关键字提供了独占的加锁方式,一旦一个线程持有了锁对象,其他线程将进入阻塞状态或者等待状态,直到前一个线程释放锁,中间过程不可中断。

 

原子性: synchronized关键字的不可中断性保证了它的原子性。

 

可见性:synchronized关键字包含了两个JVM指令:monitor enter和monitor exit,它能够保证在任何时候任何线程执行到monitor enter时都必须从主内存中获取数据,而不是从线程工作内存获取数据,在monitor exit之后,工作内存被更新后的值必须存入主内存,从而保证了数据可见性。

 

有序性:synchronized关键字修改的同步方法是串行执行的,但其所修饰的代码块中的指令顺序还是会发生改变的,这种改变遵守java happens-before规则。

 

可重入性:如果一个拥有锁持有权的线程再次获取锁,则monitor的计数器会累加1,当线程释放锁的时候也会减1,直到计数器为0表示线程释放了锁的持有权,在计数器不为0之前,其他线程都处于阻塞状态。

 

关键字synchronized的用法

synchronized关键字锁的是对象,修饰的可以是代码块和方法,但是不能修饰class对象以及变量。

 

代码块,锁对象即是object

 

private final Object obj = new Object();

public void sync(){

        synchronized (obj){  

                    

        }         

   }

方法,锁对象即是this

 

public synchronized void syncMethod(){

 

 }

静态方法,锁对象既是class

 

public synchronized static void syncStaticMethod(){

 

 }

勾勾在开发中最常用的是用synchronized关键字修饰对象,可以控制锁的粒度,所以针对最常用的场景勾勾去了解了它的字节码文件,先来看看勾勾的测试用例:

 

public class TestSynchronized {

    private int index;

    private final static int MAX = 100;

    public void sync(){        

        synchronized (new Object()){                 

            while (index < MAX){                         

                index ++;

            }

        }

    }

}

运行命令 “javac -encoding UTF-8 TestSynchronized.java”编辑成class文件,然后

 

运行命令“javap -c TestSynchronized.class”得到字节码文件:

 

public com.example.demo.articles.thread.TestSynchronized();  

   Code:

      0: aload_0

      1: invokespecial #1                  // Method java/lang/Object."<init>":()V

      4: return

 

 public void sync();

   Code:

      0: new           #2                  // class java/lang/Object

      3: dup

      4: invokespecial #1                  // Method java/lang/Object."<init>":()V

      7: dup

      8: astore_1

      9: monitorenter  //进入同步代码块

     10: aload_0       //加载数据

     11: getfield      #3                  // Field index:I

     14: bipush        100

     16: if_icmpge     32

     19: aload_0

     20: dup

     21: getfield      #3                  // Field index:I

     24: iconst_1

     25: iadd          // 加1操作

     26: putfield      #3                  // Field index:I

     29: goto          10 //跳转至10行

     32: aload_1       

     33: monitorexit  // 退出同步代码块

     34: goto          42 //跳转至42行

     37: astore_2     // 刷新数据

     38: aload_1

     39: monitorexit   

     40: aload_2

     41: athrow

     42: return

   Exception table:

      from    to  target type

         10    34    37   any

         37    40    37   any

monitorenter和monitorexit是成对出现的,有时候你看到的是一个monitorenter对应多个monitorexit,但是能肯定的一定点是每一个monitorexit之前必有一个monitorenter。

 

从字节码文件中可以看到monitorenter之后执行了aload操作,monitorexit之后执行了astore操作。

 

TIPS:在使用synchronized关键字时注意事项

 

锁的对象不能为空;

锁的范围不宜太大;

不要试图使用不同的monitor来锁同一个方法;

避免多个锁交叉等待导致死锁;

锁膨胀

在jdk1.6之前,线程在获取锁时,如果锁对象已经被其他线程持有,此线程将挂起进入阻塞状态,唤醒阻塞线程的过程涉及到了用户态和内核态的切换,性能损耗比较大。

 

synchronized作为亲儿子,混的太差肯定不行,在jdk1.6对其进行了优化,将锁状态分为了无锁状态,偏向锁,轻量级锁,重量级锁。

 

锁的升级过程既是:

 Java开发入门到精通之Java并发编程之Synchronized关键字

 

在了解锁的升级过程之前,勾勾重点理解了monitor和对象头。

 

在第一次研究锁膨胀的时候因为没有花时间去理解这两个概念,勾勾对锁升级的记忆只持续了3天,最后勾勾又用了两天的时间去学习对象头和monitor,才算是真正的理解锁的膨胀原理。所以大家在学习一个知识的时候,不要靠背去记忆一个知识点,一定要知其然。

 

每一个对象都与一个monitor相关联,monitor对象与实例对象一同创建并销毁,monitor是C++支持的一个监视器。锁对象的争夺既是争夺monitor的持有权。

 

勾勾在OpenJdk源码中找到了ObjectMonitor的源码:

 

 // initialize the monitor, exception the semaphore, all other fields

  //  are simple integers or pointers     

  ObjectMonitor() {   

    _header       = NULL;

    _count        = 0;

    _waiters      = 0,

    _recursions   = 0;

    _object       = NULL;

    _owner        = NULL;

    _WaitSet      = NULL;

    _WaitSetLock  = 0 ;

    _Responsible  = NULL ;

    _succ         = NULL ;

    _cxq          = NULL ;

    FreeNext      = NULL ;

    _EntryList    = NULL ;

    _SpinFreq     = 0 ;

    _SpinClock    = 0 ;

    OwnerIsThread = 0 ;

  }

 protected:                         // protected for jvmtiRawMonitor

  void *  volatile _owner;          // pointer to owning thread OR BasicLock

  volatile intptr_t  _recursions;   // recursion count, 0 for first entry

 private:

  int OwnerIsThread ;               // _owner is (Thread *) vs SP/BasicLock

  ObjectWaiter * volatile _cxq ;    // LL of recently-arrived threads blocked on entry.

                                    // The list is actually composed of WaitNodes, acting

                                    // as proxies for Threads.

 protected:

  ObjectWaiter * volatile _EntryList ;     // Threads blocked on entry or reentry.

 private:

  Thread * volatile _succ ;          // Heir presumptive thread - used for futile wakeup throttling

  Thread * volatile _Responsible ;

  int _PromptDrain ;                // rqst to drain cxq into EntryList ASAP

}

owner:指向线程的指针。即锁对象关联的monitor中的owner指向了哪个线程表示此线程持有了锁对象。

 

waitSet:进入阻塞等待的线程队列。当线程调用wait方法之后,就会进入waitset队列,可以等待其他线程唤醒。

 

entryList:当多个线程进入同步代码块之后,处于阻塞状态的线程就会被放入entryList中。

 

那什么是对象头呢,它与synchronized又有什么关系呢?

 

在JVM中,对象在内存中分为3块区域:

 

对象头Mark Word(标记字段):用于存储对象的hashcode,分代年龄,锁标志位,是否可偏向标志,在运行期间,其存储的数据会发生变化。Klass Point(类型指针):该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。

实例数据用于存放类的数据信息

填充数据虚拟机要求对象起始地址必须是8字节的整数倍,当不满足时需对其填充。

我们先通过一张图了解下在锁升级的过程中对象头的变化:

 Java开发入门到精通之Java并发编程之Synchronized关键字

 

接下来我们分析锁升级的过程:

 

第一个分支锁标志为01:

当线程运行到同步代码块时,首先会判断锁标志位,如果锁标志位为01,则继续判断偏向标志。

 

如果偏向标志为0,则表示锁对象未被其他线程持有,可以获取锁。此时当前线程通过CAS的方法修改线程ID,如果修改成功,此时锁升级为偏向锁。

 

如果偏向标志为1,则表示锁对象已经被占有。

 

进一步判断线程id是否相等,相等则表示当前线程持有的锁对象,可以重入。

 

如果线程id不相等,则表示锁被其他线程占有。

 

需进一步判断持有偏向锁的线程的活动状态,如果原持有偏向锁线程已经不活动或者已经退出同步代码块,则表示原持有偏向锁的线程可以释放偏向锁。释放后偏向锁回到无锁状态,线程再次尝试获取锁。主要是因为偏向锁不会主动释放,只有其他线程竞争偏向锁的时候才会释放。

 

如果原持有偏向锁的线程没有退出同步代码块,则锁升级为轻量级锁。

 

偏向锁的流程图如下:

 Java开发入门到精通之Java并发编程之Synchronized关键字

 

第二个分支锁标志为00:

在第一个分支中我们了解到在如果偏向锁已经被其他线程占有,则锁会被升级为轻量级锁。

 

此时原持有偏向锁的线程的栈帧中分配锁记录Lock Record,将对象头中的Mark Word信息拷贝到锁记录中,Mark Word的指针指向了原持有偏向锁线程中的锁记录,此时原持有偏向锁的线程获取轻量级锁,继续执行同步块代码。

 

如果线程在运行同步块时发现锁的标志位为00,则在当前线程的栈帧中分配锁记录,拷贝对象头中的Mark Word到锁记录中。通过CAS操作将Mark Word中的指针指向自己的锁记录,如果成功,则当前线程获取轻量锁。

 

如果修改失败,则进入自旋,不断通过CAS的方式修改Mark Word中的指针指向自己的锁记录。

 

当自旋超过一定次数(默认10次),则升级为重量锁。

 

轻量锁的锁是主动释放的,持有轻量锁的线程在执行完同步代码块后,会先判断Mark Word中的指针是否依然指向自己,且自己锁记录中的Mark Word信息与锁对象的Mark Word信息一致,如果都一致,则释放锁成功。

 

如果不一致,则锁有可能已经被升级为重量锁。

 

轻量级流程图如下图:

Java开发入门到精通之Java并发编程之Synchronized关键字 

 

第三个分支锁标志位为10:

锁标志为10时,此时锁已经为重量锁,线程会先判断monitor中的owner指针指向是否为自己,是则获取重量锁,不是则会挂起。

 

整个锁升级过程中的流程图如下,如果看懂了一定要自己画一遍。

 Java开发入门到精通之Java并发编程之Synchronized关键字

 

 

总结:

synchronized关键字是一种独占的加锁方式,不可中断,保证了原子性,可见性,和有序性。

 

synchronized关键字可用于修饰方法和代码块,不能用于修饰变量和类。

 

多线程在执行同步代码块时获取锁的过程在不同的锁状态下不一样,偏向锁是修改Mark Word中的线程ID,轻量锁是修改Mark Word的指针指向自己的锁记录,重量锁是修改monitor中的指针指向自己。

 

并发编程、JVM、数据结构基础知识更新完了,后续还会慢慢补充!


我是小职,记得找我

✅ 解锁高薪工作

✅ 免费获取学习教程,开发工具,代码大全,参考书籍

Java开发入门到精通之Java并发编程之Synchronized关键字

本文由 @小职 发布于职坐标。未经许可,禁止转载。
喜欢 | 0 不喜欢 | 0
看完这篇文章有何感觉?已经有0人表态,0%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved

208小时内训课程