synchronized总结
synchronized基础用法
- synchronized可以用于修饰类的实例方法、静态方法和代码块。它保护的是对象(包括类对象)而非代码,只要访问的是同一个对象的synchronized方法,即使是不同的代码,也会被同步顺序访问。
- 每个对象有一个锁(又叫监视器)和一个锁等待队列,锁只能被一个线程持有,其他试图获得同样锁的线程需要等待,执行synchronized实例方法的过程大概如下:
- 尝试获得锁,如果能够获得锁,继续下一步,否则加入锁等待队列,线程的状态变为BLOCKED,阻塞并等待唤醒
- 执行被锁住的方法或者代码块
- 释放锁,如果等待队列上有等待的线程,从中取一个并唤醒,如果有多个等待的线程,唤醒哪一个是不一定的,不保证公平性
- 一般在保护变量时,需要在所有访问该变量的方法上加上synchronized。
- 任何对象都可以作为synchronized锁的对象。
理解synchronized
可重入性
可重入是指:对同一个执行线程,它在获得了锁之后,在调用其他需要同样锁的代码时,可以直接调用。
可重入是通过记录锁的持有线程和持有数量来实现的,当调用被synchronized保护的代码时,检查对象是否已被锁,如果是,再检查是否被当前线程锁定,如果是,增加持有数量,如果不是被当前线程锁定,才加入等待队列,当释放锁时,减少持有数量,当数量变为0时才释放整个锁。
内存可见性
除了保证原子操作外,synchronized还有一个重要的作用,就是保证内存可见性,在释放锁时,所有写入都会写回内存,而获得锁后,都会从内存中读最新数据。
如果只是简单地操作变量的话,可以用volatile修饰该变量,替代synchronized以减少成本。
加了volatile之后,Java会在操作对应变量时插入一个cpu指令(又叫内存栅栏),保证读写到内存最新值,而非缓存的值。
死锁
死锁就是类似这种现象,比如, 有a, b两个线程,a持有锁A,在等待锁B,而b持有锁B,在等待锁A,a,b陷入了互相等待,最后谁都执行不下去。
避免死锁的方案:
- 应该尽量避免在持有一个锁的同时去申请另一个锁,如果确实需要多个锁,所有代码都应该按照相同的顺序去申请锁。
- 使用显式锁接口Lock,它支持尝试获取锁(tryLock)和带时间限制的获取锁方法,使用这些方法可以在获取不到锁的时候释放已经持有的锁,然后再次尝试获取锁或干脆放弃,以避免死锁。
死锁检查工具:Java自带的jstack命令
同步容器及其注意事项
同步容器
Collections类有一些方法,它们可以返回线程安全的同步容器,比如:
public static <T> Collection<T> synchronizedCollection(Collection<T> c) public static <T> List<T> synchronizedList(List<T> list) public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
它们是给所有容器方法都加上synchronized来实现安全的。当多个线程并发访问同一个容器对象时,不需要额外的同步操作,也不会出现错误的结果。
加了synchronized,所有方法调用变成了原子操作,但是也不是就绝对安全了,比如:
复合操作,比如先检查再更新
例如:
public V putIfAbsent(K key, V value){ V old = map.get(key); if(old!=null){ return old; } map.put(key, value); return null; }
假设map的每个方法都是安全的,但这个复合方法putIfAbsent是安全的吗?显然是否定的,这是一个检查然后再更新的复合操作,在多线程的情况下,可能有多个线程都执行完了检查这一步,都发现Map中没有对应的键,然后就会都调用put,而这就破坏了putIfAbsent方法期望保持的语义。
伪同步,比如同步错对象。
那给该方法加上synchronized就能实现安全吗?如下所示:
public synchronized V putIfAbsent(K key, V value){ V old = map.get(key); if(old!=null){ return old; } map.put(key, value); return null; }
答案是否定的!为什么呢?同步错对象了。putIfAbsent同步锁住的的是当前类的对象,如果该类还存在其他操作map的实例方法的话,那么它操作map时同步锁住的是map,两者是不同的对象。随意要解决这个问题应该给map加锁,如:
public V putIfAbsent(K key, V value){ synchronized(map){ V old = map.get(key); if(old!=null){ return old; } map.put(key, value); return null; } }
迭代
对于同步容器对象,虽然单个操作是安全的,但迭代并不是。遍历的同时容器如果发生了结构性变化,就会抛出ConcurrentModificationException异常,同步容器并没有解决这个问题,如果要避免这个异常,需要在遍历的时候给整个容器对象加锁
并发容器
除了以上这些注意事项,同步容器的性能也是比较低的,当并发访问量比较大的时候性能很差。所幸的是,Java中还有很多专为并发设计的容器类,比如:
-
- CopyOnWriteArrayList
- ConcurrentHashMap
- ConcurrentLinkedQueue
- ConcurrentSkipListSet