前言
多线程并发过程中,存在不可避免地竞态条件和内存可见性问题,为了保证结果的正确性,确保相应过程的同步执行,可以使用synchronized关键字作为其中之一的解决方案。
从1.0版本开始,Java中的每一个对象都有一个内部锁。如果一个方法使用synchronized关键字修饰,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。
使用wait方法添加一个线程到等待队列中,notifyAll/notify方法解除等待线程的阻塞状态。
小总结
总而言之,每个对象都有一个内部锁,并且该锁有一个内部条件,由锁来管理那些试图进入synchronized方法的线程(没锁就进不了方法),由条件来管理那些调用wait的线程(不满足条件就wait)。
基本用法
synchronized关键字可以用于修饰类的实例方法、静态方法和代码块。
修饰实例方法
举个栗子
public class Counter {
private int count;
public synchronized void incr() {
count++;
}
public synchronized int getCount() {
return count;
}
}
Counter是一个简单的计数器类,加了synchronized后,方法内的代码就变成了原子操作(要执行就一起执行,不执行就都不执行),当多个线程并发更新同一个Counter对象时,也不会出错。
synchronized发挥的作用
那么synchronized发挥了什么作用呢?
是保护实例方法,使得同时只有一个线程可以访问实例方法嘛?
显然不是,因为就算多个线程同时执行同一个synchronized实例方法,也是可以的,只要它们访问的对象不同即可。
比如
Counter c1 = new Counter();
Counter c2 = new Counter();
Thread t1 = new CouterThread(c1);
Thread t2 = new CounterThread(c2);
t1.start();
t2.start();
这样也是可以的,因为这里的c1和c2是不同的对象,是允许同时执行incr这个实例方法的。
也就是说,
实际上保护的是同一个对象的方法调用,确保只有一个线程执行,具体而言,保护的是当前实例对象,this。
this对象,有一个锁,以及等待获得该锁的等待队列,该锁只能被一个线程持有,其他需要获得该锁的线程需要等待(位于等待队列中的线程处于BLOCKED状态)。
总结
此时的synchronized,保护的是对象而非代码。
只要访问的是同一个对象的synchronized实例方法,即使是不同的代码,也会被同步顺序访问。比如同一个对象的getCount()和incr()方法就不能同时执行,只能顺序访问。
但是针对不同的对象,可以同时执行它们中相同实例方法。
修饰静态方法
举个栗子
public class StaticCounter {
private static int count = 0;
public static synchronized void incr() {
count++;
}
public static synchronized int getCount() {
return count;
}
}
在实例方法中,我们说synchronized保护的是this,那么静态方法呢?
由于访问静态方法,一般都是通过类进行访问的,所以说,此时保护的是类对象。
实际上,每个对象都有一个锁和一个等待队列,连类对象也不例外。
总结
不过,由于synchronized静态方法和synchronized实例方法保护的对象不同,所以允许两个线程,一个访问synchronized静态方法,一个访问synchronized实例方法。
修饰代码块
除了修饰方法外,synchronized还可以用于包装代码块
public class Counter {
private int count;
public void incr() {
synchronized (this) {
count++;
}
}
public int getCount() {
synchronized (this) {
return count;
}
}
}
synchronized括号里就是保护的对象,对于实例方法,是this,{}里面是同步执行的代码。
而静态方法,相当于
public class StaticCounter {
private static int count;
public static void incr() {
synchronized (StaticCounter.class) {
count++;
}
}
public static int getCount() {
synchronized (StaticCounter.class) {
return count;
}
}
}
synchronized同步的对象可以是任意对象,任意对象都有一个锁和等待队列,或者说,任何对象都可以作为锁对象。
深入一步
1. 可重入性
synchronized是可重入的。
什么是可重入呢?
指的是,对于一个获得对象锁的线程而言,如果它再去调用另外一个需要相同的锁的代码时,可以直接调用。
实现方式
可重入是通过记录锁的持有线程和持有数量来实现的。
当调用被synchronized保护的代码时,检查对象是否已被锁,如果是,再检查是否被当前线程锁定,如果是,增加持有数量,如果不是被当前线程锁定,才加入等待队列,当释放锁时,减少持有数量,当数量变为0时才释放整个锁。
内存可见性
synchronized除了保证原子操作外,还可以保证内存可见性。
实现方式
在释放锁时,所有的写入都会被写回内存,而获得锁后,会从内存中读取当前最新数据。
不过,如果只是为了保证内存可见性,可以使用更轻量级的volatile修饰符。
死锁
这是所有锁,都避不开的一个问题。
所谓死锁,简单来说,是指两个线程各自持有一个锁,却都想去申请对方持有的锁,最后相互等待,谁都执行不下去的现象。
解决方式
- 首先应该避免在持有一个锁的同时去申请另一个锁;
- 如果确实需要多个锁,所有代码都应该按照相同的顺序去申请,比如说,都先申请lockA,再申请lockB;
- 使用显式锁接口Lock,它支持尝试获取锁tryLock和带有时间限制的获取锁方法(使用这些方法可以在获取不到锁的时候释放已持有的锁,然后再次尝试获取锁或干脆放弃,以避免死锁)。