JAVA SE学习笔记(十三)Java并发编程

1 线程的创建和启动

  • 并发:指的是一系列任务同时运行, Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例

1.1 继承Thread类创建线程类

  • 继承Thread类,并覆盖run()方法:
    • 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务;
    • 创建Thread子类的实例,即创建了线程对象;
    • 调用线程对象的start()方法来启动线程。
  • run方法体中,获得当前线程对象的两种方法:
    • 直接使用this获取当前线程
    • 使用Thrad.currentThread()方法来获取当前线程
public class ThreadExample extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
        System.out.println(currentThread().getName());
        System.out.println(this.getName());
        System.out.println(getName());
    }
    public static void main(String[] args) {
        Thread t = new ThreadExample();
        t.start();
    }
}
  • 当程序开始运行后,程序至少会创建一个主线程,main()方法的方法体代表主线程的线程执行体
  • 注意:使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量

1.2 实现Runnable接口创建线程类

  • 实现Runnable接口,并实现run()方法。
    • 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体;
    • 创建Runnable实现类的实例,并以此实例作为Threadtarget来创建Thread对象,该Thread对象才是真正的线程对象;
    • 调用线程对象的start()方法来启动该线程。
public class RunnableThread implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
    public static void main(String[] args) {
        RunnableThread rt = new RunnableThread();
        Thread t = new Thread(rt,"Runnable");
        t.start();
    }
}
  • 注意:Runnable对象仅仅作为Thread对象的targetRunnable实现类里包含的run()方法仅作为线程执行体,而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其targetrun()方法。
  • 采用Runnable接口的方式创建的多个线程可以共享线程类的实例变量

1.3 实现Callable和Future创建线程类

  • Callable接口提供了一个call()方法可以作为线程执行体,且call()方法功能更强大
    • call()方法可以有返回值
    • call()方法可以声明抛出异常
  • Java提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runnable接口——可以作为Thread类的target,在Future接口里定义了几个公共方法用来控制与它关联的Callable任务
    • boolean cancel(boolean mayInterruptIfRunning):试图取消该Future里关联的Callable任务
    • V get():返回Callable任务里call()方法的返回值。调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值
    • V get(long timeout, TimeUnit unit):返回Callable任务里call()方法的返回值。该方法让程序最多阻塞timeout和unit指定的时间,如果经过指定时间后Callable任务依然没有返回值,将会抛出TimeoutException异常
    • boolean isCancelled():如果在Callable任务正常完成前被取消,则返回true
    • boolean isDone():如果Callable任务已完成,则返回true
  • Callable接口里的泛型形参类型与call()方法返回值类型相同,而且CallableRunnable接口是函数式接口,可以使用Lambda表达式创建Callable对象
  • 创建并启动有返回值的线程的步骤:
    • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值,再创建Callable实现类的实例
    • 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;
    • 使用FutureTask对象作为Thread对象的target创建并启动新线程;
    • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
class CallableThread implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        int i = 0;
        for (; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i);
        }
        return i;
    }
}
public class Main {
    public static void main(String[] args) {
        CallableThread ct = new CallableThread();
        FutureTask<Integer> task = new FutureTask<>(ct);
        Thread t = new Thread(task,"Callable");
        t.start();
        try {
            System.out.println(task.get());
        }
        catch (ExecutionException | InterruptedException e){
            System.out.println(e.getMessage());
        }

        // Lambda版本
        FutureTask<Integer> task1 = new FutureTask<>(() -> {
            int i = 0;
            for (; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i);
            }
            return i;
        });
        Thread thread = new Thread(task1,"Lambda");
        thread.start();
        try {
            System.out.println(task1.get());
        }
        catch (ExecutionException | InterruptedException e){
            System.out.println(e.getMessage());
        }
    }
}

1.4 创建线程的三种方式对比

  • 采用实现RunnableCallable接口的方式创建多线程的优缺点:
    • 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类
    • 在这种方式下,多个线程可以共享同一个targrt对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想
    • 劣势是,编程稍稍复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法
  • 采用继承Thread类的方式创建多线程的优缺点:
    • 劣势是,因为线程类已经继承了Thread类,所以不能再继承其他父类
    • 优势是,编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程
  • 注意:一般推荐采用实现Runnable接口、Callable接口的方式来创建多线程

2 线程的生命周期

2.1 新建和就绪状态

  • 当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的对象一样,仅仅由JVM为其分配内存,并初始化其成员变量的值;但是不会被执行
  • 注意:启动线程使用start()方法,如果调用的是run()方法,那么执行的是一个普通方法而不是线程,当调用了线程的run()方法之后,该线程已经不再处于新建状态,不要再次调用线程对象的start()方法,否则将引发IllegalThreadStateException
  • 调用线程对象的start()方法之后,该线程立即进入就绪状态,等待执行,不可两次调用start()方法
  • 如果希望子线程立即执行,只需要将主线程Thread.sleep(1)睡眠就好

2.2 运行和阻塞状态

  • 如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态
  • 当发生如下情况时,线程将进入阻塞状态:
    • 线程调用sleep()方法主动放弃所占用的CPU资源
    • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
    • 线程试图获得一个同步监视器(信号量),但该同步监视器正被其他线程持有
    • 线程在等待某个通知(notify)
    • 程序调用了线程的suspend()方法将该线程挂起
  • 被阻塞的线程在合适的时机(上述情况的反向)重新进入就绪状态,重新等待线程调度器的调度

2.3 线程死亡

  • 线程进入死亡状态的方式:
    • run()call()方法执行完成,线程正常结束
    • 线程抛出一个未捕获的ExceptionError
    • 直接调用该线程的stop()方法来结束该线程(不推荐使用)
  • 当主线程结束时,其它线程不受任何影响,并不会随之结束,一旦子线程启动起来之后,就拥有和主线程相同的地位,不受主线程的影响
  • 不要试图对一个已经死亡的线程调用start()方法使它重新启动,否则将引发IllegalThreadStateException,死亡的线程不可再次作为线程执行。
  • 使用线程对象的isAlive()方法来判断该线程是否死亡

2.4 线程中断

  • Java中提供了中断机制,来结束一个线程,这种机制要求线程检查它是否被中断了,然后决定是否响应这一个中断请求,线程允许忽略中断请求并继续执行
  • 样例代码:
public class Main extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println(getId());
            //第二步:检测是否有中断请求
            if (isInterrupted()) {
                //第三步:决定是否响应中断
                System.out.println("中断");
                return;
            }
        }
    }
    public static void main(String[] args) {
        Thread t = new Main();
        t.start();
        try {
            t.sleep(5000);
        } catch (InterruptedException e) {
            System.out.println(e.getMessage());
        }
        //第一步:发出中断请求
        t.interrupt();
    }
}
  • 检查线程被申请中断的两种方法:
    • Thread类的isInterrupted()实例方法,检测完之后,不改变线程的中断状态
    • Thread类的静态方法interrupted()方法,检测完之后,将清除线程的中断状态标志。
  • 线程中断的控制:通过InterruptedException来实现中断的控制,配合中断检测机制,实现了线程中断的递归解决方案。

2.5 线程休眠和恢复

  • 线程休眠的方法:

    • 使用sleep(long millis)方法,来使当前线程休眠;
    • 使用TimeUnit.SECONDS.sleep(int secs)方法,使用非毫秒单位将当前线程休眠;
    • yield()方法,该方法通知JVM这个线程对象可以释放CPU,但是JVM并不保证遵循这个要求
  • 注意:当调用sleep()方法之后,线程会释放CPU并且不再继续执行任务,在这段时间内,线程不占用CPU时钟,所以CPU可以执行其他的任务;如果休眠中线程被中中断,该方法就会立即抛出InterruptedException异常,而不需要等待到线程休眠时间结束。

3 控制线程

3.1 join线程

  Thread提供了让一个线程等待另一个线程完成的方法——join()方法,当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。join()方法通常由使用线程的程序调用,以将大问题划分成许多小问题,每个小问题分配一个线程,当所有的小问题得到处理后,再调用主线程来进一步操作。

public class JoinThread extends Thread {
    // 提供一个有参数的构造器,用于设置该线程的名字
    public JoinThread(String name) {
        super(name);
    }

    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + "  " + i);
        }
    }

    public static void main(String[] args) throws Exception {
        // 启动子线程1
        new JoinThread("新线程").start();
        for (int i = 0; i < 100; i++) {
            if (i == 20) {
                // main线程调用了jt线程的join()方法,main线程
                // 必须等jt执行结束才会向下执行,但是jt线程和新线程是并发执行的
                JoinThread jt = new JoinThread("被Join的线程");
                jt.start();
                jt.join();
            }
            System.out.println(Thread.currentThread().getName() + "  " + i);
        }
    }
}
  • join()方法有如下三种重载形式:
    • join():等待被join的线程执行完成
    • join(long millis):等待被join的线程的时间最长为millis毫秒,如果在millis毫秒内被join的线程还没有执行结束,则不再等待
    • join(long millis, int nanos):等待被join的线程的时间最长为millis毫秒加nanos豪微秒

3.2 后台线程/守护线程

  守护线程是为了给其他线程提供给服务,可以通过调用t.setDaemon(true)将线程t转换成守护线程,但是,请注意,当只剩下守护线程的时候,虚拟机就退出了。同时守护线程应该永远不去访问固有资源,因为它会在任意时刻中断。

  • 守护线程:线程优先级低,只有当别的线程不执行的时候,才执行守护线程,当守护线程是程序中唯一执行的线程时,守护线程执行完毕,程序也执行完毕,典型的守护线程——gc
  • 设置一个线程为守护线程:setDaemon(true),注意,该方法只能是在start()方法被调用之前设置,一旦线程开始运行,将不能再修改守护状态
  • 检测一个线程为守护线程:isDaemon()
public class DaemonThread extends Thread {
    // 定义守护线程的线程执行体与普通线程没有任何区别
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println(getName() + "  " + i);
        }
    }

    public static void main(String[] args) {
        DaemonThread t = new DaemonThread();
        // 将此线程设置成守护线程
        t.setDaemon(true);
        // 启动守护线程
        t.start();
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()
                    + "  " + i);
        }
        // -----程序执行到此处,前台线程(main线程)结束------
        // 守护线程也应该随之结束而不管其是否执行完毕
    }
}

3.3 线程睡眠:sleep

  • sleep():将当前正在执行的线程暂停一段时间,并进入阻塞状态
    • static void sleep(long millis):让当前正在执行的线程暂停millis毫秒,并进入阻塞状态
    • static void sleep(long millis, int nanos):让当前正在执行的线程暂停millis毫秒加nanos豪微秒,并进入阻塞状态
  • 当当前线程调用sleep()方法进入阻塞状态后,其在睡眠时间段内,该线程不会获得执行的机会

3.3 线程让步:yield

  • Thread.yield()方法可以让正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态,yield()方法只是让当前线程暂停一下,让系统的线程调度器重新调度一次,当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会
  • sleep()方法和yield()方法的区别:
    • sleep()方法暂停当前线程后,会给其它线程执行机会,不会理会其他线程的优先级,但yield()方法只会给优先级相同,或优先级更高的线程执行机会
    • sleep()方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态;而yeild()不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程调用yield()方法暂停后,立即再次获得处理器资源被执行
    • sleep()方法声明抛出了InterruptedException异常,所以调用sleep()方法时要么捕捉该异常,要么显式声明抛出该异常,而yield()方法则没有声明抛出任何异常
    • sleep()方法比yield()方法有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行

3.4 线程属性

  • Thread类的属性信息:
    • ID:保存了线程的唯一标识符
    • Name:保存了线程名称
    • Priority:保存了线程对象的优先级(1~10),JVM使用线程的Priority属性来决定某一刻由哪一个线程来使用CPU,并且根据线程的情景为它们设置实际状态
    • Status:保存了线程的状态
  • 注意:线程的ID和Status是不可以修改的,线程类也没有提供相关方法
  • 在Java中,每一个线程有一个优先级,在默认情况下,一个线程继承它的父线程的优先级。也可以使用setPriority方法提高或降低任何一个线程的优先级,建议不要过度使用线程优先级,假如使用的话推荐使用静态常量优先级,这样可以保证最好的可移植性
    • getPriority():返回指定线程的优先级
    • setPriority(int newPriority):设置指定线程的优先级,参数可以是一个整数,范围是1-10之间,也可以使用如下的三个静态常量:
      • MAX_PRIORITY:值是10
      • MIN_PRIORITY:其值是1
      • NORM_PRIORITY:其值是5

4 线程同步

为了解决多线程执行时出现的线程安全问题

  • Java语言提供的两种基本同步机制:
    • synchronized关键字机制;
    • Lock接口及其实现机制。

4.1 同步代码块

  • 使用同步监视器来解决进程间执行的不确定性,阻止两个线程对同一个共享资源进行并发访问,使用同步监视器的通用方法就是同步代码块,同步代码块的语法格式如下:
Synchronized(obj){
    //同步代码块
    //obj就是同步监视器
}
  • 线程在执行同步代码块之前,必须先获得对同步监视器的锁定,任何时候只能有一个线程获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定

4.2 同步方法

  • 同步方法:使用synchronized关键字修饰的方法,该关键字修饰的方法的同步监视器是this,也就是调用该方法的对象,在修饰实例方法时无须显式指定同步监视器
  • 通过使用同步方法可以非常方便的实现线程安全的类,线程安全的类具有如下特征:
    • 该类的对象可以被多个线程安全的访问
    • 每个线程调用该对象的任意方法之后都将得到正确结果
    • 每个线程调用该对象的任意方法后,该对象状态依然保持合理状态
  • synchronized关键字可以修饰方法、代码块,但不能修饰构造器和成员变量等,synchronized关键字会降低应用程序的性能,更好的使用synchronized的方法是:通过synchronized关键字保护代码块而不是整个方法,这样可以获取更好的性能,同时临界区的代码尽可能的短,当这样使用synchronized关键字的时候必须把对象引用作为传入参数,通常使用this关键字来引用正在执行的方法所属的对象,也可以使用其它对象
  • 可变类线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以采用如下策略:
    • 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源的方法进行同步
    • 如果可变类有两种运行环境:单线程和多线程环境,则应该为该可变类提供两种版本,即线程不安全版本和线程安全版本
  • 每一个Java对象都有一个内部锁,如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。
  • 内部对象锁只有一个相关条件,wait方法添加一个线程到等待集中,notifyAllnotify方法解除等待线程的阻塞状态。
  • 静态方法也可以被该关键字修饰,但是被修饰的静态同步方法被调用时,该类的class对象被锁锁住,因此,没有其他线程可以调用同一个类的这个或任何其他的同步静态方法。
  • 内部锁和条件的局限:
    • 不能中断一个正在试图获得锁的线程;
    • 试图获得锁时不能设定超时;
    • 每个锁仅有单一的条件,可能是不够的。

4.3 释放同步监视器的锁定

  • 程序无法显式释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定:
    • 当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器
    • 当前线程的同步代码块、同步方法中遇到breakreturn终止了该代码块、该方法的继续执行,当前线程即释放同步监视器
    • 当前线程的同步代码块、同步方法中出现了未处理的ErrorException,导致了该代码块、该方法异常结束时,当前线程即释放同步监视器
    • 当前线程的同步代码块、同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器
  • 在如下情况下,线程不会释放同步监视器
    • 线程执行同步代码块或同步方法时,程序调用Thread.sleep()Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器
    • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,当前线程不会释放同步监视器。当然,程序应该尽量避免使用suspend()resume()方法来控制线程。

4.4 同步锁:Lock

  • 同步锁——Lock对象,通过显式定义同步锁对象来实现同步,Lock允许实现更灵活的结构,可以具有差别很大的属性,并且支持多个相关的Condition对象,Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
  • 某些锁可能允许对共享资源并发访问,如ReadWriteLock
    • Lock实现类——ReentrantLock
    • ReadWriteLock实现类——ReentrantReadWriteLock
    • Java 8新增了新型的StampedLock类,替代传统的ReentrantReadWriteLock
  • 在实现线程安全的控制中,比较常用的是ReentrantLock,使用该Lock对象可以显式地加锁、释放锁,其使用格式如下:
class X {
    //定义锁对象
    private final ReentrantLock lock = new ReentrantLock();
    //定义需要保证线程安全的方法
    public void safeMethod(){
        //加锁
        lock.lock();
        try {
            //方法体
        }
        catch(Exception e) {
            //异常处理
        }
        finally {
            //释放锁
            lock.unlock();
        }
    }
}
  • 锁机制相较于synchronized机制的优点:
    • 支持更灵活的同步代码结构
    • 提供了更多的功能;
    • Lock接口允许分离读和写操作,允许多个读线程和只有一个写线程;
    • 具有更好的性能。
  • 注意:如果使用锁,就不能使用带资源的try语句

5 线程通信

5.1 传统的线程通信

  • 为了实现线程通信可以使用Object类提供的wait()、notify()、notifuAll()三个方法,这三个方法必须由同步监视器对象来调用
    • wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该进程,调用wait()方法的当前线程会释放对该同步监视器的锁定
    • notify():唤醒在此同步监视器上等待的单个线程
    • notifyAll():唤醒在此同步监视器上等待的所有线程
  • 对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法
  • 对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这三个方法

5.2 使用Condition控制线程通信

  • 当使用Lock对象来保证同步时,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程
  • Condition类提供了如下三个方法:
    • await():类似wait,导致当前线程等待
    • signal():类似notify,唤醒在此Lock对象上等待的单个线程
    • signalAll():类似notifyAll,唤醒在此Lock对象上等待的所有线程
  • 一个锁对象可以有一个或多个相关的条件对象,通过Lock对象的newCondition方法或得一个条件对象Condition
  • Lock和Condition对象的使用建议:
    • 最好是不使用任何一种方法;
    • 优先使用synchronized关键字;
    • 需要Lock和Condition对象的独有特性的时候,才使用其。

5.3 使用阻塞队列(BlockingQueue)控制线程通信

  • BlockingQueue:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞
  • BlockingQueue提供的两个支撑阻塞的方法:
    • put(E e):给队列加入元素,如果队列已满,阻塞
    • take():取出队列的头部元素,如果队列为空,阻塞

表1-1 BlockingQueue包含的方法之间的对应关系

方法抛出异常不同返回值阻塞线程指定超时时长
队尾插入元素add(e)offer(e)put(e)offer(e, time, unit)
队头删除元素remove()poll()take()poll(time, unit )
获取、不删除元素element()peek()

6 线程异常及线程池

6.1 线程组和未捕获异常处理器

  线程的run方法不能跑出任何受查异常,当线程中出现运行时异常时,线程将被终止,在这种情形下,线程就死亡了,在该线程死亡之前,异常将被传递到一个用于未捕获异常的处理器,该处理器必须属于一个实现Thread.UncaughtExceptionHandler接口的类。这个接口只有一个方法:void uncaughtException(Thread t, Throwable e),可以用setUncaughtExceptionHandler方法为任何线程安装一个处理器,也可以使用Thread.setDefaultUncaughtExceptionHandler为所有线程暗转给一个默认的处理器,如果不安装默认的处理器,默认的处理器为空,此时采用线程组(ThreadGroup)的处理器。

  • Java使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制,对线程组的控制相当于同时控制这批线程
  • 用户创建的所有线程都属于指定线程组,如果没有显式指定线程属于哪个线程组,则线程属于默认线程组,在默认情况下,子线程和创建它的父线程处于同一个线程组内
  • 一旦某个线程加入了指定线程组之后,该线程将一直属于该线程组,直到该线程死亡,线程运行中不能改变它所属的线程组
  • Thread类提供了如下几个构造器来设置新线程属于哪个线程组:
    • Thread(ThreadGroup group, Runnable target):以target的run()方法作为线程执行体创建新线程,属于group线程组
    • Thread(ThreadGroup group, Runnable target, String name):以target的run()方法作为线程执行体创建新线程,属于group线程组,且线程名是name
    • Thread(ThreadGroup group, String name):创建新线程,新线程名为name,属于group线程组
  • 查询线程所属的线程组:getThreadGroup()
  • 查询线程组的名称:getName()
  • 创建线程组实例:
    • ThreadGroup(String name):以指定的线程组名字来创建新的线程组
    • ThreadGroup(ThreadGroup parent, String name):以指定的名字、指定的父线程组创建一个新线程组
  • ThreadGroup类提供的常用方法:
    • int activeCount():返回此线程组中活动线程的数目
    • interrupt():中断此线程组中的所有线程
    • isDaemon():判断该线程组是否是后台线程组
    • setDaemon(boolean daemon):把该线程组设置成后台线程组
    • setMaxPriority(int pri):设置线程组的最高优先级
  • ThreadGroup内还定义了一个void uncaughtException(Thread t, Throwable e),该方法可以处理该线程组所抛出的未处理异常
  • 如果线程执行过程中跑出了一个未处理异常,JVM在结束该线程之前会自动查找是否有对应的Thread.UncaughtExceptionHandler对象,如果找到该处理器对象,则会调用该对象uncaughtException(Thread t, Throwable e)方法来处理异常
  • Thread类提供了如下两个方法来设置异常处理器:
    • static setDefaultUncaughtExceptionHandler(Thread. UncaughtExceptionHandler eh):为该线程类的所有线程实例设置默认的异常处理器
    • setUncaughtExceptionHandler(Thread. UncaughtExceptionHandler eh):为指定的线程实例设置异常处理器
  • ThreadGroup类实现了Thread.UncaughtExceptionHandler接口,所以每个线程所述的线程组将会作为默认的异常处理器。当一个线程跑出未处理异常时,JVM会首先查找该异常对应的异常处理器,如果找到该异常处理器,则将调用该异常处理器处理异常;否则,JVM将会调用该线程所属的线程对象的uncaughtException()方法来处理该异常,线程组处理异常的默认流程如下:
    • 如果该线程组有父线程组,则调用父线程组的uncaughtException()方法来处理该异常
    • 如果该线程实例所属的线程类有默认的异常处理器,那么就调用该异常处理器来处理该异常
    • 如果该异常对象是ThreadDeath的对象,则不做任何处理,否则,将异常跟踪栈的信息打印到System.err错误输出流,并结束该线程
// 定义自己的异常处理器
class MyExHandler implements Thread.UncaughtExceptionHandler {
    // 实现uncaughtException方法,该方法将处理线程的未处理异常
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println(t + " 线程出现了异常:" + e);
    }
}
public class ExHandler {
    public static void main(String[] args) {
        // 设置主线程的异常处理器
        Thread.currentThread().setUncaughtExceptionHandler(new MyExHandler());
        int a = 5 / 0; 
        System.out.println("程序正常结束!");
    }
}
  • 异常处理器与try/catch捕获异常的区别:当使用catch捕获异常时,异常不会向上传播给上一级调用者;但使用异常处理器对异常进行处理之后,异常依然会传播给上一级调用者。

6.2 线程池

线程池在系统启动的时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个线程来执行它们的run()或call()方法,当run()或call()方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run()或call()方法,除此之外,线程池还可以有效地控制系统中并发线程的数量,当系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致JVM崩溃,而线程池的最大线程数参数可以控制系统中并发线程的数量不超过此数

6.2.1 Java 8 改进的线程池

表1-1 执行器工厂方法

方法描述
newCachedThreadPool必要时创建新县城;空闲线程会被保留60秒
newFixedThreadPool该池包含固定数量的线程;空闲线程会一直被保留
newSingleThreadExecutor只有一个线程的“池”,该线程顺序执行每一个提交的任务
newScheduledThreadPool用于预定执行而构建的固定线程池,替代java.util.Timer
newSingleThreadScheduledExecutor用于预定执行而构建的单线程“池”
  • ExecutorService对象代表一个线程池,可以执行Runnable对象和Callable对象所代表的的线程,ScheduledExecutorService对象在指定延迟后执行线程任务,work stealing相当于守护线程池
  • ExecutorService代表尽快执行线程的线程池,程序只要将一个Runnable对象或Callable独享提交给线程池,该线程池就会尽快执行该任务。ExecutorService里提供了如下三个方法:
    • Future<?> submit(Runnable task):将一个Runnable对象提交给指定的线程池,线程池在有空闲线程时执行Runnable对象代表的任务
    • <T> Future<T> submit(Runnable task, T result):将一个Runnable对象提交给指定的线程池,线程池在有空闲线程时执行Runnable对象代表的任务,其中result显式指定线程执行结束后的返回值
    • <T> Future<T> submit(Callable<T> task):将一个Callable对象提交给指定的线程池,线程池在有空闲线程时执行Callable对象代表的任务,其中Future代表Callable对象里call()方法的返回值
  • ScheduledExecutorService代表可在指定延迟后或周期性地执行线程任务的线程池
    • ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit):指定callable任务将在delay延迟后执行
    • ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit):指定command任务将在delay延迟后执行
    • ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) :指定command任务将在delay延迟后执行,而且以设定频率重复执行
    • ScheduledFuture<?> scheduleWithFixedRate(Runnable command, long initialDelay, long delay, TimeUnit unit):创建并执行一个在给定初始延迟后首次启用的定期操作,随后在每一次任务执行终止和下一次执行开始之间都存在给定的延迟
  • 用完线程池之后,调用线程池的shutdown()方法,该方法将启动线程池的关闭序列,调用shutdown()方法后的线程池不再接受新任务,但会将以前所有提交任务执行完成,也可以调用shutdownNow()强制停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务序列
  • 使用线程池来执行线程任务的步骤:
    • 调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池
    • 创建Runnable实现类或Callable实现类的实例,作为线程执行任务
    • 调用ExecutorService对象的submit()方法来提交Runnable实例或Callable实例;
    • 当不想提交任何任务时,调用ExecutorService对象的shutdown()方法来关闭线程池。
public class ThreadPoolTest {
    public static void main(String[] args)
            throws Exception {
        // 创建足够的线程来支持4个CPU并行的线程池
        // 创建一个具有固定线程数(6)的线程池
        ExecutorService pool = Executors.newFixedThreadPool(6);
        // 使用Lambda表达式创建Runnable对象
        Runnable target = () -> {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName()
                        + "的i值为:" + i);
            }
        };
        // 向线程池中提交两个线程
        pool.submit(target);
        pool.submit(target);
        // 关闭线程池
        pool.shutdown();
    }
}

6.2.2 Java 8 增强的ForkJoinPool

  • ForkJoinPool支持将一个任务拆分成多个小任务并行计算,再把多个小任务的结果合并成总的计算结果,ForkJoinPool是ExecutorService的实现类,因此是一种特殊的线程池,提供如下两个构造器:
    • ForkJoinPool(int parallelism):创建一个包含parallelism个并行线程的ForkJoinPool
    • ForkJoinPool():以Runtime.availableProcessor()方法的返回值作为parallelism参数来创建ForkJoinPool
  • ForkJoinPool类通过如下两个静态方法提供通用池功能:
    • ForkJoinPool commonPool():该方法返回一个通用池,通用池的运行状态不会受shutdown()或shutdownAll()方法的影响
    • int getCommonPoolParallelism():该方法返回通用池的并行级别
  • 创建了ForkJoinPool实例之后,就可以调用ForkJoinPool的submit(ForkJoinTask task)或invoke(ForkJoinTask task)来执行任务,其中ForkJoinTask代表一个可以并行、合并的任务。ForkJoinTask是一个抽象类,它还有两个抽象子类:RecursiveAction和RecursiveTask,其中RecursiveTask代表有返回值的任务,而RecursiveAction代表没有返回值的任务
// 继承RecursiveAction来实现"可分解"的任务
class PrintTask extends RecursiveAction {
    // 每个“小任务”只最多只打印50个数
    private static final int THRESHOLD = 50;
    private int start;
    private int end;

    // 打印从start到end的任务
    public PrintTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected void compute() {
        // 当end与start之间的差小于THRESHOLD时,开始打印
        if (end - start < THRESHOLD) {
            for (int i = start; i < end; i++) {
                System.out.println(Thread.currentThread().getName()
                        + "的i值:" + i);
            }
        } else {
            // 如果当end与start之间的差大于THRESHOLD时,即要打印的数超过50个
            // 将大任务分解成两个小任务。
            int middle = (start + end) / 2;
            PrintTask left = new PrintTask(start, middle);
            PrintTask right = new PrintTask(middle, end);
            // 并行执行两个“小任务”
            left.fork();
            right.fork();
        }
    }
}

public class ForkJoinPoolTest {
    public static void main(String[] args)
            throws Exception {
        ForkJoinPool pool = new ForkJoinPool();
        // 提交可分解的PrintTask任务
        pool.submit(new PrintTask(0, 300));
        pool.awaitTermination(2, TimeUnit.SECONDS);
        // 关闭线程池
        pool.shutdown();
    }
}
import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;

// 继承RecursiveTask来实现"可分解"的任务
class CalTask extends RecursiveTask<Integer> {
    // 每个“小任务”只最多只累加20个数
    private static final int THRESHOLD = 20;
    private int arr[];
    private int start;
    private int end;

    // 累加从start到end的数组元素
    public CalTask(int[] arr, int start, int end) {
        this.arr = arr;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        // 当end与start之间的差小于THRESHOLD时,开始进行实际累加
        if (end - start < THRESHOLD) {
            for (int i = start; i < end; i++) {
                sum += arr[i];
            }
            return sum;
        } else {
            // 如果当end与start之间的差大于THRESHOLD时,即要累加的数超过20个时
            // 将大任务分解成两个小任务。
            int middle = (start + end) / 2;
            CalTask left = new CalTask(arr, start, middle);
            CalTask right = new CalTask(arr, middle, end);
            // 并行执行两个“小任务”
            left.fork();
            right.fork();
            // 把两个“小任务”累加的结果合并起来
            return left.join() + right.join();    // ①
        }
    }
}

public class Sum {
    public static void main(String[] args) throws Exception {
        int[] arr = new int[100];
        Random rand = new Random();
        int total = 0;
        // 初始化100个数字元素
        for (int i = 0, len = arr.length; i < len; i++) {
            int tmp = rand.nextInt(20);
            // 对数组元素赋值,并将数组元素的值添加到sum总和中。
            total += (arr[i] = tmp);
        }
        System.out.println(total);
        // 创建一个通用池
        ForkJoinPool pool = ForkJoinPool.commonPool();
        // 提交可分解的CalTask任务
        Future<Integer> future = pool.submit(new CalTask(arr, 0, arr.length));
        System.out.println(future.get());
        // 关闭线程池
        pool.shutdown();
    }
}

6.3 线程相关类

6.3.1 ThreadLocal类

  • ThreadLocal类:它代表一个线程局部变量,通过把数据放在ThreadLocal中就可以让每一个线程创建一个该变量的副本,从而避免并发访问的线程安全问题,线程局部变量的作用是为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突,ThreadLocal类提供了如下三个public方法:
    • T get():返回此线程局部变量中当前线程副本中的值
    • void remove():删除此线程局部变量中当前线程的值
    • void set(T value):设置此线程局部变量中当前线程副本的值
  • ThreadLocal并不能代替同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式,而ThreadLocal是为了隔离多个线程的数据共享,从根本上避免多个线程之间对共享资源的竞争,也就不需要对多个线程进行同步了
  • 建议:如果多个线程之间需要共享资源,以达到线程之间的通信功能,就是用同步机制,如果仅仅需要隔离多个线程之间的共享冲突,则可以使用ThreadLocal

6.3.2 包装线程不安全的集合

  • ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是线程不安全的,通过Collections提供的静态方法将这些集合包装成线程安全的集合:
    • <T> Collection<T> synchronizedCollection(Collection<T> c):返回指定Collection对应的线程安全的Collection
    • static <T> List<T> synchronizedList(List<T> list):返回指定List对象对应的线程安全的List对象
    • static <K,V> Map<K,V> synchronizedMap(Map<K,V> map):返回指定Map对象对应的线程安全的Map对象
    • static <T> Set<T> synchronizedSet(Set<T> set):返回指定Set对象对应的线程安全的Set对象
    • static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m):返回指定SortedMap对象对应的线程安全的SortedMap对象
    • static <T> SortedSet <T> synchronizedSortedSet(SortedSet <T> s):返回指定SortedSet对象对应的线程安全的SortedSet对象

6.3.3 线程安全的集合类

  • 以Concurrent开头的集合类:ConcurrentHashMap、ConcurrentSkipListmap、ConcurrentSkipListSet、ConcurrentLinkedQueue和ConcurrentLinkedDeque,代表了支持并发访问的集合,他们可以支持多线程并发写入访问
  • 以CopyOnWrite开头的集合类:CopyOnWriteArrayList、CopyOnWriteArraySet
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值