Synchronized总结

所有的并发模式在解决线程安全问题时,采用的方案都是序列化访问临界资源。即同一时刻只能有一个线程访问临界资源,也称作同步互斥访问。 Java提供synchronizedLock两种方式来实现同步互斥访问。

synchronized内置锁是一种对象锁锁的是对象而非引用作用粒度对象,可用来实现对临界资源的同步互斥访问,是可重入的,是Java中解决并发问题的一种最常用最简单的方法,它可以确保线程互斥的访问同步代码,保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性

原理

虚拟机支持方法级的同步方法内部一段指令序列的同步,两种同步都使用管程Monitor来支持的。

方法级的同步是隐式的,无须通过字节码指令来控制,它实现在方法调用返回操作之中。虚拟机可以从方法常量池方法结构中ACC_SYNCHRONIZED访问标志得知方法是否声明为同步方法。当方法调用时,调用的指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,若被设置,执行线程就要求先成功持有管程Monitor,然后才能执行方法,最后当方法执行完成,无论是否正常完成释放管程Monitor,方法执行期间,执行线程持有了管程Monitor,其他任何线程都无法再获得同一个管程Monitor。若同步方法执行期间抛出异常,且方法内部无法处理异常,同步方法所持有的管程Monitor将在异常抛到同步方法外时自动释放

Monitor监视器锁

Monitor监视器锁的实现依赖底层操作系统的Mutex lock互斥锁实现,是一个重量级锁性能较低Java1.5之后版本做了重大的优化,如锁粗化锁消除轻量级锁偏向锁适应性自旋等技术来减少锁操作的开销。

1
2
3
4
5
6
7
8
9
10
11
12
13
public synchronized void method() {
System.out.println("Synchronized");
}
// 编译后的具体信息
public synchronized void method();
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 Synchronized
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return

同步一段指令集序列通常是通过synchronized语句块来完成,虚拟机的指令集使用monitorentermonitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要Javac编译器Java虚拟机共同协作支持,编译器必须保证方法通过任何方式完成,方法中调用过的每条monitorenter指令都必须执行其对应的monitorexit指令无论该方法是否正常结束

为了保证方法异常完成时monitorentermonitorexit指令能正确配对执行,编译器会自动产生一个异常处理器,且声明可处理的所有异常,来执行monitorexit指令

monitorentermonitorexit这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。若synchronized明确指定了对象参数,那就是这个对象的reference,若没有明确指定,就根据synchronized修饰的是实例方法还是类方法,则取对应的对象实例Class对象来作为锁对象。

monitorexit指令出现了两次,第1次为同步正常退出释放锁第2次为发生异常退出释放锁wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException异常。

虚拟机规范要求,执行monitorenter指令时,首先尝试获取对象的锁,若对象没有被锁定或当前线程已经拥有这个对象的锁,将锁的计数器加一,执行monitorexit指令时将锁计数器减一,当计数器为零时锁被释放。若获取对象锁失败,当前线程阻塞等待,直到对象锁被另一个线程释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public void method() {
synchronized (this) {
System.out.println("Synchronized");
}
}

// 编译后的具体信息
public void method();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String Synchronized
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return

synchronized同步块对同一线程是可重入的不会将自己死锁,但不可中断。同步块在已进入的线程执行完成之前,会阻塞后面其他线程的进入。Java线程是映射到操作系统原生线程上的,阻塞或唤醒线程都需要操作系统帮忙,需要从用户状态转换到核心态中,因此转态转换需要耗费很多处理器时间

1
2
3
4
5
6
7
8
9
10
private final static Object object = new Object();
public static void reentrantlock(){
String tname = Thread.currentThread().getName();
synchronized (object) {
System.out.println(String.format("{}:) hold {}->monitor lock",tname,object));
synchronized (object){
System.out.println(String.format("{}:) re-hold {}->monitor lock",tname,object));
}
}
}

对象头MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址,Monitor是由ObjectMonitor实现的,其位于HotSpot虚拟机源码ObjectMonitor.hpp文件。

wait与notify

wait和notify都是Object的成员函数,因为synchronized关键字可以加在任何对象的成员函数上,任何对象都可以成为锁,所以wait和notify只能放在Object里面。

wait内部会先释放锁,然后进入阻塞状态,当被另一个线程notify后,重新拿锁,在执行后续逻辑。

对于生产者消费者模型来说,由于wait和notify所作用对象和synchronized作用的对象是同一个,只能是一个对象,无法区分队列空或队列满两个条件。

使用场景

Java中使用synchronized可使用在代码块方法中,synchronized用的位置不同锁得对象就不同。

分类 被锁对象 实例
实例方法 类得实例对象 public synchronized void sync1() {}
静态方法 类对象 public static synchronized void sync2() {}
实例对象 类得实例对象 synchronized (this) {}
class对象 类对象 synchronized (SynchronizedDemo.class) {}
任意实例对象Object 实例对象Object String lock = ""; synchronized (lock) {}

当一个线程访问某对象synchronized方法或者synchronized代码块时,其他线程对该对象的该synchronized方法或者synchronized代码块的访问将被阻塞;其他线程仍可以访问该对象的非同步代码块;其他线程对该对象其他synchronized方法或者synchronized代码块的访问将被阻塞

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
SynchronizedDemo instance = new SynchronizedDemo();
Thread thread1 = new Thread(instance, "thread1");
Thread thread2 = new Thread(instance, "thread2");
thread1.start();
thread2.start();
while (thread1.isAlive() || thread2.isAlive()) {
}
}
同步代码块

锁的对象是括号里面的this对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SynchronizedDemo implements Runnable {
@Override
public void run() {
synchronized (this) {
System.out.println("I am " + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 运行结束");
}
}
}

最终打印结果如下,可以很明显的看出run方法里的代码是顺序执行。

1
2
3
4
I am thread1
thread1 运行结束
I am thread2
thread2 运行结束

如下写法运行结果一样,除了使用this对象作为锁,也可以定义一个专门的锁对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SynchronizedDemo implements Runnable {
private Object lock = new Object();
@Override
public void run() {
synchronized (lock) {
System.out.println("I am " + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 运行结束");
}
}
}

可以定义多个锁对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class SynchronizedDemo implements Runnable {
private Object lock1 = new Object();
private Object lock2 = new Object();
@Override
public void run() {
synchronized (lock1) {
System.out.println("I am lock1 " + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " lock1部分运行结束");
}
synchronized (lock2) {
System.out.println("I am lock2 " + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " lock2部分运行结束");
}
}
}

最终打印结果如下,可以看出run方法里的代码是时被执行的。当thread1释放掉lock1的锁时,thread2即可获得lock1的锁,所以thread2lock1中的同步代码块可以与thread1lock2中的同步代码块并行执行。

1
2
3
4
5
6
7
8
I am lock1 thread1
thread1 lock1部分运行结束
I am lock2 thread1
I am lock1 thread2
thread1 lock2部分运行结束
thread2 lock1部分运行结束
I am lock2 thread2
thread2 lock2部分运行结束
方法锁

锁对象是当前实例对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SynchronizedDemo3 implements Runnable {
@Override
public void run() {
this.method();
}
public synchronized void method() {
System.out.println("方法锁:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
}

最终打印结果如下

1
2
3
4
方法锁:thread1
thread1运行结束
方法锁:thread2
thread2运行结束
类锁

锁对象是当前类对象Java类可能有多个对象,但只有一个Class对象,而类锁的锁对象就是该类的Class对象,类锁有两种形式:将synchronized加在static方法上和synchronized(*.class)代码块synchronized加在static方法上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SynchronizedClassStatic4 implements Runnable {
@Override
public void run() {
method();
}
public static synchronized void method() {
System.out.println("静态方法锁:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
public static void main(String[] args) {
SynchronizedClassStatic4 instance1 = new SynchronizedClassStatic4();
SynchronizedClassStatic4 instance2 = new SynchronizedClassStatic4();
Thread thread1 = new Thread(instance1, "thread1");
Thread thread2 = new Thread(instance2, "thread2");
thread1.start();
thread2.start();
while (thread1.isAlive() || thread2.isAlive()) {
}
System.out.println("finished");
}
}

最终打印结果如下

1
2
3
4
静态方法锁:thread2
thread2运行结束
静态方法锁:thread1
thread1运行结束

synchronized(*.class)代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class SynchronizedClassClass5 implements Runnable {
@Override
public void run() {
synchronized (SynchronizedClassClass5.class) {
System.out.println("类锁同步代码块形式:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
}
public static void main(String[] args) {
SynchronizedClassClass5 instance1 = new SynchronizedClassClass5();
SynchronizedClassClass5 instance2 = new SynchronizedClassClass5();
Thread thread1 = new Thread(instance1, "thread1");
Thread thread2 = new Thread(instance2, "thread2");
thread1.start();
thread2.start();
while (thread1.isAlive() || thread2.isAlive()) {
}
System.out.println("finished");
}
}

最终打印结果如下

1
2
3
4
类锁同步代码块形式:thread1
thread1运行结束
类锁同步代码块形式:thread2
thread2运行结束

多线程访问synchronized同步方法常见的7情况:

  • 两个线程同时访问一个对象的同步方法:阻塞
  • 两个线程访问的是两个对象的同步方法:非阻塞
  • 两个线程访问的是synchronized静态方法:阻塞
  • 同时访问同步方法非同步方法:非阻塞
  • 访问同一对象不同普通同步方法:阻塞
  • 同时访问静态synchronized非静态synchronized方法:非阻塞
  • 方法抛出异常后,会释放锁

可重入

同一个方法可重入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public synchronized void method(int a) {
System.out.println("a = " + a);
if (a == 0) {
method(a + 1);
}
}

private final static Object object = new Object();
public static void reentrantLock() {
String tname = Thread.currentThread().getName();
synchronized (object) {
System.out.println(String.format("{}:) hold {}->monitor lock", tname, object));
synchronized (object) {
System.out.println(String.format("{}:) re-hold {}->monitor lock", tname, object));
}
}
}

不同的方法可重入

1
2
3
4
5
6
7
8
public synchronized void methodA() {
System.out.println("methodA");
methodB();
}

public synchronized void methodB() {
System.out.println("methodB");
}

不同的类可重入

1
2
3
4
5
6
7
8
9
10
11
12
public class SynchronizedDemo {
public synchronized void methodB() {
System.out.println("methodB");
}
}

class TestClass extends SynchronizedDemo {
public synchronized void methodB() {
System.out.println("subclass methodB");
super.methodB();
}
}