主要介绍synchronized的用法、synchronized的原理,以及synchronized是如何提供原子性、可见性和有序性保障的等。
一.synchronize 的用法
synchronized是Java提供的一个并发控制的关键字。主要有两种用法,分别是同步方法
和同步代码块
。也就是说,synchronized既可以修饰方法也可以修饰代码块。
/**
*
* @author YvesHe
*
*/
public class SynchronizedDemo {
// 同步方法
public synchronized void doSth() {
System.out.println("Hello World");
}
// 同步代码块
public void doSth1() {
synchronized (SynchronizedDemo.class) {
System.out.println("Hello World");
}
}
}
被synchronized修饰的代码块及方法,在同一时间,只能被单个线程访问。
二.synchronize 的原理
public synchronized void doSth();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
public void doSth1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: ldc #5 // class com/hollis/SynchronizedTest
2: dup
3: astore_1
4: monitorenter
5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #3 // String Hello World
10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
通过反编译后代码可以看出:
对于同步方法,JVM采用ACC_SYNCHRONIZED标记符来实现同步。
对于同步代码块。JVM采用monitorenter、monitorexit两个指令来实现同步。
无论是ACC_SYNCHRONIZED还是monitorenter、monitorexit都是基于Monitor实现的,在Java虚拟机(HotSpot)中,Monitor是基于C++实现的,由ObjectMonitor实现。
ObjectMonitor类中提供了几个方法,如enter、exit、wait、notify、notifyAll等。sychronized加锁的时候,会调用objectMonitor的enter方法,解锁的时候会调用exit方法。
参考:
https://blog.csdn.net/u012465296/article/details/53022317
http://www.hollischuang.com/archives/1883
http://cmsblogs.com/?p=2071
三.synchronized 保证线程安全
synchronized是如何提供原子性
原子性是指一个操作是不可中断的,要全部执行完成,要不就都不执行。
在Java中,为了保证原子性,提供了两个高级的字节码指令monitorenter和monitorexit。在synchronize 的原理
中介绍过,这两个字节码指令,在Java中对应的关键字就是synchronized。
通过monitorenter和monitorexit指令,可以保证被synchronized修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。因此,在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。
synchronized是如何提供可见性
可见性是指当多个线程访问同一个变量时(可以理解成类中的全局变量),一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。所以,就可能出现线程1改了某个变量的值,但是线程2不可见的情况。
在java中synchronized修饰的代码,在开始执行时会加锁,执行完成后会进行解锁。而为了保证可见性,有一条规则是这样的:对一个变量解锁之前,必须先把此变量同步回主存中。
这样解锁后,后续线程就可以访问到被修改后的值。
所以,synchronized关键字锁住的对象,其值是具有可见性的。
synchronized是如何提供有序性
有序性即程序执行的顺序按照代码的先后顺序执行。
在java内存模型中,由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,比如 load->add->save 有可能被优化成 load->save->add 。这就是可能存在有序性问题。
synchronized是无法禁止指令重排和处理器优化的。也就是说,synchronized无法避免上述提到的问题。那么它是如何做到保证代码的有序性的呢?
这里需要提到
as-if-serial语义
as-if-serial语义
的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不会改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义。
简单说就是,as-if-serial语义保证了单线程中,指令重排是有一定的限制的,而只要编译器和处理器都遵守了这个语义,那么就可以认为单线程程序是按照顺序执行的.
当然,实际上还是有重排的,只不过我们无须关心这种重排的干扰。
由于synchronized修饰的代码,同一时间只能被同一线程访问。也就是单线程执行的。遵循as-if-serial
语义,所以它可以保证程序的整体有序性。
四.synchronized实战例子
synchronized-synchronized设计一个死锁程序
/**
* 死锁问题,在设计程序时就应该避免双方相互持有对方的锁的情况
*
* @author YvesHe
*
*/
public class DeadLock implements Runnable {
private String tag;
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public void setTag(String tag) {
this.tag = tag;
}
@Override
public void run() {
if (tag.equals("a")) {
synchronized (lock1) {
try {
System.out.println("当前线程 : " + Thread.currentThread().getName() + " 进入lock1执行");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("当前线程 : " + Thread.currentThread().getName() + " 进入lock2执行");
}
}
}
if (tag.equals("b")) {
synchronized (lock2) {
try {
System.out.println("当前线程 : " + Thread.currentThread().getName() + " 进入lock2执行");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("当前线程 : " + Thread.currentThread().getName() + " 进入lock1执行");
}
}
}
}
public static void main(String[] args) {
DeadLock d1 = new DeadLock();
d1.setTag("a");
DeadLock d2 = new DeadLock();
d2.setTag("b");
Thread t1 = new Thread(d1, "t1");
Thread t2 = new Thread(d2, "t2");
t1.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
synchronized-synchronized脏读例子
业务整体需要使用完整的synchronized,保持业务的原子性。所以应该在使用共用变量(读取和设置)的方法中都使用synchronized来进行同步.
public class DirtyRead {
public static void main(String[] args) throws Exception {
final User dr = new User();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
dr.setValue("newName", "newPassword");
}
});
t1.start();
Thread.sleep(1000);
dr.getValue();
}
public static class User {
private String username = "oldName";
private String password = "oldPassword";
public synchronized void setValue(String username, String password) {
this.username = username;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.password = password;
System.out.println("setValue最终结果:username = " + username
+ " , password = " + password);
}
// public synchronized void getValue() {
public void getValue() {
System.out.println("getValue方法得到:username = " + this.username
+ " , password = " + this.password);
}
}
}
结果:
synchronized-synchronized可重入锁例子
public class Reentry1 {
public synchronized void method1() {
System.out.println("method1..");
method2();// 重入method2
}
public synchronized void method2() {
System.out.println("method2..");
method3();// 重入method3
}
public synchronized void method3() {
System.out.println("method3..");
}
public static void main(String[] args) {
final Reentry1 sd = new Reentry1();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
sd.method1();
}
});
t1.start();
}
}
// 运行结果:
// method1..
// method2..
// method3..
例子2:
public class Reentry2 {
static class Main {
public int i = 10;
public synchronized void operationSup() {
try {
i--;
System.out.println("Main print i = " + i);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class Sub extends Main {
public synchronized void operationSub() {
try {
while (i > 0) {
i--;
System.out.println("Sub print i = " + i);
Thread.sleep(100);
this.operationSup();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
Sub sub = new Sub();
sub.operationSub();
}
});
t1.start();
}
}
// 运行结果:
// Sub print i = 9
// Main print i = 8
// Sub print i = 7
// Main print i = 6
// Sub print i = 5
// Main print i = 4
// Sub print i = 3
// Main print i = 2
// Sub print i = 1
// Main print i = 0
synchronized-锁对象引用的改变,会影响锁的情况
public class ChangeLock {
private String lock = "lock";
private void method() {
synchronized (lock) {
try {
System.out.println("当前线程 : " + Thread.currentThread().getName() + "开始");
lock = "change lock";// 改变锁引用
Thread.sleep(2000);
System.out.println("当前线程 : " + Thread.currentThread().getName() + "结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final ChangeLock changeLock = new ChangeLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
changeLock.method();
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
changeLock.method();
}
}, "t2");
t1.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
结果:
我们发现么有等t1结束t2就获得锁开始了.
synchronized-锁对象中属性值的改变,不会影响锁的情况
synchronized-锁引用的分类: 类锁,本对象锁,任意对象锁
synchronized-使用同步代码块减小锁的粒度,提高性能
参考:
http://cmsblogs.com/?p=1643#13synchronized (Java并发编程实战—–synchronized)
https://www.hollischuang.com/archives/2637(synchronized是什么)
http://www.hollischuang.com/archives/2550 (Java内存模型是什么)