前言
synchronized关键字,多线程并发编程最常用关键字
作用
对于synchronized关键字的作用,简单来说一句话就可以概况:
在同一时刻,最多只有一个线程能执行该段代码。
以串行化的方式执行代码,自然不存在线程安全问题。
用法
synchronized关键字的用法大致可以分为两类:对象锁和类锁。当然,Java中一切皆对象,类也是对象。所以这里的对象锁中的对象可以认为是狭义上的对象。
- 对象锁:锁住的是一个对象,具体用法是利用synchronized关键字修饰非静态方法或者synchronized代码块中的参数传入对象实例(比如:this)
- 类锁:锁住的是整个类(或者说是
java.lang.Class
的对象),具体用法是用synchronized修饰静态方法或者synchronized代码块中的参数传入xxx.class
对象锁
-
synchronized关键字修饰
非静态方法
public class ObjectLockDemo implements Runnable { private static int count = 0; public static void main(String[] args) throws InterruptedException { ObjectLockDemo instance = new ObjectLockDemo(); Thread thread1 = new Thread(instance); Thread thread2 = new Thread(instance); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(count); } @Override public void run() { increase(); } public synchronized void increase() { for (int i = 0; i < 100000; i++) { count++; } } }
-
synchronized代码块中的参数传入对象实例
public class ObjectLockDemo implements Runnable { private static int count = 0; public static void main(String[] args) throws InterruptedException { ObjectLockDemo instance = new ObjectLockDemo(); Thread thread1 = new Thread(instance); Thread thread2 = new Thread(instance); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(count); } @Override public void run() { increase(); } public void increase() { synchronized (this) { for (int i = 0; i < 100000; i++) { count++; } } } }
两个实例都是实例锁,即当thread1和thread2传入同一个instance时,才能起到锁的效果。想要看到不正确的写法,thread1和thread2中传入不同的实例即可。
// 其他代码不变,只改下这两行代码
Thread thread1 = new Thread(new ObjectLockDemo());
Thread thread2 = new Thread(new ObjectLockDemo());
类锁
-
synchronized修饰静态方法
public class ClassLockDemo implements Runnable { private static int count = 0; public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(new ClassLockDemo()); Thread thread2 = new Thread(new ClassLockDemo()); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(count); } @Override public void run() { for (int i = 0; i < 100000; i++) { increase(); } } public synchronized static void increase() { count++; } }
-
synchronized代码块中的参数传入
xxx.class
public class ClassLockDemo implements Runnable { private static int count = 0; public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(new ClassLockDemo()); Thread thread2 = new Thread(new ClassLockDemo()); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(count); } @Override public void run() { for (int i = 0; i < 100000; i++) { increase(); } } public static void increase() { synchronized (ClassLockDemo.class) { count++; } } }
类锁锁住的是整个类,或者说是Class类的对象,只要传入的都是同一个类的实例,就能起到互斥的作用。
以上是synchronized关键字的两种基本用法:对象锁和类锁。深入理解这两种用法,是正确使用synchronized的关键。
synchronized实现原理
synchronized实现原理同样分成两种情况:修饰代码块、修饰方法。
修饰代码块
public class ObjectLockDemo {
public void increase() {
synchronized (this) {
System.out.println("increase()");
}
}
}
代码编译后,使用反编译命令javap
查看字节码
运行javap -v -p ObjectLockDemo.class
命令后会看到如下字节码,因为synchronized 关键字在increase()
方法中,所以只需要看increase()
相关的字节码
public void increase();
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 increase()
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关键字修饰代码块时,是通过monitorenter
指令和monitorexit
指令来时实现的,因为正常执行完成和抛出异常都会释放锁,所以这个方法的字节码会有两个monitorexit
指令,分别处于正常分支和异常分支。
修饰方法
public class ObjectLockDemo {
public synchronized void increase() {
System.out.println("increase()");
}
}
运行javap -v -p ObjectLockDemo.class
命令后会看到如下字节码,因为synchronized 关键字修饰increase()
方法,所以只需要看increase()
相关的字节码
public synchronized void increase();
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 increase()
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 11: 0
line 12: 8
通过字节码可以看到,synchronized修饰方法时,并不是通过monitorenter
和monitorexit
指令实现的,而是在flags中加入了ACC_SYNCHRONIZED
标识。
也就是说synchronized修饰代码块时,通过monitorenter
和monitorexit
指令实现同步锁;修饰方法时,通过flags中ACC_SYNCHRONIZED
实现同步锁。monitorenter
、monitorexit
、ACC_SYNCHRONIZED
,都是基于Monitor(监视器)来实现的。
Monitor其实是一种同步工具,也可以说是一种同步机制,它通常被描述为一个对象
在HotSpot虚拟机中,Monitor是通过ObjectMonitor
类来实现的,主要属性如下:
ObjectMonitor() {
_header = NULL;
_count = 0; // 线程获取锁的次数
_waiters = 0,
_recursions = 0; // 锁重入的次数
_object = NULL;
_owner = NULL; // 指向持有monitorObject对象的线程
_WaitSet = NULL; // 等待池
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 锁池
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
Java中每个对象都与一个监视器相关联,一次只有一个线程可以锁定监视器。
当多个线程同时访问同一段同步代码时,首先包装成ObjectWaiter对象进入到_EntryList
集合里面,当线程获取到对象的Monitor后,会进入_object
区域,并把_owner
指向当前线程,_count
变量加1。
如果线程调用wait()
方法,会释放当前持有的Monitor,_owner
恢复成NULL,_count
减1。线程进入_WaitSet
集合中等待被唤醒。如果当前线程执行完毕,也会释放Monitor锁,并且复位对应变量的值。
synchronized特性
从不同的角度来划分的话,synchronized是独占锁、非公平锁、悲观锁、可重入锁、不可中断锁。
这里重点介绍下synchronized的可重入和不可中断性。
可重入性
若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。
通俗点说就是:线程获取锁进入一个synchronized方法/代码块,又调用了一个synchronized方法/代码块,在进入第二个synchronized方法/代码块时,不需要先释放进入第一个synchronized时获取的锁,也不需要再次争抢锁,而是直接进入。所以可重入锁也叫递归锁。
synchronized的可重入性不需要两个synchronized方法是同一个方法、也不需要是同一个类中的方法,仅仅要求是同一个线程。下面演示可重入性
-
同一个方法
public class ObjectLockDemo { private static int count = 0; public static void main(String[] args) { new ObjectLockDemo().display(); } public synchronized void display() { System.out.println("display():count=" + count); if (count++ == 0) { display(); } } }
执行结果:
display():count=0 display():count=1
从输出结果可以看出,获得锁的线程两次进入同一个synchronized方法并不会产生死锁,因为synchronized具有可重入性。
-
不同的方法
public class ObjectLockDemo { public static void main(String[] args) { new ObjectLockDemo().displayA(); } public synchronized void displayA() { System.out.println("displayA()"); displayB(); } public synchronized void displayB() { System.out.println("displayB()"); } }
执行结果
displayA() displayB()
从输出结果可以看出,获得锁的线程两次进入不同的synchronized方法并不会产生死锁,因为synchronized具有可重入性。
-
不同的类
public class ObjectLockDemo { public synchronized void display() { System.out.println("ObjectLockDemo.display()"); } } public class ReentryDemo { public static void main(String[] args) { new ReentryDemo().display(); } public synchronized void display() { ObjectLockDemo lockDemo = new ObjectLockDemo(); System.out.println("ReentryDemo.display()"); lockDemo.display(); } }
执行结果
ReentryDemo.display() ObjectLockDemo.display()
从输出结果可以看出,获得锁的线程两次进入不同的类中synchronized方法并不会产生死锁,因为synchronized具有可重入性。
可重入性的优点:避免死锁和提高封装性。避免死锁在上面三个例子中已经有了很好的体现。可重入性避免了使用了同一把锁时,反复的加锁解锁,提高了封装性,简化了锁的使用难度。
不可中断
一旦锁被某个线程获得了,别的线程想要获得锁,只能选择等待或者阻塞,直到那个线程释放了这个锁。如果那个线程永远不释放,其他线程只能永远等下去。
synchronized缺点
- 试图获得锁时,不能设置超时时间,不能中断正在等待获取锁的线程
- 加锁和解锁条件单一,加锁仅仅有单一条件(对象或者类)
- 早期版本效率低下(重量级锁)