Java并发编程之synchronized关键字
整理一下synchronized关键字相关的知识点。
在多线程并发编程中synchronized扮演着相当重要的角色,synchronized关键字是用来控制线程同步的,可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块,保证一个线程的变化(主要是共享变量的变化)被其他线程所看到,即保证可见性,可以替代volatile。
1、Synchronized具体表现形式
synchronized的实现和对象锁有关,Java中的每一个对象都可以作为锁,具体表现为以下三种形式:
- 修饰普通方法:作用于当前实例加锁,进入同步代码前要获得当前实例的锁;
public synchronized void method1()
{
// todo
}
- 修饰静态方法:作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁;
public static synchronized void method2()
{
// todo
}
- 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。锁是Synchonized括号里配置的对象。配置的对象分为以下三种情况:
1、实例对象
synchronized(this) {
// todo
}
2、类对象
synchronized(Demo.class) {
// todo
}
3、任意实例对象Object
String lock = “”;
synchronized(lock) {
// todo
}
需要注意的是:如果锁的是类对象的话,尽管new多个实例对象,但他们仍然是属于同一个类依然会被锁住,即线程之间保证同步关系。
2、Synchonized实现原理
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。同步代码块为显示同步,使用monitorenter 和monitorexit指令实现。同步方法为隐式同步,由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现。
public class SynchronizedDemo {
public static void main(String[] args) {
synchronized (SynchronizedDemo.class) { }
}
public synchronized void method() { }
}
先看下上面这段代码,包含一个同步代码块和一个同步方法。通过javap –v 命令查看编译后的class文件。
先来看下main方法,上图中第4、6、12句命令就是添加synchronized之后生成的。执行同步代码块之前要先执行monitorenter指令,退出和异常的时候执行monitorexit指令。其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,否则将会被阻塞在同步块和同步方法的入口处,同时添加到一个同步队列中。每个对象都存在着一个 monitor 与之对应,当 monitor 被线程持有后,它就处于锁定状态,其他线程不能访问。
从上图可以看出,method同步方法没有看到monitorenter和monitorexit指令,而是通过ACC_SYNCHRONIZED标识指明method是一个同步方法。方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor,然后再执行方法,方法完成(无论是正常完成还是非正常完成)时释放monitor。Synchronized先天具有重入性,在同一锁程中,线程不需要再次获取同一把锁。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。
对象,对象监视器,同步队列以及执行线程状态之间的关系如下图:
3、Java对象头
java对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下:
实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。
对象头:synchronized用的锁是存在Java对象头里的,主要结构是Mark Word 和 Class Metadata Address。当对象是数组时,会多一个Array length来存储数组的长度。
- Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。32位JVM的Mark Word默认存储结构如下:
- Class Metadata Address存储了类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。
对象头的信息与对象自身定义的数据是没有关系的,在运行时,Mark Word里存储的数据会随着锁标志位的变化而变化。除了默认存储结构,还有可能变化成以下结构:
这部分照搬了《Java并发编程的艺术》书中的一段,这块的东西只是简单的过了一遍。
4、synchronized的优化
Java 6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,锁可以升级但不能降级。