Java并发

来源:互联网 发布:c语言中double 编辑:程序博客网 时间:2024/06/09 16:15
1.定义任务:实现Runnable接口,或者继承Thread,推荐使用实现Runnable接口而不是继承Thread,因为继承Thread后就不能再继承别的类了
2.并发任务启动推荐以下方式启动:
  1. ExecutorService exec = Executors.newCachedThreadPool();
  2. exec.execute(Task:实现了Runnable接口)
CachedThreadPool:缓存线程池,不够就启动新线程,够的话就从已经创建的线程中拿
FixedThreadPool:固定数量的线程的线程池
SingleThreadExecutor:线程为1的FixedThreadPool

3.ExecutorService.submit() 会产生Future对象,它用Callable返回结果的特定类型,可以用isDone返回来查询Future时候已经完成,也可以不加isDone判断,因为Future的get()方法是阻塞的。
比如:
Class TaskWithResult implement Callable<String>{
     private int id;
     public TaskWithResult(int id){
          this.id = id;
     }
     public string call(){
          return "result of TaskWithResult " + id;
     }
     public static void main(String[] args){
          ExecutorService exec = Executor.newCachedThreadPool();
          Future<String> results = exec.submit(new TaskWithResult(1));
          try{
               if(result.isDone()){
                     result.get();
               }
          }catch(InterruptedException e){
          }
     }
}
4.线程优先级:尽管JDK有10个优先级,但是它与多数操作系统不能映射得很好。Sun的Solaris有2的31次方个优先级。唯一可移植的方法是当调整优先级的时候,只使用了MAX_PRIORITY、NORM_PRIORITY和MIN_PRIORITY三种级别
5.线程让步:线程调度暗示:任务做的差不多了,用yield来让步(只是一种暗示,没有任何机制保证它将会被采纳)。当调用yield()方法时,你也是在建议具有相同优先级的其他线程可以运行。注意:对于任何重要的控制或在调整应用时,都不能依赖于yield()。
6.后台线程:在线程启动之前将线程SetDaemon(true),就设置成后台线程了。注意:如果前台线程全部执行完了,后台线程会自动中断结束的。
7.等待该线程终止:调用join,对join()方法的调用可以被中断,做法是在调用线程上调用interrupt()方法,但是请用try-catch捕获,因为被中断的时候会抛出InterruptedException异常。推荐:Java SE5的java.util.concurrent类库包含CyclicBarrier,它可能比最初的线程类库中的join()更加适合
8.线程组:最好把线程组看成一次不成功的尝试,你只要忽略他就好了。建议不要使用
9.解决共享资源竞争:基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案。
注意:
1.在使用并发时,将域设置为private是非常重要的,否则,synchronized关键字就不能防止其他任务直接访问域,这样就会产生冲突
2.每个访问临界共享资源的方法都必须被同步,否则他们就不会正确地工作
10.同步控制:synchronized、Lock
Lock:对象必须被显示地创建、锁定、释放。注意:记得带上try-finally
优点:具有更细粒度的控制力,比如:异常清理
缺点:代码多
比如:
private Lock lock = new ReentrantLock();
public void testLock(){
     lock.lock();
     try{
          System.out.print("hihihi");
     }finally{
          lock.unlock();
     }
}
synchronized方法:
优点:代码简洁
缺点:如果某些事物失败了,那么就会抛出一个异常。你就没有机会去做任何清理工作了。lock就可以做到清理了

11.原子性与易变性
1》原子性可以应用于除了long和double之外的所有基本类型之上的“简单操作”。对于读取和写入除融合double之外的基本类型变量这样操作,可以保证它们会被当作不可分(原子)的操作来操作内存。
2》当你定义long或double变量时,如果使用volatile关键字,就会获得(简单的赋值与返回操作的)原子性。(注意:Java SE5 之前,volatile一直未能正确地工作)
3》如果一个域完全由synchronized方法或语句块来防护,那就不必将其设置为是volatile的
4》使用volatile而不是synchronized的唯一安全情况是类中只有一个可变域。第一选择应该是synchronized关键字,这是最安全的方式,而尝试其他任何方式都是有风险的。
5》如果一个域可能会被多个任务同时访问,或者这些任务中至少有一个是写入任务,那么你就应该将这个域设置为volatile
12.原子类:Java SE5 引入了诸如AtomicInteger、AtomicLong、AtomicReference等特殊的原子性变量类,它们提供下面形式的原子性条件更新操作:boolean compareAndSet(expectedValue,updateValue);

13.线程本地存储:
1》防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。
2》创建和管理线程本地存储可以用java.lang.ThreadLocal类来实现。
比如:
private static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){
     private Random rand = new Random(47);
     protected synchronized Integer initialValue(){
          return rand.nextInt(10000);
     }
};
3》ThreadLocal对象通常当作静态域存储,在创建ThreadLocal时,你只能通过get()和set()方法来访问该对象的内容,get()方法将返回与其线程关联的对象的副本,而set()会将参数插入到为其线程存储的对象中,并返回存储中原有的对象。ThreadLocal能保证不会出现竞争条件。

14.在阻塞时终结
1》线程状态:新建、就绪、阻塞、死亡
2》进入阻塞状态:有如下原因
①通过sleep(milliseconds)使任务进入休眠状态
②通过调用wait()使线程挂起,直到线程得到了notify()或notifyAll()消息(或者再java SE5java.util.concurrent类库中等价的signal()或signalAll()消息),线程才会进入就绪状态。
③任务在等待某个输入/输出完成
④任务试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获取了这个锁
3》中断:
第一种方式:Thread.interrupt()终止被阻塞的任务,设置完后线程被设置为中断状态。如果线程已经被阻塞,或者执行阻塞操作,那么设置这个线程中断状态将抛出InterruptedException,抛出异常后中断状态复位。
第二种方式(推荐使用):Executor上调用ShutdownNow(),它将发送一个interrupt()调用给它启动的所有线程,然后关闭Executor的所有任务。如果想单出关闭某一项任务,那么使用Executor的submit()方法,而不是executor来启动任务,submit()返回一个泛型Future<?>,然后调用Future的cancel(true)来interrupt停止这个线程,cancel()是一种中断由Executor启动的单个线程方式。
4》SleepBlock可中断示例、IOBlocked和SynchronizedBlocked不可中断阻塞示例。Java SE5  ReetrantLock上阻塞的任务可被中断

15.检查中断:应该在run()返回中使用它来处在中断状态被设置时,被阻塞和不被阻塞的各种可能
比如:
public void run(){
     try{
          while(!Thread.interrupted()){
               doSomething();
          }
     }catch(InterruptedException e){
          print("Exiting InterruptedException");
     }
}

16.线程之间的协作
1》调用sleep()、yield()的时候锁并没有被释放,这一点值得注意。
2》在wait()期间对象释放锁。
3》通过notify()、notifyAll(),或者令时间到期,从wait()中恢复执行。
4》注意:wait()、notify()、notifyAll() 只能在同步控制方法或同步控制块里面调用。
5》使用显示的Lock和Condition对象
①Condition 调用await()来挂起任务,通过signal来唤醒一个任务、或者signalAll()来唤醒所有在这个Condition上被其自身挂起的任务(与使用notifyAll相比,signalAll()是更安全的方式)
②lock()的调用都必须紧跟一个try-finally子句,用来保证在所有情况下都可以释放锁。使用内建版本时,任务调用await()、signal()、signalAll之前,必须拥有这个锁。

17.生成者-消费者与队列
1》wait()、notifyAll()是一种非常低级的解决任务互斥操作方法,因为每次交付时都握手。
2》可以使用同步队列来解决任务协作问题,同步队列任何时刻都只允许一个任务插入或移除元素。(java.util.concurrent.BlockingQueue接口中提供),阻塞队列可以解决非常大量的问题,相比wait()、notifyAll()简单并可靠得多。

18.任务间使用管道进行输入、输出
1》PipedWriter:允许任务向管道写
PipedWriter out = new PipedWriter();
out.write();
2》PipedReader:允许不同任务从同一个管道中读取
PipedReader in = new PipedReader(out);
3》如果启动了一个没有构造完毕的对象,在不同平台上管道可能会产生不一致的行为(注意:BlockingQueue使用起来更加健壮而容易)

19.死锁
1》当以下四条同时满足时,就会发生死锁
①互斥条件。任务使用的资源中至少有一个是不能共享。一根Chopstick一次就只能被一个Philosopher使用
②至少有一个任务它必须持有一个资源且正在等待获取一个当前被别的任务持有资源。也就是说,要发生死锁,Philosopher必须拿着一根Chopstick并且等待另一根。
③资源不能被任务抢占,任务必须把资源释放当作普通事件。Philosopher很有礼貌,他们不会从其他Philosopher那里抢Chopstick。
④必须有循环等待。一个任务等待其他任务所持有的资源,后者又在等待另外一个任务所持有的资源,这样一直下去,直到最后一个等待第一个任务所持有的资源,使得都被锁住。
2》解决死锁最容易的方法就是破坏第4个条件。让其不要循环等待资源就可以了,就像5个筷子,都循环的向右拿筷子,最后一个就向左拿筷子,就解决了死锁问题

20.新类库中的构件:Java SE5的java.util.concurrent引入了大量设计用来解决并发问题的新类
1》CountDownLatch:用来同步一个或多个任务,强制他们等待由其他任务执行的一组操作完成。(只能触发一次的事件)
2》CyclicBarrier:适用这种情况,你希望创建一组任务,它们并行地执行工作,然后在进行下一个步骤之前等待,直到所有任务都完成(看起来有些像join())。(CyclicBarrier栅栏队列,可触发多次)比如:赛马,要等所有马手都准备好后,会自动执行CyclicBarrier中的Runnable。
new CyclicBarrier(nHorses,new Runnable(){
     public void run(){
          //开始赛马
     }
});
3》DelayQueue:用于放置实现了Delayed接口的对象,其中的对象只能在其到期才能从队列中取走。DelayQueue可以作为一种优先级队列的变体,根据里面存放的DelayTask(实现Delayed接口)设置的到期时间长度来排序,时间最短的先从队列取出,然后运行。
4》PriorityBlockingQueue:优先级队列,具有可阻塞的读取操作。如果队列没有元素,将直接阻塞读取者。
5》使用ScheduledExecutor的温室控制器:温室事件是一个在预定时间运行的任务,ScheduledThreadPoolExecutor可以解决该问题。通过使用schedule()(运行一次任务)或者scheduleAtFixedRate()(每隔规则时间重复执行任务)。
6》Semaphore:正常的锁(来自concurrent.locks或内建的synchronized锁)在任何时刻都只允许一个任务访问一项资源,而计数信号量允许n个任务同时访问这个资源。
7》Exchanger:两个任务之间交换对象的栅栏。当这些任务进入栅栏时,他们各自拥有一个对象,当它们离开时,它们都拥有之前由对象持有的对象。应用场景:一个任务创建对象,对象的生产代价很高昂,而另外一个任务在消费这些对象。通过这种方式,可以有更多的对象在被创建的同时被消费。

21.仿真
1》饭店仿真:Java SE5 SynchronousQueue 是一种没有内部容量的阻塞队列,因此每个put()都必须等待一个take(),反之亦然。比如:你把一个对象交个某人,没有任何桌子可以放置这个对象,因此只有在这个人伸出手,准备好接受这个对象时,你才能工作。建议:接受任务将处理对象,将其当作一个消息来对待,而不是向它发送消息。如果可能就准许这项技术,那么构建出的并发系统的可能性就会大大增加。

21.性能调优
1》Atomic:只有在非常简单的情况下才有用,只有一个被修改的Atomic对象,并且这个对象独立于其他所有的对象。
2》synchronized:比Lock可读性高
3》Lock:性能测试使用lock比synchronized要高效许多,而synchronized开销变化范围太大,而Lock相对比较一致。
更安全的做法:用传统的互斥方式入手,只有在性能方面需求能够明确指示时,才替换为Atomic
4》免锁容器:
①免锁容器通用策略:对容器的修改可以与读取操作同时发生,只要读取者只能看到完成修改的结果即可。
②CopyOnWriteArrayList好处是当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModifycationException,因此你不必编写特殊的代码去防范这种异常。
③CopyOnWriteArrySet:使用CopyOnWriteArrayListlai 实现其免锁行为。
④ConcurrentHashMap和ConcurrentLinkedQueue免锁机制类似,允许并发的读取和写入,但容器中只有部分内容而不是整个容器可以被复制和修改。修改在完成之前,读取者仍旧不能看到它们。ConcurrentHashMap不会抛出ConcurrentModifycationException异常。
5》乐观锁:
①synchronized ArrayList无论读取者和写入者的数量多少,都具有大致相同的性能,读取者和其他读取者竞争的方式和写入相同。CopyOnWriteArrayList在没有写入者时,速度回快很多。在列表写入的影响没有超过短期同步整个列表时,尽量使用CopyOnWriteArrayList。
6》乐观加锁:
Atomic:用compareAndSet()方法设置旧值和新值来更新,如果Atomic对象发现不一致,那么这个操作就会失败。意味着某个其他任务已经于此操作执行期间修改了这个对象。注意:正常情况下我们最好使用互斥(synchronized或Lock)来防止多个任务同时修改一个对象。如果对失败不能执行某些恢复操作,那么你就不要使用Atomic,而是使用互斥。
7》ReadWriteLock:能否提高程序性能是完全不可确定的,它取决于诸如数据被读取的频率与被修改的频率相比较的结果。只有当你在搜索可以提供性能的方法是,才应该想到它。只有在必须时才引入ReadWriteLock。

22.总结
1》并发程序设计使我们理解:
①可以运行多个任务
②必须考虑当这些任务关闭是,可能出现的所有问题
③任务可能会在共享资源上彼此干涉。互斥锁是用来防止这种冲突的基本工具
④如果任务设计得不够自信,就有可能会死锁
⑤明白什么时候应该使用并发,什么时候应该避免使用并发,使用它们的主要原因:1.要处理很多任务,应用并发能够更有效地使用计算机。2.要能够更好地组织代码。3.要更便于用户使用。
2》多线程的主要缺陷有:
①等待共享资源的时候性能降低
②需要处理线程的额外CPU花费
③糟糕的程序设计导致不必要的复杂度
④有可能产生一些病态行为,如:饿死、竞争、死锁、活锁(多个运行各自任务的线程是的整体无法完成)
⑤不同平台导致的不一致性。比如:竞争条件在一些机器上很快出现,有些机器不出现。
3》线程创建数目要有上界,因为达到一定数量后,线程性能会很差。

23.进阶书籍推荐:
1》Java Concurrency in Practice 译名:java 并发编程实战  作者:Brian Goetz  
2》Concurrent Programming in Java,Second Edition  作者:Doug Lea
3》The Java Language Specification,Third Edition 作者:Gosling、Joy
Thinking in Java 这本书强烈推荐大家好好看看,以上是我看并发部分总结出来的一些点,收获很多。此章218页虽然长,并发值得每一个后台程序员仔细阅读。

参看:Thinking in Java 并发

0 0
原创粉丝点击