Java多线程

  • 多线程的创建
  • 方式一:继承Thread类
  • 方式二:实现Runnable接口
  • 方式三:JDK 5.0新增:实现Callable接口
  • 创建线程三种方式对比
  • Thread的常用方法
  • 线程安全
  • 线程安全问题是什么、发生的原因
  • 线程安全问题案例模拟
  • 线程同步
  • 同步思想概述
  • 方式一:使用synchronized的同步代码块
  • 方式二:使用synchronized的同步方法
  • 方式三:Lock锁
  • 线程通信
  • 线程池
  • 线程池概述
  • 线程池实现的API、参数说明
  • int corePoolSize
  • int maximumPoolSize
  • long keepAliveTime
  • TimeUnit unit
  • BlockingQueue workQueue
  • ThreadFactory threadFactory
  • RejectedExecutionHandler handler
  • 线程池处理Runnable任务
  • 线程池处理Callable任务
  • Executors工具类实现线程池
  • newFixedThreadPool(int nThreads)
  • newSingleThreadExecutor ()
  • newCachedThreadPool()
  • newScheduledThreadPool(int corePoolSize)
  • Executors可能存在的陷阱对比总结
  • 定时器
  • Timer
  • ScheduledExecutorService
  • 并发、并行
  • 线程的生命周期


多线程的创建

方式一:继承Thread类

Thread类: Java是通过java.lang.Thread 类来代表线程的,下图是jdk1.8文档中介绍:

java多线程 知乎 java多线程教程_线程池

继承Thread类创建多线程的步骤:

1、定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
2、创建MyThread类的对象
3、调用线程对象的start()方法启动线程(启动后还是执行run方法的)

代码实现:

public class ThreadDemo1 {
    public static void main(String[] args) {
        Thread myThread = new MyThread();  // 多态的写法,只有继承和接口才可以使用多态
        /**
         * 注意事项:
         * 1、要通过调用start启动线程,本质上还是调用run方法,但是不能直接调用run方法,直接调用run方法会认为就是一个普通的类;
         * 2、子线程的start一定要放在主线程之前,要不然等到主线程内容执行完毕了再启动就达不到线程效果了。
         */
        myThread.start();  // 一定要放在主线程任务的前面进行启动,要不然主线程一直先运行,显示的为单线程效果。

        for(int i = 0; i < 10; i++){
            System.out.println(i+ "主线程正在运行~~~~");
        }

    }
}

// 创建子类MyThread继承Thread类并重写run方法
class MyThread extends Thread{
    @Override
    public void run() {
        for(int i = 0; i < 10; i++){
            System.out.println(i+ "子线程正在运行~~~~");
        }
    }
}

java多线程 知乎 java多线程教程_多线程_02


问题1、为什么不直接调用了run方法,而是调用start启动线程。

直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。只有调用start方法才是启动一个新的线程执行。

问题2、为什么要把子线程的启动(调用start)放在主线程(mian)任务之前?
如果主线程任务放在线程启动前面,这样主线程一直是先跑完的,相当于是一个单线程的效果了。

3、继承Thread类创建线程的优缺点是什么?
优点:编码简单缺点:存在单继承的局限性,线程类继承Thread后,不能继承其他类,不便于扩展。

方式二:实现Runnable接口

Thread的构造器

构造器

说明

public Thread(String name)

可以为当前线程指定名称

public Thread(Runnable target)

封装Runnable对象成为线程对象

public Thread(Runnable target ,String name )

封装Runnable对象成为线程对象,并指定线程名称

实现Runnable接口创建线程步骤

1、定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法。
2、创建MyRunnable任务对象。
3、把MyRunnable任务对象交给Thread处理。
4、调用线程对象的start()方法启动线程。

代码实现:

public class ThreadDemo2 {
    public static void main(String[] args) {
        // 3. 创建实现类对象
        Runnable myRunnable = new MyRunnable();  // 多态的写法
        // 4. 把MyRunnable任务对象交给Thread处理
        Thread thread = new Thread(myRunnable);
        thread.start(); // 启动线程

        for (int i = 0; i < 10; i++) {
            System.out.println("主线程正在执行~~~" + i);
        }
    }
}

// 1. 实现Runnable接口的类
class MyRunnable implements Runnable{
    // 2. 重写run方法
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("子线程正在执行~~~" + i);
        }
    }
}

实现Runnable接口(匿名内部类形式)

1、可以创建Runnable的匿名内部类对象。
2、交给Thread处理。
3、调用线程对象的start()启动线程。

使用匿名内部类创建线程代码

public class ThreadDemo2Others {
    public static void main(String[] args) {
        Runnable myRunnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("子线程1再运行" + i);
                }
            }
        };
        Thread thread = new Thread(myRunnable);
        thread.start();
        
        // 把new Runnable()对象直接放在Thread类里面创建线程
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("子线程2再运行" + i);
                }
            }
        });
        thread1.start();
        
        /**
         * Runnable接口为函数式接口,可以进行Lambda表达式进行简化
         * @FunctionalInterface
         * public interface Runnable {
         *public abstract void run ();
         *}
         */
        new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    System.out.println("子线程3再运行" + i);
                }
            }
        ).start();


        for (int i = 0; i < 10; i++) {
            System.out.println("主线程再运行" + i);
        }
    }
}

问题1:实现Runnable接口创建线程优缺点?
优点:线程任务类只是实现了Runnale接口,可以继续继承和实现
缺点:如果线程有执行结果是不能直接返回的

方式三:JDK 5.0新增:实现Callable接口

通过继承Thread和实现Runnable创建线程的方式都具有一个共同的缺点,那就是他们重写的run方法均不能直接返回结果,显然不适合需要返回线程执行结果的业务场景。

为了解决这个问题,JDK 5.0提供了Callable和FutureTask来实现,这种方式的优点是:可以得到线程执行的结果。

利用Callable、FutureTask接口创建线程

1、得到任务对象
	定义类实现Callable接口,重写call方法,封装要做的事情。
	用FutureTask把Callable对象封装成线程任务对象。
2、把线程任务对象交给Thread处理;
3、调用Thread的start方法启动线程,执行任务
4、线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。

FutureTask的API

方法名称

说明

public V get() throws Exception

获取线程执行call方法返回的结果。

public FutureTask<>(Callable call)

把Callable对象封装成FutureTask对象。

代码实现:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadDemo3 {
    public static void main(String[] args) {
        // 3. 创建Callable任务对象
        Callable<String> myCallable = new MyCallable(100);
        // 4. 把Callable任务对象  交给  FutureTask对象(FutureTask间接上是Runnable的实现类)
        // public class FutureTask<V> implements RunnableFuture<V>
        // public interface RunnableFuture<V> extends Runnable, Future<V> {
        // FutureTask对象的作用:
        // 1、是Runnable的对象(实现了Runnable接口),可以交给Thread了;
        // 2、可以在线程执行完毕之后通过调用其get方法得到线程执行完成的结果;
        FutureTask<String> futureTask1 = new FutureTask<>(myCallable);
        // 5、这里一定要注意Thread构造方法接收的数据类型有那些,不可以直接接收Callable对象,所以要转化为Runable对象进行接收
        Thread thread = new Thread(futureTask1);
        // 6、启动线程
        thread.start();

        Callable<String> myCallable2 = new MyCallable(200);
        FutureTask<String> futureTask2 = new FutureTask<>(myCallable2);
        Thread thread2 = new Thread(futureTask2);
        thread2.start();

        try {
            String s = futureTask1.get();
            System.out.println("线程1返回结果:" + s);
        } catch (Exception e) {
            e.printStackTrace();
        }


        try {
            String s = futureTask2.get();
            System.out.println("线程2返回结果:" + s);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

/**
 * 1、定义一个任务类 实现Callable接口 应该声明线程任务执行完毕后的结果的数据类型,也就是泛型
 * 查看Callable源码:V为泛型
 * @FunctionalInterface
 * public interface Callable<V> {
 *      V call()throws Exception;
 * }
 */
class MyCallable implements Callable<String>{
    private int n;
    public MyCallable(int n){
        this.n = n;
    }

    /**
     * 2、重写call方法(任务方法)
     */
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return String.valueOf(sum);
    }
}

java多线程 知乎 java多线程教程_java多线程 知乎_03


问题1:方式三优缺点

优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强,可以在线程执行完毕后去获取线程执行的结果
缺点:编码复杂一点

创建线程三种方式对比

方式

优点

缺点

继承Thread类

编程比较简单,可以直接使用Thread类中的方法

扩展性较差,不能再继承其他的类,不能返回线程执行的结果

实现Runnable接口

扩展性强,实现该接口的同时还可以继承其他的类。

编程相对复杂,不能返回线程执行的结果

实现Callable接口

扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果

编程相对复杂


Thread的常用方法

Thread常用API说明
Thread常用方法:获取线程名称getName()、设置名称setName()、获取当前线程对象currentThread()。
至于Thread类提供的诸如:yield、join、interrupt、不推荐的方法 stop 、守护线程、线程优先级等线程的控制方法,在开发中很少使用,这些方法会在高级篇以及后续需要用到的时候再为大家讲解

问题1:当有很多线程在执行的时候,我们怎么去区分这些线程呢?
此时需要使用Thread的常用方法:getName()、setName()、currentThread()等

Thread获取和设置线程名称

方法名称

说明

String getName()

获取当前线程的名称,默认线程名称是Thread-索引

void setName(String name)

将此线程的名称更改为指定的名称,通过构造器也可以设置线程名称

Thread类获得当前线程的对象

方法名称

说明

public static Thread currentThread():

返回对当前正在执行的线程对象的引用

注意

1、此方法是Thread类的静态方法,可以直接使用Thread类调用。
 2、这个方法是在哪个线程执行中调用的,就会得到哪个线程对象。

Thread类的线程休眠方法

方法名称

说明

public static void sleep(long time)

让当前线程休眠指定的时间后再继续执行,单位为毫秒

继承Thread创建子线程

public class MyThread extends Thread{
    public MyThread(){
    }

    public MyThread(String name){
        // 为当前线程对象设置名称,送给父类有参数构造器初始化名称
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "线程正在运行" + i);
        }
    }
}

测试案例

public class ThreadDemo1 {
    public static void main(String[] args) {
        Thread thread1 = new MyThread();  // 多态的写法,只能调用Thread和MyThread共有的方法
        thread1.setName("1号子线程");
        thread1.start();
//        System.out.println(thread1.getName());  // 如果没有setName默认为Thread-0

        Thread thread2 = new MyThread();
        thread2.start();
        thread2.setName("2号子线程");
//        System.out.println(thread2.getName());  // 如果没有setName默认为Thread-1

        // 哪个线程执行它,它就得到哪个线程对象(当前线程对象)
        // 主线程的名称就叫main
        Thread thread = Thread.currentThread();  // 获得当前正在运行的线程,一般为主线程
        thread.setName("最牛的主线程");
//        System.out.println(thread.getName());  // 如果没有setName默认为main

        for (int i = 0; i < 5; i++) {
            System.out.println(thread.getName() + "线程正在运行" + i);
        }
    }
}

java多线程 知乎 java多线程教程_java_04


线程安全

线程安全问题是什么、发生的原因

多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题

取钱模型演示

需求:

小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元。


如果小明和小红同时来取钱,而且2人都要取钱10万元,可能出现什么问题呢?

java多线程 知乎 java多线程教程_java多线程 知乎_05


问题1:线程安全问题出现的原因?

1、存在多线程并发

2、同时访问共享资源

3、存在修改共享资源

线程安全问题案例模拟

取钱业务
需求:
小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万。

分析:

①:需要提供一个账户类,创建一个账户对象代表2个人的共享账户。

②:需要定义一个线程类,线程类可以处理账户对象。

③:创建2个线程对象,传入同一个账户对象。

④:启动2个线程,去同一个账户对象中取钱10万。

创建Account账户

public class Account {
   private String cardId;
   private double money;

   public Account(){
   }

   public Account(String cardId, double money){
       this.cardId = cardId;
       this.money = money;
   }
	// 省略掉了getXXX/setXXX方法....

    public void drawMoney(double money) {
       if(this.money >= money){
           System.out.println(Thread.currentThread().getName() + "取钱为:" + money);
           this.money -= money;
           System.out.println("账户余额为:" + this.money);
       }
    }
}

继承Thread方式创建线程

public class DrawThread extends Thread {
    private Account account;

    DrawThread(){
    }

    public DrawThread(String name, Account account){
        super(name);  // 设置线程的名字
        this.account = account;
    }

    @Override
    public void run() {  // 取钱操作
        account.drawMoney(100000);
    }
}

创建测试类

public class TestSafeDemo {
    public static void main(String[] args) {
        // 1、 创建一个共享的账户对象
        Account account = new Account("ICBC-111", 100000);

        // 2、创建2个线程对象,操作同一个账户对象
        new DrawThread("小明", account).start();
        new DrawThread("小李", account).start();
    }
}

java多线程 知乎 java多线程教程_java_06


线程同步

同步思想概述

线程同步的核心思想加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来

java多线程 知乎 java多线程教程_java_07

方式一:使用synchronized的同步代码块

同步代码块作用: 把出现线程安全问题的核心代码给上锁。

原理: 每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。
格式

synchronized(同步锁对象){
		操作共享资源的核心代码;
}

示例代码:

/**
     * 对于实例方法建议使用this作为锁对象。
     *  对于静态方法建议使用字节码(类名.class)对象作为锁对象。
     */
    public void drawMoney(double money) {
        synchronized (this) {
            if(this.money >= money){
                System.out.println(Thread.currentThread().getName() + "取钱成功:" + money + "账户余额为:" + (this.money - money));
                this.money -= money;
            }else {
                System.out.println(Thread.currentThread().getName() + "发现账户余额不足,仅还有" + this.money +", 无法进行取钱。");
            }
        }
    }

java多线程 知乎 java多线程教程_System_08


问题1: 锁对象用任意唯一的对象好不好呢?

不好,会影响其他无关线程的执行

问题2:同步代码块是如何实现线程安全的?
对出现问题的核心代码使用synchronized进行加锁,每次只能一个线程占锁进入访问

问题3:同步代码块的同步锁对象有什么要求?
对于实例方法建议使用this作为锁对象
对于静态方法建议使用字节码(类名.class)对象作为锁对象

方式二:使用synchronized的同步方法

同步方法作用:把出现线程安全问题的核心方法给上锁。

原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
格式

修饰符 synchronized 返回值类型 方法名称(形参列表) {	 
		操作共享资源的代码
}

示例代码

public synchronized void drawMoney(double money) {
        if(this.money >= money){
            System.out.println(Thread.currentThread().getName() + "取钱成功:" + money + ", 账户余额为:" + (this.money - money));
            this.money -= money;
        }else {
            System.out.println(Thread.currentThread().getName() + "发现账户余额不足,仅还有" + this.money +", 无法进行取钱。");
        }
    }

java多线程 知乎 java多线程教程_线程池_09


同步方法底层原理

1、同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。


2、如果方法是实例方法:同步方法默认用this作为的锁对象。但是代码要高度面向对象!


3、如果方法是静态方法:同步方法默认用类名.class作为的锁对象。

问题1:是同步代码块好还是同步方法好一点?
同步代码块锁的范围更小,同步方法锁的范围更大

问题2:同步方法是如何保证线程安全的?
对出现问题的核心方法使用synchronized修饰,每次只能一个线程占锁进入访问

问题3:同步方法的同步锁对象的原理?
对于实例方法默认使用this作为锁对象
对于静态方法默认使用类名.class对象作为锁对象

方式三:Lock锁

Lock锁

为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便。

Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象。

java多线程 知乎 java多线程教程_多线程_10

Lock的API

方法名称

说明

void lock()

获得锁

void unlock

释放锁

java多线程 知乎 java多线程教程_多线程_11

方法名称

说明

public ReentrantLock()

获得Lock锁的实现类对象

代码实现:

public class Account {
   private String cardId;
   private double money;
   // final修饰后:锁对象是唯一和不可替换的,非常专业
   // Lock是一个接口,不能直接实例化对象,一般使用ReentrantLock()获取对象
   // private final Lock lock = new ReentrantLock();  // 多态的写法
   private final ReentrantLock lock = new ReentrantLock();

   public Account(String cardId, double money){
       this.cardId = cardId;
       this.money = money;
   }

  // 省略了getXXX/setXXX方法

    public  void drawMoney(double money) {
       lock.lock();  // 上锁
        try {
            if(this.money >= money){
                System.out.println(Thread.currentThread().getName() + "取钱成功:" + money + ", 账户余额为:" + (this.money - money));
                this.money -= money;
            }else {
                System.out.println(Thread.currentThread().getName() + "发现账户余额不足,仅还有" + this.money +", 无法进行取钱。");
            }
        } finally {
            lock.unlock();  // 解锁,无论try中是否抛出异常都会运行finally中的解锁功能
        }
    }
}

java多线程 知乎 java多线程教程_线程池_12


线程通信

线程通信时多个线程等待与唤醒的一种机制。多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助控制线程之间对同一资源的协调访问

线程通信常见模型

生产者与消费者模型:生产者线程负责生产线程,消费者线程负责消费生产者产生的数据。

要求:生产者线程生产完数据唤醒消费者,然后等待自己,消费者消费完该数据后唤醒生产者,然后等待自己。

Object类的等待和唤醒方法

方法名称

说明

void wait()

让当前线程等待并释放所占锁,直到另一个线程调用notify()或者notifyAll()方法

void notify()

唤醒正在等待的单个线程

void notifyAll()

唤醒正在等待的所有线程

线程通信案例模拟

java多线程 知乎 java多线程教程_System_13

Account账户类

public class Account {
    private String cardId;
    private double money;

    public Account(){}

    public Account(String cardId, double money){
        this.cardId = cardId;
        this.money = money;
    }

   // 省略了getXXX/setXXX方法

    // 小明,小红这两个线程取钱,要上锁
    public synchronized void drawMoney(double money) {
        try {
            String name = Thread.currentThread().getName();
            if(this.money > 0){  // 可以取钱
                this.money -= money;
                System.out.println(name + "取款前账户余额为:" + (this.money + money) + ", 取款后账户余额为:" + this.money);

                this.notifyAll();  // 唤醒所有
                this.wait();  // 自己等待

            }else {
                this.wait();  // 自己等待
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 父亲,岳父以及干爹存钱
    public synchronized void depositMoney(double money){
        try {
            String name = Thread.currentThread().getName();
            if(this.money > 0){  // 证明还有钱,直接等待即可
                this.wait();  // 自己等待
            }else { // 证明账户里面没有钱了,需要往里面存钱
                this.money += money;
                System.out.println(name + "存款前账户余额为:" + (this.money - money) + ", 存款后账户余额为:" + this.money);

                this.notifyAll();  // 唤醒所有
                this.wait();  // 自己等待
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

小明、小红取钱线程类DrawThread

public class DrawThread extends Thread{
    private Account account;
    public DrawThread(){};

    public DrawThread(Account account, String name){
        super(name);
        this.account = account;
    }

    @Override
    public void run() {
        while (true){
            account.drawMoney(10000);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

父亲、岳父、干爹存钱线程类DepositThread

public class DepositThread extends Thread{
    private Account account;

    public DepositThread(){}

    public DepositThread(Account account, String name){
        super(name);
        this.account = account;
    }

    @Override
    public void run() {
        while (true){
            account.depositMoney(10000);

            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

测试类TestDemo

public class TestDemo {
    public static void main(String[] args) throws Exception {
        // 重定向到文件中
        PrintStream ps = new PrintStream("E:/log.log");
        System.setOut(ps);

        Account account = new Account("ICBC-1111", 0);

        // 创建两个取钱线程: 小明 和 小红
        new DrawThread(account, "小明").start();
        new DrawThread(account, "小红").start();

        // 创建三个存钱线程: 干爹 亲爹 和 岳父
        new DepositThread(account, "干爹").start();
        new DepositThread(account, "亲爹").start();
        new DepositThread(account, "岳父").start();
    }
}

java多线程 知乎 java多线程教程_多线程_14



线程池

线程池概述

线程池就是一个可以复用线程的技术

不使用线程池的问题 :如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能。

java多线程 知乎 java多线程教程_多线程_15

线程池实现的API、参数说明

谁代表线程池? JDK 5.0起提供了代表线程池的接口:ExecutorService

如何得到线程池对象
方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象

java多线程 知乎 java多线程教程_System_16

方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象

ThreadPoolExecutor构造器的参数说明

public ThreadPoolExecutor(int corePoolSize,                         
						int maximumPoolSize,                          
						long keepAliveTime,                         
						TimeUnit unit,                          
						BlockingQueue<Runnable> workQueue,                          
						ThreadFactory threadFactory,                          
						RejectedExecutionHandler handler) 



参数一:指定线程池的线程数量(核心线程): corePoolSize     --------> 不能小于0
参数二:指定线程池可支持的最大线程数: maximumPoolSize      --------> 最大数量 >= 核心线程数量
参数三:指定临时线程的最大存活时间: keepAliveTime         --------> 不能小于0
参数四:指定存活时间的单位(秒、分、时、天): unit           --------> 时间单位
参数五:指定任务队列: workQueue                          --------> 不能为null
参数六:指定用哪个线程工厂创建线程: threadFactory          --------> 不能为null
参数七:指定线程忙,任务满的时候,新任务来了怎么办: handler --------> 不能为null

面试常见问题1:临时线程什么时候创建啊?
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程

面试常见问题2:什么时候会开始拒绝任务?
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝

面试常见问题3:谁代表线程池?
ExecutorService接口

int corePoolSize

作用:指定线程池的线程数量(核心线程),不能为0.

int maximumPoolSize

作用:指定线程池可支持的最大线程数,最大数量 >= 核心线程数量.

long keepAliveTime

作用:指定临时线程的最大存活时间.

TimeUnit unit

作用:时间单位,指定存活时间的单位(秒、分、时、天)。

通过查询Java的API可知TimeUnit为一个时间枚举类, 封装有DAYS, HOURS, MINUTES, SECONDS等常量,一般常用SECONDS.

java多线程 知乎 java多线程教程_java_17

java多线程 知乎 java多线程教程_线程池_18

BlockingQueue workQueue

作用:指定任务队列,不能为null

通过查询Java的API可知BlockingQueue为接口,实现类有ArrayBlockingQueue ,LinkedBlockingDeque 等。

java多线程 知乎 java多线程教程_java_19

ThreadFactory threadFactory

作用:指定用哪个线程工厂创建线程,不能为null

通过查询Java的API可知ThreadFactory 类为一个接口,长使用Executors.defaultThreadFactory()默认方式创建线程工厂。

java多线程 知乎 java多线程教程_线程池_20

java多线程 知乎 java多线程教程_java_21

RejectedExecutionHandler handler

作用: 指定线程忙,任务满的时候,新任务来了怎么办,不能为null

handler有四种形式(拒绝策略)

策略

说明

ThreadPoolExecutor.AbortPolicy

丢弃任务并抛出RejectedExecutionException异常。是默认的策略

ThreadPoolExecutor.DiscardPolicy:

丢弃任务,但是不抛出异常 这是不推荐的做法

ThreadPoolExecutor.DiscardOldestPolicy

抛弃队列中等待最久的任务 然后把当前任务加入队列中

ThreadPoolExecutor.CallerRunsPolicy

由主线程负责调用任务的run()方法从而绕过线程池直接执行

线程池处理Runnable任务

ThreadPoolExecutor创建线程池对象示例

ExecutorService pools = new ThreadPoolExecutor(3, 5, 8 , 
												TimeUnit.SECONDS,   new ArrayBlockingQueue<>(6),            
												Executors.defaultThreadFactory() , new ThreadPoolExecutor.AbortPolicy());

ExecutorService的常用方法

方法名称

说明

void execute(Runnable command)

执行任务/命令,没有返回值,一般用来执行 Runnable 任务

Future<\T> submit(Callable<\T> task)

执行任务,返回未来任务对象获取线程结果,一般拿来执行 Callable 任务

void shutdown()

等任务执行完毕后关闭线程池

List<Runnable> shutdownNow()

立刻关闭,停止正在执行的任务,并返回队列中未执行的任务

通过实现Runnable接口创建线程

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "输出了:Hello World ===>");

        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

创建线程池对象

public class ThreadPoolExecutorDemo {
    public static void main(String[] args) {
        // 1. 创建线程池对象
        /**
         * 核心线程个数:2
         * 线程池可支持的最大线程数:4(那么临时线程最多有 5 - 2 = 3个)
         * 临时线程的最大存活时间:6
         * 存活时间的单位:TimeUnit.SECONDS(表示秒)
         * 任务队列:new ArrayBlockingQueue<>(5) (表示任务队列中最多存储5个任务)
         * 指定用哪个线程工厂创建线程:Executors.defaultThreadFactory()(表示用默认的方式创建线程工厂)
         * 指定线程忙,任务满的时候,新任务来了怎么办:new ThreadPoolExecutor.AbortPolicy()(表示新任务来了丢弃任务并抛出RejectedExecutionException异常)
         */
        ExecutorService pool = new ThreadPoolExecutor(2, 4, 6,
                                                    TimeUnit.SECONDS, new ArrayBlockingQueue<>(5),
                                                    Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

        // 2、给任务线程池处理
        Runnable target = new MyRunnable();
        pool.execute(target);
        pool.execute(target);  // 到这里核心线程都在运行

        pool.execute(target);  // 添加到任务队列中、
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);  // 五个线程都添加到任务队列当中

        // 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程
        pool.execute(target);  // maximumPoolSize - corePoolSize = 4 - 2,则可以额外创建2个线程
        pool.execute(target);

        // 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝
//        pool.execute(target);  // 报错.RejectedExecutionException  pool size = 4, active threads = 4, queued tasks = 5, completed tasks = 0

        // 关闭线程池(开发中一般不会使用)。
        // pool.shutdownNow(); // 立即关闭,即使任务没有完成,会丢失任务的!
        // pool.shutdown(); // 会等待全部任务执行完毕之后再关闭(建议使用的)
    }
}

java多线程 知乎 java多线程教程_多线程_22

java多线程 知乎 java多线程教程_线程池_23

线程池处理Callable任务

ExecutorService的常用方法

方法名称

说明

void execute(Runnable command)

执行任务/命令,没有返回值,一般用来执行 Runnable 任务

Future<\T> submit(Callable<\T> task)

执行任务,返回未来任务对象获取线程结果,一般拿来执行 Callable 任务

void shutdown()

等任务执行完毕后关闭线程池

List<Runnable> shutdownNow()

立刻关闭,停止正在执行的任务,并返回队列中未执行的任务

定义一个任务类,实现Callable接口,应该声明线程任务执行完毕后的结果数据类型

public class MyCallable implements Callable<String> {
    private int n;

    public MyCallable(int n){
        this.n = n;
    }
    
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }

        return Thread.currentThread().getName() + "执行从 1-" + n + "的和为:" + sum;
    }
}

创建线程池对象

public class ThreadPoolsSubmitDemo {
    public static void main(String[] args) throws Exception{
        // 1、创建线程池对象
        /**
         * 核心线程个数:3
         * 线程池可支持的最大线程数:5(那么临时线程最多有 5 - 3 = 2个)
         * 临时线程的最大存活时间:6
         * 存活时间的单位:TimeUnit.SECONDS(表示秒)
         * 任务队列:new ArrayBlockingQueue<>(5) (表示任务队列中最多存储5个任务)
         * 指定用哪个线程工厂创建线程:Executors.defaultThreadFactory()(表示用默认的方式创建线程工厂)
         * 指定线程忙,任务满的时候,新任务来了怎么办:new ThreadPoolExecutor.AbortPolicy()(表示新任务来了丢弃任务并抛出RejectedExecutionException异常)
         */
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 6,
                TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(5),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

        // 2、给任务线程吃处理
        Future<String> f1 = pool.submit(new MyCallable(10));
        Future<String> f2 = pool.submit(new MyCallable(20));
        Future<String> f3 = pool.submit(new MyCallable(30));
        Future<String> f4 = pool.submit(new MyCallable(40));
        Future<String> f5 = pool.submit(new MyCallable(50));
        Future<String> f6 = pool.submit(new MyCallable(60));
        Future<String> f7 = pool.submit(new MyCallable(70));

        // String res = f1.get();
//        System.out.println();
        System.out.println(f1.get());  // 55
        System.out.println(f2.get());  // 210
        System.out.println(f3.get());  // 465
        System.out.println(f4.get());  // 820
        System.out.println(f5.get());  // 1275
        System.out.println(f6.get());  // 1830
        System.out.println(f7.get());  // 2485
    }
}

java多线程 知乎 java多线程教程_java多线程 知乎_24

Executors工具类实现线程池

Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。 四种静态方法,可以直接通过类名.方法()进行调用

Executors得到线程池对象的常用方法

方法名称

说明

public static ExecutorService newFixedThreadPool(int nThreads)

创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。

public static ExecutorService newSingleThreadExecutor ()

创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。

public static ExecutorService newCachedThreadPool()

线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了一段时间则会被回收掉。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。

newFixedThreadPool(int nThreads)

作用:创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。

newFixedThreadPool底层源码查看核心线程数量和线程池中的最大线程数量都为nThreads, 但是对任务队列的长度没有限制,也就是说允许请求的任务队列长度是Integer.MAX_VALUE,可能出现OOM错误( java.lang.OutOfMemoryError )

java多线程 知乎 java多线程教程_System_25

newSingleThreadExecutor ()

作用:创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。

newSingleThreadExecutor 底层源码查看核心线程数量和线程池中的最大线程数量都为1个, 相当于单线程一样,但是对任务队列的长度没有限制,也就是说允许请求的任务队列长度是Integer.MAX_VALUE,可能出现OOM错误( java.lang.OutOfMemoryError )

java多线程 知乎 java多线程教程_java多线程 知乎_26

newCachedThreadPool()

作用:线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了一段时间则会被回收掉。

newCachedThreadPool底层源码查看:其中corePoolSize核心线程设置为0,创建的线程数量最大上限是Integer.MAX_VALUE,线程数可能会随着任务1:1增长,也可能出现OOM错误( java.lang.OutOfMemoryError )

java多线程 知乎 java多线程教程_多线程_27

newScheduledThreadPool(int corePoolSize)

作用:创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。

newScheduledThreadPool底层源码查看:其中corePoolSize核心线程设置为corePoolSize,创建的线程数量最大上限是Integer.MAX_VALUE,线程数可能会随着任务1:1增长bing,也可能出现OOM错误( java.lang.OutOfMemoryError )

java多线程 知乎 java多线程教程_线程池_28

java多线程 知乎 java多线程教程_System_29

Executors可能存在的陷阱对比总结

Executors使用可能存在的陷阱: 大型并发系统环境中使用Executors如果不注意可能会出现系统风险。

java多线程 知乎 java多线程教程_java多线程 知乎_30


例如阿里巴巴开发手册中就对Executors使用可能存在的陷阱做出明确规定:

java多线程 知乎 java多线程教程_java_31


问题1:Executors工具类底层是基于什么方式实现的线程池对象?

线程池ExecutorService的实现类:ThreadPoolExecutor

问题2:Executors是否适合做大型互联网场景的线程池方案?
不合适。建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确线程池的运行规则,规避资源耗尽的风险

注意:Executors的底层其实也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的


定时器

定时器是一种控制任务延时调用,或者周期调用的技术。作用一般是闹钟、定时邮件发送

Timer

构造器

说明

public Timer()

创建Timer定时器对象

方法

说明

public void schedule(TimerTask task, long delay, long period)

开启一个定时器,按照计划处理TimerTask任务

注意:TimerTask是一个抽象类,继承了Object类实现了Runnable接口, delay表示推迟多少毫秒第一次执行TimerTask任务,period表示每隔多少毫秒再次执行TimerTask

java多线程 知乎 java多线程教程_多线程_32


启动1个没有问题的任务计划

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

public class TimerDemo1 {
    public static void main(String[] args) throws Exception {
        // 1. 创建Timer定时器对象
        Timer timer = new Timer();

        // 2. 调用共schedule方法, delay为3000毫秒,period设置为2000毫秒
         // 2. 调用共schedule方法
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
//                String threadName = Thread.currentThread().getName();  // 获取当前线程名字
//
//                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  // 时间格式化
//                Date date = new Date();
//                String strTime = sdf.format(date);
//                System.out.println(threadName + "执行AAA ====> " + strTime);
//                System.out.println(10 / 0);
                // 上面几行代码相当于下面一行代码
                System.out.println(Thread.currentThread().getName() + "执行输出AAA ==> " + new SimpleDateFormat("yyyy-MM-dd HH-mm-ss").format(new Date()));
            }
        }, 3000, 2000);
       }
   }

java多线程 知乎 java多线程教程_线程池_33

启动2个没有问题的任务计划

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

public class TimerDemo1 {
    public static void main(String[] args) throws Exception {
        // 利用了输出重定向
//        PrintStream ps = new PrintStream("E:/timer.log");
//        System.setOut(ps);

        // 1. 创建Timer定时器对象
        Timer timer = new Timer();

        // 2. 调用共schedule方法
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "执行输出AAA ==> " + new SimpleDateFormat("yyyy-MM-dd HH-mm-ss").format(new Date()));
            }
        }, 3000, 2000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                 System.out.println(Thread.currentThread().getName() + "执行输出BBB ==> " + new SimpleDateFormat("yyyy-MM-dd HH-mm-ss").format(new Date()));
    }
}

java多线程 知乎 java多线程教程_线程池_34

启动2个没有问题的任务计划,设置执行A计划的线程出现问题时候:

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

public class TimerDemo1 {
    public static void main(String[] args) throws Exception {
        // 1. 创建Timer定时器对象
        Timer timer = new Timer();

        // 2. 调用共schedule方法
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                public void run() {
                 System.out.println(Thread.currentThread().getName() + "执行输出AAA ==> " + new SimpleDateFormat("yyyy-MM-dd HH-mm-ss").format(new Date()));
                System.out.println(10 / 0);  // 存在问题代码
            }
        }, 3000, 2000);

        timer.schedule(new TimerTask() {
            @Override
             public void run() {
                 System.out.println(Thread.currentThread().getName() + "执行输出BBB ==> " + new SimpleDateFormat("yyyy-MM-dd HH-mm-ss").format(new Date()));
        }, 3000, 2000);
    }
}

通过下面实验截图可以发现,线程执行完A计划后,遇到10 / 0报错,线程Thread-0挂掉,任务B并没有被执行,这是Timer定时器存在的问题。

java多线程 知乎 java多线程教程_多线程_35


Timer定时器的特点和存在的问题

1、Timer是单线程,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入

2、可能因为其中的某个任务的异常使Timer线程死掉,从而影响后续任务执行

ScheduledExecutorService

ScheduledExecutorService定时器是 jdk1.5中引入了并发包,目的是为了弥补Timer的缺陷, ScheduledExecutorService内部为线程池。

Executors的方法

说明

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

得到线程池对象

ScheduledExecutorService的方法

说明

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)

周期调度方法

利用ScheduledExecutorService定时器启动3个任务计划A,B,C,其中任务B中存在代码System.out.println(10 / 0),执行查看效果

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * ScheduledExecutorService是 jdk1.5中引入了并发包,目的是为了弥补Timer的缺陷, ScheduledExecutorService内部为线程池。
 */
public class TimerDemo2 {
    public static void main(String[] args) {
        // 1. 创建线程池对象
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);

        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "执行输出AAA ==> " + new SimpleDateFormat("yyyy-MM-dd HH-mm-ss").format(new Date()));
            }
        }, 3, 2, TimeUnit.SECONDS);


        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "执行输出BBB ==> " + new SimpleDateFormat("yyyy-MM-dd HH-mm-ss").format(new Date()));
                System.out.println(10 / 0);
            }
        }, 3, 2, TimeUnit.SECONDS);


        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "执行输出CCC ==> " + new SimpleDateFormat("yyyy-MM-dd HH-mm-ss").format(new Date()));
            }
        }, 3, 2, TimeUnit.SECONDS);
        
    }
}

任务B中存在一段有问题的代码,线程poo-1-thread-2执行任务B时候遇到了问题代码,该线程挂掉,线程池由生成了一个名字为poo-1-

thread-2的线程(保持线程池中的数量始终为3个),但是仍然不影响任务A和任务C的运行,这就是比Timer计时器的优势所在。

java多线程 知乎 java多线程教程_多线程_36

ScheduledExecutorService的优点:
1、基于线程池,某个任务的执行情况不会影响其他定时任务的执行


并发、并行

正在运行的程序(软件)就是一个独立的进程, 线程是属于进程的,多个线程其实是并发与并行同时进行的

并发的理解(CPU分时轮询的执行线程)

CPU同时处理线程的数量有限

CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发

java多线程 知乎 java多线程教程_java_37


并行的理解(同一个时刻同时在执行)

在同一个时刻上,同时有多个线程在被CPU处理并执行

java多线程 知乎 java多线程教程_System_38


并行和并发区别一

并发是指一个处理器同时处理多个任务。
并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。
并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。
一个恰当的比喻:并发是一个人同时吃三个馒头(吃的时候有先后顺序),而并行是三个人同时吃三个馒头。

并行和并发区别二

并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。这就好像两个人用同一把铁锨,轮流挖坑,一小时后,两个人各挖一个小一点的坑,要想挖两个大一点得坑,一定会用两个小时。

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。就好像两个人各拿一把铁锨在挖坑,一小时后,每人一个大坑。所以无论从微观还是从宏观来看,二者都是一起执行的。

java多线程 知乎 java多线程教程_System_39


并行和并发区别三

当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态。这种方式我们称之为并发(Concurrent)。

当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。


线程的生命周期

线程的状态:也就是线程从生到死的过程,以及中间经历的各种状态及状态转换,理解线程的状态有利于提升并发编程的理解能力。

Java总共定义了6种状态,6种状态都定义在Thread类的内部枚举类中,如下查看Thread源码信息:

public enum State {
        NEW, // 新建状态
        
        RUNNABLE,  // 可运行状态
        
        BLOCKED,  // 阻塞状态

        WAITING,  // 无线等待状态

        TIMED_WAITING,  // 计时等待状态

        TERMINATED;  // 终止状态
    }

    public State getState() {
        // get current thread state
        return sun.misc.VM.toThreadState(threadStatus);
    }

Java线程的6中状态
1、初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2、运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3、阻塞(BLOCKED):表示线程阻塞于锁。
4、等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
5、超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
6、终止(TERMINATED):表示该线程已经执行完毕。

线程的6种状态总结

线程状态

描述

NEW(新建)

线程刚被创建,但是并未启动。

Runnable(可运行)

线程已经调用了start()等待CPU调度

Blocked(锁阻塞)

线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态;。

Waiting(无限等待)

一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒

Timed Waiting(计时等待)

同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。带有超时参数的常用方法有Thread.sleep 、Object.wait。

Teminated(被终止)

因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

线程的6种状态互相转换:

java多线程 知乎 java多线程教程_System_40