Java并发编程之Synchronized关键字

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

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

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

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

勾勾从一下几个方面来学习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

 
 
 
 
  1. private final Object obj = new Object(); 
  2. public void sync(){ 
  3.         synchronized (obj){  
  4.                     
  5.         }         
  6.    } 

 方法,锁对象即是this

 
 
 
 
  1. public synchronized void syncMethod(){ 
  2.  
  3.  } 

 静态方法,锁对象既是class

 
 
 
 
  1. public synchronized static void syncStaticMethod(){ 
  2.  
  3.  } 

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

 
 
 
 
  1. public class TestSynchronized { 
  2.     private int index; 
  3.     private final static int MAX = 100; 
  4.     public void sync(){        
  5.         synchronized (new Object()){                 
  6.             while (index < MAX){                         
  7.                 index ++; 
  8.             } 
  9.         } 
  10.     } 

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

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

 
 
 
 
  1. public com.example.demo.articles.thread.TestSynchronized();  
  2.    Code: 
  3.       0: aload_0 
  4.       1: invokespecial #1                  // Method java/lang/Object."":()V 
  5.       4: return 
  6.  
  7.  public void sync(); 
  8.    Code: 
  9.       0: new           #2                  // class java/lang/Object 
  10.       3: dup 
  11.       4: invokespecial #1                  // Method java/lang/Object."":()V 
  12.       7: dup 
  13.       8: astore_1 
  14.       9: monitorenter  //进入同步代码块 
  15.      10: aload_0       //加载数据 
  16.      11: getfield      #3                  // Field index:I 
  17.      14: bipush        100 
  18.      16: if_icmpge     32 
  19.      19: aload_0 
  20.      20: dup 
  21.      21: getfield      #3                  // Field index:I 
  22.      24: iconst_1 
  23.      25: iadd          // 加1操作 
  24.      26: putfield      #3                  // Field index:I 
  25.      29: goto          10 //跳转至10行 
  26.      32: aload_1       
  27.      33: monitorexit  // 退出同步代码块 
  28.      34: goto          42 //跳转至42行 
  29.      37: astore_2     // 刷新数据 
  30.      38: aload_1 
  31.      39: monitorexit   
  32.      40: aload_2 
  33.      41: athrow 
  34.      42: return 
  35.    Exception table: 
  36.       from    to  target type 
  37.          10    34    37   any 
  38.          37    40    37   any 

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

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

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

  1. 锁的对象不能为空;
  2. 锁的范围不宜太大;
  3. 不要试图使用不同的monitor来锁同一个方法;
  4. 避免多个锁交叉等待导致死锁;

锁膨胀

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

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

锁的升级过程既是:

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

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

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

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

 
 
 
 
  1.  // initialize the monitor, exception the semaphore, all other fields 
  2.   //  are simple integers or pointers     
  3.   ObjectMonitor() {   
  4.     _header       = NULL; 
  5.     _count        = 0; 
  6.     _waiters      = 0, 
  7.     _recursions   = 0; 
  8.     _object       = NULL; 
  9.     _owner        = NULL; 
  10.     _WaitSet      = NULL; 
  11.     _WaitSetLock  = 0 ; 
  12.     _Responsible  = NULL ; 
  13.     _succ         = NULL ; 
  14.     _cxq          = NULL ; 
  15.     FreeNext      = NULL ; 
  16.     _EntryList    = NULL ; 
  17.     _SpinFreq     = 0 ; 
  18.     _SpinClock    = 0 ; 
  19.     OwnerIsThread = 0 ; 
  20.   } 
  21.  protected:                         // protected for jvmtiRawMonitor 
  22.   void *  volatile _owner;          // pointer to owning thread OR BasicLock 
  23.   volatile intptr_t  _recursions;   // recursion count, 0 for first entry 
  24.  private: 
  25.   int OwnerIsThread ;               // _owner is (Thread *) vs SP/BasicLock 
  26.   ObjectWaiter * volatile _cxq ;    // LL of recently-arrived threads blocked on entry. 
  27.                                     // The list is actually composed of WaitNodes, acting 
  28.                                     // as proxies for Threads. 
  29.  protected: 
  30.   ObjectWaiter * volatile _EntryList ;     // Threads blocked on entry or reentry. 
  31.  private: 
  32.   Thread * volatile _succ ;          // Heir presumptive thread - used for futile wakeup throttling 
  33.   Thread * volatile _Responsible ; 
  34.   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字节的整数倍,当不满足时需对其填充。

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

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

第一个分支锁标志为01:

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

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

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

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

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

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

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

偏向锁的流程图如下:

第二个分支锁标志为00:

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

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

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

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

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

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

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

轻量级流程图如下图:

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

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

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

总结:

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

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

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

今天就学到这里了!收工!

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

网站名称:Java并发编程之Synchronized关键字
URL链接:http://www.shufengxianlan.com/qtweb/news39/360289.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联