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

并发编程实战学习

作者: 后来丶_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