同步:串行执行 非同步:并行执行,会出现线程安全问题,Synchronized
就是一种解决线程安全问题方案
同步方法一般通过使用Synchronized
关键字实现,保证同一时间只有一个线程可以执行指定代码,从而保证了线程安全。有两种方式对方法进行加锁操作:
- 同步方法:在方法签名出使用
Synchronized
关键字,这里分为两种情况(不同方法和static
静态方法) - 同步代码块:使用
Synchronized
(对象或类)同步代码块,使用对象作为锁的对象锁,以及使用类作为锁的类锁
对象锁:只能控制这个对象运行的同步方法,无法同步不同对象的同步方法,因为锁对象一样
类锁:全局锁定,无论哪个对象运行的同步方法都会被同步
这里的使用原则是锁的范围尽量小,锁的时间尽量短,能锁对象,就不要锁类;能锁代码块,就不要锁方法
常见加锁情况
总的来说常见情况有四种,是根据上面两种加锁方式具体来说的
普通方法(非static)加锁
public synchronized void commonMethodAddLock() {
// TODO
}
这种情况使用的锁对象是当前调用者对象this
,属于对象锁
静态static
方法加锁
public static synchronized void staticMethodAddLock() {
// TODO
}
这种情况使用的锁对象是方法所属类,属于类锁
类锁作为同步代码块锁对象
public void methodBlockByClassLock() {
// TODO
synchronized (Object.class) {
// 同步操作
}
// TODO
}
这里的类锁对象没有限制,只要使用一个类的Class对象就行。从某种意义上来说,也是用一个对象去加锁的,因为是Class对象嘛,只不过这是一个很特别的对象
对象锁作为同步代码块锁对象
public void methodBlockByObjectLock() {
// TODO
synchronized (this) {
// 同步操作
}
// TODO
}
这里来看几个对象锁同步方法的变种来判断是否能同步,依据就是:是否是同一个锁对象
对象锁变种说明
- 变种1:
- 变种2:
- 变种3:
- 变种4:在变种3中使用了String对象作为锁对象,通过new出来的String对象,如果使用一个字面量呢?
这里似乎和变种2发生情况冲突了,实际上是String字面量存在常量池中只有一份,也相当于使用了同一个锁对象
加锁原理
JVM
底层通过监视锁来实现Synchronized
同步的,监视锁即monitor
,是每一个对象与生俱来的一个隐藏字段。JVM
根据Synchronized
当前使用环境,找到对象的monitor
,再根据monitor
状态进行加,解锁。当线程进入同步方法/同步代码块时,会获取其所属对象的monitor
进行加锁,如果成功就成为当前唯一的持有者,在monitor
被释放之前,其他线程不能再获取
来看下反汇编(javap -c
)的效果
同步代码块中monitorenter
和monitorexit
这两个字节码指令就是用于获取和释放monitor
的。如果使用monitorenter
进入时monitor
为0,表示该线程可以执行后续同步代码,并将monitor
加1;如果当前线程已经持有monitor
,那么继续加1;如果monitor
非0,其他线程就会进入阻塞状态
释放锁的时机
- 当同步代码运行后,自动释放锁
- 当同步代码运行中出现异常并捕获后,自动释放锁
Synchronized
只能通过自动释放的方式解锁,无法手动控制
改变对Synchronized的印象
之前对于同步方法的印象一直不好,因为同步方法必然导致未获得锁的对象只能等待,存在性能问题
但是在JDK6
之后的版本通过对底层不断地优化,使得性能已经比之前有很大的提升了。具体就是JVM
会将Synchronized
关键字
偏向锁就是再对象头上设置第一个申请锁的线程ID,表示这个对象偏向于这个线程
偏向锁是为了在资源还没被多线程竞争的情况下尽量减少锁带来的性能损耗,它可以降低无竞争开销,但是它不是互斥的。当出现锁的竞争情况,偏向锁就会自动被撤销并升级为轻量级锁;如果资源竞争非常激烈,会升级为重量级锁
Synchronized的性质
可重入
public synchronized void method01() {
System.out.println("我是同步方法1" + Thread.currentThread().getName());
method02();
}
public synchronized void method02() {
System.out.println("我是同步方法2" + Thread.currentThread().getName());
}
public static void main(String[] args) {
SynchronizedTest test = new SynchronizedTest();
new Thread(() -> {
test.method01();
}).start();
}
两个打印语句会同时输出,说明同一个锁对象对象的同步方法/代码块是可以重入的,不需要再次去获得锁
不可中断
同步方法/铜鼓代码块在执行过程种,如果没有出现异常等特殊情况,是无法被中断的,其他线程只能阻塞等待
保证可见性
因为已经同步,所以对共享资源的修改是可以保证可见性的,修改的数据会在锁释放之前将其从线程内存中写回到主内存中
多线程访问同步方法的7种情况
两个线程访问一个对象的同步方法
结论:同步
两个线程访问两个对象的同步方法
结论:无法同步
两个线程访问的是synchronized修饰的静态方法
结论:同步
前三种情况在上边对象锁变种说明里已经分析过
两个线程同时访问同步方法和非同步方法
public synchronized void method01() {
System.out.println("我是同步方法" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void method02() {
System.out.println("我是非同步方法" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SynchronizedTest test = new SynchronizedTest();
new Thread(() -> {
test.method01();
}).start();
new Thread(() -> {
test.method02();
}).start();
}
结果会同时打印出结果,说明同步方法和非同步方法之间互不影响
两个线程访问同一个对象的不同的普通同步方法
public synchronized void method01() {
System.out.println("我是同步方法" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void method02() {
System.out.println("我是同步方法" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SynchronizedTest test = new SynchronizedTest();
new Thread(() -> {
test.method01();
}).start();
new Thread(() -> {
test.method02();
}).start();
}
结果会串行执行,因为使用this
作为锁对象,在上边对象锁变种说明里也已分析
两个线程同时访问静态synchronized和非静态synchronized方法
public static synchronized void method01() {
System.out.println("我是同步方法" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void method02() {
System.out.println("我是非同步方法" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SynchronizedTest test = new SynchronizedTest();
new Thread(() -> {
test.method01();
}).start();
new Thread(() -> {
test.method02();
}).start();
}
结果会同时打印出结果,因为静态同步方式使用Class对象作为锁,普通同步方法使用this作为锁,不同的锁对象
两个线程同时访问普通同步方法,一个线程抛出异常并捕获,另一个线程是否受到影响
public synchronized void method01() {
System.out.println("我是同步方法" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(3);
// 出现异常并捕获
throw new RuntimeException();
} catch (Exception e) {
e.printStackTrace();
}
}
public synchronized void method02() {
System.out.println("我是同步方法" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SynchronizedTest test = new SynchronizedTest();
new Thread(() -> {
test.method01();
}).start();
new Thread(() -> {
test.method02();
}).start();
}
结论如图:
说明不同线程之间没有影响,并且出现异常并捕获之后JVM
会自动释放锁
总结
判断多线程下调用同步方法/同步代码块能否同步,重点在于判断是否是同一个锁对象,如果是则同步串行执行,否则无法同步,不同锁对象之间互不影响,同步方法与普通方法之间互不影响
一个最简单的死锁例子
两个线程互相持有一个锁,还需要去获取对方的锁,这个时候就会出现死锁,在编程中一定要避免这种情况发生
public void method01() {
synchronized (this) {
System.out.println("操作1" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Object.class) {
System.out.println("操作2" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void method02() {
synchronized (Object.class) {
System.out.println("操作2" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(6);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this) {
System.out.println("操作1" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
SynchronizedTest test = new SynchronizedTest();
new Thread(() -> {
test.method01();
}).start();
new Thread(() -> {
test.method02();
}).start();
}
缺点以及如何选择
Synchronized
是加锁,解锁的时机都是自动完成的,没法干预,与Lock
接口相比不够灵活,并且没法感知到是否加锁成功。线程试图获取锁时不能设置超时时间(Lock
可以),所以会一直傻傻地阻塞等待,同时也不能中断一个正在尝试获得锁的线程
在JUC
包下的类,Lock
,Synchronized
选择,推荐优先使用JUC
包下的并发类,其次使用Synchronized
,最后使用Lock
。理由就是:避免主动去控制并发,以此避免出错
线程到底有几种状态
这个问题与Synchronized
无关,只不过存在很多说法,四种,五种,六种的说法都有,实际上是把进程和线程的状态搞混了。Java中线程有六种状态,定义在Thread
类内部的State
枚举类中
具体来说:
NEW
:线程创建RUNNABLE
:线程处于就绪,可运行状态BLOCKED
:线程阻塞WAITING
:线程无限等待,直到就绪可以运行TIMED_WAITING
:线程指定时间的等待TERMINATED
:线程终止
各种状态可触发的情况,在其注释中也有说明