synchronized总结

synchronized基础用法

  • synchronized可以用于修饰类的实例方法、静态方法和代码块。它保护的是对象(包括类对象)而非代码,只要访问的是同一个对象的synchronized方法,即使是不同的代码,也会被同步顺序访问。
  • 每个对象有一个锁(又叫监视器)和一个锁等待队列,锁只能被一个线程持有,其他试图获得同样锁的线程需要等待,执行synchronized实例方法的过程大概如下:
  1. 尝试获得锁,如果能够获得锁,继续下一步,否则加入锁等待队列,线程的状态变为BLOCKED,阻塞并等待唤醒
  2. 执行被锁住的方法或者代码块
  3. 释放锁,如果等待队列上有等待的线程,从中取一个并唤醒,如果有多个等待的线程,唤醒哪一个是不一定的,不保证公平性
  • 一般在保护变量时,需要在所有访问该变量的方法上加上synchronized。
  • 任何对象都可以作为synchronized锁的对象。

理解synchronized

可重入性

可重入是指:对同一个执行线程,它在获得了锁之后,在调用其他需要同样锁的代码时,可以直接调用。

可重入是通过记录锁的持有线程和持有数量来实现的,当调用被synchronized保护的代码时,检查对象是否已被锁,如果是,再检查是否被当前线程锁定,如果是,增加持有数量,如果不是被当前线程锁定,才加入等待队列,当释放锁时,减少持有数量,当数量变为0时才释放整个锁。

内存可见性

除了保证原子操作外,synchronized还有一个重要的作用,就是保证内存可见性,在释放锁时,所有写入都会写回内存,而获得锁后,都会从内存中读最新数据。

如果只是简单地操作变量的话,可以用volatile修饰该变量,替代synchronized以减少成本。

加了volatile之后,Java会在操作对应变量时插入一个cpu指令(又叫内存栅栏),保证读写到内存最新值,而非缓存的值。

死锁

死锁就是类似这种现象,比如, 有a, b两个线程,a持有锁A,在等待锁B,而b持有锁B,在等待锁A,a,b陷入了互相等待,最后谁都执行不下去。

避免死锁的方案:

    1. 应该尽量避免在持有一个锁的同时去申请另一个锁,如果确实需要多个锁,所有代码都应该按照相同的顺序去申请锁。
    2. 使用显式锁接口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
原文地址:https://www.cnblogs.com/JackPn/p/9426044.html