美文网首页
并发编程实战学习

并发编程实战学习

作者: 后来丶_a24d | 来源:发表于2020-04-07 13:34 被阅读0次

    目录

    • 线程风险
    • 线程风险预防
    • 线程安全设计
    • 并发工具
    • 显示锁
    • 构建自定义的同步工具
    • java内存模型

    并发编程

    线程风险

    • 线程带来的风险:
    1. 安全性
      多个线程的操作执行顺序不可预测。
      存在指令重排序和寄存器缓存,这些都增加多线程程序的复杂度。
    2. 活跃性
      活跃度关注:某件正确的事情最终会发生,主要问题包括死锁(循环等待获取锁)、饥饿(优先级高的线程能够插队并优先执行,这样如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级线程无法得到执行)、以及活锁(线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。最后有可能自己解开)。
    3. 性能
      保存和切换上下文,会丢失局部性,影响CPU性能。
      同步机制往往会抑制某些编译器的优化,使内存缓冲区数据无效,以及增加共享内存总线的同步流量。

    线程风险预防

    1. 超时放弃
    2. 以确定的顺序获得锁
    • 饥饿预防: 要避免使用线程优先级,因为这会增加平台依赖性,并可能导致活跃性问题。在大多数并发应用程序中,都可以使用默认的线程优先级
    • 活锁预防 比如重试机制中引入随机性(不同时间)。 例如,在网络上,如果有两台机器尝试使用相同的载波来发送数据包,那么这些数据包就会发生冲突。这两台机器都检查到了冲突,并都在稍后再次发送。 如果二者都选择了在0.1秒后重试,那么会再次冲突,并且不断冲突下去,这时候需要改变重试时间。

    线程安全设计

    线程安全概念
    • 对共享的和可变的状态的访问控制,有三种方法
    1. 线程之间不共享状态ThreadLocal
    2. 状态变量不可变, 包括原子性保证
    3. 访问变量时同步 synchronize,ReenterLock之类,即加锁,轻量级同步cas
    ThreadLocal提供线程安全类
    • ThreadLocalContext 为内部持有一个ThreadLocal保证线程安全性
    public final class ThreadLocalContext {
    
        private static final int CONTEXT_DEFAULT_SIZE = 1 << 6;
    
        private static final ThreadLocal<Map<String, Object>> CONTEXT = new ThreadLocal<Map<String, Object>>() {
            @Override
            protected Map<String, Object> initialValue() {
                return new ConcurrentHashMap<>(CONTEXT_DEFAULT_SIZE);
            }
        };
    
        public static void add(String key, Object value) {
            if (CONTEXT.get().containsKey(key)) {
                throw new RuntimeException(String.format("conflict xxxx", key));
            }
            CONTEXT.get().put(key, value);
        }
    
        public static <T> void add(Class<T> clazz, T value) {
            add(clazz.getName(), value);
        }
    
        public static <T> void update(Class<T> clazz, T value) {
            update(clazz.getName(), value);
        }
    
    
        public static <T> T get(Class<T> clazz) {
            return (T) get(clazz.getName());
        }
       
        public static void clear() {
            CONTEXT.get().clear();
        }
      
        public static Object remove(String key) {
            return CONTEXT.get().remove(key);
        }
    }
    
    
    • 添加es日志时也是用了ThreadLocalContext ,这样即使多线程环境也是线程安全的了
    不可变之竞态
    • 先检查后执行操作,即通过一个可能失效的观测结果来决定下一步的动作
    不可变之原子性保证
    • lastFactors缓存的数值应该一直等于lastNumber缓存数值的因数。要保持不变性,就需要在单个原子操作中更新所有相关的状态变量
    // 因数分解,如果当前计算数值和上一个计算数值相同,直接返回缓存结果,并将当前计算结果缓存。
    @NotThreadSafe
    public class UnsafeCachingFactorizer implements Servlet {
        private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
        private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();
    
        public void service(ServletRequest req, ServletResponse resp) {
            BigInteger i = extraceFromRequest(req);
            if (i.equals(lastNumber.get())
                encodeIntoResponse(resp, lastFactors.get());
            else {
                BigInteger[] factors = factor(i);
                lastNumber.set(i);
                lastFactors.set(factors);
                encodeIntoResponse(resp, factors);
            }
        }
    }
    
    不可变之volatile可见性保证
    • 保证可见性
    • 防止指令重排
    this逃逸
    • 不要在构造函数中启动一个线程。因为构造函数中创建的线程很有可能将this引用包含进去,而当前构造函数还没有完成,却可能运行了线程,导致this还没有构造完成却被运行,产生诡异问题
    不可变之不变性
    • final域能确保初始化过程的安全性,共享final数据时不需要同步。换句话说,final的对象初始化完成之后,java会负责将数据进行同步(volatile的逻辑,只是不需要声明volatile),而final数据构造完成之后当然更加安全,因为数据不可变
    不可变之不变对象设计
    • CopyOnWriteArrayList的同步容器试用与读多写少,就是采用不可变的思想保证线程安全
    • 在访问和更新多个相关变量出现的竞争条件问题,可以通过将这些变量全部保存在一个不可变对象中来消除
    @Immutable
    class OneValueCache {
        private final BigInteger lastNumber;
        private final BigInteger[] lastFactors;
    
        public OneValueCache(BigInteger i, BigInteger[] factors) {
            lastNumber = i;
            
            // copy主要目的防止factors改变cache内部数值,毕竟factors是对外暴露的,有可能被调用者不小心修改。
            lastFactors = Arrays.copy(factors, factors.length);
        }
    
        public BigInteger[] getFactors(BigInteger i) {
            if (lastNumber == null || !lastNumber.equals(i)) 
                return null;
            else
                return Arrays.copy(lastFactors, lastFactors.length);
        }
    }
    
    @ThreadSafe
    public class UnsafeCachingFactorizer implements Servlet {
        // 使用volatile引用不可变对象来保证可见性,这是一种有用的使用套路。
        private volatile OneValueCache cache = new OneValueCache(null, null);
    
        public void service(ServletRequest req, ServletResponse resp) {
            BigInteger i = extraceFromRequest(req);
    
            // 这里读取到的都是不可变数值,即使别的线程修改了cache,也是另外一个cache对象
            BigInteger[] factors = cache.getFactors(i);
            if (factors == null) {
                factors = factor(i);
                cache = OneValueCache(i, factor)
            }
    
            // 可以分析一下,这里无论如何计算都保持了不变性。
            // 1)如果命中缓存,factors就是对应i分解的因子(所以需要将factors保存起来,如果这里使用cache.getFactors(i)就不对了)。
            // 2)如果没有命中缓存,那么factors也是对应i分解的因子,只是说cache原子性修改成另外一个对象。
            encodeIntoResponse(resp, factors);
        }
    }
    
    加锁设计线程安全类
    • ConcurrentHashMap就是采用分段锁保证线程安全
    • 不变性条件中涉及的所有变量都需要由同一个锁来保护,同一个不变性由一个锁进行控制。加锁的含义不仅仅局限于互斥行为,还包括内存可见性
    @ThreadSafe
    public class UnsafeCachingFactorizer implements Servlet {
        @GuardedBy("this") private BigInteger lastNumber;
        @GuardedBy("this") private BigInteger[] lastFactors;
    
        public void service(ServletRequest req, ServletResponse resp) {
            BigInteger i = extraceFromRequest(req);
            BigInteger[] factors = null;
    
            // 将读和写分离,先用一个同步块进行读
            synchronized (this) {
                if (i.equals(lastNumber.get())
                    // 注意访问的是clone,如果直接引用LastFactors,可能导致最后返回客户端的是别的线程修改后的数值,违背了不变性。
                   factors = lastFactors.clone();
            }
        
            if (factors == null) {
                // 将计算时间长的代码提取出来,不要放到同步块中
                factors = factor(i);
    
                // 将写使用同步块保护,注意也是clone保持不变性。
                synchronized (this) {
                    lastNumber = i;
                    lastFactors = factors.clone();
                }
            }
    
            encodeIntoResponse(resp,factors);
        }
    }
    
    设计线程安全类理论知识
    • 对象不可变需满足
    1. 对象创建完之后其状态就不能修改
    2. 对象的所有与都是 final 类型
    3. 对象时正确创建的(创建期间没有 this 的逸出)
    • 包含三大要素
    1. 找出构成对象状态的所有变量。
    2. 找出约束状态变量的不变性条件。
    3. 建立对象状态的并发访问控制策略(不可变、线程封闭、加锁机制)
    • 发布一个状态变量,需要满足
    1. 变量本身是线程安全的(比如上面不可变的cache类)
    2. 没有任何不变性条件来约束(不存在暴露出去的变量和别的变量有相互依赖关系,有约束关系)
    3. 变量操作上不存在不允许的状态转换(封装在变量自身类的接口中,而不要让外面瞎改数据状态)
    并发技巧清单
    1. 尽量将域声明为final,除非需要他们是可变的。
    2. 不可变对象一定是线程安全的。
    3. 封装有助于管理复杂性。
    4. 用锁来保护每个可变变量。
    5. 当保护同一个不变性条件中的所有变量时,要使用同一个锁。
    6. 在执行复合操作期间,要持有锁。
    7. 如果从多个线程中访问同一个可变变量时没有同步机制,那么程序会出现问题。
    8. 不要故作聪明地推断出不需要同步。
    9. 将同步策略文档化。

    并发工具

    java线程取消与关闭
    • java线程取消与关闭
    • 在java中没有一种安全的抢占式方法来停止线程,因此也就没有安全的抢占式方法来停止任务。只有一些协作式的机制,使请求取消的任务和代码都遵循一种协商好的协议
    • 取消方法
    1. 使用状态变量。线程中循环遍历状态变量,检测是否需要结束当前线程。
    2. 使用中断。系统提供的大多数阻塞方法会相应中断
    • 并不是所有的阻塞方法都可以响应中断:比如java.io包中的同步Socket/IO接口;Selector.select接口;或者是等待某一个不可响应中断的内置锁。这时候可改写当前线程或者包装类FutureTask的取消接口,在其中调用对应的取消函数
    • 普通线程和守护线程之间的差异在于当线程退出时发生的操作。但一个线程退出时,JVM会检查其他正在运行的线程,如果这些线程都是守护线程,那么JVM会正常退出操作。当JVM停止时,所有仍然存在的守护线程都将被抛弃:不会执行finally代码块,而是直接退出。守护进程最好是用户执行内部任务,而不需要额外的退出处理代码。
    性能
    • 有三种方式可以降低锁的竞争程度
    1. 减少锁的持有时间。 尽管缩小同步代码块可以提高可伸缩性,但同步代码块也不能过小:需要采用原子方法执行的操作还是要包含在一个代码块中
    2. 降低锁的请求频率。锁分解(独立变量使用独立的锁)和锁分段(根据访问区域划分锁,ConcurrentHashMap对key进行分段加锁
    3. 使用带有协调机制的独占锁。 可以使用ReadWriteLock或者基于无锁操作的AtomicLong等
      原子组价来提高伸缩性

    显示锁

    • 加锁有两种机制,一种是语法糖形式的内置锁synchronized,另一种是可重用的显示加锁ReentrantLock。两者区别:
    1. 加锁语义。synchronized内置锁只有一种加锁形式:一直等待。而ReentrantLock额外提供了可定时和可轮询的tryLock接口,支持特定的业务的定制化处理。(比如为了避免死锁,可以加定时锁或者轮询加锁)
    2. 可中断的锁。内置锁一个蛋疼的特性是不支持中断,如果需要中断,需要使用ReentrantLock的lockInterruptibly接口,同时,对InterruptedExecption进行处理。
    3. 公平性。内置锁是非公平加锁,而ReentrantLock构造时可选择是否是公平锁。
    • 读写锁ReentrantReadWriteLock是一种扩展语义范畴的显示调用锁,对读和写分别进行加锁,更好地提高了读多于写情况下的并发性能

    构建自定义的同步工具

    • 如下几种方式自定义同步器
    1. 内置的条件队列。 每个内置锁只能有一个相关联的条件队列,
    2. 显示的Condition变量。一个Condition和一个Lock相关联,但可以根据需要生成多个不同的Condition来分别管理
    3. 基于AbstractQueuedSynchronizer构建 最灵活,目前大多并发组件都是基于AQS的

    java内存模型

    1. 有序性: 通过Lock前缀指令生成内存屏障, 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
    2. 可见性1: 发送一条Lock前缀的指令会强制将对缓存的修改操作立即写入主内存;
    3. 可见性2:如果是写操作,它会导致其他CPU中对应的缓存行无效。为了保证各个处理器缓存一致,每个处理会通过嗅探在总线上传播的数据来检查 自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态。

    参考文章

    相关文章

      网友评论

          本文标题:并发编程实战学习

          本文链接:https://www.haomeiwen.com/subject/sxagphtx.html