并发编程系列之深入理解Synchronized

Java内存模型内存间交互操作

  在介绍synchronized之前先简单的介绍一下JMM的交互操作
  Java内存模型定义了8个操作来完成主内存和工作内存的交互操作。

  • read:把一个变量的值从主内存传输到工作内存中
  • load:在read之后执行,把read得到的值放入工作内存的变量副本中
  • use:把工作内存中一个变量的值传递给执行引擎
  • assign:把一个从执行引擎接收到的值赋给工作内存的变量
  • store:把工作内存的一个变量的值传送到主内存中
  • write:在store之后执行,把store得到的值放入主内存的变量中
  • lock:作用于主内存的变量
  • unlock

synchronized

  synchronized的底层是使用操作系统的mutex lock实现的。

  内存可见性:同步块的可见性是由“如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值”、“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”这两条规则获得的。
  操作原子性:持有同一个锁的两个同步块只能串行地进入

锁的内存语义

  • 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中
  • 当线程获取锁时,JMM会把线程对应的本地内存置为无效。从而使得监视器保护的临界区代码必须从主内存中读取共享变量

锁释放和锁获取的内存语义

  • 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。
  • 线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
  • 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息

synchronized锁

  synchronized锁的是对象的头。

  JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步是使用monitorenter和monitorexit指令实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。

  根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经有用了那个对象的锁,把锁的计数器加1;相应地,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放

Mutex Lock

  监视器锁(Monitor)本质是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。每个对象都对应一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

  互斥锁:用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。

synchronized的使用场景

分类 被锁住的对象 伪代码
方法 实例方法 类的实例对象 //实例方法,锁住的是该类的实例对象public synchronized void method(){   ……}
静态方法 类对象 //静态方法,锁住的是类对象public static synchronized void method(){   ……}
代码块 实例对象 类的实例对象 //同步代码块,锁住的是该类的实例对象synchronized(this){   ……}
class对象 类对象 //同步代码块,锁住的是该类的类对象synchronized(xxx.class){   ……}
任意实例对象Object 实例对象Object //同步代码块,锁住的是配置的实例对象//String对象作为对象锁String lock = "";synchronized(lock){   ……}

Java对象保存在内存中的组成

1.对象头
2.实例数据
3.对其填充字节

对象头

  java的对象头由以下三部分组成:

  • 1.Mark Word
  • 2.指向类的指针
  • 3.数组长度(只有数组对象才有)

Mark Word

  Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。
  Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
  Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的:

  其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态
  PS:JDK1.6以后的版本在处理同步锁时存在锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁。

指向类的指针

  该指针在32位JVM中的长度是32bit,在64位的JVM中的长度是64bit。

数组长度

  只有数组对象保存了这部分数据。该数据再32位和64位JVM中的长度都是32bit。

实例数据

  在java代码中能看到的属性及其值。

对齐填充字节

  因为JVM要求java对象所占的内存大小是8bit的倍数,所以后面有几个字节用于把对象的大小补全至8bit的倍数。

synchronized锁的升级

  下面通过一张图来看一下synchronized锁升级的过程(图片来自网络)。

  JVM中一般是这样使用锁的:

  无锁状态

  当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁位是0。

  无锁-->偏向锁

  1)只有一个线程抢锁

  当对象被当做同步锁并有一个线程抢到了锁,锁标志位是01,是否偏向锁位是1,前32bit记录抢到锁的线程id,表示进入偏向锁状态。

  2)有一个线程已经抢到锁,其他线程也来抢锁

  线程2试图再次获取锁时,JVM发现同步锁对象处于偏向状态,Mark Word中记录的线程id就是线程2的id,表示线程2已经获得了这个偏向锁,可以执行同步锁的代码。

  当线程3视图获取这个锁时,JVM发现同步锁对象处于偏向状态,但是Mark Word中记录的线程id不是线程3的id,那么线程3会先用CAS操作试图获得锁。如果抢锁成功,就把Mark Word里的线程id改为线程3的id,代表线程3获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。

  偏向锁-->轻量级锁

  当同步对象处于偏向锁状态线程抢锁失败将会升级成轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改为00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈。JVM会使用自旋锁,不断的重试,尝试抢锁。从jdk1.7开始,自旋锁默认启用,自旋次数由jvm决定。如果抢锁成功则执行同步锁代码,如果抢锁失败轻量级锁将升级为重量级锁。

  轻量级锁-->重量级锁

  当线程自旋获取轻量级锁失败将会升级成重量级锁。锁标志位改为10.这个状态下未抢到锁的线程都会被阻塞。

你可能感兴趣的:(并发编程,java)