Java 多线程

线程概述线程和进程

几乎所有的操作系统都支持进程的概念,所有运行中的任务通常对应一个进程(Process)。当一个程序进入内存运行时,即变成一个进程。

进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位

一般而言,进程包含如下三个特征

  • 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个进程不可以直接访问其他进程的地址空间。
  • 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,程序则不具备这些概念
  • 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响

多线程使得一个进程可以同时并发处理多个任务。线程(Thread)也被称作轻量级进程(Lightweight Process),线程是进程的执行单元。

线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。

线程的创建和使用

Java 使用 Thread 类代表线程,所有的线程对象都必须是 Thread 类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流(一段顺序执行的代码)。Java使用线程执行体来代表这段程序流。

继承 Thread 类创建线程类

通过继承 Thread 类来创建并启动多线程的步骤如下

  1. 定义Thread 类的子类,并重写该类的 run() 方法。 该run() 方法的方法体就代表了线程需要完成的任务
  2. 创建 Thread 子类的实例,即创建了线程对象
  3. 调用线程对象的 start() 方法来启动线程
public class FirstThread extends Thread{    private int i = 0;    @Override    public void run() {        for (  ; i < 100; i++) {            // Thread 对象的getName() 返回当前线程的名字            System.out.println(getName() + " " + i);        }    }    public static void main(String[] args) {        for (int i = 0; i < 100; i++) {            // 获取当前线程并输出 线程名 + i            System.out.println(Thread.currentThread().getName() + " " + i);            if(i == 20){                // 创建启动第一个线程                new FirstThread().start();                // 创建启动第二个线程                new FirstThread().start();            }        }    }}

输出

main 18main 19main 20main 21main 22main 23Thread-0 0main 24Thread-1 0Thread-0 1Thread-1 1Thread-1 2

实现 Runnable 接口创建线程类

实现 Runnable 接口来创建并启动多线程的步骤如下

  1. 定义 Runnable 接口的实现类,并重写该接口的 run() 方法,该 run() 方法的方法体同样是该线程的线程知兴替
  2. 创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 来创建 Thread 对象
  3. 调用 Thread 对象的 start() 方法来启用线程
public class RunnableTest {    public static void main(String[] args) {        // 创建 Runnable 匿名实现类对象        Runnable runnable = new Runnable() {            @Override            public void run() {                // 线程执行体...            }        };        // 以Runnable对象为线程执行体 创建Thread对象        Thread th = new Thread(runnable);        // 启动线程        th.start();    }}

由于从 Java 8 开始,Runnable 接口使用了 @FunctionalInterface 修饰,说明 Runnable 接口时函数式接口,可使用 Lambda 来创建 Runnable 对象

Thread th = new Thread(()->{    // 线程执行体...});

通过实现 Runnable 接口来获取当前线程对象只能使用 Thread.currentThread() 方法

使用 Callable 和 Future 创建线程

从 Java 5 开始,Java 提供了 Callable 接口, Callable 接口提供了一个 call() 方法,该方法可以有返回值并且可以声明抛出异常

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() 方法的返回值。 该方法让程序最多阻塞 timeoutunit 指定的时间,如果经过指定时间后 Callable 任务依然没有返回值,将会抛出 TimeoutException 异常
  • boolean isCanceled() 如果在 Callable 任务正常完成前被取消,则返回 true
  • boolean isDone() 如果 Callable 任务已完成,则返回 true

下面程序通过实现 Callable 接口来实现线程类

public class CallableTest {    public static void main(String[] args) throws Exception {        // 使用 Lambda 表达式创建 Callable 对象        // 使用 Future 来包装 Callable 对象        FutureTask task = new FutureTask(()->{            int i = 0;            for ( ; i < 100; i++) {                System.out.println(Thread.currentThread().getName() + " " + i);            }            // call() 方法可以有返回值            return i;        } );        // 启动线程        new Thread(task).start();        for (int i = 0; i < 100; i++) {            if(i == 20){                // 阻塞主线程直至 获取到子线程的值                System.out.println("子线程的返回值:"+ task.get());            }            System.out.println(Thread.currentThread().getName() + " " + i);        }    }}

输出

..........main 17main 18main 19Thread-0 34Thread-0 35..........Thread-0 98Thread-0 99子线程的返回值:100main 20main 21main 22..........

线程的生命周期

在线程的生命周期,它要经过以下 5 种状态

  • 新建(New):当程序使用 new 关键字 创建了一个线程后,该线程就处于新建状态
  • 就绪(Ready):当线程调用 start() 方法之后,该线程处于就绪状态,jvm 会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了
  • 运行(Running):处于就绪状态的线程获得了 CPU,开始执行run() 方法的线程执行体,则该线程处于运行状态
  • 阻塞(Blocked):在某种特殊情况下,被认为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
  • 死亡(Dead):线程完成了它的全部工作或线程被提前强制性的终止或出现异常导致结束

当一个线程开始运行后,它不可能一直处于运行状态(除非线程执行体非常短),线程在运行过程中需要被中断,目的是使其他线程获得执行的机会

当正在执行的线程被阻塞之后,其他线程就可以获得执行的机会。被阻塞的线程会再合适的时候重新进入就绪状态。

为了测试某个线程是否已经死亡,可以调用线程对象的 isAlive() 方法,当线程处于 就绪、运行、阻塞三种状态时,该方法将返回 true;当线程处于 新建、死亡两种状态时,该方法将返回 false

图片[1] - Java 多线程 - MaxSSL

注意启动线程使用 start() 方法,而不要调用线程的 run() 方法。如果直接调用 run() 方法,系统不会为其创建线程,会将其当成一个普通方法,并且无法再调用start() 方法,否则将引发 IllegalThreadStateException 异常

程序只能对新建状态的线程调用 start() 方法

控制线程

Java 的线程提供了一些便捷的工具方法,通过这些便捷的工具方法可以很好地控制线程执行

join 线程

Thread 提供了让一个线程等待另一个线程完成的方法——join() 方法。当在某个程序执行流中调用了其他线程的join() 方法时,调用像线程将被阻塞,知道被 join() 方法加入的 join 线程执行完为止

join() 方法有如下三种重载形式

  • join() 等待被 join 的线程执行完成
  • join(long millis) 等待被 join 的线程的时间最长为 millis 毫秒。如果在 mills 毫秒内被 join 的线程还没有执行结束,则不在等待
  • join(long mills,int nanos) 等待被 join 的线程的时间最长为 mills 毫秒 加 nanos 微毫秒
public class JoinThread {    public static void main(String[] args) throws Exception {        for (int i = 0; i  {                    for (int j = 0; j < 100; j++) {                        System.out.println(Thread.currentThread().getName() + " " + j);                    }                }, "被 Join 的线程");                thread.start();                // main 线程必须等待 thread 线程执行结束后才可以向下执行                thread.join();            }            System.out.println(Thread.currentThread().getName() + " " + i);        }    }}

输出

.....被 Join 的线程 97被 Join 的线程 98被 Join 的线程 99main 20main 21main 22....

后台线程

有一种线程,它是在后台运行的,它的任务是为其他的线程提供服务,这种线程被称为 后台线程,也被称为 守护线程 或 精灵线程

后台线程有一个特征:如果所有的前台线程都死亡,后台线程会自动死亡

通过调用 Thread 对象的 setDaemon(true) 方法 可将指定线程设置为后台线程

Thread t = new MyThread();t.setDaemon(true);t.start();

Thread 类还提供了一个 isDaemon() 方法,用于判断指定线程是否为后台线程

setDaemon(true) 必须在 start() 之前调用,否则会引发 IllegalThreadStateException 异常

线程睡眠

如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以调用 Thread 类的静态 sleep() 方法来实现。

sleep() 方法有两种重载形式

  • static void sleep(long millis) 让当前正在执行的线程暂停 millis 毫秒,并进入阻塞状态
  • static void sleep(long millis,int nanos) 让当前正在执行的线程暂停 millis 毫秒 加 nanos 毫微秒,并进入阻塞状态
Thread.sleep(1000); // 让当前线程暂停1s

yield() 方法

yield() 方法是 Thread 的一个静态方法,它可以让当前执行的线程暂停,但该线程不会进入阻塞状态,它只是将该线程转入就绪状态

yield() 方法只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能情况是:当某个线程调用了yield() 方法暂停后,线程调度器又将其调度出来重新执行

Thread.yield(); // 将当前线程转入就绪状态

改变线程优先级

每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的则获得较少的执行机会

每个线程默认的优先级都与创建它的父线程的优先级相同,在默认情况下, main线程具有普通优先级(NORM_PRIORITY)

Thread 类提供了 setPriority(int newPriority)getPriority() 方法来设置和返回指定线程的优先级,其中setPriority() 方法的参数可以是一个整数,范围是 1~10 之间,也可以使用 Thread 类的如下三个静态常量

  • MAX_PRIORITY 其值是10
  • MIN_PRIORITY 其值是1
  • NORM_PRIORITY 其值是5
Thread.currentThread().setPriority(Thread.NORM_PRIORITY);

虽然 Java 提供了10个优先级级别,但这些优先级级别需要操作系统的支持。因此应尽量避免直接指定优先级,而应该优先使用常量值来设置优先级,这样能保证程序具有最好的可移植性

线程同步

由于系统的线程调度具有一定的随机性。当使用多个线程同时读写同一个数据时,很容易出现线程安全问题

下面模仿两个用户同时对一个账户取钱的操作

定义一个账户类

public class Account {    // 封装账号编号,账号金额两个成员变量    private String accountNo;    private  double balance;    public Account(String accountNo, double balance) {        this.accountNo = accountNo;        this.balance = balance;    }    public String getAccountNo() {        return accountNo;    }    public void setAccountNo(String accountNo) {        this.accountNo = accountNo;    }    public double getBalance() {        return balance;    }    public void setBalance(double balance) {        this.balance = balance;    }}

定义一个取钱线程类

public class DrawThread extends Thread{    private Account account;    // 取钱数    private double drawAmount;    public DrawThread(String name, Account account, double drawAmount) {        super(name);        this.account = account;        this.drawAmount = drawAmount;    }    @Override    public void run() {        if (account.getBalance() >= drawAmount){            System.out.println("取钱成功!吐出钞票:"+ drawAmount);            // 将余额减去取钱数            account.setBalance(account.getBalance() - drawAmount);            System.out.println("余额为:" + account.getBalance());        }else{            System.out.println(getName() + "取钱失败");        }    }}

模拟取钱

public class DrawTest {    public static void main(String[] args) {        // 创建一个账户        Account account = new Account("123456",1000);        // 模拟两个线程对同一个账户取钱        new DrawThread("甲",account,800).start();        new DrawThread("乙",account,800).start();    }}

输出

取钱成功!吐出钞票:800.0取钱成功!吐出钞票:800.0余额为:200.0余额为:-600.0

同步代码块

上面就是因为程序中有两个并发线程在修改 Account 对象引发的问题,为了解决这个问题,Java 的多线程支持引入了同步监视器来解决这个问题

使用同步监视器的通用方法就是同步代码块。同步代码块的语法格式如下

synchronized (obj){    ...    // 此处的代码就是同步代码块}

上述语法中的 synchronized 后括号里的obj 就是同步监视器,上述代码的含义:线程开始执行代码块之前,必须先获得对同步监视器的锁定,任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定

修改一下 DrawThread 类

public class DrawThread extends Thread {    private Account account;    // 取钱数    private double drawAmount;    public DrawThread(String name, Account account, double drawAmount) {        super(name);        this.account = account;        this.drawAmount = drawAmount;    }    @Override    public void run() {        synchronized (account) {            if (account.getBalance() >= drawAmount) {                System.out.println("取钱成功!吐出钞票:" + drawAmount);                // 将余额减去取钱数                account.setBalance(account.getBalance() - drawAmount);                System.out.println("余额为:" + account.getBalance());            } else {                System.out.println(getName() + "取钱失败");            }        }    }}

输出

取钱成功!吐出钞票:800.0余额为:200.0乙取钱失败

Java 程序允许使用任何对象作为同步监视器,但想一下同步监视器的目的:组织两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器

同步方法

使用 synchronized 修饰的方法称为同步方法。对于 synchronized 修饰的实例方法(非static 方法)而言,无需显示指定同步监视器,同步方法的同步监视器是 this,也就是调用该方法的对象

改写 Account 类

public class Account {    // 封装账号编号,账号金额两个成员变量    private String accountNo;    private  double balance;    public Account(String accountNo, double balance) {        this.accountNo = accountNo;        this.balance = balance;    }    public double getBalance() {        return balance;    }    // 提供一个线程安全的 draw() 方法来完成取钱操作,同步监视器是this    public synchronized void draw(double drawAmount){        if( balance >= drawAmount){            System.out.println(Thread.currentThread().getName() + "取钱成功!吐出钞票:" + drawAmount);            balance -= drawAmount;            System.out.println("余额为:"+balance);        }else{            System.out.println(Thread.currentThread().getName()+ "取钱失败");        }    }}

改写 DrawThread 的 run() 方法

@Overridepublic void run() {    account.draw(drawAmount);}

输出

甲取钱成功!吐出钞票:800.0余额为:200.0乙取钱失败

同步锁 (Lock)

从 Java 5 开始, Java 提供了一种功能更强大的线程同步机制——通过显式定义同步锁对象来实现同步,在这种机制下,同步锁由 Lock 对象充当

其中 Lock、ReadWriteLock 是 Java 5 提供的两个根接口,并为 Lock 提供了 ReentrantLock (可重入锁)实现类,为 ReadWriteLock 提供了ReentranReadWriteLock 实现类

在实现线程安全的控制中,比较常用的是 ReentrantLock(可重入锁)。使用该 Lock 对象可显示地加锁,释放锁

通常使用 ReentrantLock 地代码格式如下

public class X {    private final ReentrantLock lock = new ReentrantLock();        public void m(){        // 加锁        lock.lock();        try{            // 需要保证线程安全的代码            // ... method body        }finally {            lock.unlock();        }    }}

ReentrantLock 锁具有可重入性,一个线程可以对已被加锁地 ReentrantLock 锁再次加锁,ReentrantLock 对象会维持一个计数器来追踪 lock() 方法地嵌套调用,线程在每次调用 lock() 加锁之后,必须显式调用 unlock() 来释放锁,所以一段被保护的代码可以调用另一个被相同锁保护的方法

死锁

当两个线程相互等待对象释放同步监视器时就会发生死锁, Java虚拟机没有检测,也没有采取措施来处理死锁情况,因此多线程编程时应该采取措施避免思索出现。 一旦出现死锁,这个歌程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续

public class A {    public synchronized void foo(B b) {        System.out.println("当前线程名:" + Thread.currentThread().getName() + " 进入了A实例地 foo() 方法");        try {            Thread.sleep(200);        } catch (InterruptedException ex) {            ex.printStackTrace();        }        System.out.println("当前线程名:" + Thread.currentThread().getName() + " 试图调用 B 实例的 last() 方法");        b.last();    }    public synchronized void last() {        System.out.println("进入了A类的last()方法内部");    }}
public class B {    public synchronized void bar(A a) {        System.out.println("当前线程名:" + Thread.currentThread().getName() + " 进入了B实例地 bar() 方法");        try {            Thread.sleep(200);        } catch (InterruptedException ex) {            ex.printStackTrace();        }        System.out.println("当前线程名:" + Thread.currentThread().getName() + " 试图调用 A 实例的 last() 方法");        a.last();    }    public synchronized void last() {        System.out.println("进入了B类的last()方法内部");    }}
public class DeadLock {    public static void main(String[] args) {        A a = new A();        B b = new B();        Thread aThread = new Thread(()->{           a.foo(b);        },"A线程");        Thread bThread = new Thread(()->{            b.bar(a);        },"B线程");        aThread.start();        bThread.start();    }}

输出

当前线程名:A线程 进入了A实例地 foo() 方法当前线程名:B线程 进入了B实例地 bar() 方法当前线程名:B线程 启动调用 A 实例的 last() 方法当前线程名:A线程 启动调用 B 实例的 last() 方法

可以看到程序既无法向下执行,也不会抛出任何异常,就一直“僵持着”,原因是因为 A线程在保持着A对象锁的同时想要访问B对象的同步方法,而此时B线程也在保持B对象的锁并访问 A对象的同步方法,两个对象都在等待对方释放锁,因此出现 死锁

死锁是不应该出现在程序中的,编写程序时应该尽量避免死锁。可以通过下面几种常见方式来解决死锁问题

  • 避免多次锁定:尽量避免同一个线程对多个同步监视器进行锁定,比如上面程序,A线程要对A、B两个对象进行锁定,B线程也要对A、B两个对象进行锁定
  • 具有相同的加锁顺序:如果多个线程需要对多个同步监视器进行锁定,则应该保证它们以相同的顺序请求加锁。例如上面程序,A线程先对A对象加锁,再对B对象加锁,B线程先对B对象加锁,再对A对象加锁,进而导致死锁
  • 使用定时锁:程序调用 Lock 对象的 tryLock() 方法加锁时可以指定 time 和 unit 参数,当超过指定事件后会自动释放对 Lock 的锁定,这样就可以解开死锁
  • 死锁检测:这是一种依靠算法来实现的死锁预防机制,主要针对哪些不可能实现按序加锁,也不能使用定时锁的场景

线程通信

当线程在系统内运行时,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换执行,但 Java 也提供了一些机制来保证线程协调运行

使用 wait(), notify(), notifyAll() 控制线程通信

通过借助 Object 类提供的 wait(), notify(), notifyAll() 三个方法,这三个方法必须由同步监视器对象来调用,可分为以下两种情况

  • 对于使用 synchronized 修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法
  • 对于使用 synchronized 修饰的同步代码块,同步监视器是 synchronized 后括号里的对象,所以必须使用该对象调用这三个方法

关于这三个方法的解释如下

  • wait() 导致当前线程等待,直到其他前程调用该同步监视器的 notify() 方法或 notifyAll() 方法来唤醒该线程(可以通过参数指定最大等待时间),调用 wait() 方法的当前线程会释放对该同步监视器的锁定。
  • notify() 唤醒在此同步监视器上等待的单个线程。如果有多个线程都在此同步监视器上等待,则会选择唤醒其中一个线程
  • notifyAll() 唤醒在此同步监视器上等待的所有线程

案例:现设现在系统中有两个线程,这两个线程分别代表存款者和取钱者,要求存款这和取钱者不断地重复存钱、取钱的动作,而且要求每当存款者将钱存入指定账户后,取钱者就立即取出这笔钱。不允许连续两次存钱,也不允许连续两次取钱

账户类

public class Account {    // 封装账号编号,账号金额两个成员变量    private String accountNo;    private  double balance;// 表示账户中是否已存款    private boolean flag = false;    public Account(String accountNo, double balance) {        this.accountNo = accountNo;        this.balance = balance;    }    public double getBalance() {        return balance;    }    public synchronized void draw(double drawAmount){        try {            // 如果 flag 为 false,表明账户还没有人存钱进去,取钱方法阻塞            if(!flag){                wait();            }else{                System.out.println(Thread.currentThread().getName() + " 取钱:"+drawAmount);                balance -= drawAmount;                System.out.println("余额为:"+balance);                // 将是否存款的旗标改为false                flag = false;                // 唤醒其他线程                notifyAll();            }        }catch (InterruptedException ex){            ex.printStackTrace();        }    }    public synchronized void deposit(double depositAmount){        try{            // 如果为true 表明已有存款,存钱方法阻塞            if(flag){                wait();            }else{                System.out.println(Thread.currentThread().getName() + " 存款:"+depositAmount);                balance += depositAmount;                System.out.println("账户余额为:"+ balance);                flag = true;                // 唤醒其他线程                notifyAll();            }        }catch (InterruptedException ex){            ex.printStackTrace();        }    }}

取钱线程类

public class DrawThread extends Thread {    private Account account;    // 取钱数    private double drawAmount;    public DrawThread(String name, Account account, double drawAmount) {        super(name);        this.account = account;        this.drawAmount = drawAmount;    }    @Override    public void run() {        // 执行100次取钱动作        for (int i = 0; i < 100; i++) {            account.draw(drawAmount);        }    }}

存钱线程类

public class DepositThread extends Thread {    private Account account;    private double depositAmount;    public DepositThread(String name, Account account, double depositAmount) {        super(name);        this.account = account;        this.depositAmount = depositAmount;    }    @Override    public void run() {        // 执行100次存钱线程类        for (int i = 0; i < 100; i++) {            account.deposit(depositAmount);        }    }}

模拟存款 取款

public class DrawTest {    public static void main(String[] args) {        // 创建一个账户        Account account = new Account("123456",0);        new DrawThread("取钱者",account,800).start();        new DepositThread("存钱者甲",account,800).start();        new DepositThread("存钱者乙",account,800).start();        new DepositThread("存钱者丙",account,800).start();    }}

输出

存钱者丙 存款:800.0账户余额为:800.0取钱者 取钱:800.0余额为:0.0存钱者甲 存款:800.0账户余额为:800.0取钱者 取钱:800.0余额为:0.0存钱者丙 存款:800.0账户余额为:800.0取钱者 取钱:800.0余额为:0.0存钱者甲 存款:800.0账户余额为:800.0取钱者 取钱:800.0余额为:0.0存钱者丙 存款:800.0账户余额为:800.0取钱者 取钱:800.0余额为:0.0存钱者甲 存款:800.0账户余额为:800.0

使用 Condition 控制线程通信

当使用 Lock 对象来保证同步时, Java 提供了一个 Condition 类来保持协调,使用 Condition 可以让哪些已经得到 Lock 对象却无法继续执行的线程释放 Lock 对象,Condition 对象也可以唤醒其他处于等待的线程

Condition 实例被绑定在一个 Lock 对象上。要获得特定 Lock 实例的 Condition 实例,调用 Lock对象的 newCondition() 方法即可。

Condition 类提供了如下三个方法

  • await() 导致当前线程等待,直到其他前程调用Condition的 signal() 方法或 signalAll() 方法来唤醒该线程(可以通过参数指定最大等待时间),该 await() 方法由更多变体,如 long awaitNanos(long nanosTimeout)void awaitUnineterruptibly()awaitUntil(Date deadline)
  • signal() 唤醒在此Lock 对象上等待的单个线程。如果有多个线程都在此Lock 对象上等待,则会选择唤醒其中一个线程
  • notifyAll() 唤醒在此Lock 对象上等待的所有线程

可以将上述 Account 类改为

public class Account {    // 封装账号编号,账号金额两个成员变量    private String accountNo;    private  double balance;    private boolean flag = false;    public Account(String accountNo, double balance) {        this.accountNo = accountNo;        this.balance = balance;    }    public double getBalance() {        return balance;    }   // 显示定义Lock 对象    private final Lock lock = new ReentrantLock();    // 获得指定 Lock 对象对应的Condition    private final Condition cond = lock.newCondition();    public void draw(double drawAmount){        lock.lock();        try {            if(!flag){                cond.await();            }else{                System.out.println(Thread.currentThread().getName() + " 取钱:"+drawAmount);                balance -= drawAmount;                System.out.println("余额为:"+balance);                flag = false;                cond.signalAll();            }        }catch (InterruptedException ex){            ex.printStackTrace();        }finally {            lock.unlock();        }    }    public void deposit(double depositAmount){        lock.lock();        try{            if(flag){                cond.await();            }else{                System.out.println(Thread.currentThread().getName() + " 存款:"+depositAmount);                balance += depositAmount;                System.out.println("账户余额为:"+ balance);                flag = true;                cond.signalAll();            }        }catch (InterruptedException ex){            ex.printStackTrace();        }finally {            lock.unlock();        }    }}

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

Java 5 提供了一个 BlockingQueue 接口,虽然 BlockingQueue 也是Queue的子接口,但它的主要用途并不是作为容器,而是作为线程同步的工具。

BlockingQueue 具有一个特征:当生产者线程试图向 BlockingQueue 中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图 从 BlockingQueue 中取出元素时,如果该队列已空,则该线程被阻塞

BlockingQueue 提供如下两个支持阻塞的方法

  • put(E e) 尝试把 E 元素放入 BlockingQueue 中,如果该队列的元素已满,则阻塞线程
  • take() 尝试从 BlockingQueue 的头部取出元素,如果该队列的元素一空,则阻塞线程

下面以 ArrayBlockingQueue 实现类为例介绍阻塞队列的功能和用法

public class BlockingQueueTest {    public static void main(String[] args) {        BlockingQueue bq = new ArrayBlockingQueue(2);        Thread producer = new Thread(()->{            for (int i = 0; i {           while (true){               try {                   // 取出元素,如果队列已空,则线程被阻塞                   System.out.println("消费者取出:" + bq.take());                   Thread.sleep(200);               } catch (InterruptedException e) {                   throw new RuntimeException(e);               }           }        });        producer.start();        consumer.start();    }}

线程组

Java 使用 ThreadGroup 来表示线程组,它可以对一批线程进行分类管理,Java 允许程序直接对线程组进行控制。对线程组的控制相当于同时控制这批线程。

用户创建的所有线程都属于指定线程组,如果没有显示指定线程组,则该线程属于默认线程组。在默认情况下,子线程和创建它的父线程处于同一个线程组内。

一旦某个线程加入了指定线程组之后,该线程将一直属于该线程组,直到该线程死亡,线程运行中途不能改变它所属的线程组,因此 Thread类没有提供 setThreadGroup() 方法,但提供了getThreadGroup() 方法来获得 ThreadGroup对象

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 线程组

ThreadGroup 类提供了如下两个构造器来创建实例

  • ThreadGroup(String name) 以指定的线程组名字来创建新的线程组
  • ThreadGroup(ThreadGroup parent,String name) 以指定的名字、指定的父线程组创建一个新线程组

ThreadGroup 类提供了如下几个常用的方法来操作整个线程组里的所有线程

  • int activeCount() 返回此线程组中活动线程的数目
  • interrupt() 中断此线程组中的所有线程
  • isDaemon() 判断该线程组是否是后台线程组
  • setDaemon(boolean daemon) 把该线程组设置成后台线程组。后台线程组具有一个特征——当后台线程组的最后一个线程执行完毕或销毁后,后台线程组将自动销毁
  • setMaxPriority(int pri) 设置线程组的最高优先级
public class ThreadGroupTest {    public static void main(String[] args) {        ThreadGroup tg = new ThreadGroup("tg线程组");        // 通过Thread(ThreadGroup group, Runnable target) 构造器 创建一个实例,并将该线程实例加入tg线程组        Thread thread = new Thread(tg,()->{            for (int i = 0; i < 100; i++) {                System.out.println(Thread.currentThread().getName() + " " + i);            }        });        // 设置为后台线程组        tg.setDaemon(true);        System.out.println("是否是后台线程组:"+ tg.isDaemon());        thread.start();    }}

异常处理

从 Java 5开始,Java 加强了线程的异常处理,如果线程执行过程中抛出了一个未处理异常,JVM 在结束线程之前会自动查找是否有对应的 Thread.UncaughtExceptionHandler 对象,如果找到该处理器对象,则会调用该对象的uncaughtException(Thread t,Throwable e) 方法来处理该异常

Thread.UncaughtExceptionHandler 是Thread 类一个函数式接口,该接口内只有一个方法:void uncaughtException(Thread t, Throwable e); 其中t代表出现异常的线程,而 e 代表该线程抛出的异常

Thread 类提供了如下两个方法来设置异常处理器

  • static setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh) 为该线程类的所有线程实例设置默认的异常处理器
  • setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh) 为指定的线程实例设置异常处理器
public class ExHandler {    public static void main(String[] args) {        // 设置主线程的异常处理        Thread.currentThread().setUncaughtExceptionHandler(((t, e) -> {            System.out.println(t + "线程出现了异常:" + e);        }));        int a = 5 / 0;    }}

输出

Thread[main,5,main]线程出现了异常:java.lang.ArithmeticException: / by zero

线程池

启动一个新线程的成本是比较高的,因为它涉及与操作系统交互。这种情况下,使用线程池可以很好地提高性能

线程池在系统启动时即创建大量空闲的线程,程序将一个 Runnable 对象或 Callable 对象传给线程池,线程池就会启动一个空闲的线程来执行它们地 run()call 方法,当执行完毕后,该线程并不会死亡,而是再次回到线程池中成为空闲状态

使用线程池管理线程

从 Java 5 开始,Java内建支持线程池。 Java 5 新增了一个 Executors 工厂类来产生线程池,该工厂类包含如下几个静态工厂方法来创造线程池

  • static ExecutorService newCachedThreadPool() 创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中
  • static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用的,具有固定线程数的线程池
  • static ExecutorService newSingleThreadExecutor() 创建一个只有单线程的线程池,它相当于调用 newFixedThread Pool() 方法时,参入传入1
  • static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。 corePoolSize 指池中所保存的线程数,即使线程数是空闲的也被保存在线程池内。
  • static ScheduledExecutorService newSingleThreadScheduledExecutor() 创建只有一个线程的线程池,它可以在指定延迟后执行线程任务
  • static ExecutorService newWorkStealingPool(int parallelism) 创建持有足够的线程的线程池来支持给定的并行级别,该方法还会使用多个队列来减少竞争
  • static ExecutorService newWorkStealingPool() 上个方法的简化版本。如果当前机器有4个 CPU,则目标并行界别设置为 4,也就是相当于为前一个方法传入 4 作为参数

ExecutorService 代表尽快执行线程的线程池,程序只需要将一个 Runnable 对象或 Callable 对象提交给该线程池,该线程池就会尽快执行该任务

ExecutorService 里提供了如下三个方法

  • Future submit(Runnable task) 将一个 Runnable 对象提交给指定的线程池,线程池将在有空闲时执行 Runnable 对象代表的任务。
  • Future submit(Runnable task, T result); 将一个 Runnable 对象提交给指定的线程池,线程池将在有空闲时执行 Runnable 对象代表的任务。result 显示指定线程结束后的返回值
  • Future submit(Callable task) 将一个 Callable 对象提交给指定的线程池,线程池将在有空闲时执行 Callable 对象代表的任务。

ScheduledExecutorService 代表可在制定延迟后或周期性地执行线程任务和线程池,它提供了如下4个方法

  • ScheduledFuture schedule(Callable 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 延迟后执行,而且以设定频率重复执行(在 initialDelay 后开始执行,依次在 (initialDelay+period、initialDelay + 2* period)) 处重复执行

  • ScheduledFuture scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit) 创建并执行一个在给定初始延迟后首次启用的定期操作,随后在每一次执行中止和下一次执行开始之间都存在给定的延迟。如果任务在任意一次执行时遇到一场,就会取消后续执行;否则,只能通过程序来显示取消或中止该任务

用完一个线程池后,应该调用该线程池的shutdown() 方法,该方法将启动线程池的关闭序列,调用 shutdown() 方法后的线程不再接受新任务,但会将以前所有已提交任务执行完成

public class ThreadPoolTest {    public static void main(String[] args) throws Exception{        // 创建一个具有固定线程数(6)的线程池        ExecutorService pool = Executors.newFixedThreadPool(6);        // 创建 Runnable 对象        Runnable runnable = ()->{            for (int i = 0; i < 100; i++) {                System.out.println(Thread.currentThread().getName() + " " + i);            }          };        // 提交两个线程        pool.submit(runnable);        pool.submit(runnable);        // 关闭线程池        pool.shutdown();    }}

输出

...pool-1-thread-1 5pool-1-thread-1 6pool-1-thread-1 7pool-1-thread-2 1pool-1-thread-1 8pool-1-thread-2 2...

使用 ForkJoinPool

Java 7 提供了 ForkJoinPool 来支持将一个任务拆分成多个“小任务”并行计算,再把多个“小任务”的结果合并成总的计算结果。ForkJoinPool 时 ExecutorService 的实现类,因此是一种特殊的线程池。

ForkJoinPool 提供了如下两个常用的构造器

  • ForkJoinPool(int parallelism) 创建一个包含 paralleism 个并行线程的 ForkJoinPool。
  • ForkJoinPool()Runtime.availableProcessor() 方法的返回值作为 parallelism 参数来创建 ForkJoinPool

Java 8 为ForkJoinPool增加了通用池功能。ForkJoinPool 类通过如下两个静态方法提供通用吃功能

  • ForkJoinPool commonPool() 该方法返回一个通用池,通用池的运行状态不会受 shutdown()shutdownNow() 方法的影响。
  • int getCommonPoolParallelism() 该方法返回通用池的并行级别

创建了 ForkJoinPool 实例之后,就可以调用 ForkJoinPool 的 submit(ForkJoinTask task)invoke(ForkJoinTask task) 方法来执行指定任务了

ForkJoinTask 代表一个可以并行、合并的任务,是一个抽象类,它还有两个抽象子类:ResursiveAction和RecursiveTask。其中 ResursiveAction 表示有返回值的任务,RecursiveTask表示无返回值的任务

图片[2] - Java 多线程 - MaxSSL

public class CalTask extends RecursiveTask {    // 每个小任务最多累加到20个数    private static final int THRESHOLD = 20;    private int arr[];    private int start;    private int 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 个数字元素,并且将每个数字元素累加到 total上        for (int i = 0; i < arr.length; i++) {            int tmp = rand.nextInt(20);            total += (arr[i] = tmp);        }        // 打印通过遍历计算出的总值        System.out.println(total);        // 创建一个通用池        ForkJoinPool pool = ForkJoinPool.commonPool();        // 提交可分解的任务        Future future = pool.submit(new CalTask(arr,0,arr.length));        // 打印通过多线程计算出的总值        System.out.println(future.get());        // 关闭线程池        pool.shutdown();    }}

输出

954954

ThreadLocal

ThreadLocal 类代表一个线程局部变量,相当于为每个使用该变量的线程都提供一个变量值的副本,每个线程都可以独立地改变自己地副本,而不会和其他线程地副本冲突。

ThreadLocal 提供了如下三个 public 方法

  • T get() 返回此线程局部变量中当前线程副本中的值
  • void remove() 删除此线程局部变量中当前线程的值
  • void set(T value) 设置此线程局部变量中当前线程副本中的值

示例

public class A {    private ThreadLocal v = new ThreadLocal();    public Integer getV() {        return v.get();    }    public void setV(Integer v) {        this.v.set(v);    }}

新建一个线程类,让不同线程修改同个 A 对象中的 ThreadLocal 成员值

public class TestThread extends Thread{    private A a;    public TestThread(String name, A a) {        super(name);        this.a = a;    }    @Override    public void run() {        a.setV(0);        for (int i = 0; i < 100; i++) {            int v = a.getV();            System.out.println(Thread.currentThread().getName() + "的值为" + v);            a.setV(v + 1);        }    }}
public class Test {    public static void main(String[] args) {        A a = new A();        TestThread t1 = new TestThread("线程1", a);        TestThread t2 = new TestThread("线程2", a);        t1.start();        t2.start();    }}

输出

线程1的值为97线程2的值为94线程1的值为98线程2的值为95线程1的值为99线程2的值为96线程2的值为97

可以看到 线程1 和 线程2 中 A对象的 ThreadLocal 对象的值都是属于各自的

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享