并发编程随笔

时间:2021-06-02 17:59:49

1.死锁

某资源加锁后没有释放或者没有正确释放该锁,另一线程无法获取该资源锁,引起死锁。
出现死锁可以dump线程信息,查看死锁原因,从而解决。

避免死锁的常见方法

  1. 避免一个线程同时获取多个锁
  2. 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
  3. 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
  4. 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

2.资源限制

资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源,例如,服务器的带宽只有2MB/s,某个资源的下载速度是1MB/s,系统启动10个线程下载资源,下载速度不会变成10MB/s。所有在进行并发编程时要考虑这些资源的限制。

如何解决

对于硬件资源限制,可以考虑使用集群并行执行程序,对于软件资源,可以考虑使用资源池将资源复用,在已有资源不能变的情况下,例如带宽下载,可以考虑别的方面,例如IO的读写速度。

3.volatile

  1. volatile变量追加字节到处理器位数(例如CPU64bit),这样变量就可以填满高速cache的缓存行,例如队列情况就可以避免头节点和尾节点加载到同一个缓存行,使头尾节点在修改时不会相互锁定
  2. 当把变量声明为volatile时,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序,volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型变量时总会返回最新写入的值。

并不是所有volatile变量都需要填充

  • 缓存行并非64字节宽的处理器
  • 共享变量不会被频繁的写
    不过这种追加字节的方式在JDK7下可能无效,Java7 更加智慧,他会淘汰或重新排列无用字段,需要使用其他追加字节的方式。

不建议过度依赖volatile提供可见性

如果在代码中依赖volatile变量来控制状态的可见性,通常比使用锁的代码更脆弱,也更难理解与维护。
仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用他们

经典用法:检查某个状态标记以判断是否退出循环,某个操作是否完成,状态标识。

锁既可以保证原子性也可以保证可见性,volatile只能保证可见性

4.无状态对象

无状态对象一定是线程安全的

@ThreadSafe
public class StatelessFactorizer implements Servlet {
    public void service(ServletRequest req,ServletResponse res){
        BigInteger i =extractFromRequest(req);
        BigInterger[] factors = factor(i);
        encodeIntoResponse(resp,factors);
    }
}

StatelessFactorizer是无状态的,它既不包含任何域,也不包含任何对其他类中域的引用,计算过程中的临时状态仅存在于线程栈上的局部变量中,并且只能由正在执行的线程访问。

5.Final域

对于Final域,编译器处理器遵守的两个重排序规则
- 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作不能重排序
- 初次读一个包含final域的对象引用,与随后初次读这个final域,这两个操作不能重排序。

Final域能确保初始化过程的安全性,从而可以不受限制的访问不可变对象,并在共享这些对象时无须同步。

//线程安全
class OneValueCache {
    private final BigInteger lastNumber;
    private final BigInteger[] 
    lastFactors;

    //构造函数内包含final域,保证对象初始化过程的可见性
    public OneValueCache(BigInteget i,BigInteger[] factors){
        lastNumber =i;
        //采用副本,不然引用溢出
        lastFactors = Arrays.copyOf(factors,factors.length);
    }

    public BigInteger[] getFactors(BigInteger i){
        if(lastNumber == null || !lastNumber.equal(i))
        return null;
        else
            return Arrays.copyOf(lastFactors,lastFactors.length);

    }
}    



public class VolatileCachedFactorizer implements Servlet {
    private volatile OneValueCache cache = new OneValueCache(null,null);

    public void service(ServletRequest req,ServletResponse resp){
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = cache.getFactors(i);
        if(factors == null){
            factors = factor(i);
            //final 初始化与引用赋值不能重排序保证线程安全
            cache = new OneValueCache(i,factors);
        }
        encodeIntoResponse(resp,factors);
    }
}

任何线程都可以在不需要额外同步的情况下安全的访问不可变对象,即使在发布这些对象时没有使用同步。

6.对象的安全发布

要安全的发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见,一个正确构造的对象可以通过以下方式来安全的发布:
- 在静态初始化函数中初始化一个对象引用
- 将对象的引用保持到volatile类型的域或者AtomicRederance对象中
- 将对象的引用保存到某个正确构造对象的final类型中
- 将对象的引用保存到一个由锁保护的域中