Java并发编程实战笔记1.0

1.并发编程领域可以抽象成三个核心问题:分工、同步和互斥

分工指的是如何高效地拆解任务并分配给线程,而同步指的是线程之间如何协作,互斥则是保证同一时刻只允许一个线程访问共享资源。

2.并发编程领域问题的产生原因:

源头之一:缓存导致的可见性问题

    一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。

源头之二:线程切换带来的原子性问题

    我们把一个或者多个操作在CPU执行的过程中不被中断的特性称为原子性。

源头之三:编译优化带来的有序性问题

    编译器为了优化性能,有时候会改变程序中语句的先后顺序,编译器调整了语句的顺序,大多数情况下不影响程序的最终结果。不过有时候编译器及解释器的优化可能导致意想不到的 Bug。

缓存导致的可见性问题,线程切换带来的原子性问题,编译优化带来的有序性问题,其实缓存、线程、编译优化的目的和我们写并发程序的目的是相同的,都是提高程序性能。但是技术在解决一个问题的同时,必然会带来另外一个问题,所以在采用一项技术的同时,一定要
清楚它带来的问题是什么,以及如何规避。

解决可见性、有序性合理的方案应该是按需禁用缓存以及编译优化。解决原子性问题需要互斥锁。

3.死锁

死锁的定义是:一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。
只有以下这四个条件都发生时才会出现死锁:
    互斥,共享资源 X 和 Y 只能被一个线程占用;
    占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
    不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
    循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。

反过来分析,也就是说只要我们破坏其中一个,就可以成功避免死锁的发生。其中,互斥这个条件我们没有办法破坏,因为我们用锁为的就是互斥。不过其他三个条件都是有办法破坏掉的,到底如何做呢?对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样
就不存在等待了。对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。

4.等待-通知机制

synchronized用法:


class Allocator {
  private List<Object> als;
  // 一次性申请所有资源
  synchronized void apply(bject from, Object to){
    // 经典写法
    while(als.contains(from) ||als.contains(to)){
      try{
        wait();
      }catch(Exception e){
      }   
    } 
    als.add(from);
    als.add(to);  
  }
  // 归还资源
  synchronized void free(
    Object from, Object to){
    als.remove(from);
    als.remove(to);
    notifyAll();
  }
}

关于notify() 和notifyAll():
    notify() 是会随机地通知等待队列中的一个线程,而 notifyAll() 会通知等待队列中的所有线程。从感觉上来讲,应该是notify()更好一些,因为即便通知所有线程,也只有一个线程能够进入临界区。但那所谓的感觉往往都蕴藏着风险,实际上使用notify()也很有风险,它的风险在于可能导致某些线程永远不会被通知到。
    除非经过深思熟虑,否则尽量使用notifyAll()。那什么时候可以使用 notify() 呢?需要满足以下三个条件:
    1.所有等待线程拥有相同的等待条件;
    2.所有等待线程被唤醒后,执行相同的操作;
    3.只需要唤醒一个线程。

lock用法:


public class BlockedQueue<T>{
  final Lock lock = new ReentrantLock();
  // 条件变量:队列不满  
  final Condition notFull = lock.newCondition();
  // 条件变量:队列不空  
  final Condition notEmpty = lock.newCondition();

  // 入队
  void enq(T x) {
    lock.lock();
    try {
      while (队列已满){
        // 等待队列不满 
        notFull.await();
      }  
      // 省略入队操作...
      //入队后,通知可出队
      notEmpty.signal();
    }finally {
      lock.unlock();
    }
  }
  // 出队
  void deq(){
    lock.lock();
    try {
      while (队列已空){
        // 等待队列不空
        notEmpty.await();
      }
      // 省略出队操作...
      //出队后,通知可入队
      notFull.signal();
    }finally {
      lock.unlock();
    }  
  }
}

5.创建多少线程合适?

对于 CPU 密集型的计算场景,在工程上,线程的数量一般会设置为“CPU核数+1”,这样的话,当线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上,从而保证 CPU 的利用率。
对于 I/O 密集型的计算场景,最佳的线程数是与程序中CPU计算和I/O操作的耗时比相关的,我们可以总结出这样一个公式:
    最佳线程数 =CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]

猜你喜欢

转载自blog.csdn.net/TP89757/article/details/105635775
今日推荐