多线程

1.中断线程
当对一个线程调用interrupt方法时,线程的中断状态被置位。
判断线程是否被中断:
Thread.currentThread().isInterrupted();//Thread.currentThread():获得当前线程
如果线程被阻塞,就无法检测中断状态。
在中断状态被置位时调用sleep方法,它不会休眠,将会清除这一状态并抛出InterruptedException。
2.线程状态
六种状态:
New(新生),Runnable(可运行),Blocked(被阻塞),Waiting(等待),
Timed waiting(计时等待),Terminated(被终止)
2.1新生线程
new操作符创建一个新线程时,线程还没有开始运行。
2.2可运行线程
一旦调用start方法,线程处于Runnable状态。一个可运行的线程可能正在运行也可能没有运行。
抢占式调度系统给每一个可运行线程一个时间片来执行任务。时间片用完,选择下一个线程时,操作系统考虑线程的优先级。
具有多个处理器的机器上,每一个处理器运行一个线程,可有多个线程并行运行。
2.3被阻塞线程和等待线程
当一个线程试图获取一个内部的对象锁(不是java.util.concurrent库中的锁),而该锁被其他线程持有,则该线程进入阻塞状态。
当线程等待另一个线程通知调度器一个条件时,他自己进入等待状态。
有几个带有超时参数的方法,调用它们导致线程进入计时等待状态。
2.4被终止的线程
(1)因run方法正常退出而自然死亡;
(2)因一个没有捕获的异常终止了run方法而意外死亡。
3.线程属性
包括:线程优先级、守护线程、线程组及处理未捕获异常的处理器。
3.1线程优先级
默认一个线程继承它的父线程的优先级。可用setPriority方法设置优先级:MIN_PRIORITY(Thread类中定义为1)与MAX_PRIORITY(定义为10)间值,NORM_PRIORITY定义为5.
线程优先级高度依赖于系统,优先级个数会变。
Windows有7个优先级别;Sun为Linux提供的Java虚拟机,所有线程具有相同优先级。
static void yield():导致当前执行线程处于让步状态。如果有其他具有至少与此线程同样高优先级的可运行线程,那么这些线程接下来会被调度。
3.2守护线程
将线程转换为守护线程:调用t.SetDaemon(true);//必须在线程启动之前调用只剩下守护线程时,虚拟机就退出了。
3.3未捕获异常处理器
线程的run方法不能抛出任何被检测的异常,但不被检测的异常会导致线程终止。
在线程死亡前,异常被传递到一个用于未捕获异常的处理器。该处理器必须属于一个实现Thread.UncaughtExceptionHandler接口的类。该接口只有一个方法:
void UncaughtException(Thread t,Throwable e
SE 5.0开始可用setUncaughtExceptionHandler方法为任何线程安装一个处理器;也可用Thread类的静态方法setDefaultUncaughtExceptionHandler为所有线程安装一个默认的处理器。
若默认处理器为空,如果不为独立的线程安装处理器,此时的处理器就是该线程的ThreadGroup对象。
4.同步
4.1详解竞争条件
4.2锁对象
(1)显示锁:synchronized关键字自动提供一个锁及相关的“条件”。
(2)SE 5.0引入了ReentrantLock类。

   //ReentrantLock保护代码块的基本结构:
   mylock.lock();
   try{
     ...
   }
   finally{
      mylock.unlock();//确保释放锁
   }
   !!这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。
     当其他线程调用lock时,他们被阻塞,直到第一个线程释放锁对象。

“如果两个线程访问不同的Bank对象,每一个线程得到不同的锁对象,两个线程都不会发生阻塞(线程在操作不同Bank实例时,线程之间不会相互影响)”。
锁是可重入的,线程可以重复获得已经持有的锁。锁保持一个持有计数来跟踪对lock方法的嵌套调用。线程在每次调用lock都要调用unlock来释放锁。由此,被一个锁保护的代码可以调用另一个使用相同锁的方法。
“transfer方法调用getTotalBalance方法,也会封锁bankLock对象,此时bankLock对象的持有计数为2.当getTotalBalance方法退出时,持有计数变为1.当transfer方法退出时,持有计数变为0.线程释放锁。”
4.3条件对象
一个锁对象可以有一个或多个相关的条件对象。可用newCondition方法获得一个条件对象:

   private Condition sufficientFunds = bankLock.newCondition();
   //条件不满足时,阻塞线程并释放锁
   sufficientFunds.await();
   //当锁可用时,该线程不能马上解除阻塞,直到另一个线程调用同一条件上的signalAll方法时为止。
   //另一个线程转账时调用:
   sufficientFunds.signalAll();//激活因这一条件而等待的所有线程。
   //一旦某个等待的线程的锁成为可用的,该线程将从await调用返回,获得该锁,并从被阻塞的地方继续执行。
   !!调用signalAll()并不会立即激活一个等待线程。它仅仅解除等待线程的阻塞,以便这些线程可以在当前线程退出
   同步方法后,通过竞争实现对对象的访问。
   !!另一个方法signal可以随机解除等待集中某个线程的阻塞状态。它更有效,但危险:随机选择的线程发现自己仍然
   不能运行,再次被阻塞(没有其他线程再次调用signal,系统就死锁了)。

死锁:如果没有其他线程(调用signalAll)激活等待(调用await)的线程,它就不能再运行了。
如果没有任何线程可以解除其他线程的阻塞,那么该程序就被挂起了。
4.4synchronized关键字
1.0版开始,Java中每个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法(要调用该方法,线程必须获得内部的对象锁)。
内部对象锁只有一个相关条件。wait方法添加一个线程到等待集中,notifyAll/notify方法解除等待线程的阻塞状态。
!!wait、notifyAll/notify方法是Object类的final方法。
将静态方法声明为synchronized也是合法的。如果调用这种方法,该方法获得相关的类对象的内部锁。
内部锁和条件存在一些局限:
(1)不能中断一个正在试图获得锁的线程;
(2)试图获得锁时不能设定超时;
(3)每个锁仅有单一的条件,可能是不够的。
4.5同步阻塞
每个Java对象有一个锁,线程可通过调用同步方法获得锁,还有另一种机制可以获得锁:通过进入一个阻塞队列。
客户端锁定:使用一个对象的锁来实现额外的原子操作。

  //错误,
  public void transfer(Vector<Double> accounts,int from,int to,int amount){
      accounts.set(from,accounts.get(from)-amount);
      accounts.set(to,accounts.get(to)+amount);
  }
  //Vector类的get和set方法是同步的。但在第一次对get的调用完成之后,一个线程有可能在transfer方法中被
  剥夺运行权,于是另一个线程可在相同存储位置存入不同值。
  //但是,我们可以截获这个锁,下面是正确的
  public void transfer(Vector<Double> accounts,int from,int to,int amount){
      synchronized(accounts){
          accounts.set(from,accounts.get(from)-amount);
          accounts.set(to,accounts.get(to)+amount);
      }
  }

4.6监视器
不需要考虑如何加锁时,就可保证多线程的安全性,最成功的解决方案之一是监视器。
监视器特性:
(1)监视器是只包含私有域的类;
(2)每个监视器类的对象有一个相关的锁;
(3)使用该锁对所有的方法进行加锁。
(4)该锁可以有任意多个相关条件。
如果一个方法用synchronized关键字声明,那么他表现的就像是一个监视器方法,通过调用wait、notifyAll/notify方法来访问条件变量。但在下述3个方面Java对象不同于监视器,使线程的安全性下降:
(1)域不要求是private;
(2)方法不要求必须是synchronized;
(3)内部锁对客户可用。
4.7Volatile域
!! Brian Goetz给出的“同步格言”:如果向一个变量写入值,而此变量接下来可能会被另一个线程读取;或者,从一个变量读值,而这个变量可能是之前被另一个线程写入的,此时必须使用同步。
volatile关键字为实例域的同步访问提供了一种免锁机制。若声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。

   //使用内部锁可能会发生阻塞
   private boolean done;
   public synchronized boolean isDone(){return done;}
   public synchronized void setDone(){done=true;}
   //将域声明为volatile是合理的:
   private volatile boolean done;
   public boolean isDone(){return done;}
   public void setDone(){done=true;}
   !!volatile变量不能提供原子性。
   public void flipDone(){done = !done;}//非原子操作,不能确保改变域中的值   
   !!在这种非常简单的情况下,存在第三种可能性,使用AtomicBoolean。这个类有方法get和set,且确保是原子的。

在以下3个条件下,域的并发访问是安全的:
(1)域是final,且在构造器调用完成之后被访问;
(2)对域的访问由公有的锁进行保护;
(3)域是volatile的。
4.8死锁
死锁:某个任务在等待另一个任务,而后者又等待别的任务,这样一直下去,直到这个链条上的任务又在等待第一个任务释放锁。这得到了一个任务之间相互等待的连续循环,没有哪个线程能继续。
4.9锁测试与超时
tryLock方法试图申请一个锁,在成功获得锁后返回true,否则,立即返回false,而且线程可以立即离开去做其他事情。

   if(myLock.tryLock()){  //myLock.tryLock(100,TimeUnit.MILLISECONDS):超时参数
       try{
         ...
       }
       finally{
         myLock.unLock();
       }
   else{
       ...
   }

如果调用带有超时参数的tryLock,那么如果一个线程在等待期间被中断,将抛出InterruptedException异常。这一特性允许程序打破死锁。
4.10读/写锁
java.util.concurrent.locks包定义了两个锁类,ReentrantLock和ReentrantReadWriteLock。
如果很多线程从一个数据结构读取数据而很少线程修改其中数据,ReentrantReadWriteLock类是很有用的。这种情况下,允许对读者线程共享访问是合适的,然而写者线程依然必须是互斥访问的。

   使用读/写锁的必要步骤:
   1)构造一个ReentrantReadWriteLock对象
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
   2)抽取读锁和写锁
    private Lock readLock = rwl.readLock();//可被多个读操作共用的读锁,但会排斥所有写操作
    private Lock writeLock = rwl.writeLock();//排斥所有其他的读操作和写操作
   3)对所有访问者加读锁
    public double getTotalBalance(){
       readLock.lock();
       try{...}
       finally{readLock.unLock();}
    }
   4)对所有修改者加写锁
    public void transfer(){
       writeLock.lock();
       try{...}
       finally{writeLock.unLock();}
    }

5.阻塞队列
当试图向队列添加元素而队列已满,或是想从队列移出元素而队列为空时,阻塞队列导致线程阻塞。
协调多个线程间的合作时,阻塞队列是一个有用的工具。
这里写图片描述
6.线程安全的集合
6.1高效的映像、集合和队列
java.util.concurrent包提供了映像、有序集和队列的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和ConcurrentLinkedQueue。
这些集合通过允许并发的访问数据结构的不同部分来使竞争极小化。
并发的散列映射表,可高效的支持大量的读者和一定数量的写者(默认16个写者线程同时执行)。
6.2写数组的拷贝
CopyOnWriteArrayList和CopyOnWriteArraySet是线程安全的集合,其中所有的修改线程对底层数组进行复制。
7.Callable与Future
Runnable封装一个异步运行的任务,可把他想象成一个没有参数和返回值的异步方法。
Callable与Runnable类似,但是有返回值。Callable是一个参数化的类型,只有一个方法call。

  public interface Callable<V>{
     V call() throws Exception;
  }

Future保存异步计算的结果。

  //Future接口有下面的方法
  public interface Future<V>{
     V get() throws ...;
     V get(long timeout,TimeUnit unit) throws ...;
     void cancel(boolean mayInterrupt);
     boolean isCancelled();
     boolean isDone();
  }

FutureTask包装器是一种非常便利的机制,可将Callable转换成Future和Runnable,同时实现二者的接口。
8.执行器
如果要创建大量的生命期很短的线程,应该使用线程池。线程池还可以减少并发线程的数目。
线程池中包含许多准备运行的空闲线程。将Runnable对象交给线程池,就会有一个线程调用run方法,当run方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。
执行器类(Executor)有许多静态工厂方法用来构建线程池:
这里写图片描述
8.1线程池

  //可以以下方法之一将一个Runnable对象或Callable对象提交给ExecutorService:
  Future<?> submit(Runnable task);//可用来调用isDone、cancel或isCancelled.get方法在完成时仅简单返回null。
  Future<T> submit(Runnable task,T result);//Future的get方法在完成时返回指定的result对象
  Future<T> submit(Callable<T> task);//返回的Future对象将在计算结果准备好时得到他。

shutdownNow:线程池取消尚未开始的所有任务并试图中断正在运行的线程。
shutdown:被关闭的执行器不再接受新的任务,当所有任务都完成后,线程池中的线程死亡。
8.2预定执行
ScheduledExecutorService接口具有为预定执行或重复执行任务而设计的方法。
可预定Runnable或Callable在初始的延迟之后只运行一次,也可预定一个Runnable对象周期性的运行。
8.3控制任务组
invokeAny方法提交所有对象到一个Callable对象的集合中,并返回某个已经完成了的任务的结果。
invokeAll方法提交所有对象到一个Callable对象的集合中,并返回一个Future对象的列表,代表所有任务的解决方案。当计算结果可获得时,可像下面这样对结果进行处理:

  List<Callable<T>> tasks = ...;
  List<Future<T>> results = executor.invokeAll(tasks);
  for(Future<T> result:results){
     processFurther(result.get());
  }
  !!缺点:如果第一个任务恰巧花去了很多时间,则可能不得不进行等待。

常规方法获得一个执行器,然后构建一个ExecutorCompletionService,提交任务给完成服务(completion service)。该服务管理Future对象的阻塞队列,其中包含已经提交的任务的执行结果(当这些结果成为可用时)。
一个更有效的组织形式:

  ExecutorCompletionService service = new ExecutorCompletionService(executor);
  for(Callable<T> task:tasks){
     service.submit(task);
  }
  for(int i=0;i<tasks.size();i++){
     processFurther(service.take().get());
  }

9.同步器
这里写图片描述
9.1信号量
9.2倒计时门栓
9.3障栅
SyclicBarrier类实现了一个集结点称为障栅。
考虑大量线程运行在一次计算的不同部分的情形:当所有部分都准备好时,需把结果组合在一起。当一个线程完成了它的那部分任务后,让他运行到障栅处。一旦所有的线程都到达了这个障栅,障栅就撤销,线程就可以继续运行。
实现过程:

  //构造一个障栅,给出参与的线程数:
  SyclicBarrier barrier = new SyclicBarrier(nthreads);
  //每个线程做一些工作,完成后在障栅上调用await:
  public void run(){
     doWork();
     barrier.await();//barrier.await(100,TimeUnit.MILLISECONDS)
     ...
  }
  !!可提供一个可选的障栅动作,当所有线程到达障栅时就会执行:
  Runnable barrierAction = ...;
  SyclicBarrier barrier = new SyclicBarrier(nthreads,barrierAction);

9.4交换器
9.5同步对列
同步队列是一种将生产者与消费者线程配对的机制。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值