创建时间: | 2018/8/8 11:03 |
来源: | https://www.jianshu.com/p/19f861ab749e |
简书 占小狼
转载请注明原创出处,谢谢!
synchronized可以保证方法或代码块在运行时,同一时刻只有一个线程可以进入到临界区(互斥性),同时它还保证了共享变量的内存可见性。
Java中的每个对象都可以作为锁。
先看一个场景
等待 / 通知机制
直接上代码:
import java.util.concurrent.TimeUnit;
/**
* Created by j_zhan on 2016/7/6.
*/
public class WaitNotify {
static boolean flag = true;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread A = new Thread(new Wait(), "wait thread");
A.start();
TimeUnit.SECONDS.sleep(2);
Thread B = new Thread(new Notify(), "notify thread");
B.start();
}
static class Wait implements Runnable {
@Override
public void run() {
synchronized (lock) {
while (flag) {
try {
System.out.println(Thread.currentThread() + " flag is true");
lock.wait();
} catch (InterruptedException e) {
}
}
System.out.println(Thread.currentThread() + " flag is false");
}
}
}
static class Notify implements Runnable {
@Override
public void run() {
synchronized (lock) {
flag = false;
lock.notifyAll();
try {
TimeUnit.SECONDS.sleep(7);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
其相关方法定义在java.lang.Object上,线程A在获取锁后调用了对象lock的wait方法进入了等待状态,线程B调用对象lock的notifyAll()方法,线程A收到通知后从wait方法处返回继续执行,线程B对共享变量flag的修改对线程A来说是可见的。
整个运行过程需要注意一下几点:
那么,它是如何实现线程之间的互斥性和可见性?
先看一段代码:
public class SynchronizedTest {
private static Object object = new Object();
public static void main(String[] args) throws Exception{
synchronized(object) {
}
}
public static synchronized void m() {}
}
上述代码中,使用了同步代码块和同步方法,通过使用javap工具查看生成的class文件信息来分析synchronized关键字的实现细节。
public static void main(java.lang.String[]) throws java.lang.Exception;
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // Field object:Ljava/lang/Object
3: dup
4: astore_1
5: monitorenter //监视器进入,获取锁
6: aload_1
7: monitorexit //监视器退出,释放锁
8: goto 16
11: astore_2
12: aload_1
13: monitorexit
14: aload_2
15: athrow
16: return
public static synchronized void m();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 9: 0
从生成的class信息中,可以清楚的看到
无论哪种实现,本质上都是对指定对象相关联的monitor的获取,这个过程是互斥性的,也就是说同一时刻只有一个线程能够成功,其它失败的线程会被阻塞,并放入到同步队列中,进入BLOCKED状态。
我们继续深入了解一下锁的内部机制
一般锁有4种状态:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。
在进一步深入之前,我们先认识下两个概念:对象头和monitor。
什么是对象头?
在hotspot虚拟机中,对象在内存的分布分为3个部分:对象头,实例数据,和对齐填充。
mark word被分成两部分,lock word和标志位。
Klass ptr指向Class字节码在虚拟机内部的对象表示的地址。
Fields表示连续的对象实例字段。
mark word 被设计为非固定的数据结构,以便在及小的空间内存储更多的信息。比如:在32位的hotspot虚拟机中:如果对象处于未被锁定的情况下。mark word 的32bit空间中有25bit存储对象的哈希码、4bit存储对象的分代年龄、2bit存储锁的标记位、1bit固定为0。而在其他的状态下(轻量级锁、重量级锁、GC标记、可偏向)下对象的存储结构为
什么是monitor?
monitor是线程私有的数据结构,每一个线程都有一个可用monitor列表,同时还有一个全局的可用列表,先来看monitor的内部
那么monitor的作用是什么呢?在 java 虚拟机中,线程一旦进入到被synchronized修饰的方法或代码块时,指定的锁对象通过某些操作将对象头中的LockWord指向monitor 的起始地址与之关联,同时monitor 中的Owner存放拥有该锁的线程的唯一标识,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码。
接下去,我们可以深入了解下在锁各个状态下,底层是如何处理多线程之间对锁的竞争。
下述代码中,当线程访问同步方法method1时,会在对象头(SynchronizedTest.class对象的对象头)和栈帧的锁记录中存储锁偏向的线程ID,下次该线程在进入method2,只需要判断对象头存储的线程ID是否为当前线程,而不需要进行CAS操作进行加锁和解锁(因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟)。
/**
* Created by j_zhan on 2016/7/6.
*/
public class SynchronizedTest {
private static Object lock = new Object();
public static void main(String[] args) {
method1();
method2();
}
synchronized static void method1() {}
synchronized static void method2() {}
}
利用了CPU原语Compare-And-Swap(CAS,汇编指令CMPXCHG)。
线程可以通过两种方式锁住一个对象:
获取锁(monitorenter)的大概过程:
释放锁(monitorexit)的大概过程:
/**
* Created by j_zhan on 2016/7/6.
*/
public class SynchronizedTest implements Runnable {
private static Object lock = new Object();
public static void main(String[] args) {
Thread A = new Thread(new SynchronizedTest(), "A");
A.start();
Thread B = new Thread(new SynchronizedTest(), "B");
B.start();
}
@Override
public void run() {
method1();
method2();
}
synchronized static void method1() {}
synchronized static void method2() {}
}
当锁处于这个状态下,其他线程试图获取锁都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程。
对java内存模型不熟悉的同学,可以参考这边文章java内存模型。
END。
我是占小狼。
在魔都艰苦奋斗,白天是上班族,晚上是知识服务工作者。
读完我的文章有收获,记得关注和点赞哦,如果非要打赏,我也是不会拒绝的啦!