核心技术(卷一)08、第14章-多线程

本文详细介绍了线程与进程的概念,讲解了如何在Java中创建和管理线程,包括线程的生命周期、优先级、中断处理、同步机制、锁与条件锁的使用,以及高级主题如守护线程、未捕获异常处理器等。同时,探讨了线程安全的集合、阻塞队列、Callable与Future、执行器、fork-join框架等并发编程技术。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

多线程

  1. 线程与进程

    操作系统为进程分配系统资源,进程中的线程共享这些资源。线程是系统分配时间片的接受者。

  2. 创建线程

    如果需要执行一个耗时的任务,就应该使用独立的线程。下面是一个单独的线程执行一个简单任务的过程:

    • 将任务代码移到实现了Runnable接口的类的run方法中。
    //该接口只有一个run方法
    public interface Runnable{
      void run();
    }
    

    实现一个类:

    class MyRunnable implements Runnable{
      public void run(){
        do something...;
      }
    }
    
    • 创建一个类对象
    Runnable r = new MyRunnable();
    
    • Runnable创建一个Thread对象
    Thread t - new Thread(r);
    
    • 启动线程
    t.start();
    

    也可以构建一个Thread的子类定义一个线程:

    class MyThread extends Thread{
      public void run(){
        do something...;
      }
    }
    

    然而,并不推荐这样的实现。如果任务很多的话,这样做将会创建非常多的并行线程,创建线程的代价是很大的。使用实现Runnable接口的方式,我们就可以使用线程池,减少不必要的线程数量。

    不要调用Thread类或者是Runnable类的run方法。调用run方法,只会执行同一线程中的任务,而不会启动新线程

  3. 中断线程

    调用Thread对象的interrupt方法, 可以用来请求终止线程。

    package com.heisenberg.test;
    import java.util.logging.Logger;
    
    public class Chapter14{
      public static void main(String[] args){
        Runnable r1 = new MyRunnable("thread1");
        Thread t1 = new Thread(r1);
        Runnable r2 = new MyRunnable("thread2");
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
        try{
          Thread.sleep(100);
          t1.interrupt();
          Thread.sleep(500);
          t2.interrupt();
        }catch(InterruptedException e){
    
        }
      }
    }
    
    class MyRunnable implements Runnable{
      public static final Logger logger = Logger.getLogger("com.heisenberg.test");
      private String name;
    
      public MyRunnable(String name){
          this.name = name;
      }
      public void run(){
        int i = 0;
        try{
          while(!Thread.currentThread().isInterrupted()){
            i++;
            /*
             *在每次工作迭代之后调用sleep方法,isInterrupted检测将没有作用
             *因为当中断位被置位后,调用slepp方法线程不会休眠还会清楚该状态且抛出InterruptedException,
             *因此,在代码什么位置捕获该异常就非常重要了
             *在循环内捕获,线程将不会中断
             *在循环外铺货,线程会中断
             */
            Thread.sleep(200);
            if(Thread.currentThread().isInterrupted()){
              logger.info("isInterrupted is Ture");
            }
          }
        }catch(InterruptedException e){
          logger.severe("InterruptedException happend");
          if(!Thread.currentThread().isInterrupted()){
            logger.info("isInterrupted is False");
          }
        }
        logger.info("Thread:" + name + " runs "+ i + " times");
      }
    }
    

    通过Threadinterrupt方法申请中断线程,在线程的run方法内通过isInterrupted检测中断位,如此配合来起到中断线程的作用。

  4. 线程状态

    线程可以6种状态:

    • 新创建(New)
    • 可运行(Runnable)
    • 被阻塞(Blocked)
    • 等待(Waiting)
    • 计时等待(Timed waiting)
    • 被终止(Terminated)
    Runnable r = new MyRunnable();
    Thread t = new Thread(r);//新创建状态
    
    t.start();//可运行状态
    //可运行状态的线程可能正在运行,也可能没有运行,这取决于是否获得时间片
    
    //若此是线程t试图获取一个内部的对象锁,而该锁被其它线程持有,该线程进入阻塞状态。
    
    //若此时线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。
    
    t.sleep(100);//线程t现在处于计时等待状态。还有几个类似sleep的方法。
    
    //当t线程的run方法正常退出而自然死亡,或因一个没有捕获异常终止了run的而意外死亡,此时t处于被终止状态。
    
  5. 线程优先级

    Java程序设计语言中,每个线程都有一个优先级,子线程默认继承父线程的优先级。优先级从Thread.MIN_PRIORITY(1)到Thread.MXA_PRIORITY(10)之间的任意值。使用setPriority()为线程设置优先级。

  6. 守护线程

    守护线程的唯一作用就是为用户线程提供服务,例如计时线程或清空过时缓存的线程。使用setDaemon(true)方法,将线程设置为守护线程。这个语句必须在start方法被调用之前执行。

  7. 未捕获异常处理器

    线程的run方法不能抛出任何的已检查异常。未检测异常会导致线程终止,但是在线程死亡之前,异常被传递到一个用于未捕获异常的处理器。该处理器实现Thread.UncaughtExceptionHandler接口的类,该接口只有一个方法:

    void uncaughtException(Thread t, Throwable e)
    

    可以用setUncaughtExceptionHandler方法为任何线程安装一个处理器:

    Thread t = new Thread(r);
    t.setUncaughtExceptionHandler(new UncaughtExceptionHandler(){
      public void uncaughtException(Thread t, Throwable e){
        ...;
      }
    });
    t.start();
    

    可以使用静态方法Thread.setDefaultUnCaughtExceptionHandler()为所有的线程设置一个默认的处理器。如果安装默认的处理器,默认的处理器为空。如果不为独立的线程安装处理器,那么这个线程的处理器就是该线程的ThreadGroup对象。ThreadGroup类是想了Thread.UncauthghtException接口。它的uncaughtException方法处理流程如下:

    • 如果该线程组有父线程组,调用父线程组的uncaughtException;
    • 否则,如果Thread.getDefualtUncaughtExcetionHandler方法返回一个非空处理器,则调用该该处理器的uncaughtException
    • 否则,如果ThrowableThreadDeath的实例(当调用线程的stop方法时,线程终止,且抛出ThreadDeath异常),什么都不做;
    • 否则,线程的名字以及Throwable的栈踪迹被输出到System.err上。
  8. 同步-锁对象

    ReentrantLock类为临界区加锁与释放锁。

    Lock lock = new ReentrantLock();
    lock.lock();
    try{
       //临界区
       ...
     }finally{
       lock.unlock();
     }
    
    
  9. 同步-条件锁

    锁对象实现临界资源的互斥访问,条件锁实现临界资源的同步访问。
    使用Condition类为一个条件添加锁。

    • 当一个线程不满足某个条件时,调用条件锁对象的await方法,使得该线程放弃资源锁,并进入该条件的等待集,进入阻塞状态。
    • 当另一个线程进入临界资源,执行了一些可能会导致其它线程满足条件的操作后,调用该条件锁对象的signalAll方法,通知所有在该条件等待集中的线程,使得这些线程进入可运行状态。
    • 其中一个线程获得时间片后,从上次阻塞的地方重新执行,再次检查是否满足条件,满足便执行下去,不满足便再次进入等待集。

    也可以调用signal方法,随机通知一个等待集中的线程进入可运行状态,但是这样做风险很高,更容易造成死锁。

    Lock lock = new ReentrantLock();
    Condition condition = new Condition();
    lock.lock();
    try{
      if (!(ok to proceed)){
        condition.await();
      }
       //临界区
       ...
       condition.signalAll();
     }finally{
       lock.unlock();
     }
    
  10. 内部方法锁

Java的每个对象都有一个内部锁,使用synchronized关键字来声明一个方法,那么该对象的内部锁将保护这个方法。一个内有多个方法被synchronized声明,那么并行调用同一个对象的多个方法时,不同的方法之间竞争同一个内部锁。

public synchronized void method(){
  method body;
}

内部锁只有一个相关条件。wait方法将线程添加到等待集,notifyAll或者notify方法解除等待线程的阻塞状态。

public synchronized void method(){
  method body
  if(!(ok to proceed)){
    wait();
  }
  method body
  notifyAll();
}

Lock/Condition对象还是同步方法(内部锁)?

  • 最好两者都不使用,在通常情况下,最好使用java.util.concurrent包中的机制,它会处理所有的加锁。
  • 如果synchronized关键字适合你的程序,大胆的使用。
  • 如果特别需要Lock/Condition结构提供的独有特性时,才使用它。
  1. 同步阻塞
synchronized(obj){ //获得了obj对象的锁
  //对obj对象的操作
}

或:

Object lock1 = new Object();
Object lock2 = new Object();

synchronized(lock1){//获得第一个锁
  critical section
}
...
synchronized(lock2){//获得第二个锁
  critical section
}

上面的Object对象仅仅是用来充当代码块的锁。

我们可以使用一个对象的锁来实现原子操作:

//下面两行代码应该是一个原子操作,但是现在不是
from -= amount;
to += amount;

//使用一个对象锁,使该代码块成为一个原子操作
Object lock = new Object();
synchronized(lock){
  from -= amount;
  to += amount;
}
  1. Volatile域

    Volatile关键字为实例域的同步访问提供了一种免锁机制。

    private volatile boolean done;
    public boolean isDone(){return done}
    public void setDone(){done=true}
    

    Volaatile域不能提供原子性。

  2. 线程局部变量

    使用ThreadLocal辅助类为各个线程提供各自的实例,来避免共享变量。

    //为每个线程都实例化一个随机数生成器
    public static final ThreadLocal<Random> random = new ThreadLocal<Random>(){
      protected Random initialValue(){
        return new Random();
      }
    };
    
    //第一次调用get时,会调用initialValue方法,以后将返回特定于该线程的Radom实例
    random.get().nextInt();
    
  3. 锁测试与超时

    线程在调用lock方法来获得另外一个线程所持有的锁是,很可能发生阻塞。应该更加谨慎的申请锁。tryLock方法试图申请一个锁,在成功获得锁后返回true,否则立即返回false,而线程可以立即离开去做其它事情:

    if (myLock.tryLock()){
      //申请到了锁
    }else{
      //没有申请到锁
    }
    
在调用`tryLock`方法时,可以使用超时参数:
```java
/*
 *尝试获得锁,阻塞不会超过给定的时间
 *TimeUnit是一个枚举类型,可以取的值包括:SECONDS、MILLISECONDS、MICROSECONDS和NANOSECONDS
 */
if (myLock.lock(100,TimeUnit.MILLISECONDS)){
  ...
}else{
  ...
}
  1. 读/写锁

    我们可以使用ReentrantLock为任何的临界区加锁,现在,我们可以使用ReentrantReadWriteLock为读数据临界区加读锁,为写数据临界区加写锁

    //构造一个ReentrantReadWriteLock对象
    ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    //抽取读锁和写锁
    private Lock readLock = rwl.readLock();
    private Lock writeLock = rwl.writeLock();
    //对所有的读去方法加读锁
    public double getTotalBalance(){
      readLock.lock();
      try{
        ...
      }finally{
        readLock.unlock();
      }
    }
    //对所有的写方法加写锁
    public void transfer(){
      writeLock.lock();
      try{
        ...
      }finally{
        writeLock.unlock();
      }
    }
    
  2. 阻塞队列

    生产这线程向队列插入数据,消费者线程向队列取数据。使用队列,可以安全地从一个线程向另一个线程传递数据。

    阻塞队列方法:


    5893832-f941d530026db1cc.png
    阻塞队列分类
    方法正常动作特殊情况下动作
    add添加一个元素如果队列满,则抛出IllegalStateException
    element返回队列的头元素如果队列空,抛出NoSuchElementException
    offer添加一个元素并返回true 如果队列满,则返回false
    peek返回队列的头元素如果队列空,则返回null
    poll 移除 并返回队列头元素如果队列空,则返回null
    put添加一个元素如果队列满,则阻塞
    remove 移除 并返回头元素如果队列空,则抛出NoSuchElementException
    take 移除 并返回头元素如果队列空,则阻塞

    如果要将阻塞队列作为线程管理工具来使用,则要使用putteak方法。

  3. 线程安全的集合
    java.util.concurrent包提供了一下线程安全的集合类

    5893832-ffa889e9e18d7dda.png
    线程安全的集合类

    具体使用参考java.util.concurrent包的AIP

  4. CallableFuture

    Runable封装了一个异步方法,可以把它想成是没有返回参数的异步方法。CallableRunable类似,但是又返回值。Callable接口是一个参数化的类型,只有一个方法call

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

    Future保存异步计算的结果。可以启动一个计算,将Future对象交给某个线程。Future接口有一下方法:

    public interface Future<V>{
      //阻塞,直到计算完成;计算过程中被中断,抛出InterruptedException
      V get() throws ...;
      //调用超时,抛出TimeoutException异常;计算过程中被中断,抛出InterruptedException
      V get(long timeout, TimeUnit unit) throws ...;
      //取消计算
      void cancle(boolean mayInterrrupt);
    
      boolean isCancelled();
      //获取计算执行状态
      boolean isDone();
    }
    

    FutureTask包装器是一个非常便利的机制,它将Callable转换为FutureRunable,因为它实现了这两个接口。

    Callable<Integer> call = new myCallable();
    FuterTask<Integer> task = new FuterTask(call);
    Thread t = new Thread(task); //此时task是一个Callable
    t.start();
    Integer result = t.get();//此时task是一个Future
    

    通过上面的方式来使用FuterCallable,以获取异步计算结果。

  5. 执行器

    如果程序中创建了大量生命周期很短的线程,就应该使用线程池。使用线程池可以减少并发线程的数目。

    执行器(Executor) 类有许多静态工厂方法用来创建线程池。下面是对这些方法的汇总:

    方法描述
    newCachedThreadPool必要时创建新线程,空闲线程保留60秒
    newFixedThreadPool该池保留固定数量的线程,空闲线程会被一直保留
    newSingleThreadExecutor只有一个线程的“池”,该线程顺序执行提交的任务
    newScheduledThreadPool用于预定执行而构建的固定线程池,替代java.util.Timer
    newSingleThreadScheduledExecutor用于预定执行而构架的线程“池”

    newCachedThreadPoolnewFixedThreadPoolnewSingleThreadExecutor三个静态方法都会返回一个实现了ExecutorService接口的ThreadPoolExecutor,ExecutorService接口主要有一下几个方法:

    public interface ExecutorService extends Executor{
      //提交一个Callable对象,返回的Future对象将在计算结果准备好时得到
      <T> Future<T> submit(Callable<T> task);
      //提交一个Runnable对象,并且Future的get方法在完成时返回给result对象
      <T> Future<T> submit(Runnable task, T result);
      //返回一个Futre<?>对象,可以使用这个对象来调用isDone,cancel,isCancelled方法,但是get方法只会在完成时返回一个null
      Future<?> submit(Runnable task);
      //该方法启动该池的关闭序列,被关闭的执行器接收新的任务;当所有任务都完成后,所有线程死亡
      void  shutdown();
    }
    

    预订执行:

    newScheduledThreadPoolnewSingleThreadScheduledExecutor方法返回实现了ScheduledExecutorService接口的对象。ScheduledExecutorService接口具有为预定执行(Scheduled Execution)或重复执行任务而设计的方法。

    public interface ScheduledExecutorService extends ExecutorService{
      //预定在指定时间后执行指定任务
      <V> ScheduledFuture<V>    schedule(Callable<V> callable, long delay, TimeUnit unit);
      //预定在指定时间后执行指定任务
      ScheduledFuture<?>    schedule(Runnable command, long delay, TimeUnit unit);
      //预定在初始的延迟结束后,周期性的运行指定的任务,周期长队为period
      ScheduledFuture<?>    scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit);
      //预定在初始延迟结束后周期性的执行指定任务,在一次调用完成后和下一次调用开始之间有长度为delay的延迟
      ScheduledFuture<?>    scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit);
    }
    
  6. fork-join框架

    • 提供扩展了RecursiveTask<T>(有计算结果)或者RecursiceAction(无计算结果)的类。
    • 覆盖comput方法来生成并调用子任务
    • 将结果合并
    public class ForkJoinTest(){
      public void main(String[] args){
        final int SIZE = 10000000;
        double[] numbers = new double[SIZE];
        for(int i = 0; i < SIZE; i++){
          numbers[i] = Math.random();
        }
        Counter counter = new Counter(0,SIZE,numbers);
        ForkJoinPool pool = new ForkJoinPool();
        pool.invoke(counter);
      }
    }
    
    class Counter extends RecursiveTask<Integer>{
      private int from,to;
      private double[] values;
      public static final int THRESHOLD = 1000;
      public Counter(int from,int to,double[] values){
        this.from = from;
        this.to = to;
        this.values = values;
      }
      protected Integer comput(){
        if (to - from < THRESHOLD){
          int count = 0;
          for (int i = from; i < to; i++){
            if (values[i] > 0.5){
              count++;
            }
          }
          return count;
        }else{
          int mid = (to+from)/2;
          Counter counter1 = new Counter(from,mid,values);
          Counter counter2 = new Counter(mid,to,values);
          invokeAll(counter1,counter2);
          return counter1.join() + counter2.join();
        }
      }
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值