• 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
  • 相关阅读:
    ubuntu+ROS安装turtulebot3
    jQuery $.isNumeric vs. $.isNaN vs. isNaN
    idea配置maven阿里巴巴中央仓库
    idea创建Maven项目
    Fiddler工作原理
    Sql Server 多数据库联合查询
    热烈庆祝我的blogs终于上线了!欢迎大家做客我的博客站点哦...
    Sql Server 2005 中文乱码 解决
    JS面向对象编程 for Menu Demo
    C# 网站静态页面生成器 for 多线程版
  • 原文地址:https://www.cnblogs.com/JackPn/p/9426044.html
Copyright © 2020-2023  润新知