对于synchronized你们应该都很熟悉,主要做用是在多线程并发时,保证线程访问共享数据时的线程安全。html
它的做用有三点:java
它的用法主要是从两个维度上来区分:安全
这个对象是新建的,跟其余对象无关:数据结构
public class SynchronizeDemo implements Runnable {
@Override
public void run() {
test1();
}
private void test1(){
System.out.println(Thread.currentThread().getName() + "_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
synchronized (new SynchronizeDemo()){
try {
System.out.println(Thread.currentThread().getName() + "_start_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "_end_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
SynchronizeDemo sd1 = new SynchronizeDemo();
Thread thread1 = new Thread(new SynchronizeDemo(),"thread1");
Thread thread2 = new Thread(new SynchronizeDemo(),"thread2");
Thread thread3 = new Thread(sd1,"thread3");
Thread thread4 = new Thread(sd1,"thread4");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
复制代码
运行结果如图多线程
四个线程同时开始,同时结束,由于做为锁的对象与线程是属于不一样的实例并发
无所谓哪一个类,都会被拦截oracle
private void test2(){
System.out.println(Thread.currentThread().getName() + "_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
synchronized (SynchronizeDemo.class){
try {
System.out.println(Thread.currentThread().getName() + "_start_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "_end_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
复制代码
运行结果以下:jvm
能够看到,类锁一次只能经过一个。ide
就是把synchronized (SynchronizeDemo.class)改成synchronized (this)工具
控制台打印结果
可能这显示结果有点歧义,其实多运行几回咱们会发现,1和2是同时结束的,3和4永远有前后,由于3,4同属于一个实例
private synchronized void test4(){
...
}
复制代码
打印的结果以下:
thread1_: 22:42:04
thread3_: 22:42:04
thread2_: 22:42:04
thread3_start_: 22:42:04
thread1_start_: 22:42:04
thread2_start_: 22:42:04
thread1_end_: 22:42:06
thread3_end_: 22:42:06
thread2_end_: 22:42:06
thread4_: 22:42:06
thread4_start_: 22:42:06
thread4_end_: 22:42:08
复制代码
对于非静态方法,同一个实例的线程访问会被拦截,非同一实例能够同时访问,即此时默认的就是对象锁(this)
在上面方法上加static
thread1_: 22:42:42
thread1_start_: 22:42:42
thread1_end_: 22:42:44
thread4_: 22:42:44
thread4_start_: 22:42:44
thread4_end_: 22:42:46
thread3_: 22:42:46
thread3_start_: 22:42:46
thread3_end_: 22:42:48
thread2_: 22:42:48
thread2_start_: 22:42:48
thread2_end_: 22:42:50
复制代码
同样的能够看出来,静态方法默认使用的就是类锁
实际上,在JVM中,只区分两种不一样的用法,修饰代码块与修饰方法,咱们能够查看SE8规范,docs.oracle.com/javase/spec…
多说无益,直接看它的字节码
public class Test {
public static void main(String[] args) {
}
public synchronized void test1() {
}
public void test2() {
synchronized (this) {
}
}
}
复制代码
最简单的程序,经过使用javap -v Test.class
来查看它的字节码(注意是class文件,不是java文件)
public synchronized void test1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this LTest;
public void test2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter //监视器进入,获取锁
4: aload_1
5: monitorexit //监视器退出,释放锁
6: goto 14
9: astore_2
10: aload_1
11: monitorexit
12: aload_2
13: athrow
14: return
复制代码
能够看到,果真字节码中,synchronized修饰代码块时,是使用monitorenter
和monitorexit
来控制,而synchronized修饰方法的时候,是使用ACC_SYNCHRONIZED
标识。
本质上都是对一个对象的monitor进行获取,而这个获取的过程是排他的,也就是同一时刻只能有一个线程得到同步块对象的监视器monitor。
线程执行到monitorenter指令时,会尝试获取对象所对应的monitor全部权,也就是尝试获取锁,执行到monitorexit,也就是释放全部权,释放锁。
要想理清synchronized的锁的原理,须要掌握两个重要的概念:
在Hotspot虚拟机中,对象在内存中的存储布局,能够分为三块:对象头Header,实例数据Instance Data,对齐填充Padding。
Hotspot虚拟机的对象头包含了两部分信息:
32位HotSpot虚拟机的对象头存储结构以下
为了验证上图的正确,咱们能够查看hotspot的源码
在线地址:hg.openjdk.java.net/jdk8u/jdk8u…
public:
// Constants
enum { age_bits = 4,//分代年龄
lock_bits = 2,//锁标识
biased_lock_bits = 1,//是否偏向锁
max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits,//hask
cms_bits = LP64_ONLY(1) NOT_LP64(0),
epoch_bits = 2//偏向时间戳
};
复制代码
hash:保存对象的哈希码
age:对象的分代年龄
biased_lock:偏向锁标识位
lock:锁状态标识位
JavaThread*:保存持有偏向锁的线程ID
epoch:保存偏向时间戳
因此,对象头中的Mark Word,synchronized源码就是用了对象头中的Mark Word来标识对象加锁状态。
Monitor Record是线程私有的数据结构,每个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每个被锁住的对象都会和一个monitor record关联(对象头的MarkWord中的LockWord指向monitor record的起始地址),同时monitor record中有一个Owner字段存放拥有该锁的线程的惟一标识,表示该锁被这个线程占用。以下图所示为Monitor Record的内部结构
Candidate:用来避免没必要要的阻塞或等待线程唤醒,由于每一次只有一个线程可以成功拥有锁,若是每次前一个释放锁的线程唤醒全部正在阻塞或等待的线程,会引发没必要要的上下文切换(从阻塞到就绪而后由于竞争锁失败又被阻塞)从而致使性能严重降低。Candidate只有两种可能的值0表示没有须要唤醒的线程1表示要唤醒一个继任线程来竞争
简单总结一下,同步块使用monitorenter和monitorexit指令,而同步方法是依靠方法修饰符上的flag——ACC_SYNCHRONIZED来完成的。其本质都是对一个对象监视器monitor进行获取,这个获取过程是排他的,也就是同一时刻只能有一个线程得到由synchronized所保护的对象的监视器。而这个监视器,也能够理解为一个同步工具,它是由java对象进行描述的,在Hotspor中,是经过ObjectMonitor来实现,每一个对象中自然都内置了一个ObjectMonitor对象。
在java中,synchronized在编译后,会在同步块的先后分别造成一个monitorenter和monitorexit这两个字节码指令,这两个字节码都须要一个reference类型的参数来指明要锁定和解锁的对象,若是java程序中明确指定了对象,那就是这个对象的reference,若是没有指明,那么根据synchronized修饰的是实例方法仍是类方法,去取对应的对象实例或者类Class对象来作锁对象。
在执行monitorenter时,首先会尝试获取对象的锁,若是这个对象没有锁,或者当前线程已经拥有了这个对象的锁,那个锁的计数器加1,相应的,在执行monitorexit时指令时,会将锁计数器减1,当计数器为0时,这个锁就被释放。若是获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。
synchronized同步块对同一线程来讲是可重入的,不会出现本身把本身锁死的状况,其次,同步块在已进入的线程执行完成前,会阻塞后面的其余线程进入。咱们知道,Java的线程是映射到操做系统中的的原生线程上的,若是要阻塞或者唤醒一个线程,都须要操做系统来帮忙,这就须要咱们从用户态切换到核心态,所以这个状态转换是很是耗费CPU。若是这个代码很是简单的同步块,可能切换状态的时间比代码执行时间还长。因此synchronized是一个重量级的操做,虚拟机自己也作了大量的优化,引入了偏向锁,轻量级锁,重量级锁等,这一部分锁的升级,能够等之后有时间了,再慢慢探讨。固然还能够引入重入锁,解决synchronized过于重量的问题。
参考
《深刻理解Java虚拟机》
下面是个人公众号,欢迎你们关注我