java 并发

并发

并发编程可以使程序执行速度得到极大提高,或者为设计某些类型的程序提供更易用的模型,或者两者皆有。

 

1、并发的多面性

使用并发时需要解决的问题多个,而实现并发的方式也有多种,并且在这两者之间没有明显的映射关系。

用并发解决的问题大体上可以分为“速度”和“设计可管理性”两种。

(1)更快的执行

  如果你有一台多处理的机器,那么就可以在这些处理器之间分布多个任务,从而可以极大地提高吞吐量。但是,并发通常是提高运行在单处理器上的程序的性能。

  在单处理器上使用并发可以进行阻塞(如果程序中的某个任务因为该程序控制范围之外的某些条件(通常是I/O)而导致不能继续执行,那么我们就说这个任务或线程阻塞了)。如果使用并发来编写程序,那么当一个任务阻塞时,程序中的其他任务还可以继续执行,因此这个程序可以保持继续向前执行。

 

(2)改进代码设计

  在单CPU机器上使用多任务的程序在任意时刻仍旧只在执行一项工作,但是并发提供了一个重要的组织结构上的好处:你的程序设计可以极大地简化。

  Java的线程机制是抢占式的,这表示调度机制会周期性地中断线程,将上下文切换到另一个线程,从而为每个线程都提供时间片,使得每个线程都会分配到数量合理的时间去驱动它的任务。

 

2、基本的线程机制

  并发编程使我们可以将程序划分为多个分离的、独立运行的任务。通常使用多线程机制,这些独立任务(也被称为子任务)中的每一个都将由执行线程来驱动。一个线程就是在进程中的一个单一的顺序控制流,因此,单个进程可以拥有多个并发执行的任务,但是你的程序使得每个任务都好像有其自己的CPU一样。

  线程的一大好处是可以使你从这个层次抽身出来,即代码不必知道它是运行在具有一个还是多个CPU的机器上。所以,使用线程机制是一种建立透明、可扩展的程序的方法。

(1)定义任务

  要想定义任务,只需实现Runnable接口并编写run()方法。任务的run()方法通常总会有某种形式的循环,使得任务一直运行下去直到不再需要,所以要设定跳出循环的条件。

  Thread.yield()的调用是对线程调度器的一种建议,它在声明:“我已经执行完生命周期中最重要的部分了,此刻正是切换给其他任务执行一段时间的大好时机。”

 1 public class LiftOff implements Runnable{
 2     protected int countDown = 10;
 3     private static int taskCount = 0;
 4     private static  int id = taskCount++;
 5     public LiftOff(){}
 6     public LiftOff(int countDown){ this.countDown = countDown; }
 7     public String status(){
 8         return "#" + id + "(" +
 9                 (countDown>0 ? countDown : "LiftOff!") + ").";
10     }
11     @Override
12     public void run() {
13         while (countDown-- >0){
14             System.out.println(status());
15             Thread.yield();
16         }
17     }
18 }

 

(2)Thread类

  将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread构造器。

  main()和LiftOff.run()是程序中与其他线程“同时”执行的代码。单一线程(main())在创建所有的LiftOff线程。但是,如果多个线程在创建LiftOff线程,那么就有可能会有多个LiftOff拥有相同的id。

 1 public class Test {
 2     public static void main(String[] args) {
 3         for(int i=0;i<2;i++)
 4             new Thread(new LiftOff()).start();
 5         System.out.println("Waiting for LiftOff");
 6     }
 7 }
 8  9 /*Output:
10 Waiting for LiftOff
11 #0(4). #0(4). #0(3). #0(3). #0(2). #0(2). #0(1). #0(1). #0(LiftOff!). #0(LiftOff!). 
12 */

 

(3)Executor

  Executor(执行器)将为你管理Thread对象,从而简化了编程。

  对shutdown()方法的调用可以防止新任务被提交给这个Executor。

线程池作用
newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
newFixedThreadPool 可以一次性预先执行代价高昂的线程分配。创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
1 public class Test {
2     public static void main(String[] args) {
3         ExecutorService exec = Executors.newCachedThreadPool();
4         for(int i=0;i<2;i++)
5             exec.execute(new LiftOff());
6         System.out.println("Waiting for LiftOff");
7     }
8 }

 

(4)从任务中产生返回值

  Runnable是执行工作的独立任务,但是它不返回任何值。如果希望返回一个值,那么就可以实现Callable接口。

  从方法call()中返回值,并且必须使用ExecutorService.submit()方法调用它。submit()方法会产生Future对象。可以使用isDone()方法来查询Future是否已经完成。也可以不使用isDone()进行检查而直接调用get(),get()将阻塞,直至结果准备就绪。

 1 public class Test {
 2     public static void main(String[] args) {
 3         ExecutorService exec = Executors.newCachedThreadPool();
 4         ArrayList<Future<String>> results =
 5                 new ArrayList<Future<String>>();
 6         for(int i=0;i<6;i++)
 7             results.add(exec.submit(new TaskWithResult(i)));
 8         for(Future<String> fs: results){
 9             try {
10                 System.out.println(fs.get());
11             }catch (InterruptedException e){
12                 System.out.println(e);
13                 return;
14             }catch (ExecutionException e){
15                 System.out.println(e);
16             }finally{
17                 exec.shutdown();
18             }
19         }
20     }
21 }
22 23 class TaskWithResult implements Callable<String>{
24     private int id;
25     public TaskWithResult(int id){
26         this.id = id;
27     }
28 29     @Override
30     public String call() throws Exception {
31         return "id = " + id;
32     }
33 }
34 35 /*Output:
36 id = 0
37 id = 1
38 id = 2
39 id = 3
40 id = 4
41 id = 5
42 */

 

(5)休眠

  影响任务行为的一种简单方法是调用sleep(),这将是使任务中止执行给定的时间。

  因为异常不能跨线程传播回main(),所以你必须在本地处理所有在任务内部产生的异常。

 1 public class Test {
 2     public static void main(String[] args) {
 3         ExecutorService exec = Executors.newCachedThreadPool();
 4         for(int i=0;i<3;i++)
 5             exec.execute(new SleepingTask());
 6         exec.shutdown();
 7     }
 8 }
 9 10 class SleepingTask extends LiftOff{
11     public void run(){
12         try {
13             while ((countDown-- > 0)){
14                 System.out.print(status());
15                 TimeUnit.MILLISECONDS.sleep(100);
16             }
17         }catch (InterruptedException e){
18             System.out.println(e);
19         }
20     }
21 }
22 23 /*Output:
24 #0(4).#0(4).#0(4).#0(3).#0(3).#0(3).#0(2).#0(2).#0(2).#0(1).
25 #0(1).#0(1).#0(LiftOff!).#0(LiftOff!).#0(LiftOff!).
26 */

 

(6)优先级

  线程的优先级将该线程的重要性传递给了调度器。尽管CPU处理现有线程集的顺序是不确定的,但是调度器将倾向于让优先权最高的线程先执行。这并不意味着优先权会导致死锁,只是优先级较低的线程仅仅是执行的频率较低。

  可以用getPriority()来读取现有线程的优先级,通过setPriority()来修改它。优先级有三种级别:MAX_PRIORITY、NORM_PRIORITY和MIN_PRIORITY。

 1 class SimplePriorities implements Runnable{
 2     private int countDown = 3;
 3     private volatile double d;
 4     private int priority;
 5     public SimplePriorities(int priority){
 6         this.priority = priority;
 7     }
 8  9     @Override
10     public String toString() {
11         return Thread.currentThread().getName() + ":" + countDown;
12     }
13 14     public void run(){
15         Thread.currentThread().setPriority(priority);
16         while (true){
17             for(int i=0;i<100000;i++){
18                 d+=(Math.PI + Math.E) / (double) i;
19                 if(i%1000 == 0)
20                     Thread.yield();
21             }
22             System.out.println(this + ";" + Thread.currentThread().getPriority());
23             if(--countDown == 0) return;
24         }
25     }
26 27     public static void main(String[] args) {
28         ExecutorService exec = Executors.newCachedThreadPool();
29         for(int i=0;i<3;i++)
30             exec.execute(new SimplePriorities(Thread.MIN_PRIORITY));
31         exec.execute(new SimplePriorities(Thread.MAX_PRIORITY));
32     }
33 }
34 35 /*Output:
36 pool-1-thread-4:3;10
37 pool-1-thread-4:2;10
38 pool-1-thread-4:1;10
39 pool-1-thread-3:3;1
40 pool-1-thread-1:3;1
41 pool-1-thread-2:3;1
42 pool-1-thread-3:2;1
43 pool-1-thread-1:2;1
44 pool-1-thread-2:2;1
45 ...
46 */

 

(7)让步

  如果知道已经完成了在run()方法的循环的一次迭代过程中所需的工作,就可以给线程调度机制一个暗示:你的工作已经做得差不多了,可以让别的线程使用CPU了。这个暗示将通过调用yield()方法来作出(不过这只是一个暗示,没有任何机制保证它将会被采纳)。

 

(8)后台线程

  所谓后台线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程,意味着即使后台线程中有未执行的finally语句,该语句也不会再执行。

  必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。可以通过调用isDaemon()方法来确定线程是否是一个后台线程。

  如果是一个后台线程,那么它创建的任何线程将被自动设置成后台线程。

 1 class DaemonThreadFactory implements ThreadFactory{
 2     public Thread newThread(Runnable r){
 3         Thread t = new Thread(r);
 4         t.setDaemon(true);//设置为后台线程
 5         return t;
 6     }
 7 }
 8  9 class DaemonThreadPoolExecutor extends ThreadPoolExecutor{
10     public DaemonThreadPoolExecutor(){
11         super(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,
12                 new SynchronousQueue<Runnable>(),
13                 new DaemonThreadFactory());
14         /*
15         * corePoolSize:指定了线程池中的线程数量
16         * maximumPoolSize:指定了线程池中的最大线程数量
17         * keepAliveTime:当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁
18         * unit:keepAliveTime的单位
19         * workQueue:任务队列,被添加到线程池中,但尚未被执行的任务
20         * threadFactory:线程工厂,用于创建线程,一般用默认即可*/
21     }
22 }
23 24 public class DaemonFromFactory implements Runnable{
25     public void run(){
26         try{
27             while(true){
28                 TimeUnit.MILLISECONDS.sleep(100);
29                 System.out.println(Thread.currentThread() + " " + this);
30             }
31         }catch (InterruptedException e){
32             System.out.println(e);
33         }
34     }
35 36     public static void main(String[] args) throws InterruptedException {
37         ExecutorService exec = new DaemonThreadPoolExecutor();
38         for(int i=0;i<10;i++)
39             exec.execute(new DaemonFromFactory());
40         System.out.println("All daemons started");
41         TimeUnit.MILLISECONDS.sleep(500);
42     }
43 }

 

(9)编码的变体

  以上的例子都是通过实现Runnable接口来实现任务类,也可以通过直接继承Thread这种可替换的方式。同时还可以通过创建内部类的方式来创建任务类和启动线程。

src/net/mindView/threads/Threads.java · sumAll/Java编程思想(第四版) - 码云 - 开源中国 (gitee.com)

 

(10)加入一个线程

  一个线程可以在其他线程上调用join()方法,其效果是等待一段时间直到第二个线程结束才继续执行。如果某个线程在另一个线程t上调用t.join(),此线程将被挂起,直到目标线程t结束才恢复(即t.isAlive()返回为假)。

  在调用join()时带上一个超时参数(单位可以是毫秒,或者毫秒和纳秒),这样如果目标线程在这段时间到期时还没有结束的话,join()方法总能返回。

  对join()方法的调用可以被中断,做法是在调用线程上调用interrupt()方法。当另一个线程在该线程上调用interrupt()时,将给该线程设定一个标志,表明该线程已经被中断。然而,异常被捕获时将清理这个标志,所以在catch子句中,在异常被捕获的时候这个标志总是为假。

 

(11)捕获异常

  由于线程的本质特性,使得你不能捕获从线程中逃逸的异常。一旦异常逃出任务的run()方法,它就会向外传播到控制台。

  为了解决这个问题,我们要修改Executor产生线程的方式。Thread.UncaughtExceptionHandler是Java SE5中的新接口,它允许你在每个Thread对象上都附着一个异常处理器。

class ExceptionThread implements Runnable{
    public void run(){
        Thread t = Thread.currentThread();
        System.out.println("run by " + t);
        System.out.println("eh = " + t.getUncaughtExceptionHandler());
        throw new RuntimeException();
    }
}
​
class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("caught " + e);
    }
}
​
class HandlerThreadFactory implements ThreadFactory{
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        System.out.println("created " + t);
        t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        System.out.println("eh = " + t.getUncaughtExceptionHandler());
        return t;
    }
}
​
class CaptureUncaughtException{
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool(new HandlerThreadFactory());
        exec.execute(new ExceptionThread());
    }
}
​
/*Output:
created Thread[Thread-1,5,main]
eh = com.my.chapter21.MyUncaughtExceptionHandler@5a2fb0f4
caught java.lang.RuntimeException
*/

 

3、共享受限资源

(1)不正确地访问资源

  如果有多个线程正在访问同一个共享资源,例如在run()方法中对某一值进行递增并返回,对该方法如果不是一次性执行完毕,可能某个线程执行时会返回一个不正确的值。

 

(2)解决共享资源竞争

  由于不知道一个线程何时在运行,所以对共享资源的访问会产生冲突。防止这种冲突的办法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它。

  在解决线程冲突问题的时候,基本上都是采用序列化访问共享资源的方案。通常在访问资源前加上锁语句,产生了一种互相排斥的效果,所以这种机制常常称为互斥量。

  要控制对共享资源的访问,得先把它包装进一个对象。然后把所有要访问这个资源的方法标记为synchronized。当在对象上调用其任意synchronized方法的时候,此对象都被加锁,这时该对象上的其他synchronized方法只有等到前一个方法调用完毕并释放了锁之后才能调用。对于某个特定对象来说,其所有synchronized方法共享同一个锁,这可以被用来防止多个任务同时访问被编码为对象的内存。

  在使用并发时,要将域设置为private,因为synchronized关键字不能防止其他任务直接访问域。

  一个任务可以多次获得对象的锁。存在一个任务在一个方法中调用了同一个对象的第二个方法,这时候会对调用的synchronized修饰的方法个数进行计数(相当于锁的数量)。每当任务离开一个synchronized方法,计数递减。当计数为0时,锁被完全释放。

  Java SE5的java.util.concurrent类库还包含有定义在java.util.concurrent.locks中的显式的互斥机制。Lock对象必须被显式地创建、锁定和释放。

  用synchronized关键字不能尝试着获取锁且最终获取锁会失败,或者尝试着获取锁一段时间,然后放弃它,要实现这些,你必须使用concurrent类库。

 1 class SynchronizedEventGenerator{
 2     private int currentEventValue = 0;
 3     public synchronized int next(){
 4         ++currentEventValue;
 5         Thread.yield();
 6         ++currentEventValue;
 7         return currentEventValue;
 8     }
 9 }
10 11 class MutexEventGenerator{
12     private int currentEventValue = 0;
13     private Lock lock = new ReentrantLock();
14     public int next(){
15         lock.lock();
16         try {
17             ++currentEventValue;
18             Thread.yield();
19             ++currentEventValue;
20             return currentEventValue;
21         }finally {
22             lock.unlock();
23         }
24     }
25 }

 

(3)原子性与易变性

  原子性:是指一个操作或多个操作要么全部执行,且执行的过程不会被任何因素打断,要么就都不执行。

  原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”。JVM把long和double的读取和写入当做两个分离的32位操作来执行,所以可能看到不正确的结果。

  当你定义long和double变量时,如果使用volatile关键字,就会获得简单的赋值与返回操作的原子性。

  易变性表示可视性(当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改)和有序性(即程序执行的顺序按照代码的先后顺序执行)。

  volatile关键字还保证了应用中的可视性。volatile无法工作的情况:

    ①当一个域的值依赖于它之前的值时(例如递增一个计数器)

    ②某个域的值受到其他域的值的限制

  使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域。

  在进行并发编程时,最明智的做法是遵循Brian的同步规则。

Brain的同步规则—如果你正在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且,读写线程都必须用相同的监视器所同步。

 

(4)原子类

  在java.util.concurrent.atomic包中提供了原子类型专门确保变量操作的原子性。例如:AtomicInteger、AtomicLong、AtomicReference等。

 

(5)临界区

  防止多个线程同时访问方法内部的部分代码被称为临界区,它也使用synchronized关键字建立。这也被称为同步控制块,在进入此段代码前,必须得到syncObject对象的锁。

1 synchronized(syncObject){
2     //This code can be accessed
3     //by only one task at a time
4 }

 

  使用同步控制块而不是对整个方法进行同步控制的典型原因是使得其他线程能更多地访问(在安全的情况下尽可能多)。

 1 //1、通过synchronized关键字创建临界区
 2 private int x;
 3 public void increment(){
 4     synchronized (this){
 5         x++;
 6     }
 7 }
 8  9 10 //2、通过Lock对象创建临界区
11 private int x;
12 private Lock lock = new ReentrantLock();
13 public void increment(){
14     lock.lock();
15      try {
16          x++;
17      }finally {
18          lock.unlock();
19      }
20 }

 

(6)在其他对象上同步

  有时必须在另一个对象上同步,此时必须确保所有相关的任务都是在同一个对象上同步。两个任务可以同时进入同一个对象,只要这个对象上的方法是在不同的锁上同步即可。

  f()方法在this同步,而g()方法有一个在syncObject上同步的synchronized块。因此,这两个同步是互相独立的。

 1 class DualSynch{
 2     private Object syncObject = new Object();
 3     public synchronized void f(){
 4         for(int i=0;i<5;i++){
 5             System.out.println("f()");
 6             Thread.yield();
 7         }
 8     }
 9     public void g(){
10         synchronized (syncObject){
11             for(int i=0;i<5;i++){
12                 System.out.println("g()");
13                 Thread.yield();
14             }
15         }
16     }
17 }
18 19 class SyncObject{
20     public static void main(String[] args) {
21         final DualSynch ds = new DualSynch();
22         new Thread(){
23             public void run(){
24                 ds.f();
25             }
26         }.start();
27         ds.g();
28     }
29 }
30 31 /*Output:
32 g()
33 g()
34 g()
35 f()
36 g()
37 f()
38 f()
39 g()
40 f()
41 f()
42 */

 

(7)线程本地存储

  防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。线程本地存储是一种自动化机制,可以为使用相同变量的每个不同线程都创建不同的存储。

 1 class ThreadLocalVariableHolder{
 2     private static ThreadLocal<Integer> value = 
 3             new ThreadLocal<Integer>(){
 4                 private Random rand = new Random(47);
 5                 protected synchronized Integer initialValue(){
 6                     return rand.nextInt(10000);
 7                 }
 8             };
 9     public static void increment(){ value.set(value.get() + 1); }
10     public static int get(){ return value.get(); }
11 }

 

4、终结任务

(1)线程状态

  ①新建(new):当线程被创建时,它只会短暂地处于这种状态。此时它已经分配了必需的系统资源,并执行了初始化。

  ②就绪(Runnable):只要调度器把时间分配给线程,线程就可以运行。

  ③阻塞(Blocked):线程能够运行,但有某个条件阻止它的运行。

  ④死亡(dead):线程将不再是可调度的,并且再也不会得到CPU时间。

 

(2)进入阻塞状态的原因

  ①通过调用sleep(milliseconds)使任务进入休眠状态,在这种情况下,任务在指定的时间内不会运行。

  ②调用wait()使线程挂起。直到线程得到了notify()或notifyAll()消息(或者在java.util.concurrent类库中等价的signal()或signalAll()消息),线程才会进入就绪            状态。

  ③任务在等待某个输入\输出完成。

  ④任务试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获取了这个锁。

 

(3)中断

  当你打断阻塞的任务时,可能需要清理资源。正因为这一点,在任务的run()方法中间打断,更像是抛出的异常,因此在Java线程中的这种类型的异常中断用到了异常。

  Thread类中的与中断相关的方法:

    ①interrupt()方法,给线程设置一个中断标志,线程仍会继续运行。但当这个线程已经被阻塞,或者试图执行一个阻塞操作,那么将抛出InterruptedException,并清除线程的中断状态。

    ②interrupted()方法,测试当前线程是否被中断(检查中断标志),此方法清除线程的中断状态。

    ③isInterrupted()方法,只测试此线程是否被中断 ,不清除中断状态。

  Executor也可以中断阻塞线程:

    ①调用shutdownNow(),它将发送一个interrupt()调用给它启动的所有线程。

    ②调用submit()而不是executor()来启动任务。将返回一个Future对象,在该对象上调用cancel(),就可以中断某个特定任务。

  对阻塞线程的中断,只能中断对sleep()的调用(或者任何要求抛出InterruptedException的调用),不能中断正在试图获取synchronized锁或者试图执行I/O操作的线程。但是可以通过关闭在其上发生阻塞的底层资源来实现中断。

  Java SE5并发类库中添加了一个特性,即在ReentrantLok上阻塞的任务具备可以被中断的能力。

 

(4)检查中断

  当你在线程上调用interrupt()时,中断发生的唯一时刻是在任务要进入到阻塞操作中,或者已经在阻塞操作内部时。

  可以通过调用interrupted()来检查中断状态,这不仅可以告诉你interrupt()是否被调用过,而且还可以清除中断状态。

1 public void run(){
2     try{
3         while(!Thread.interrupted()){
4             //要执行的任务
5         }
6     }catch(InterruptedException e){
7         System.out.println("Exiting via InterruptedException");
8     }
9 }

 

  调用interrupt()后,该段代码退出有两种情况:

    ①interrupt()在非阻塞的操作过程中被调用,那么首先循环将结束,然后所有的本地对象将被销毁,最后循环会经由while语句的顶部退出。

    ②interrupt()在while语句之后,但是在阻塞操作sleep()之前或其过程中被调用,那么这个任务就会在第一次试图调用阻塞操作之前,经由 InterruptedException退出。

 

5、线程之间的协作

  某些任务需要线程之间的协作,因为在这类问题中,某些部分必须在其他部分被解决之前解决。

(1)wait()方法

  wait()方法使你可以等待某个条件发生变化,而改变这个条件超出了当前方法的控制能力。通常,这种条件将由另一个任务改变。

  wait()方法会在等待外部世界产生变化的时候将任务挂起,避免了忙等待(不断地进行空循环),并且只有在notify()或notify All()发生时,这个任务才会被唤醒并去检查所产生的变化。

  线程调用wait()后被挂起,并释放对象上的锁。实际上,只能在同步控制方法或同步控制块里调用wait()、notify()和notify All(),因为在调用这些方法前必须“拥有”(获取)对象的锁。

  wait()方法有两种形式:

    ①接受毫秒数作为参数,可以通过notify()、notify All(),或者令时间到期,从wait()中恢复。

    ②不接受任何参数,只能通过notify()或notify All()唤醒。

  必须用一个检查感兴趣条件的while循环包围wait()。因为线程会由于某些原因被唤醒,然后线程会在执行wait()之后继续执行,此时应该通过while循环来再次判断是否是感兴趣的条件发生了变化。

 

(2)notify()与notifyAll()

  使用notify()时,在众多等待同一个锁的任务中只有一个会被唤醒,因此如果你希望使用notify(),就必须保证被唤醒的是恰当的任务。使用notify()必须满足一下规则,否则只能使用notifyAll()。

    ①所有任务必须等待相同的条件

    ②当条件发生变化时,必须只有一个任务能够从中受益

    ③以上的限制对所有可能存在的子类都必须总是起作用的

  当notifyAll()因某个特定锁而被调用时,只有等待这个锁的任务才会被唤醒。

 

(3)使用显式的Lock和Condition对象

  使用互斥并允许任务挂起的基本类是Condition,你可以通过在Condition上调用await()来挂起一个任务。当外部条件发生变化时,可以通过调用signal()来通知这个任务,从而唤醒一个任务,或者调用notifyAll()来唤醒所有在这个Condition上被其自身挂起的任务。

src/com/my/chapter21/practice27.java · sumAll/Java编程思想(第四版) - 码云 - 开源中国 (gitee.com)

 

(4)生产者—消费者与队列

  使用同步队列来解决任务协作问题,同步队列在任何时刻都只允许一个任务插入或移除元素。在java.util.concurrent.BlockingQueue接口中提供了这个队列,这个接口有大量的标准实现。

src/com/my/chapter21/practice29.java · sumAll/Java编程思想(第四版) - Gitee.com

 

(5)任务间使用管道进行输入/输出

  提供线程功能的类库以“管道”的形式对线程间的输入\输出提供了支持。它们在Java输入/输出类库中的对应物就是PipedWriter类(允许任务向管道写)和PipedReader类(允许不同任务从同一个管道中读取)。

 1 class Sender implements Runnable {
 2     private Random rand = new Random(47);
 3     private PipedWriter out= new PipedWriter();
 4     public PipedWriter getPipedWriter() { return out; }
 5     public void run() {
 6         try {
 7             while(true)
 8                 for(char c = 'A'; c <= 'z'; c++) {
 9                     out.write(c);
10                     TimeUnit.MILLISECONDS.sleep(rand.nextInt(500));
11                 }
12         } catch(InterruptedException e) {
13             System.out.println(e + " Sender interrupted");
14         }catch (IOException e){
15             System.out.println(e + " Sender write exception");
16         }
17     }
18 }
19 20 class Receiver implements Runnable {
21     private PipedReader in;
22     public Receiver(Sender sender) throws IOException { in = new PipedReader(sender.getPipedWriter()); }
23     public void run() {
24         try {
25             while(true) {
26                 // Blocks until characters are there:
27                 System.out.print("Read: " + (char)in.read() + ", ");
28             }
29         }catch (IOException e){
30             System.out.println(e + " Receiver read exception");
31         }
32     }
33 }
34 35 public class Test {
36     public static void main(String[] args) throws Exception {
37         Sender sender = new Sender();
38         Receiver receiver = new Receiver(sender);
39         ExecutorService exec = Executors.newCachedThreadPool();
40         exec.execute(sender);
41         exec.execute(receiver);
42         TimeUnit.SECONDS.sleep(4);
43         exec.shutdownNow();
44     }
45 }
46 47 /*Output:
48 Read: A, Read: B, Read: C, Read: D, Read: E, Read: F, Read: G, Read: H, 
49 java.lang.InterruptedException: sleep interrupted Sender interrupted
50 java.io.InterruptedIOException Receiver read exception
51 */

 

6、死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。—百度百科

  当以下四个条件同时满足时,就会发生死锁:

    ①互斥条件。任务使用的资源中至少有一个是不能共享的。

    ②至少有一个任务它必须持有一个资源且正在等待获取一个当前被别的任务持有的资源。

    ③资源不能被抢占,任务必须把资源释放当作普通事件。

    ④必须有循环等待,这时,一个任务等待其他任务所持有的资源,后者又在等待另一个任务所持有的资源,这样一直下去,直到有一个任务在等待第一个任务所持有的资源,使得大家都被锁住。

 

7、新类库中的构件

(1)CountDownLatch

  它被用来同步一个或多个任务,强制它们等待由其他任务执行的一组操作完成。

  可以向CountDownLatch对象设置一个初始技术值,任何在这个对象上调用await()的方法都将阻塞,直至这个计数值达到0。其他任务在结束工作时,可以在该对象上调用countDown()来减小这个计数值。

  CountDownLatch被设计为只触发一次,计数值不能重置。

1 CountDownLatch latch = new CountDownLatch(6);
2 latch.await();
3 latch.countDown();

 

(2)CyclicBarrier

  CyclicBarrier适用于这样的情况:你希望创建一组任务,它们并行地执行工作,然后在进行下一个步骤之前等待,直至所有任务都完成。

  CyclicBarrier像CountDownLatch,但CountDownLatch只触发一次的事件,而CyclicBarrier可以多次重复。

  可以向CyclicBarrier提供一个“栅栏动作”,它是一个Runnable,当计数值到达0时自动执行。当所有持有CyclicBarrier的任务都执行完毕并调用await()方法后,就调用该“栅栏动作”。若没有执行exec.shutdownNow()则继续原先的顺序执行。

src/net/mindView/concurrency/HorseRace.java · sumAll/Java编程思想(第四版) - 码云 - 开源中国 (gitee.com)

 

(3)DelayQueue

  DelayQueue用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。这种队列是有序的,即到期时间最长的放到队头。如果没有任何到期的对象,那么就没有队头元素,take()会返回null。

 1 class DelayedTask implements Runnable,Delayed{
 2     private int final delta;
 3     public DelayedTask(int delta){ this.delta = delta; }
 4     public int compareTo(Delayed arg){
 5         DelayedTask that = (DelayedTask)arg;
 6         if(delta < that.delta)  return -1;
 7         if(delta > that.delta)  return 1;
 8         return 0;
 9     }
10     public void run(){}
11 }
12 13 class DelayedTaskConsumer implements Runnable{
14     private DelayQueue<DelayedTask> q;
15     public DelayedTaskConsumer(DelayQueue<DelayedTask> q){
16         this.q = q;
17     }
18     public void run(){
19         try{
20             while(!Thread.interrupted())
21                 q.take().run();
22         }catch(InterruptedException e){
23             
24         }
25     }
26 }
27 28 29 public class DelayQueueDemo{
30     public static void main(String[] args){
31         Random rand = new Random(47);
32         ExecutorService exec = Executors.newCachedThreadPool();
33         DelayQueue<DelayedTask> queue = new DelayQueue<DelayedTask>;
34         for(int i=0;i<20;i++){
35             queue.put(new DelayTask(rand.nextInt(5000)));
36         }
37         exec.execute(new DelayedTaskConsumer(queue));
38     }
39 }

 

(4)PriorityBlockingQueue

  PriorityBlockingQueue是一个优先级队列,它具有可阻塞的读取操作。

 

(5)ScheduledExecutor

  ScheduledThreadPoolExecutor通过使用schedule()(运行一次任务)或者scheduleAtFixedRate()(每隔规则的时间重复执行任务),可以将Runnable对象设置为在将来的某个时刻执行。

src/net/mindView/concurrency/GreenhouseScheduler.java · sumAll/Java编程思想(第四版) - 码云 - 开源中国 (gitee.com)

 

(6)Semaphore

正常的锁(来自concurrent.locks或内建的synchronized锁)在任何时刻都只允许一个任务访问一项资源,而Semaphore允许n个任务同时访问这个资源。

 1 Semaphore semaphore = new Semaphore(1);
 2 //同步关键类,构造方法传入的数字是多少,则同一个时刻,只运行多少个进程同时运行制定代码
 3 public void do(){
 4     try {
 5         semaphore.acquire();
 6         System.out.println("doing");
 7          Thread.sleep(2000);
 8         semaphore.release();
 9      } catch (InterruptedException e) {
10          e.printStackTrace();
11      }
12 }
13 //在 semaphore.acquire() 和 semaphore.release()之间的代码,同一时刻只允许指定个数的线程进入,此处只能有一个线程进入

 

(7)Exchanger

  Exchanger是在两个任务之间交换对象的栅栏。Exchanger的典型应用场景是:一个任务在创建对象,这些对象的生产代价很高昂,而另一个任务在消费这些对象。通过这种方式,填充一个列表和消费另一个列表便可以同时发生了,可以有更多的对象在被创建的同时被消费。

src/com/my/chapter21/practice34.java · sumAll/Java编程思想(第四版) - 码云 - 开源中国 (gitee.com)

 

8、仿真

(1)银行出纳员仿真

src/net/mindView/concurrency/BankTellerSimulation.java · sumAll/Java编程思想(第四版) - 码云 - 开源中国 (gitee.com)

 

(2)饭店仿真

src/net/mindView/concurrency/RestaurantWithQueue.java · sumAll/Java编程思想(第四版) - 码云 - 开源中国 (gitee.com)

 

(3)分发工作

src/net/mindView/concurrency/CarBuilder.java · sumAll/Java编程思想(第四版) - 码云 - 开源中国 (gitee.com)

 

9、性能调优

(1)免锁容器

  免锁容器的通用策略:对容器的修改可以与读取操作同时发生,只要读取者只能看到完成修改的结果即可。

  CopyOnWriteArrayList中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。当修改完成时,一个原子性的操作将把新的数组换入,使得新的读取操作可以看到这个新的修改。CopyOnWriteArrayList的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModificationException。

  CopyOnWriteArraySet使用CopyOnWriteArrayList来实现其免锁行为。

  ConcurrentHashMap和ConcurrentLinkedQueue使用了类似的技术,允许并发的读取和写入,但容器中只有部分内容可以被复制和修改。ConcurrentHashMap不会抛出ConcurrentModificationException异常。

 

(2)乐观加锁

  乐观加锁意味着当你执行某项计算时,实际上没有使用互斥,但是在这项计算完成,并且准备更新这个Atomic对象时,你需要调用compareAndSet()方法,将旧值和新值一起提交给该方法,如果旧值与它在Atomic对象中发现的值不一致,那么这个操作就失败—这意味着某个其他的任务已经于此操作执行期间修改了这个对象。

 

(3)ReadWriteLock

  ReadWriteLock使得你可以同时有多个读取者,只要它们都不试图写入即可。如果写锁已经被其他任务持有,那么任何读取者都不能访问,直至这个写锁被释放为止。

1 ReentrantReadWriteLock lock =
2             new ReentrantReadWriteLock(true);
3 Lock wlock = lock.writeLock();
4 Lock rlock = lock.readLock();

 

10、活动对象

  每个对象都维护着它自己的工作器线程和消息队列,并且所有对这种对象的请求都将进入队列排队,任何时刻都只能运行其中的一个。

    ①每个对象都可以拥有自己的工作器线程

    ②每个对象都将维护对它自己的域的全部控制权(这比普通的类要更严苛一些,普通的类只是拥有防护它们的域的选择权)

    ③所有在活动对象之间的通信都将以在这些对象之间的消息形式发生

    ④活动对象之间的所有消息都要排队

 

 

参考于《Java编程思想》,第650~767页

 

posted @ 2021-07-23 09:03  sumAll  阅读(63)  评论(0编辑  收藏  举报