今天说一下并发的基本同步工具synchronized以及volatile。
- synchronized
synchronized是并发编程中接触的最基本的同步工具,是一种重量级锁,也是java内置的同步机制,首先我们知道synchronized提供了互斥性的语义和可见性,那么我们可以通过使用它来保证并发的安全。 - synchronized三种用法:
- 对象锁
当使用synchronized修饰类普通方法时,那么当前加锁的级别就是实例对象,当多个线程并发访问该对象的同步方法、同步代码块时,会进行同步。 - 类锁
当使用synchronized修饰类静态方法时,那么当前加锁的级别就是类,当多个线程并发访问该类(所有实例对象)的同步方法以及同步代码块时,会进行同步。 - 同步代码块
当使用synchronized修饰代码块时,那么当前加锁的级别就是synchronized(X)中配置的x对象实例,当多个线程并发访问该对象的同步方法、同步代码块以及当前的代码块时,会进行同步。
使用同步代码块时要注意的是不要使用String类型对象,因为String常量池的存在,所以很容易导致出问题
- 对象锁
- synchronized实现原理
synchronized与其他锁不同,它是内置在JVM中的,从JVM规范中看,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。
方法级的同步是隐式的, 即无须通过字节码指令来控制, 它实现在方法调用和返回操作之中。 虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。 当方法调用时, 调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置, 如果设置了, 执行线程就要求先成功持有管程, 然后才能执行方法, 最后当方法完成(无论是正常完成还是非正常完成) 时释放管程。 在方法执行期间, 执行线程持有了管程, 其他任何线程都无法再获取到同一个管程。 如果一个同步方法执行期间抛出了异常, 并且在方法内部无法处理此异常, 那么这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。
我们通过一段代码来看一下:
public class Test {
static final Test object = new Test();
static final Test object1 = new Test();
public static void main(String[] args) throws InterruptedException {
Test t = new Test();
new Thread(new Runnable() {
@Override
public void run() {
t.test();
}
}).start();
}
int a(int a) {
if (a == 0) {
throw new NullPointerException();
} else {
return a;
}
}
public void test() {
synchronized (object) {
//进入同步快,获取锁
try {
System.out.println(Thread.currentThread().getId() + "获得锁");
Thread.sleep(3000);
//当前同步块结束,释放锁
System.out.println(Thread.currentThread().getId() + "释放锁");
} catch (InterruptedException e) {
}
}
}
这里的代码大家可以自己运行一下,这只是让大家便于理解,下面我们来看一下程序实际是如何运行的,我把test()的字节码给大家标注了一下
// 获取Test类静态实例对象字段,将值压入操作数栈顶
0 getstatic #11 <Test.object>
//复制栈顶数值并将复制值压入栈顶
3 dup
//将栈顶引用类型数值存入第2个本地变量
4 astore_1
//获得对象锁
5 monitorenter
6 getstatic #12 <java/lang/System.out>
9 new #13 <java/lang/StringBuilder>
12 dup
13 invokespecial #14 <java/lang/StringBuilder.<init>>
16 invokestatic #15 <java/lang/Thread.currentThread>
19 invokevirtual #16 <java/lang/Thread.getId>
22 invokevirtual #17 <java/lang/StringBuilder.append>
25 ldc #18 <获得锁>
27 invokevirtual #19 <java/lang/StringBuilder.append>
30 invokevirtual #20 <java/lang/StringBuilder.toString>
33 invokevirtual #21 <java/io/PrintStream.println>
36 ldc2_w #22 <3000>
39 invokestatic #24 <java/lang/Thread.sleep>
42 getstatic #12 <java/lang/System.out>
45 new #13 <java/lang/StringBuilder>
48 dup
49 invokespecial #14 <java/lang/StringBuilder.<init>>
52 invokestatic #15 <java/lang/Thread.currentThread>
55 invokevirtual #16 <java/lang/Thread.getId>
58 invokevirtual #17 <java/lang/StringBuilder.append>
61 ldc #25 <释放锁>
63 invokevirtual #19 <java/lang/StringBuilder.append>
66 invokevirtual #20 <java/lang/StringBuilder.toString>
69 invokevirtual #21 <java/io/PrintStream.println>
72 goto 76 (+4)
75 astore_2
76 aload_1
//释放对象锁
77 monitorexit
//没有异常发生,直接返回
78 goto 86 (+8)
81 astore_3
82 aload_1
//这里是catch块中,当发行异常,无条件释放锁
83 monitorexit
84 aload_3
85 athrow
86 return
可以看到,synchronized在底层是如何运行的。
- volatile
- 可见性
我们知道volatile可以看做是一种synchronized的轻量级锁,他能够保证并发时,被它修饰的共享变量的可见性,那么他是如何实现可见性的呢?
我们从jmm的角度来看一下,每个线程拥有自己的工作内存,实际上线程所修改的共享变量是从主内存中拷贝的副本,当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。 - 实现原理
被volatile修饰的共享变量在进行写操作的时候:- 将当前处理器缓存行的数据写回到系统内存。
- 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里
- 使用场景
1.访问变量不需要加锁(加锁的话使用volatile就没必要了)- 对变量的写操作不依赖于当前值(因为他不能保证原子性)
3.该变量没有包含在具有其他变量的不变式中。
综上所述:一般我们会用来修饰状态标志;读写锁(读>>写,对写加锁,读不加锁);DCL的单例模式中;volatile bean(例如放入HTTPSession中的对象)
了解完上面的知识,我们来做一下对比:
- 对变量的写操作不依赖于当前值(因为他不能保证原子性)
- 可见性
- 相同点:都保证了可见性
- 不同点 : volatile不能保证原子性,但是synchronized会发生阻塞(在线程状态转换中详说),开销更大。