synchronized关键字
synchronized的作用
官网解释翻译
同步方法支持一种简单的策略来防止线程干扰和内存一致性错误:如果一个对象对多个线程可见,则对该对象变量的所有读取或写入都是通过同步方法完成的。
通俗解释
能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。
synchronized的地位
- synchronized的java的关键字,被java语言原生支持
- 是最基本的互斥同步手段
- 是并发编程种的元老级角色,是并发编程的必学内容
synchronized的两种用法
对象锁
- 包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定锁对象)
代码块形式
- 手动指定锁对象
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
29
30
31public class SynchronizedObjectCodeBlock2 implements Runnable {
public static void main(String[] args) {
SynchronizedObjectCodeBlock2 s = new SynchronizedObjectCodeBlock2();
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("-----");
}
public void run() {
synchronized (this) {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
}
}
方法锁形式
- synchronized修饰普通方法,锁对象默认为this
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
28public class SynchronizedObjectCodeBlock2 implements Runnable {
public static void main(String[] args) {
SynchronizedObjectCodeBlock2 s = new SynchronizedObjectCodeBlock2();
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("-----");
}
public void run() {
test();
}
private synchronized void test() {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
}
类锁
- 指synchronized修饰静态的方法或指定锁为class对象
概念
- Java类可能有很多实例对象,但只有1个class对象
- 所谓的类锁,不过是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
27
28
29public class SynchronizedObjectCodeBlock2 implements Runnable {
public static void main(String[] args) {
SynchronizedObjectCodeBlock2 s1 = new SynchronizedObjectCodeBlock2();
SynchronizedObjectCodeBlock2 s2 = new SynchronizedObjectCodeBlock2();
Thread t1 = new Thread(s1);
Thread t2 = new Thread(s2);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("-----");
}
public void run() {
test();
}
private static synchronized void test() {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
} - 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
26
27public class SynchronizedObjectCodeBlock2 implements Runnable {
public static void main(String[] args) {
SynchronizedObjectCodeBlock2 s1 = new SynchronizedObjectCodeBlock2();
SynchronizedObjectCodeBlock2 s2 = new SynchronizedObjectCodeBlock2();
Thread t1 = new Thread(s1);
Thread t2 = new Thread(s2);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("-----");
}
public void run() {
synchronized (SynchronizedObjectCodeBlock2.class) {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
}
}
多线程访问同步方法的7种情况
两个线程同时访问一个对象的同步方法
- 会串行执行
- 同步方法默认锁的是this,也就是当前对象,每次只能有一个线程获取到锁
两个线程访问的是两个对象的同步方法
- 会并行执行
- 默认锁的是this,也就是当前对象,由于是来个对象,也就是不同的锁,所以两个线程会同时执行
两个线程访问的是synchronized的静态方法
- 会串行执行
- 静态方法锁的是class对象,class对象只有一个,所以两个线程是同一把锁
两个线程同时访问同步方法与非同步方法
- 会并行执行
- 同步方法会受到保护,非同步方法不会上锁,所以会并行执行
访问同一个对象的不同的普通同步方法
- 会串行执行
- 同步方法锁的是this当前对象,即便是不同的方法,锁的也是同一个对象,所以会串行执行
同时访问静态synchronized和非静态synchronized方法
- 并行执行
- 静态方法锁的class类,非静态方锁的是当前对象,两个方法并不是同一个锁,所以会并行执行
方法抛异常后,会释放锁
- 异常之后会释放锁,不会影响后续的执行
- 先抛出异常,后面才能进入同步方法
性质
可重入
- 什么是可重入:指的是同一个线程的外层函数获得锁之后,内层函数可以直接再次获取该锁
- 好处:避免死锁,提升封装性
比如方法1被synchronized修饰,在方法里面递归调用该方法,也是可以重入的
方法1和方法2都被synchronized修饰,那么在方法1中也是可以调用方法2的
不可中断
- 一旦这个锁已经被别人获得了,如果我还想获得,我只能选择等待或者阻塞,直到别人的线程释放这个锁,如果别人永远不释放锁,那么我只能永远地等待下去。
原理
加锁和释放锁的原理
- 获取呵呵释放锁的时机:进入和退出同步代码块(运行完毕或者抛出异常)
- 等价代码(使用lock锁必须要显示的上锁解锁,并且需要考虑异常情况,synchronized则不需要考虑这些)
- 看字节码:monitor相关指令
使用javac xxx.java
将java文件编译成class文件
再使用java -v xxx.class
反编译class文件
具体代码如下:反编译后,insert方法如下:1
2
3
4
5
6
7public class Decompilation {
private Object object = new Object();
public void insert(Thread thread){
synchronized (object){
}
}
}
从图中可以看出,第6行monitorenter指令,上锁
第8行monitorexit指令,释放锁
第14行monitorexit指令,则是在抛出异常的情况下释放锁
可重入原理:加锁次数计数器
- JVN会记录被加锁大的次数
- 第一次加锁时,次数从0变为1,之后如果再次加锁,就从1变成2,以次类推
- 退出一层同步代码块时,计数减一,当计数为0的时候表示锁释放
可见性
- 一个线程执行的结果,另一个线程不一定可见
- 线程1操作x=5,之后线程2可能读取到之前的数据x=3
- synchronized可以保证可见性
缺陷
- 效率低:锁的释放情况少,试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程。
- 不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的。
- 无法知道是否成功获取到锁
常见面试题
- 使用注意点:锁的范围不宜过大、避免锁的嵌套
- 如何选择Look和synchronized关键字?
如果synchronized能够满足需求的话,优先使用synchronized,无法满足再使用Lock锁
- 多线程访问同步方法的各种具体情况
上面所述的七种情况