Java编程的逻辑 -- 并发章 -- Synchronized
Synchronized
共享内存有两个重要问题,一个是竞态条件,一个是内存可见性。其实一种解决方案则是Synchronized
原理
我们首先来看一段synchronized修饰方法和代码块的代码:
public class Main {
//修饰方法
public synchronized void test1(){
}
public void test2(){
// 修饰代码块
synchronized (this){
}
}
}
来反编译看一下:
image
从上面可以看出,同步代码块是使用monitorenter和monitorexit指令实现的,同步方法(在这看不出来需要看JVM底层实现)依靠的是方法修饰符上的ACC_SYNCHRONIZED实现。
同步代码块:monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁。synchronized底层是通过monitor对象,对象有自己的对象头,存储了很多信息,其中一个信息标示是被哪个线程持有。
同步方法:synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象。(摘自:http://www.cnblogs.com/javaminer/p/3889023.html)
具体可参考:
- https://blog.csdn.net/chenssy/article/details/54883355
- https://blog.csdn.net/u012465296/article/details/53022317
用法
1. 实例方法
public class Counter {
private int count;
public synchronized void incr(){
count ++;
}
public synchronized int getCount() {
return count;
}
}
Synchronized可以用来修饰实例方法,静态方法,(静态或实例)代码块。
多个线程可以同时执行一个Synchronized方法,只要他们访问的不是同一个实例对象即可。
Synchronized实际上保护的是当前实例对象,而不是它仅描述的那个方法。此时this对象有一个锁和一个等待队列,锁只能被一个线程持有, 其他试图获得同样锁的线程需要等待。当前线程不能获得锁的时候,它会加入等待队列,线程的状态会变为BLOCKED。
Thread有一个与其状态对应的枚举类。Thread.State枚举类:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
关于这些状态.我们简单解释下:
- NEW:没有调用start的线程状态为NEW。
- TERMINATED:线程运行结朿后状态为TERMINATED。
-
RUNNABLE:调用start后线程在执行run方法且没有阻寒时状态为RUNNABLE,不过,
RUNNABLE不代表CPU—定在执行该线程的代码,可能正在执行也可能在等待操作系统分配时间片,只
是它没有在等待其他条件。 - BLOCKED、WAITING、TIMED_WAmNG:都表示线程被阻塞了,在等待一些条件。其中BLOCKED表示当前线程在等待的是CPU的时间片而WAITING、TIMED_WAmNG则等待的是外部条件。
Synchronized保护的是对象而非方法块,只要访问的是同一个对象的synchronized方法,即使是不同的方法块,也会被同步顺序访问。比如,对于Counter类中的两个同步实例方法get和incr,对同一个Counter对象,一个线程执行get,另一个执行incr,它们是不能同时执行的,会被synchronized同步顺序执行。
此外,需要说明的是,****synchronized方法不能防止非synchronized方法被同时执行****。比如,如果给Counler类增加一个非synchronized方法:
public void decr(){
count --;
}
则该方法可以和synchronized的incr方法同时执行,这通常会出现非期望的结果,所以,一般在保护变量时,需要在所有访问该变量的方法前加shsynchronizcd(不一定)。
2. 静态方法
public class StaticCounter {
private static int count = 0;
public static synchronized void incr() {
count++;
}
public static synchronized int getCount() {
return count;
}
}
对实例方法,synchronized保护的是当前实例对象this,对静态方法,保护的是类对象,这里是StaticCounter.class。实际上,每个对象都有一个锁和一个等待队列,Class类对象也不例外。
synchronized静态方法和synchronized实例方法保护的是不同的对象,不同的两个线程,可以一个执行synchronized静态方法,另一个执行synchronized实例方法。所以他们可以同时运行
3. 代码块
public class Counter {
private int count;
public void incr(){
synchronized(this){
count ++;
}
}
public int getCount() {
synchronized(this){
return count;
}
}
}
synchronized括号里即保护的对象,对于实例方法而言则是this,{}
是执行同步的代码。对于上例中的StaticCounter类,等价的代码则是:
public class StaticCounter {
private static int count = 0;
public static void incr() {
synchronized(StaticCounter.class){
count++;
}
}
public static int getCount() {
synchronized(StaticCounter.class){
return count;
}
}
}
synchronized同步的对象可以是任意对象,任意对象都有一个锁和等待队列,或者说,任何对象都可以作为锁对象。如下的类成员lock。所以在如下这种情况中,所有的线程中所跑的涉及域lock的方法都会在一个等待队列中排队执行。
public class Counter {
private int count;
private Object lock = new Object();
public void incr(){
synchronized(lock){
count ++;
}
}
public int getCount() {
synchronized(lock){
return count;
}
}
}
特性
- 可重入性
- 内存可见性
- 死锁
可重入性
Synchronized是一种可重入锁。
可重入锁是通过记录锁的持有线程和持有数来实现的,当调用被synchronized保护的代码时,检查对象是否已被锁,如果是,再检查是否被当前线程锁定,如果是,增加持有数量,如果不是被当前线程锁
定,才加入等待队列,当释放锁时,减少持有数量,当数量变为0时才释放整个锁。
内存可见性
Synchronized除了可以保证原子性,同时还可以保证内存可见性。在释放锁时,所有的写入都会从寄存器或缓存(工作内存)写回内存,而获得锁后都会从内存中获得最新数据。
但如果只是为了保证内存可见性,使用synchronized关键字的成本则要高于volatile修饰符的使用。
如下代码:
public class Switcher {
private boolean on;
public boolean isOn() {
return on;
}
public void setOn(boolean on) {
this.on = on;
}
}
该代码在并发执行的时候不涉及非原子操作,仅修改变量on的状态,是一个原子操作。所以这里完全不必要用synchronized修饰符来附加在setOn方法上来锁住整个对象。它并不面临原子性问题,而是面临的内存可见性问题,只需要对on变量用volatile来修饰即可。
public class Switcher {
private volatile boolean on;
public boolean isOn() {
return on;
}
public void setOn(boolean on) {
this.on = on;
}
}
死锁
经典示例:
public class DeadLockDemo {
private static Object lockA = new Object();
private static Object lockB = new Object();
private static void startThreadA() {
Thread aThread = new Thread() {
@Override
public void run() {
synchronized (lockA) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
synchronized (lockB) {
}
}
}
};
aThread.start();
}
private static void startThreadB() {
Thread bThread = new Thread() {
@Override
public void run() {
synchronized (lockB) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
synchronized (lockA) {
}
}
}
};
bThread.start();
}
public static void main(String[] args) {
startThreadA();
startThreadB();
}
}
运行后aThread和bThread陷入了相互等待。怎么解决呢?首先,座该尽量避免在持有一个锁的同时去中请另一个锁,如果确实需要多个锁,所有代码都砹该按照相同的顺序去申请锁。比如,对于上面的例子,可以约定都先申请lockA,再申请lockB,而不是像代码中那样分开申请。
不过,在复杂的项目代码中,这种约定可能难以做到。还有一种方法是显示锁接口Lock,它支持尝试获取锁(tryLock)和带时间限制的获取锁方法,使用这些方法可以在获取不到锁的时候释放已经持有的锁,然后再次尝试获取锁或干脆放弃,以避免死锁。
如采还是出现了死锁,Java不会主动处理,不过借助一些工貝,我们可以发现运行中的死锁,比如,Java自带的jsiadc命令会报告发现的死锁。
同步容器
类Collections中提供了一些方法,返回线程安全的同步容器。
image它们是給所有容器方法都加上synchronized来实现线程安全的。
static class SynchronizedCollection<E> implements Collection<E> {
final Collection<E> c; //Backing Collection
final Object mutex; //Object on which to synchronize
SynchronizedCollection(Collection<E> c) {
if(c==null)
throw new NullPointerException();
this.c = c;
mutex = this;
}
public int size() {
synchronized (mutex) {return c.size();}
}
public boolean add(E e) {
synchronized (mutex) {return c.add(e);}
}
public boolean remove(Object o) {
synchronized (mutex) {return c.remove(o);}
}
//…
}
这里的线程安全指的是容器对象,即当多个线程并发访问同一个容器对象时不需要额外的同步操作,也不会出现错误的结果。
加了synchronized之后所有操作都变成了原子操作,但并不意味着客户端在调用的时候就绝对安全了。以下情况还需注意:
- 复合操作,比如先检查后更新
- 伪同步
- 迭代
1. 复合操作
public class EnhancedMap <K, V> {
Map<K, V> map;
public EnhancedMap(Map<K,V> map){
this.map = Collections.synchronizedMap(map);
}
public V putIfAbsent(K key, V value){
V old = map.get(key);
if(old!=null){
return old;
}
return map.put(key, value);
}
public V put(K key, V value){
return map.put(key, value);
}
//…
}
代码中的putIfAbsent方法在多线程下显然是不安全的。如果多个线程都执行这一步则必然会出现竞态条件。
2. 伪同步
public synchronized V putIfAbsent(K key, V value){
V old = map.get(key);
if(old!=null){
return old;
}
return map.put(key, value);
}
如上代码即便加上synchronized关键字修饰也任然是不安全的。因为我们同步错了对象,putlfAbsent同步使用的是EnhancedMap对象,而其他方法(如代码中的put方法)使用的是Collections.synchronizedMap返回的对象map、两者是不同的对象。要解决这个问题,所有方法必须使用相同的锁,可以使用EnhancedMap的对象锁,也可以使用map对象锁。使用EnhancedMap对象作为锁,则Enhanced-Map中的所有方法都需耍加上synchronized。使用map作为锁,putlfAbsent方法可以改为:
public V putIfAbsent(K key, V value){
synchronized(map){
V old = map.get(key);
if(old!=null){
return old;
}
return map.put(key, value);
}
}
3. 迭代
对于同步容器虽然单个操作是安全的,但是迭代却不是。如下代码截取自Collections.SynchronizedList类。可以看到函数并不是同步实现的。
public ListIterator<E> listIterator() {
return list.listIterator(); // Must be manually synched by user
}
public ListIterator<E> listIterator(int index) {
return list.listIterator(index); // Must be manually synched by user
}
我们通过两个线程来并发的修改和迭代该同步容器则会出现List迭代时规定的并发修改异常。
private static void startModifyThread(final List<String> list) {
Thread modifyThread = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 100; i++) {
list.add("item " + i);
try {
Thread.sleep((int) (Math.random() * 10));
} catch (InterruptedException e) {
}
}
}
});
modifyThread.start();
}
private static void startIteratorThread(final List<String> list) {
Thread iteratorThread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
for(String str : list) {
}
}
}
});
iteratorThread.start();
}
public static void main(String[] args) {
final List<String> list = Collections
.synchronizedList(new ArrayList<String>());
startIteratorThread(list);
startModifyThread(list);
}
运行结果:
Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859)
at java.util.ArrayList$Itr.next(ArrayList.java:831)
我们知道对于遍历操作而言,如果迭代时容器发生了结构性变化。就会抛出该异常。很显然同步容器并没有解决这个问题,要想避免这个问题则需要在遍历的时候给整个容器对象加锁。 即谁无法保证线程安全,方法体中的代码块就锁谁。
private static void startIteratorThread(final List<String> list) {
Thread iteratorThread = new Thread(new Runnable() {
@Override
public void run() {
while(true) {
synchronized(list){
for(String str : list) {
}
}
}
}
});
}
4. 并发容器
在使用synchronized的时候除了需要注意以上注意事项,同时同步容器的性能也是比较低的,当并发访问量比较大的时候性能比较差。但Java还为我们提供了很多专为并发设计的容器类。比如:
- CopyOnWriteArrayList
- ConcurrentHashMap
- ConcurrentLinkedQueue
- ConcurrentSkipListSet
这些容器类都是线程安全的,仍但都没有使用synchronized,没存迭代问题,直接支持一些复合操作,
性能也搞得多。
释放锁的时机
- 当方法(代码块)执行完毕后会自动释放锁,不需要做任何的操作。
- 当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
不会由于异常导致出现死锁现象。