多线程系列-synchronized
synchronized
什么是synchronized
synchronized的应用场景是在多线程的环境下实现方法的同步或者代码块的同步,就是说,如果你的一段代码被synchronized修饰,那么你这一段代码就只能串行
package com.luban.syn;
import java.util.concurrent.TimeUnit;
/**
* synchronized 演示
*/
public class SynExample {
public synchronized void test1(){
try {
//休眠一秒钟来实现演示效果
TimeUnit.SECONDS.sleep(1L);
System.out.println(Thread.currentThread().getName()+"线程被执行,打印时间为:"+System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SynExample synExample = new SynExample();
//定义需要执行的10个线程
Thread tf[] = new Thread[10];
for (int i = 0; i < 10 ; i++) {
tf[i] = new Thread(){
@Override
public void run() {
synExample.test1();
}
};
tf[i].start();
try {
//使用join可以保证线程的有序执行
tf[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
外面来看上面 SynExample 代码块,将synchronized使用在方法上,是使用synchronized的其中一种方式,其他还可以使用在代码块。
执行以上的代码块我们可以得到以下的结果
Thread-0线程被执行,打印时间为:1580560098784
Thread-1线程被执行,打印时间为:1580560099785
Thread-2线程被执行,打印时间为:1580560100785
Thread-3线程被执行,打印时间为:1580560101786
Thread-4线程被执行,打印时间为:1580560102786
Thread-5线程被执行,打印时间为:1580560103790
Thread-6线程被执行,打印时间为:1580560104792
Thread-7线程被执行,打印时间为:1580560105792
Thread-8线程被执行,打印时间为:1580560106793
Thread-9线程被执行,打印时间为:1580560107795
synchronized的使用
在非静态方法中使用
在方法中使用,我们在第一个例子中就是一个典型的在方法中使用的例子
在静态方法中使用
public static synchronized void test2() {
try {
TimeUnit.SECONDS.sleep(1L);
System.out.println(Thread.currentThread().getName() + "线程被执行,打印时间为:" + System.currentTimeMillis());
} catch (InterruptedException var2) {
var2.printStackTrace();
}
}
在静态代码中使用和非静态代码中使用,显性的在java代码中不存在区别,具体区别在哪里,可以继续往下看原理
在代码块中使用
在代码块中使用,先上代码
对当前对象加锁
public void test3(){
synchronized (this){
try {
TimeUnit.SECONDS.sleep(1L);
System.out.println(Thread.currentThread().getName() + "线程被执行,打印时间为:" + sdf.format(new Date()));
} catch (InterruptedException var2) {
var2.printStackTrace();
}
}
}
public static void main(String[] args) {
SynExample synExample = new SynExample();
//定义需要执行的10个线程
Thread tf[] = new Thread[10];
for (int i = 0; i < 10 ; i++) {
tf[i] = new Thread(){
@Override
public void run() {
synExample.test3();
}
};
tf[i].start();
}
}
如果我们对我们的对象加锁,请保证我们的对象是同一个,上面的例子中,我们只实例化了一个对象,因此不会出现问题,演示的结果为:
Thread-0线程被执行,打印时间为:46:40
Thread-9线程被执行,打印时间为:46:41
Thread-8线程被执行,打印时间为:46:42
Thread-7线程被执行,打印时间为:46:43
Thread-6线程被执行,打印时间为:46:44
Thread-5线程被执行,打印时间为:46:45
Thread-4线程被执行,打印时间为:46:46
Thread-3线程被执行,打印时间为:46:47
Thread-2线程被执行,打印时间为:46:48
Thread-1线程被执行,打印时间为:46:49
下面来演示一个错误的例子
public static void main(String[] args) {
//定义需要执行的10个线程
Thread tf[] = new Thread[10];
for (int i = 0; i < 10 ; i++) {
tf[i] = new Thread(){
@Override
public void run() {
SynExample synExample = new SynExample();
synExample.test3();
}
};
tf[i].start();
}
上面我们的对象不同,因此,我们的同步块也不起作用,因为我们是对我们的对象加的锁,熟悉jvm的人一定知道,在外面的堆区中不同的对象没有任何关系,因此,以上外面的代码就会导致我们的同步块失效
对当前类加锁
当出现上面代码的问题,有可能是写代码的时候出现的失误,为了避免出现上述的问题,我们可以对我们的类对象加锁
public void test4(){
synchronized (SynExample.class){
try {
TimeUnit.SECONDS.sleep(1L);
System.out.println(Thread.currentThread().getName() + "线程被执行,打印时间为:" + sdf.format(new Date()));
} catch (InterruptedException var2) {
var2.printStackTrace();
}
}
}
public static void main(String[] args) {
//定义需要执行的10个线程
Thread tf[] = new Thread[10];
for (int i = 0; i < 10 ; i++) {
tf[i] = new Thread(){
@Override
public void run() {
SynExample synExample = new SynExample();
synExample.test4();
}
};
tf[i].start();
}
}
以上就是所有对sychronized的用法
synchronized 原理
在分析synchronized的原理之前,我们先了解以下我们怎么去查看class源码,因为我们要了解synchronized的原理,我们就需要去了解以下我们的java源文件被编译之后生成的文件是什么样子的。
我们可以通过
javap -v class文件
我们通过反编译可以得到相应的编译后的文件信息
public synchronized void test1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=4, locals=2, args_size=1
0: getstatic #6 // Field java/util/concurrent/TimeUnit.SECONDS:Ljava/util/concurrent/TimeUnit;
3: lconst_1
4: invokevirtual #7 // Method java/util/concurrent/TimeUnit.sleep:(J)V
7: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
10: new #9 // class java/lang/StringBuilder
13: dup
14: invokespecial #10 // Method java/lang/StringBuilder."<init>":()V
17: invokestatic #11 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
20: invokevirtual #12 // Method java/lang/Thread.getName:()Ljava/lang/String;
23: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
26: ldc #14 // String 线程被执行,打印时间为:
28: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: invokestatic #15 // Method java/lang/System.currentTimeMillis:()J
34: invokevirtual #16 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
37: invokevirtual #17 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
40: invokevirtual #18 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
43: goto 51
46: astore_1
47: aload_1
48: invokevirtual #20 // Method java/lang/InterruptedException.printStackTrace:()V
51: return
Exception table:
from to target type
0 43 46 Class java/lang/InterruptedException
LineNumberTable:
line 17: 0
line 18: 7
line 21: 43
line 19: 46
line 20: 47
line 22: 51
LocalVariableTable:
Start Length Slot Name Signature
47 4 1 e Ljava/lang/InterruptedException;
0 52 0 this Lcom/luban/syn/SynExample;
StackMapTable: number_of_entries = 2
frame_type = 110 /* same_locals_1_stack_item */
stack = [ class java/lang/InterruptedException ]
frame_type = 4 /* same */
从上面这一段代码中我们可以发现,如果我们使用在方法中加synchronized的方式的话,我们的源码就会在指令中添加 ACC_SYNCHRONIZED
这个指令的意思就是说,当我们的java虚拟机读到这里的时候,会去看当前方法是不是可以执行,如果已经有别的方法在执行了,就阻塞,如果还没有,就执行
public void test3();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=5, locals=4, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #6 // Field java/util/concurrent/TimeUnit.SECONDS:Ljava/util/concurrent/TimeUnit;
7: lconst_1
8: invokevirtual #7 // Method java/util/concurrent/TimeUnit.sleep:(J)V
11: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
14: new #9 // class java/lang/StringBuilder
17: dup
18: invokespecial #10 // Method java/lang/StringBuilder."<init>":()V
21: invokestatic #11 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
24: invokevirtual #12 // Method java/lang/Thread.getName:()Ljava/lang/String;
27: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
30: ldc #14 // String 线程被执行,打印时间为:
32: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
35: aload_0
36: getfield #5 // Field sdf:Ljava/text/SimpleDateFormat;
39: new #21 // class java/util/Date
42: dup
43: invokespecial #22 // Method java/util/Date."<init>":()V
46: invokevirtual #23 // Method java/text/SimpleDateFormat.format:(Ljava/util/Date;)Ljava/lang/String;
49: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
52: invokevirtual #17 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
55: invokevirtual #18 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
58: goto 66
61: astore_2
62: aload_2
63: invokevirtual #20 // Method java/lang/InterruptedException.printStackTrace:()V
66: aload_1
67: monitorexit
68: goto 76
71: astore_3
72: aload_1
73: monitorexit
74: aload_3
75: athrow
76: return
Exception table:
from to target type
4 58 61 Class java/lang/InterruptedException
4 68 71 any
71 74 71 any
LineNumberTable:
line 35: 0
line 37: 4
line 38: 11
line 41: 58
line 39: 61
line 40: 62
line 42: 66
line 44: 76
LocalVariableTable:
Start Length Slot Name Signature
62 4 2 var2 Ljava/lang/InterruptedException;
0 77 0 this Lcom/luban/syn/SynExample;
StackMapTable: number_of_entries = 4
frame_type = 255 /* full_frame */
offset_delta = 61
locals = [ class com/luban/syn/SynExample, class java/lang/Object ]
stack = [ class java/lang/InterruptedException ]
frame_type = 4 /* same */
frame_type = 68 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
在上面这一段就是指在代码块中添加synchronized
这里有几个关键指令
monitorenter:进入同步代码块
monitorexit:退出同步代码块
我们可以在上面的代码中发现,当前的指令集中存在了两个monitorexit
第一个是正常执行的退出
第二个是异常执行的退出
对monitorenter的解释
每个对象都拥有一个monitor(监视器)
某一个线程想要去占有这个对象的时候,需要先看一下,monitor计数器是不是等于0,
如果不是0,
如果不是已经进入的线程,说明还有其他线程占有着,不可进入
如果是已经进入过的线程,则可以进入,monitor+1(可重入性)
如果等于0,当前线程占有这个对象,并且对这个对象的monitor+1
对monitorexit的解释
当线程走到monitorexit的时候,对monitor-1
对ACC_SYNCHRONIZED的解释
如果我们的synchronized加在了方法上,那么很明显,在我们的指令集中不存在monitorexit和monitorenter,但是会有一个ACC_SYNCHRONIZED,当程序要进入方法的时候,会检查是不是存在ACC_SYNCHRONIZED,如果存在,那么会去看一下monitor计数器是不是等于0,
如果不是0,
如果不是已经进入的线程,说明还有其他线程占有着,不可进入
如果是已经进入过的线程,则可以进入,monitor+1(可重入性)
如果等于0,当前线程占有这个对象,并且对这个对象的monitor+1
monitor在对象中的位置
如上图所示,我们的对象在内存中的存储分为三个部分,我们的monitor存储在对象头当中
synchronized的优化
在jdk1.6之前,synchronized是属于重量级锁,在1.6之后,为了减少获取锁和释放锁产生的消耗,引入了偏向锁和轻量级锁,关于具体优化细节将在下一篇博客说明