目录
- 线程风险
- 线程风险预防
- 线程安全设计
- 并发工具
- 显示锁
- 构建自定义的同步工具
- java内存模型
并发编程
线程风险
- 安全性
多个线程的操作执行顺序不可预测。
存在指令重排序和寄存器缓存,这些都增加多线程程序的复杂度。
- 活跃性
活跃度关注:某件正确的事情最终会发生,主要问题包括死锁(循环等待获取锁)、饥饿(优先级高的线程能够插队并优先执行,这样如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级线程无法得到执行)、以及活锁(线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。最后有可能自己解开)。
- 性能
保存和切换上下文,会丢失局部性,影响CPU性能。
同步机制往往会抑制某些编译器的优化,使内存缓冲区数据无效,以及增加共享内存总线的同步流量。
线程风险预防
- 超时放弃
- 以确定的顺序获得锁
- 饥饿预防: 要避免使用线程优先级,因为这会增加平台依赖性,并可能导致活跃性问题。在大多数并发应用程序中,都可以使用默认的线程优先级
- 活锁预防 比如重试机制中引入随机性(不同时间)。 例如,在网络上,如果有两台机器尝试使用相同的载波来发送数据包,那么这些数据包就会发生冲突。这两台机器都检查到了冲突,并都在稍后再次发送。 如果二者都选择了在0.1秒后重试,那么会再次冲突,并且不断冲突下去,这时候需要改变重试时间。
线程安全设计
线程安全概念
- 线程之间不共享状态ThreadLocal
- 状态变量不可变, 包括原子性保证
- 访问变量时同步 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);
}
}
设计线程安全类理论知识
- 对象创建完之后其状态就不能修改
- 对象的所有与都是 final 类型
- 对象时正确创建的(创建期间没有 this 的逸出)
- 找出构成对象状态的所有变量。
- 找出约束状态变量的不变性条件。
- 建立对象状态的并发访问控制策略(不可变、线程封闭、加锁机制)
- 变量本身是线程安全的(比如上面不可变的cache类)
- 没有任何不变性条件来约束(不存在暴露出去的变量和别的变量有相互依赖关系,有约束关系)
- 变量操作上不存在不允许的状态转换(封装在变量自身类的接口中,而不要让外面瞎改数据状态)
并发技巧清单
- 尽量将域声明为final,除非需要他们是可变的。
- 不可变对象一定是线程安全的。
- 封装有助于管理复杂性。
- 用锁来保护每个可变变量。
- 当保护同一个不变性条件中的所有变量时,要使用同一个锁。
- 在执行复合操作期间,要持有锁。
- 如果从多个线程中访问同一个可变变量时没有同步机制,那么程序会出现问题。
- 不要故作聪明地推断出不需要同步。
- 将同步策略文档化。
并发工具
java线程取消与关闭
- java线程取消与关闭
- 在java中没有一种安全的抢占式方法来停止线程,因此也就没有安全的抢占式方法来停止任务。只有一些协作式的机制,使请求取消的任务和代码都遵循一种协商好的协议
- 取消方法
- 使用状态变量。线程中循环遍历状态变量,检测是否需要结束当前线程。
- 使用中断。系统提供的大多数阻塞方法会相应中断
- 并不是所有的阻塞方法都可以响应中断:比如java.io包中的同步Socket/IO接口;Selector.select接口;或者是等待某一个不可响应中断的内置锁。这时候可改写当前线程或者包装类FutureTask的取消接口,在其中调用对应的取消函数
- 普通线程和守护线程之间的差异在于当线程退出时发生的操作。但一个线程退出时,JVM会检查其他正在运行的线程,如果这些线程都是守护线程,那么JVM会正常退出操作。当JVM停止时,所有仍然存在的守护线程都将被抛弃:不会执行finally代码块,而是直接退出。守护进程最好是用户执行内部任务,而不需要额外的退出处理代码。
性能
- 减少锁的持有时间。 尽管缩小同步代码块可以提高可伸缩性,但同步代码块也不能过小:需要采用原子方法执行的操作还是要包含在一个代码块中
- 降低锁的请求频率。锁分解(独立变量使用独立的锁)和锁分段(根据访问区域划分锁,ConcurrentHashMap对key进行分段加锁
- 使用带有协调机制的独占锁。 可以使用ReadWriteLock或者基于无锁操作的AtomicLong等
原子组价来提高伸缩性
显示锁
- 加锁有两种机制,一种是语法糖形式的内置锁synchronized,另一种是可重用的显示加锁ReentrantLock。两者区别:
- 加锁语义。synchronized内置锁只有一种加锁形式:一直等待。而ReentrantLock额外提供了可定时和可轮询的tryLock接口,支持特定的业务的定制化处理。(比如为了避免死锁,可以加定时锁或者轮询加锁)
- 可中断的锁。内置锁一个蛋疼的特性是不支持中断,如果需要中断,需要使用ReentrantLock的lockInterruptibly接口,同时,对InterruptedExecption进行处理。
- 公平性。内置锁是非公平加锁,而ReentrantLock构造时可选择是否是公平锁。
- 读写锁ReentrantReadWriteLock是一种扩展语义范畴的显示调用锁,对读和写分别进行加锁,更好地提高了读多于写情况下的并发性能
构建自定义的同步工具
- 内置的条件队列。 每个内置锁只能有一个相关联的条件队列,
- 显示的Condition变量。一个Condition和一个Lock相关联,但可以根据需要生成多个不同的Condition来分别管理
- 基于AbstractQueuedSynchronizer构建 最灵活,目前大多并发组件都是基于AQS的
java内存模型
- 有序性: 通过Lock前缀指令生成内存屏障, 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 可见性1: 发送一条Lock前缀的指令会强制将对缓存的修改操作立即写入主内存;
- 可见性2:如果是写操作,它会导致其他CPU中对应的缓存行无效。为了保证各个处理器缓存一致,每个处理会通过嗅探在总线上传播的数据来检查 自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态。
参考文章
网友评论