Java编程思想之并发

1. 并发的多面性

  • 更快的执行
  • 改进代码设计

2. 基本的线程机制

线程的创建

    /* What will be run. */
    private Runnable target;
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

(1) 实现Runnable接口:new Thread(Runnable).start();

  • 将任务与线程分离,符号面向对象思想,适合多个线程处理统一资源的情况。
  • 可实现多重继承,避免单继承带来的局限性。
  • 有利于程序的健壮性,代码能够被多个线程共享。

(2) 继承Thread类:重写run方法,new Thread(){run()}.start();

  • 不能在继承其他类
  • 编写简单,可直接操纵线程,无需使用Thread.currentThread()。

线程池(ThreadPool)

(1) 使用线程池的好处

  • 降低资源消耗。通过重复利用已创的线程降低线程创建和销毁在成的消耗
  • 提高响应速度。当任务到达的时候,不需要再去创建线程就能立即执行
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控。

(2) 线程池的创建:

  • ExecutorService是具有生命周期的Executor,知道如何构建恰当的上下文来执行Runnable。
  • CachedThreadPool:将为每个任务都创建一个线程
  • FixedThreadPool:使用有限的线程集来执行所提交的任务,一次性预先执行代价高昂的线程分配,可以限制线程的数量,可以节省时间因为不用为每一个任务都固定的付出创建线程的开销。
  • SingleThreadExecutor:就像是线程数量为1的FixedThreadPool,如果向该线程池提交了多个任务,这些任务将排队,每个任务都会在下一个任务开始之前运行结束,所有的任务使用的是同一个线程。会序列化所有的任务,并会维护它自己的悬挂任务队列。
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) 

注意:线程池的创建在底层使用的都是ThreadPoolExecutor的构造方法:

  • corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
  • maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
  • keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
  • unit(线程活动保持时间):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
  • workQueue(任务队列):用于保存等待执行的任务的阻塞队列:
    • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
    • LinkedBlockingQueue: 一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
    • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
    • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
  • threadFactory(线程工厂):当Executor创建一个线程时,会使用它。可以继承ThreadFactory来编写定制的ThreadFactory可以定制Executor创建额线程的属性(后台、优先级、名称)。
  • handler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。

    • AbortPolicy: 直接抛出异常。
    • CallerRunsPolicy:只用调用者所在线程来运行任务。
    • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
    • DiscardPolicy:不处理,丢弃掉。
    • 当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。

    (3)线程池提交任务的方法

  • 我们可以使用execute提交的任务,但是execute方法没有返回值,所以无法判断任务是否被线程池执行成功。通过以下代码可知execute方法输入的任务是一个Runnable类的实例。

    threadsPool.execute(new Runnable() {
        @Override
        public void run() {
            // TODO Auto-generated method stub
        }
    });
  • 我们也可以使用submit 方法来提交任务,它会返回一个future,那么我们可以通过这个future来判断任务是否执行成功,通过future的get方法来获取返回值,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。

    Future<Object> future = executor.submit(harReturnValuetask);
    try {
        Object s = future.get();        
    } catch (InterruptedException e) {
        // 处理中断异常
    } catch (ExecutionException e) {
        // 处理无法执行任务异常
    } finally {
        // 关闭线程池
        executor.shutdown();
    }

    (4) 关闭线程池

    1. 我们可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池,它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。
    2. shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。
    3. 而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。调用该方法,当前线程将继续运行在shutdown()被调用之前所提交的所有任务,再退出。
    4. 只要调用了这两个关闭方法的其中一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow。

    (5)线程池分析

    1. 当提交一个新任务到线程池时,线程池的处理流程如下:

      1)首先线程池判断基本线程池是否已满?没满,创建一个工作线程来执行任务。满了,则进入下个流程。
      2) 其次线程池判断工作队列是否已满?没满,则将新提交的任务存储在工作队列里。满了,则进入下个流程。
      3) 最后线程池判断整个线程池是否已满?没满,则创建一个新的工作线程来执行任务,满了,则交给饱和策略来处理这个任务。

      源码如下:

      public void execute(Runnable command) {
          if (command == null)
             throw new NullPointerException();
          //如果线程数小于基本线程数,则创建线程并执行当前任务 
          if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
          //如线程数大于等于基本线程数或线程创建失败,则将当前任务放到工作队列中。
              if (runState == RUNNING && workQueue.offer(command)) {
                  if (runState != RUNNING || poolSize == 0)
                            ensureQueuedTaskHandled(command);
              }
          //如果线程池不处于运行中或任务无法放入队列,并且当前线程数量小于最大允许的线程数量,
      则创建一个线程执行任务。
              else if (!addIfUnderMaximumPoolSize(command))
              //抛出RejectedExecutionException异常
                  reject(command); // is shutdown or saturated
          }
      }
    2. 工作线程。线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后,还会无限循环获取工作队列里的任务来执行。我们可以从Worker的run方法里看到这点:

      public void run() {
          try {
              Runnable task = firstTask;
              firstTask = null;
              while (task != null || (task = getTask()) != null) {
              runTask(task);
              task = null;
              }
          } finally {
              workerDone(this);
          }
      } 

从任务中产生返回值

Runnable是执行工作的独立任务,但是它不产生返回值。实现Callable接口可产生返回值,是一种具有类型参数的泛型,它的参数表示的是从放大call()中返回的值,并且必须使用ExecutorService.submit()方法调用它。例子如下:

package com.xqq.Callable;

import java.util.concurrent.Callable;

public class TaskWithResult implements Callable<String>{

    private int id;
    public TaskWithResult(int id) {
        this.id = id;
    }
    public String call() throws Exception {
        return "result of TaskWithResult " + id;
    }
}

package com.xqq.Callable;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CallableDemo {
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        List<Future<String>> list = new ArrayList<Future<String>>();
        for(int i = 0; i < 10; i++){
            list.add(exec.submit(new TaskWithResult(i)));
        }

        for(Future<String> fs: list){
            try {
                System.out.println(fs.get());
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (ExecutionException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }finally{
                exec.shutdown();
            }
        }
    }
}

运行结果:
result of TaskWithResult 0
result of TaskWithResult 1
result of TaskWithResult 2
result of TaskWithResult 3
result of TaskWithResult 4
result of TaskWithResult 5
result of TaskWithResult 6
result of TaskWithResult 7
result of TaskWithResult 8
result of TaskWithResult 9

submit()方法会产生Future对象,它用Callable但会结果的特定类型进行参数化。可以通过isDone()来查询Future是否已经完成。当任务完成时,它具有一个结果,你可以调用get()方法来获取该结果。也可以不用isDone()进行检查就直接get(),在这种情况下,get()将阻塞,直到结果准备就绪。

后台线程

  1. 所谓后台线程是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。当所有的非后台线程结束时,程序也就终止,同时会杀死进程中所有的后台进程。
  2. 线程必须在启动之前调用setDaemon()方法,才能把它设置为后台线程。
  3. 后台进程在不执行finally子句的情况下就会终止其run()方法

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

对join()方法的调用可以被中断,做法就是在调用线程上调用interrupt()方法,即t.interrupt()。例子如下:

package com.xqq.join;

public class Joiner extends Thread{

    private Sleeper sleeper;
    public Joiner(String name, Sleeper sleeper){
        super(name);
        this.sleeper = sleeper;
        start();
    }
    public void run() {
        try {
            sleeper.join();
        } catch (Exception e) {
            System.out.println("Interupted");
        }
        System.out.println(getName() + " join completed");
    }
}

package com.xqq.join;

public class Sleeper extends Thread{

    private int duration;
    public Sleeper(String name, int sleeperTime){
        super(name);
        duration = sleeperTime;
        start();
    }

    public void run() {
        try {
            sleep(duration);
        } catch (Exception e) {
            System.out.println(getName() + " was interrupted. " + "isInterupted: " + isInterrupted());
            return;
        }
        System.out.println(getName() + " has awakened");
    }
}

package com.xqq.join;

/**
 * 如果某一个线程在另一个线程t上调用t.join(),此线程将被挂起,直到线程t运行结束
 * 对join方法的调用可以被中断,在调用线程上调用interrupt()方法。
 * @author xqq
 */
public class Joining {

    public static void main(String[] args) {
        Sleeper sleepy = new Sleeper("Sleepy", 1500);
        Sleeper grumpy = new Sleeper("Grumpy", 1500);

        new Joiner("Dopey", sleepy);
        new Joiner("Doc", grumpy);
        grumpy.interrupt();
    }
}
运行结果:
Grumpy was interrupted. isInterupted: false
Doc join completed
Sleepy has awakened
Dopey join completed

在catch子句中,将根据isInterrupted()的返回值报告这个中断,当另一个线程在该线程上调用interrupt()时,将给该线程设定一个标志,表明该线程已经被中断。然而,异常被捕获时将清理这个标志,所以在catch子句中,在异常被捕获的时候这个标志总是为假。

捕获异常
由于线程的本质特性,使得不能捕获从线程中逃逸的异常。一旦异常逃出任务的run()方法,就会向外传向控制台,在JavaSE5之前,可以使用线程组来捕获这些异常。现在可以修改Executor产生线程的方式。Thread.UncaughtExceptionHandler是Java SE5中的新接口,它允许在每个Thread对象上都附着一个异常处理器。Thread.UncaughtExceptionHandler.uncaughtException()会在线程因未捕获的异常而临近死亡时被调用。可以创建一个新类型的ThreadFactory,为每个新创建的Thread对象上附着一个异常处理器。例子如下:

package com.xqq.捕获异常;

public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{

    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("caught " + e);
    }
}

package com.xqq.捕获异常;

import java.util.concurrent.ThreadFactory;

public class HandlerThreadFactory implements ThreadFactory{

    public Thread newThread(Runnable r) {
        System.out.println(this + " creating new thread");
        Thread t = new Thread(r);
        System.out.println("created " + t);
        t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        System.out.println("eh = " + t.getUncaughtExceptionHandler());
        return t;
    }
}

package com.xqq.捕获异常;

public 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();
    }
}

package com.xqq.捕获异常;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 捕获异常:修改Executor产生线程的方式
 * 为每一个线程绑定一个异常处理器,Thread.UncaughExceptionHandler
 * @author xqq
 */
public class CaptureUncaughtExcception {

    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool(new HandlerThreadFactory());
        exec.execute(new ExceptionThread());
    }
}

运行结果:
com.xqq.捕获异常.HandlerThreadFactory@7852e922 creating new thread
created Thread[Thread-0,5,main]
eh = com.xqq.捕获异常.MyUncaughtExceptionHandler@4e25154f
run() by Thread[Thread-0,5,main]
eh = com.xqq.捕获异常.MyUncaughtExceptionHandler@4e25154f
com.xqq.捕获异常.HandlerThreadFactory@7852e922 creating new thread
created Thread[Thread-1,5,main]
eh = com.xqq.捕获异常.MyUncaughtExceptionHandler@34637630
caught java.lang.RuntimeException

3. 共享受限资源

什么时候应该同步?

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

同步

同步分为两种,一种是多个线程共享同一个资源,该资源同一时刻只能让一个线程访问,对共享资源的互斥访问,利用synchronized关键字或者Lock等实现。另一种是多个线程协作解决某一个问题,某个线程必须在另一个线程之前或者之后完成,线程之间的顺序问题,利用wait()和notify()实现。

1. 共享资源

使用线程来同时运行多个任务时,可以通过使用锁来同步两个任务的行为,从而使得一个任务不会干涉另一个任务的资源。也就是两项任务在交替着步入某项资源,可以使用互斥来使得任何时刻只有一个任务可以访问这项资源。

  1. 关键字synchronized,为防止资源冲突提供内置支持。

    synchronized的使用方法:不可以修饰域,只能锁定对象或者方法
    synchronized void f(){}
    synchronized void g(){}
    synchronized (Object){}
    如果 某个任务对对象调用了f(), 对于同一个对象而言,就只能等到f()调用结束并释放锁之后,其他任务才能调用f(),g()。

  2. 使用显示的Lock锁:Lock对象必须被显示的创建、锁定和释放。因此与内建锁相比,代码缺乏优雅性。

    1. 必须将unlock()放在finally子句中,注意return语句必须在try子句中出现,以确保unlock()不会过早的发生。
    2. 如果在使用synchronized关键字时,某些事物失败了,就会抛出一个异常,但是没有机会去做任何清理工作,以维护系统使其处于良好的状态;而显示的Lock,可以使用finally子句将系统维护在正确的状态。
    3. 使用synchronized不能尝试着获取锁且最终获取锁失败,或者尝试着获取锁一段时间,然后放弃它。
    4. ReentrantLock允许尝试获取但最终未获取锁,这样如果其他人已经获取这个锁,那你就可以决定离开去执行其他一些事情,而不是等待直至这个锁被释放。
    5. 显示的Lock对象在加锁和释放锁方面,相对于内建的synchronized锁来说,还赋予了更细粒度的控制力。
  3. 原子性与易变性
    1. 原子性: 由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store、和write这六个,基本数据类型的访问读写是具备原子性的(long和double)的非原子性协定例外),synchronized块之间的操作也具备原子性。
    2. 可见性:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将心智同步回主存,在变量读取前从主存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前从主内存刷新。因此可以说volatile保证了多线程操作时变量的可见性。
    3. 除了volatile之外,synchronized和final也能实现可见性,同步块的可见性是由“对一个变量执行unlock之前,必须先把此变量同步回到主内存中”。而final的可见性是指:被final修饰的字段在构造中一旦被初始化完成,那么在其他线程中技能看见final字段的值。
    4. 由于volatile变量只能保证可见性,在不符合一下两条规则的运算场景中,我们仍然要通过加锁来保证原子性:
      • 运算结果不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
      • 变量不需要与其他的状态变量共同参与不变约束。
    5. 使用volatile变量的第二个语义是禁止指令重排序优化。
  4. 原子类
    Java SE5 引入了诸如AtomicInteger、AtomicLong、AtomicReference等特殊的原子性变量类。

  5. 线程本地存储(ThreadLocal)
    防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。线程本地存储是一种自动化机制,可以为使用相同变量的不同线程都创建不同的存储。如果有5个线程都要使用变量x所表示的对象,那线程本地存储就会生成5个用于x的不同的存储块。ThreadLocal对象通常当做静态域存储。

    1. 每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
    2. 将一个共用的ThreadLocal静态实例的hashCode值作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。 ThreadLocal的应用场合,我觉得最适合的是多线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。
    3. 3.
  6. 锁的等级:必须给定一个在其上的进行同步的对象。

    1. 方法锁:synchronized修饰的是方法,包括static方法和实例方法,修饰static方法时,该锁为类锁,修饰实例方法时,该锁为对象锁。
    2. 对象锁:是针对一个对象的,它只在该对象的某个内存位置声明一个标志位标识该对象是否拥有锁,所以它只会锁住当前的对象。一般一个对象锁是对一个非静态成员变量进行syncronized修饰,或者对一个非静态方法进行syncronized修饰。对于对象锁,不同对象访问同一个被syncronized修饰的方法的时候不会阻塞住。
    3. 类锁:是锁住整个类的,当有多个线程来声明这个类的对象的时候将会被阻塞,直到拥有这个类锁的对象被销毁或者主动释放了类锁。这个时候在被阻塞住的线程被挑选出一个占有该类锁,声明该类的对象。其他线程继续被阻塞住。
    4. 对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步。所谓的类锁,不过是Class对象的锁而已。获取类的Class对象有好几种,最简单的就是MyClass.class的方式。
  7. yield()让步
    当调用yield()时,是在建议具有相同优先级的其它线程可以运行

2. 线程协作

如何使得多个任务彼此之间可以协作,以使得多个任务可以一起工作去解决某问题,现在的问题是线程之间的彼此协调,因为在这类问题中,某些部分必须在其他部分被解决之前解决。任务间的握手可以通过Object的方法wait()和notify()来安全的实现,Java SE5的并发类库提供了具有await()和signal方法的Condition对象。

  1. wait()、notify()、notifyAll()

    1. wait():会等待外部世界产生变化的时候将任务挂起,并且只有在notify或者notifyAll发生时,即表示发生了某些感兴趣的事情,这个任务才会被唤醒并去检查所产的变化。wait()提供了一种任务之间对活动同步的方式。
    2. notify():在众多等待同一个锁的任务中只有一个会被唤醒。注意的是在调用此方法时,并不能确切的唤醒某一个等待的线程,而是有JVM确定唤醒哪个线程,而不是按优先级。
    3. notifyAll():将唤醒所有正在等待的任务。当notifyAll()因某个特定所而被调用时,只有等待这个锁的任务才会被唤醒,注意不是给所有唤醒线程一个对象的锁,而是让它们竞争。
  2. wait()与sleep()的区别

    • sleep()是Thread的静态方法,谁调用谁就去睡觉,wait()是Object的方法。
    • 调用sleep()的时候锁并没有释放,调用yield()也属于这种情况。当一个任务在方法里遇到了对wait()的调用,线程的执行被挂起,对象的锁被释放。因为wait()将释放锁,这就意味着另一个任务可以获得这个锁,因此该对象中的其它的synchronized方法可以在wait()器件被调用。
    • 可以通过notify()或者notifyAll(),或者令时间到,从wait()中唤醒。
    • 只能在同步控制方法或同步控制块里调用,sleep()可以在非同步控制方法里调用。
  3. 为什么使用一个检查感兴趣的条件的while循环包围wait()?

    • 你可能有多个任务处于相同的原因在等待同一个锁,而第一个唤醒任务可能改变这种状态。如果属于这种情况,那么这个任务应该被再次挂起,直至有感兴趣的条件发生变化。
    • 在这个任务从起wait()中被唤醒的时刻,有可能有某个其他的任务已经做出了改变,从而是这个任务在此时不能执行,或者执行其操作已显得无关紧要。此时,应该再通过wait()来将其重新挂起。
    • 也可能某些任务处于不同的原因在等待你的对象上的锁(在这种情况下使用notifyAll())。这种情况下,你需要检查是否已经有正确的原因唤醒,如果不是,就再次调用wait()。
  4. notify()与notifyAll()

    使用notify而不是notifyAll是一种优化。当notify()因某个特定的锁而被调用时,只有等待这个锁的任务才会被唤醒。

4. 终结任务

阻塞状态

一个任务进入阻塞状态的原因:

  1. 通过调用sleep是任务进入休眠状态,在这种情况下,任务在制定的时间内不会运行。
  2. 通过调用wait是线程挂起。直到线程得到了notify或者notifyAll消息,线程才会进入就绪状态。
  3. 任务在等待某个输入/输出完成。
  4. 任务试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获取了这个锁。
中断

所谓中断就是在Runnable.run()方法的中间打断它。
退出阻塞任务的方法:

Thread类包含了interrupt()方法,因此你可以终止被阻塞的任务,这个方法将设置线程的中断状态。

  1. 如果一个线程已经被阻塞,或者试图执行一个阻塞操作,那么设置这个线程的中断状态将抛出InterruptedException。当抛出该异常或者该任务调用Thread.interrupted()时,中断状态将被复位,Thread.interrupted()提供了离开run()循环而不抛出异常的第二种方式。
  2. 为了调用interrupt(),必须持有Thread对象。
  3. 能够中断对sleep()的调用,但是不能中断正在试图获取synchronized锁或者执行I/O操作的线程。
  4. 在ReenTrantLock上阻塞的任务具备可以被中断的能力。lock.lockInterruptibly()。与I/O不同,Interrupt()可以打断被互斥所阻塞的调用。

退出无阻塞任务的方法:

  1. 如果想要调用interrupt()来停止某个任务,而在run()循环碰巧没有产生任何阻塞调用的情况下,退出任务需要第二种方式。
  2. 可以通过调用Interrupted()来检查中断状态,这不仅可以告诉你interrupt()是否被调用过,而且还可以清除中断状态。清除中断状态可以确保并发结构不会就某个任务被中断这个问题通知你两次。

5. 线程之间的协作

生产者与消费者之wait()和notify()实现
package com.xqq.生产者与消费者之wait和notify实现;

public class Item {
    private static int counter;
    private final int id = counter++;
    @Override
    public String toString() {
        return "Item " + id;
    }
}

package com.xqq.生产者与消费者之wait和notify实现;

import java.util.concurrent.TimeUnit;

public class Producer implements Runnable{
    private int delay;
    private FlowQueue<Item> output;

    public Producer(int delay, FlowQueue<Item> output) {
        this.delay = delay;
        this.output = output;
    }
    public void run() {
        for(;;){
            try {
                Item product = new Item();
                System.out.println("Product " + product);
                output.put(product);
                TimeUnit.MILLISECONDS.sleep(delay);
            } catch (Exception e) {
                return;
            }
        }
    }
}
package com.xqq.生产者与消费者之wait和notify实现;

import java.util.concurrent.TimeUnit;

public class Consumer implements Runnable{
    private int delay;
    private FlowQueue<Item> input;

    public Consumer(int delay, FlowQueue<Item> input) {
        this.delay = delay;
        this.input = input;
    }
    public void run() {
        for(;;){
            try {
                System.out.println("Consumer " + input.get());
                TimeUnit.MILLISECONDS.sleep(delay);
            } catch (Exception e) {
                return;
            }
        }
    }
}

package com.xqq.生产者与消费者之wait和notify实现;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ProducerConsumer {

    public static void main(String[] args) throws InterruptedException {
        int producerSleep = 200;
        int consumerSleep = 200;
        FlowQueue<Item> fq = new FlowQueue<Item>(10);
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(new Producer(producerSleep, fq));
        exec.execute(new Consumer(consumerSleep, fq));
        exec.execute(new Producer(producerSleep, fq));
        exec.execute(new Consumer(consumerSleep, fq));
        TimeUnit.SECONDS.sleep(2);
        exec.shutdownNow();
    }
}
运行结果:
Product Item 0
Product Item 1
Consumer Item 0
Consumer Item 1
Product Item 3
Product Item 2
Consumer Item 3
Consumer Item 2
Product Item 4
Consumer Item 4
Product Item 5
Consumer Item 5
Product Item 6
Consumer Item 6
Product Item 7
Consumer Item 7
Product Item 8
Consumer Item 8
Product Item 9
Consumer Item 9
生产者与消费者之使用显示的Lock和Condition对象
package com.xqq.生产者与消费者之显示Lock和Condition对象;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class BufferData<T> {

    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    private final Object [] items;
    private int count = 0, putPtr = 0, takePtr = 0; 

    public BufferData(int size) {
        items = new Object[size];
    }

    public void put(T item) throws InterruptedException{
        lock.lock();
        try{
            while(count == items.length){
                notFull.await();
            }
            items[putPtr++] = item;
            if(putPtr == items.length)
                putPtr = 0;
            count++;
            notEmpty.signal();
        }finally{
            lock.unlock();
        }
    }

    public T take() throws InterruptedException{
        lock.lock();
        try{
            while(count == 0){
                notEmpty.await();
            }
            @SuppressWarnings("unchecked")
            T item = (T)items[takePtr ++];
            if(takePtr == items.length)
                takePtr = 0;
            count--;
            notFull.signal();
            return item;
        }finally{
            lock.unlock();
        }
    }
}
生产者与消费者之队列
package com.xqq.生产者与消费者BlockingQueue实现;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

import com.xqq.生产者与消费者之wait和notify实现.Item;

public class Producer implements Runnable{
    private BlockingQueue<Item> sharedQueue;

    public Producer(BlockingQueue<Item> sharedQueue) {
        this.sharedQueue = sharedQueue;
    }
    public void run() {
        for(; ;){
            try {
                Item item = new Item();
                System.out.println("Produce " + item);
                sharedQueue.put(item);
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (Exception e) {
                return ;
            }
        }
    }
}

package com.xqq.生产者与消费者BlockingQueue实现;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

import com.xqq.生产者与消费者之wait和notify实现.Item;

public class Consumer implements Runnable {
    private BlockingQueue<Item> sharedQueue;
    public Consumer(BlockingQueue<Item> sharedQueue) {
        this.sharedQueue = sharedQueue;
    }
    public void run() {
        for (;;) {
            try {
                System.out.println("Consumer " + sharedQueue.take());
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (Exception e) {
                return;
            }
        }
    }
}
package com.xqq.生产者与消费者BlockingQueue实现;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

import com.xqq.生产者与消费者之wait和notify实现.Item;

public class ProducerConsumerPattern {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();
        BlockingQueue<Item> sharedQueue = new LinkedBlockingQueue<Item>();
        exec.execute(new Producer(sharedQueue));
        exec.execute(new Consumer(sharedQueue));
        exec.execute(new Producer(sharedQueue));
        exec.execute(new Consumer(sharedQueue));
        TimeUnit.SECONDS.sleep(3);
        exec.shutdownNow();
    }
}
运行结果:
Produce Item 0
Produce Item 1
Consumer Item 0
Consumer Item 1
Produce Item 3
Produce Item 2
Consumer Item 3
Consumer Item 2
Produce Item 4
Produce Item 5
Consumer Item 5
Consumer Item 4
Produce Item 7
Consumer Item 7
Produce Item 6
Consumer Item 6
Produce Item 8
Produce Item 9
Consumer Item 8
Consumer Item 9

6. 死锁

某个任务在等待另一个任务,而后者有等待别的任务,这样一直下去,直到这个链条上的任务又在等待第一个任务释放锁。这得到了一个任务之间相互等待的连续循环,没有那一个线程能继续。这称之为死锁。

死锁的四个必要条件

  1. 互斥条件:线程对所分配到的资源进行排他性使用,即在一段时间内,某资源只能被一个线程占用。如果此时还有其它线程请求该资源,则只能等待,直到占有该资源的线程用毕释放。
  2. 请求和保持条件:已经保持了至少一个资源,但是又提出新的资源请求,而资源已被其他线程占有,此时请求线程只能等待,但对自己已获得的资源保持不放。
  3. 不可抢占条件:线程已获得的资源在未使用完之前不能被抢占,只能线程使用完之后自己释放。
  4. 循环等待条件:在发生死锁时,一个任务等待其他任务所持有的资源,后者又在等待另一个任务所持有的资源,这样一直下去,直到有一个任务所持有的资源,使得大家被锁住。

哲学家就餐问题

解决哲学家就餐死锁的办法就是破破坏循环等待条件:前面的哲学家先拿右边的再拿左边的,而最后一个哲学家先拿左边的在拿右边的。

7. 新类库中的构件

1. CountDownLatch

用来同步一个或多个任务,强制他们等待由其他任务执行的一组操作完成。
可以向CountDownLatch对象设置一个初始计数值,任何在这个对象上调用await()方法都将阻塞,直至这个计数值到达0。其他任务在结束其工作时,可以调用countDown()来减小这个计数值。CountDownLatch被设计为只触发一次,计数值不能重置,如果需要能够重置计数值的版本,则可以使用CyclicBarrier。示例代码:

package com.xqq.新类库中的构件之CountDownLatch;

import java.util.concurrent.CountDownLatch;

public class WaitingTask implements Runnable{
    private CountDownLatch latch;
    private static int counter = 0;
    private final int id = counter++;

    public WaitingTask(CountDownLatch latch) {
        this.latch = latch;
    }
    public void run() {
        try {
            latch.await();
            System.out.println("Latch barrier passed for " + this);
        } catch (Exception e) {
            return ;
        }
    }
    @Override
    public String toString() {
        return String.format("%1$-3d", id);
    }
}
package com.xqq.新类库中的构件之CountDownLatch;

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class TaskPortion implements Runnable{
    private CountDownLatch latch;
    private static int counter = 0;
    private final int id = counter++;
    private Random rand = new Random(47);

    public TaskPortion(CountDownLatch latch) {
        this.latch = latch;
    }
    public void run() {
        try {
            doWork();
            latch.countDown();
        } catch (Exception e) {
            return ;
        }
    }
    private void doWork() throws InterruptedException {
        TimeUnit.MICROSECONDS.sleep(rand.nextInt(2000));
        System.out.println(this + " completed");
    }
    @Override
    public String toString() {
        return String.format("%1$-3d", id);
    }
}
package com.xqq.新类库中的构件之CountDownLatch;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * CountDownLatch:同步一个或者多个任务,将一堆任务分成两类,一类必须在另一类之前完成
 * @author xqq
 */
public class CountDownLatchDemo {
    private static final int SIZE = 10;
    public static void main(String[] args) throws InterruptedException {
        //只触发一次,计数值不能被重置
        CountDownLatch latch = new CountDownLatch(SIZE);
        ExecutorService exec = Executors.newCachedThreadPool();
        for(int i = 0 ; i < 10; i++){
            exec.execute(new WaitingTask(latch));
        }
        for(int i = 0 ; i < SIZE; i++){
            exec.execute(new TaskPortion(latch));
        }
        TimeUnit.SECONDS.sleep(1);
        System.out.println("All Tasks Lauched!");
        exec.shutdown();
    }
}
运行结果:
0   completed
8   completed
7   completed
5   completed
4   completed
9   completed
6   completed
1   completed
2   completed
3   completed
Latch barrier passed for 0  
Latch barrier passed for 1  
Latch barrier passed for 2  
Latch barrier passed for 3  
Latch barrier passed for 4  
Latch barrier passed for 5  
Latch barrier passed for 6  
Latch barrier passed for 7  
Latch barrier passed for 8  
Latch barrier passed for 9  
All Tasks Lauched!

2. CyclicBarrier

适用于这样的情况:你希望创建一组任务,他们并行的工作,然后在进行下一个步骤之前等待,直至所有的任务都完成。它使得所有的并行任务都将在栅栏处列队,因此可以一致的向前移动。赛马游戏代码:

package com.xqq.新类库中的构件之CyclicBarrier;

import java.util.Random;
import java.util.concurrent.CyclicBarrier;

public class Horse implements Runnable {
    private static int counter = 0;
    private final int id = counter++;
    private static Random rand = new Random(47);
    private CyclicBarrier barrier;
    private int strides = 0;

    public Horse(CyclicBarrier barrier) {
        this.barrier = barrier;
    }

    public int getStrides() {
        return strides;
    }

    public void run() {
        try {
            while (!Thread.interrupted()) {
                synchronized (this) {
                    strides += rand.nextInt(3);
                }
                barrier.await();
            }
        } catch (Exception e) {
            return;
        }
    }

    @Override
    public String toString() {
        return "Horse " + id + " ";
    }

    public String tracks() {
        StringBuilder s = new StringBuilder();
        for (int i = 0; i < getStrides(); i++) {
            s.append("*");
        }
        s.append(id);
        return s.toString();
    }
}
package com.xqq.新类库中的构件之CyclicBarrier;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
 * CyclicBarrier适用于这样的情况:你希望创建一组任务,他们并行工作,然后再进行下一个步骤之前等待,直至所有任务都完成
 * 它使得所有任务都在栅栏处列队,因此可以一致的向前移动。
 * @author xqq
 */
public class HorseRace {
    private static final int FINISH_LINE = 75;
    private List<Horse> horses = new ArrayList<Horse>();
    private ExecutorService exec = Executors.newCachedThreadPool();
    private CyclicBarrier barrier;

    public HorseRace(int nHorse, final int pause){
        //当barrier.await()的总数量达到nHorses时, 就会自动触发该任务,并且计数值又重置为mHorse
        barrier = new CyclicBarrier(nHorse, new Runnable() {

            public void run() {
                StringBuilder s = new StringBuilder();
                for(int i = 0; i < FINISH_LINE; i++)
                    s.append("=");
                System.out.println(s);
                for(Horse horse: horses)
                    System.out.println(horse.tracks());
                for(Horse horse: horses){
                    if(horse.getStrides() >= FINISH_LINE){
                        System.out.println(horse + "won !");
                        exec.shutdownNow();
                        return ;
                    }
                }
                try {
                    TimeUnit.MILLISECONDS.sleep(pause);
                } catch (Exception e) {
                }
            }
        });
        for(int i = 0; i < nHorse; i++){
            Horse horse = new Horse(barrier);
            horses.add(horse);
            exec.execute(horse);
        }
    }

    public static void main(String[] args) {
        int mHorse = 7;
        int pause = 200;
        new HorseRace(mHorse, pause);
    }
}

3. DelayQueue

这是一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,

4. PriorityBlockingQueue

5. ScheduledExecutor

每个期望的时间都是一个在预定时间运行的任务。ShceduledThreadPoolExecutor提供了解决该问题的服务。通过使用schedule()(运行一次任务),或者scheduleAtFixedRate()(每隔规则的时间重复执行任务),可以将Runnable对象设置为在将来的某个时刻执行。

6. Semaphore

正常的锁在任何时刻都只允许一个人物访问一项资源,而计数信号量允许n个任务同时访问这个资源。需要的使用semaphore.acquire()方法申请,用完之后使用semaphore.release()释放。

7. Exchanger
是两个任务之间交换对象的栅栏。每个人在完成一定的事情之后想与对方交换数据,第一个拿出数据的人将一直等待第二个拿着数据到来时,才能彼此交换数据。两个任务通过同一个Exchange对象,分别调用它的exchange(data)方法,将需要交换的数据置于方法中。

8. 性能调优

比较各类互斥技术
  • Atomic类
    • 和上面的类似,不激烈情况下,性能比synchronized略逊,而激烈的时候,也能维持常态。激烈的时候,Atomic的性能会优于ReentrantLock一倍左右。但是其有一个缺点,就是只能同步一个值,一段代码中只能出现一个Atomic的变量,多于一个同步无效。因为他不能在多个Atomic之间同步。
  • 关键字synchronized
    • 在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好,不管用没用过5.0多线程包的程序员都能理解。不能被中断。
  • Lock
    • 在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态。
    • ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。
    • ReentrantLock 拥有Synchronized相同的并发性和内存语义,此外还多了 锁投票,定时锁等候和中断锁等候。可以被中断。
免锁容器

CopyOnWiteArrayList

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

ConcurrenHashMap和ConcurrentLinkedQueue
使用了类似的技术,允许并发的读取和写入,但是容器中只有部分内容而不是整个容器可以被复制和修改。然而,在修改完成之前,读取者仍旧不能看到他们。

ReadWriteLock

对向数据结构相对不频繁的写入,但是有多个任务要经常读取这个数据结构的这类情况进行了优化。ReadWriteLock使得你可以同时有多个读者,只要他们都不试图写入即可。如果写锁已经被其他任务持有,那么任何读者都不能访问,直至这个写锁被释放为止。即适用于读者多于写者的情况。

对于ReadWriteLock的应用主要是:缓存和提高对数据结构的并发性。

  • 锁降级:重入还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不可能的。
  • 锁获取的中断:读取锁和写入锁都支持锁获取期间的中断。
  • Condition 支持 :写入锁提供了一个 Condition 实现,对于写入锁来说,该实现的行为与 ReentrantLock.newCondition() 提供的 Condition 实现对 ReentrantLock 所做的行为相同。当然,此 Condition 只能用于写入锁。
  • 读取锁不支持 Condition,readLock().newCondition() 会抛出 UnsupportedOperationException。
  • 重入:此锁允许 reader 和 writer 按照 ReentrantLock 的样式重新获取读取锁或写入锁。在写入线程保持的所有写入锁都已经释放后,才允许重入 reader 使用它们。

下面的代码展示了如何利用重入来执行升级缓存后的锁降级:

class CachedData {
    Object data;
    volatile boolean cacheValid;
    ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    void processCachedData() {
        rwl.readLock().lock();
        if (!cacheValid) {
        // Must release read lock before acquiring write lock
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            // Recheck state because another thread might have acquired
            //   write lock and changed state before we did.
            if (!cacheValid) {
                data = ...
                cacheValid = true;
            }
            // Downgrade by acquiring read lock before releasing write lock
            rwl.readLock().lock();
            rwl.writeLock().unlock(); // Unlock write, still hold read
        }

        use(data);
        rwl.readLock().unlock();
    }
}

在使用某些种类的 Collection 时,可以使用 ReentrantReadWriteLock 来提高并发性。通常,在预期 collection 很大,读取者线程访问它的次数多于写入者线程,并且 entail 操作的开销高于同步开销时,这很值得一试。例如,以下是一个使用 TreeMap 的类,预期它很大,并且能被同时访问。

class RWDictionary {
    private final Map<String, Data> m = new TreeMap<String, Data>();
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock r = rwl.readLock();
    private final Lock w = rwl.writeLock();

    public Data get(String key) {
        r.lock();
        try { return m.get(key); }
        finally { r.unlock(); }
    }
    public String[] allKeys() {
        r.lock();
        try { return m.keySet().toArray(); }
        finally { r.unlock(); }
    }
    public Data put(String key, Data value) {
        w.lock();
        try { return m.put(key, value); }
        finally { w.unlock(); }
    }
    public void clear() {
        w.lock();
        try { m.clear(); }
        finally { w.unlock(); }
    }
 }
  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值