JAVA-多线程

本文详细介绍了Java中的多线程概念,包括线程的创建(继承Thread类和实现Runnable接口)、线程的生命周期、线程同步(synchronized关键字和Lock锁)、线程通信(volatile和等待/通知机制)、线程组、ThreadLocal以及线程池的使用和原理,旨在帮助读者深入理解Java线程编程。
摘要由CSDN通过智能技术生成

目录

1 概述

2 多线程的创建和使用

2.1 多线程创建

2.2 多线程使用

3 线程生命周期

4 线程同步

4.1 为什么要线程同步?

4.2 线程同步实现

4.2.1 synchronized 关键字

4.2.2 lock锁

4.2.3 锁的分类

4.2.4 死锁

4.3 线程通信

4.3.1 volatile通信

4.3.2 等待/通知机制

5 线程组和线程池

5.1 Callable接口创建线程

5.2 线程组

5.3 ThreadLocal

5.4 线程池

5.4.1 为什么使用线程池

5.4.2 线程池原理

5.4.3 线程池生命周期

5.4.4 线程池分类

5.4.5 线程池案例


1 概述

        进程(process) 是程序的一次执行过程,或是一个正在执行的程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。
        后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程,线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。

2 多线程的创建和使用

2.1 多线程创建

  • 继承Thread类
public class ThreadTest01 {
    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        list.add("123");
        list.add(123);
        System.out.println(list);

        MyThread myThread = new MyThread();
        myThread.start();   //创建一个独立的栈内存空间并开启线程
        for (int i = 0; i < 1000; i++) {
            System.out.println("主线程--->" + i);
        }
    }
}

/**
 * 继承Thread 重写run()方法实现多线程
 */
class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("分支线程--->" + i);
        }
    }
}
  • 实现Runnable接口
public class ThreadTest02 {
    public static void main(String[] args) {
//        Runnable r = new MyRunnable();
//        Thread thread = new Thread(r);
        Thread thread = new Thread(new MyRunnable());   //带参构造 提供一个Runnable接口的实现类
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("匿名内部类-分支线程--->" + i);
                }
            }
        });

        thread.start();
        thread2.start();
        for (int i = 0; i < 100; i++) {
            System.out.println("主线程--->" + i);
        }
    }
}

/**
 * 实现Runnable接口 重写run()方法实现多线程
 *
 * 建议使用实现接口的方式
 * 原因:更灵活,因为实现接口还可以继承其他类......
 */
class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("分支线程--->" + i);
        }
    }
}
  • 两种方式对比
    1. 继承Thread可以直接使用Thread中的方法,代码简单,但是如果有父类则无法继承Thread,因为java是单继承机制;
    2. 实现Runnable接口避免了单继承的局限性,但是不能直接使用Thread中的方法,需要先获取到线程对象才能继续其他操作。

2.2 多线程使用

  • 获取当前线程以及名字
public class ThreadTest03 {

    public static void doSome() {
        //此句在哪儿 获取的当前线程名就是谁
        Thread currentThread = Thread.currentThread();  //获取当前线程
        System.out.print(currentThread.getName() + "***");

        //此时以下两句会报错
//        System.out.print(this.getName() + "***");
//        System.out.print(super.getName() + "***");
    }

    public static void main(String[] args) {
        //此句在哪儿 获取的当前线程名就是谁
        Thread currentThread = Thread.currentThread();  //获取当前线程
        System.out.println(currentThread.getName());

        Thread t1 = new MyThread2();
        Thread t2 = new MyThread2();
        System.out.println(t1.getName());   //获取线程名字
        System.out.println(t2.getName());
        t1.setName("分支1");  //设置线程名字
        t2.setName("分支2");
        System.out.println(t1.getName());
        System.out.println(t2.getName());

        t1.start();
//        t2.start();
    }
}

class MyThread2 extends Thread {
    @Override
    public void run() {
        //此句在哪儿 获取的当前线程名就是谁
        Thread currentThread = Thread.currentThread();  //获取当前线程
        for (int i = 0; i < 100; i++) {
            ThreadTest03.doSome();
            //常用第一个   因为this super不通用 eg:调用doSome()方法
//            System.out.print(currentThread.getName() + "***");
//            System.out.print(super.getName() + "***");
//            System.out.print(this.getName() + "***");
            System.out.println("分支线程--->" + i);
        }
    }
}
  • 休眠线程        

        sleep:让当前的正在执行的线程暂停指定的时间,并进入阻塞状态。在其睡眠的时间段内,该线程由于不是处于就绪状态,因此不会得到执行的机会。即使此时系统中没有任何其他可执行的线程,处于 sleep()中的线程也不会执行。因此sleep() 方法常用来暂停线程执行。
        注意:休眠时间以ms为单位,并且不能释放锁。并且休眠的是当前线程。
public class ThreadTest04 {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(1000 * 5); //当前线程休眠进入阻塞状态   此语句在哪儿就阻塞那个线程
        System.out.println("hello world ~~~");

        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000 * 2); //可做到每隔一定时间就执行一次特定代码
            System.out.println(i);
        }
    }
}
public class ThreadTest05 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread3();
        t.sleep(1000 * 5);  //注意:此语句在执行时候相当于Thread.sleep(1000 * 5);
        /*
            因为sleep()是一个静态方法,休眠的是当前线程
            也就是说此处休眠的还是main线程,跟t线程没有关系
         */
        System.out.println("hello world ~~~");
    }
}

class MyThread3 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getName() + i);
        }
    }
}
  • 守护线程

Java 线程中有两种线程,一种是用户线程(非守护线程),一种是守护线程。
守护线程是一种特殊的线程,它具有陪伴的含义。当进程中不存在非守护线程了,则守护线程自动销毁。
典型的就是垃圾回收线程。当进程中没有非守护线程了,则垃圾回收线程没有存在的必要,自动销毁。
任何一个守护线程都是整个 JVM 中所有非守护线程的保姆。只要当前 JVM 中有非守护线程没有结束,守护线程就在工作。只有当最后一个非守护线程不工作的时候,守护进程才随着J VM 一同结束工作。
Dammon (守护线程)的作用是为非守护线程的运行提供便利服务。守护进程最典型的应用是 GC(垃圾回 收器),它是很称职的守护者。
void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。
        on - 如果为 true ,则将该线程标记为守护线程。如果为 false, 则将该线程标记为用户线程 ;
boolean isDaemon() 测试该线程是否为守护线程
public class ThreadTest09 {
    public static void main(String[] args) {
        MyThread6 t = new MyThread6();
        t.setName("t");

        t.setDaemon(true);  //在t线程启动前,将其设置为守护线程
        t.start();

        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "---->" + i);
            try {
                Thread.sleep(1000*2);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        //当main线程结束后,守护线程也会结束
    }
}

class MyThread6 extends Thread {
    @Override
    public void run() {
        int i = 0;
        while (true) {
            System.out.println(Thread.currentThread().getName() + "---->" + (++i));
            try {
                Thread.sleep(1000*2);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
  • 加入线程

join(), 当前线程暂停 , 等待指定的线程执行结束后 , 当前线程再继续 ;
join(long millis), 可以等待指定的毫秒之后继续 ;
join(long millis, int nanos) 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
join —— 让一个线程等待另一个线程完成才继续执行。如 A 线程线程执行体中调用 B 线程的 join()方法,则 A 线程被阻塞,直到 B 线程执行完为止, A 才能得以继续执行。
join()没有时间参数,那么就得等加入的线程执行完毕才能执行;如果有时间参数,那么就相当于等待 指定时间之后,继续执行;
  • 礼让线程

static void yield() 暂停当前正在执行的线程对象,并执行其他线程。
礼让线程的方法,并没有指定暂停时间,从我们之后的图例可以看出来,礼让线程并没有阻塞;直接从运行状态转换为就绪状态;不像之前的 sleep() join()方法,会阻塞;那么礼让线程直接从运行状态切换为就绪状态,那么就存在一种可能性,立马被调用;
  • 线程优先级

void setPriority(int newPriority) 更改线程的优先级。
int getPriority() 返回线程的优先级。
IllegalArgumentException - 如果优先级不在 MIN_PRIORITY MAX_PRIORITY 范围内。
每个线程在执行时都具有一定的优先级,优先级高的线程具有较多的执行机会。每个线程默认的优先级都与创建它的线程的优先级相同。 main 线程默认具有普通优先级。
注:
        同优先级线程组成先进先出队列(先到先服务),使用时间片策略;
        对高优先级,使用优先调度的抢占式策略:高优先级的线程抢占CPU
        线程创建时继承父线程的优先级;
        低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用;
  • 终止线程

由于实际的业务需要,常常会遇到需要在特定时机终止某一线程的运行,使其进入到死亡状态。目前最通用的做法是设置一 boolean型的变量,当条件满足时,使线程执行体快速执行完毕。
现在我们知道了使用 stop() 方式停止线程是非常不安全的方式,那么我们应该使用什么方法来停止线 程呢?答案就是使用 interrupt() 方法来中断线程。
需要明确的一点的是: interrupt() 方法并不像在 for 循环语句中使用 break 语句那样干脆,马上就停止循环。调用 interrupt() 方法仅仅是在当前线程中打一个停止的标记,并不是真的停止线程也就是说,线程中断并不会立即终止线程,而是通知目标线程,有人希望你终止。至于目标线程收到通知后会如何处理,则完全由目标线程自行决定。这一点很重要,如果中断后,线程立即无条件退出,那么我们又会遇到 stop() 方法的老问题。
package com.Thread;

public class ThreadTest08 {
    public static void main(String[] args) throws InterruptedException {
        MyRunnable4 r = new MyRunnable4();
        Thread t = new Thread(r);
        t.setName("t");
        t.start();
        Thread.sleep(1000 * 5);
        r.isRun = false;    //终止线程
    }
}

class MyRunnable4 implements Runnable {

    boolean isRun = true;   //标记该线程是否允许

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (isRun) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + "--->" + i);
            } else {
                //终止线程
                //执行数据保存操作
                return;
            }
        }
    }
}

3 线程生命周期

Java线程的五种基本状态

1. 新建状态

        当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。

2. 就绪状态

        处于新建状态的线程对象被start()后,线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,已经加入了操作系统的任务调度队列,等待被操作系统调度执行,并不是说执行了t.start()此线程立即就会执行;

3. 运行状态

        当就绪状态的线程被操作系统的任务调度机制调度到,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;run()方法定义了线程的操作和功能;

4. 阻塞状态

        处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其再次进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的
原因不同,阻塞状态又可以分为三种:
        1.等待阻塞:运行状态中的线程执行 wait() 方法,使本线程进入到等待阻塞状态;
        2.同步阻塞:线程在获取 synchronized 同步锁失败 ( 因为锁被其它线程所占用 ),它会进入同步阻塞状态;
        3.其他阻塞 : 通过调用线程的 sleep() join() 或发出了 I/O 请求时,线程会进入到阻塞状态。
        当sleep() 状态超时、 join() 等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。

5. 死亡状态

        线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束。
注意:
        就绪状态转换为运行状态:当此线程得到处理器资源。
        运行状态转换为就绪状态:当此线程主动调用 yield() 方法或在运行过程中失去处理器资源。
        运行状态转换为死亡状态:当此线程线程执行体执行完毕或发生了异常。
        此处需要特别注意的是:当调用线程的 yield()方法时,线程从运行状态转换为就绪状态,但接下来CPU调度就绪状态中的哪个线程具有一定的随机性,因此,可能会出现 A 线程调用了 yield()方法后,接下来CPU仍然调度了 A 线程的情况。

4 线程同步

4.1 为什么要线程同步?

        多个线程一起执行,操作一块共享的数据区,导致最后结果不理想或者不正确,最简单的例子就是火车卖票,总共有100张票(共享数据),多个窗口(线程)一块卖,窗口1和窗口2都取到数据为98张,窗口1卖完还剩97张,而窗口2买完应该是96张,可是它取到的是98张数据,结果还是97张就造成了严重的错误。

4.2 线程同步实现

        通过加锁解锁的方式进行实现,对于共享代码块进行加锁,操作完毕再解锁即可。

4.2.1 synchronized 关键字

synchronized有两种用法:

        synchronized(共享对象){...}

        public synchronized void show (String name){...}        //也可以卸载静态方法中

                当synchronized写在方法中锁的是this;

                写在静态方法中锁的是 ‘类名.Class’。

注意事项:

        同步代码块越小,效率越高

        同步代码块越大,效率越低

        只有局部变量和常量不存在线程安全,因为局部变量不共享,而常量无法修改!

package com.Thread.threadsafe;

public class Account {
    String actID;
    Double balance;

    public Account(String actID, Double balance) {
        this.actID = actID;
        this.balance = balance;
    }

    public Account() {
    }

    public String getActID() {
        return actID;
    }

    public void setActID(String actID) {
        this.actID = actID;
    }

    public Double getBalance() {
        return balance;
    }

    public void setBalance(Double balance) {
        this.balance = balance;
    }

    public void widtDraw(Double money) {
        /*
        * 线程同步机制语法:
        * synchronized() { //同步代码块 }
        * ()小括号中填线程共享的对象
        * eg:本例子中t1和t2共享一个account,而需要同步执行的代码块在下部分,刚好this就是指的accoun(其他应用场景中并不一定是this)!
        *
        * 理解synchronized:
        * synchronized(锁) 这个锁只有一把   A占了,如果B要用就要等A用完
        * 同步代码块越小 效率越高
        * 同步代码块越大 效率越低
        *
        * 局部变量: 在栈中
        * 实例变量: 在堆中
        * 静态变量: 在方法区
        * 只有局部变量、常量(无法修改)不存在线程安全问题!因为它不共享!
        * */
        synchronized (this) {
            Double before = this.getBalance();  //获取余额
            //这里睡眠的话数据就会出问题
            try {
                Thread.sleep(1000 * 2);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            Double res = before - money;    //获取消费后的余额
            this.setBalance(res);   //更新余额
        }
    }
}

4.2.2 lock锁

jdk5之后,提供了一个更加强大的加锁机制,就是lock,原理与synchronized 类似,可以把synchronized 当做一个隐式锁,而lock是一个显示锁,需要在代码块中手动上锁和解锁。

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获取 Lock对象。
ReentrantLock实现了lock接口,可以显示加锁和释放锁。
public void m(){
    lock.lock();
    try{
        //保证线程安全的代码;
    }
    finally{
        lock.unlock();
    }
}

4.2.3 锁的分类

  1. 方法锁(synchronized 修饰方法时就是方法锁)
  2. 对象锁(synchronized 修饰方法或代码块都属于对象锁)
  3. 类锁(synchronized 修饰静态方法或者代码块)

4.2.4 死锁

        以下就是一个简单的死锁例子:

// a 线程持有锁obj1 等obj2解锁
synchronized(obj1) {
    ...
    synchronized(obj2) {
        ...
    }
}

// b 线程持有锁obj2 等obj1解锁
synchronized(obj2) {
    ...
    synchronized(obj1) {
        ...
    }
}

        a线程在等b释放obj2,b线程在等a线程释放obj1,这样就形成了死锁,解决办法就是以后写同步代码块时候不要锁嵌套锁即可完美解决。

4.3 线程通信

        线程是操作系统调度的最小单位,有自己的栈空间,可以按照既定的代码逐步的执行,但是如果每个线程间都孤立的运行,那就会造成资源浪费。所以在现实中,我们需要这些线程间可以按照指定的规则共同完成一件任务,所以这些线程之间就需要互相协调,这个过程被称为线程的通信。

4.3.1 volatile通信

java内存模型:

        当线程操作共享变量时,该线程本地内存中存的只是一个副本,因此当改变该副本数据时,仍要刷新到主内存中,但是如果在线程a未将最新的数据刷新到主内存时,b线程已经把旧值存到了它的副本中,这就造成了错误,而volatile 关键字的作用就是当线程操作共享变量副本时,其值发生改变后立即刷新到主内存中。使用起来页很简单,只需声明共享变量时加上volatile关键字即可。

// 定义一个共享变量来实现通信,它需要是volatile修饰,否则线程不能及时感知
static volatile boolean notice = false;

4.3.2 等待/通知机制

众所周知,Object类提供了线程间通信的方法:wait()、notify()、notifyAll(),它们是多线程通信的基础,而这种实现方式的思想自然是线程间通信。

等待通知机制是基于wait和notify方法来实现的,在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待直到被通知或者被唤醒。

注意:wait和 notify必须配合synchronized使用,wait方法释放锁,notify方法不释放锁;

 通过利用此机制实现两个线程打印奇偶数,线程t1打印单数,线程t2打印双数:

public class Test {
    public static void main(String[] args) {
        AtomicInteger count = new AtomicInteger(0);
        Num1 t1 = new Num1(count);
        Num2 t2 = new Num2(count);
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        t2.start();
    }
}
//输出奇数
public class Num1 extends Thread {
    AtomicInteger count;

    public Num1(AtomicInteger count) {
        this.count = count;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (count) {
                if (count.intValue() % 2 == 1) {
                    try {
                        count.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println(Thread.currentThread().getName() + "--->" + (count.addAndGet(1)));
                count.notify();

                try {
                    Thread.sleep(1000 * 1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}
//输出偶数
public class Num2 extends Thread {
    AtomicInteger count;

    public Num2(AtomicInteger count) {
        this.count = count;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (count) {
                if (count.intValue() % 2 == 0) {
                    try {
                        count.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println(Thread.currentThread().getName() + "--->" + (count.addAndGet(1)));
                count.notify();

                try {
                    Thread.sleep(1000 * 1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

5 线程组和线程池

5.1 Callable接口创建线程

        jdk1.5之后可以通过实现callable接口来创建线程,相比与之前所讲的两种线程创建方式,此方式创建的线程可以返回一个结果,但是需要借助一个FutureTask类,具体代码如下:

public class ThreadTest10 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建一个 未来任务 对象
        FutureTask ft = new FutureTask(new MyCallable());

        Thread t = new Thread(ft);
        t.start();

        Object obj = ft.get();  //会阻塞当前线程
        System.out.println((Integer) obj);
        System.out.println("main end...");
    }
}

class MyCallable implements Callable {

    @Override
    public Object call() throws Exception {
        System.out.println("call method begin...");
        Thread.sleep(1000 * 5);
        System.out.println("call method end...");
        return 100 + 200;
    }
}

5.2 线程组

        可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。

        默认情况下,线程都属于main线程组(注意是main线程组不是main线程),线程可以通过getThreadGroup()方法来获取它所属的线程组。线程组可以通过getName()方法获取线程组的名字。

MyRunable my = new Myrunable();
Thread t1 = new Thread(my, "t1");
ThreadGroup tg = t1.getThreadGroup();
String tgName = tg.getName();

        也可以自定义线程组 ,有两个构造函数:

  • ThreadGroup(String name) 构造一个新线程组。
  • ThreadGroup(ThreadGroup parent, String name) 创建一个新线程组。

        创建Thread时,可以直接指定其所属线程组:

  • Thread(ThreadGroup group, Runnable target, String name)
public class ThreadGroupTest {
    public static void main(String[] args) {
        // ThreadGroup(String name)
        ThreadGroup tg = new ThreadGroup("这是一个新的组");
        MyRunnable mr = new MyRunnable();
        // Thread(ThreadGroup group, Runnable target, String name)
        Thread t1 = new Thread(tg, mr, "张三");
        Thread t2 = new Thread(tg, mr, "李四");
        System.out.println(t1.getThreadGroup().getName());
        System.out.println(t2.getThreadGroup().getName());
        // 通过组名称设置守护线程,表示该组的线程都是守护线程 (也叫后台线程)
        tg.setDaemon(true);
    }
}

5.3 ThreadLocal

        ThreadLocal并不是一个Thread,也就是说它不是一个线程,而是线程局部变量。功能很简单,就是为每一个使用该变量的线程都提供一个副本,也就是为了数据隔离。每一个线程都有自己独立的数据副本,在对数据进行修改时,不会影响到其他线程中的副本。

public class ThreadLocalTest {
    public static void main(String[] args) {
        MyRunable myRunable = new MyRunable();
        Thread t1 = new Thread(myRunable, "t1");
        Thread t2 = new Thread(myRunable, "t2");
        t1.start();
        t2.start();
    }
}

class MyRunable implements Runnable {

    private static ThreadLocal<Count> threadLocal = new ThreadLocal<>();

    @Override
    public void run() {
        Count count = createEntity();
        Random random = new Random();
        count.setCnt(random.nextInt(100));
        System.out.println(Thread.currentThread().getName() + "第一读取的值是:" + count.getCnt());

        try {
            Thread.sleep(1500);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println(Thread.currentThread().getName() + "第二读取的值是:" + count.getCnt());
    }

    private Count createEntity() {
        Count count = threadLocal.get();
        if (count == null) {
            count = new Count(20);
            threadLocal.set(count);
        }
        return count;
    }
}

class Count {
    private int cnt;

    public Count() {
    }

    public Count(int cnt) {
        this.cnt = cnt;
    }

    public int getCnt() {
        return cnt;
    }

    public void setCnt(int cnt) {
        this.cnt = cnt;
    }
}

5.4 线程池

5.4.1 为什么使用线程池

        当我们线程的运行时间很短,并且任务量很大时候,需要频繁的创建和消耗线程,无疑它不仅消耗内存,而且速度也会降低。利用线程池就是为解决此问题,线程池里提供若干线程,当需要使用线程时,直接从线程池里拿,无需再创建,当使用完毕时还会将线程放回线程池,也无需收回。这是一种以空间换时间的优化方式,运用于并发框架的场景当中。

使用线程池的优点:

        1. 降低内存消耗,因为不需要频繁的创建销毁线程;

        2. 提高响应速度,当任务到达需要使用线程时已经创建好了,因此就会立即执行;

        3. 可以对线程进行统一管理,提高线程的可管理性。对线程进行统一分配、调优和监控。

5.4.2 线程池原理

  •  Executor接口

Executor:执行提交的线程任务的对象。这个接口提供了一种将任务提交与每个任务将如何运行实现了分离,包括线程使用、调度等细节。该接口只定义了一个execute()方法。


execute():将任务提交给线程池,由线程池为该任务创建线程并启动。注意这个方法没有返回值,获取不到线程执行结果


void execute(Runnable command) 在未来某个时间执行给定的命令。

  • ExecutorService接口

提供用于管理终止的方法如 shutDown()和shutDownNow()用于关闭线程池的方法以及判断线程池是否关闭的方法如,isShutdown(),isTerminated()的方法

提供了可以生成用于跟踪一个或多个异步任务进度的方法如 invokeAll(),submit()。这些方法的返回值都是Future类型,可以获取线程的执行结果。

ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor

void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable

<T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行Callable

void shutdown() :关闭连接池

  • AbstractExecutorService

对ExecutorService接口的实现。

  • ThreadPoolExecutor 

线程池类ThreadPoolExecutor :

核心构造函数:

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

corePoolSize:核心线程数(线程池中至少有多少个线程存在)

maximumPoolSize:最大线程数(线程池中最多有多少个线程存在)

keepAliveTime:线程最大空闲存活时间(如果空闲时间超过它就会被销毁)

unit:存活时间单位,配合keepAliveTime使用

workQueue:任务队列,当核心线程都处于工作状态,那么新进来的任务就会加入此队列

handler:拒绝策略,当线程池满并且没有空闲的线程,队列也已经满了,此时有新的任务进来就会执行相应的拒绝策略,默认时抛异常

5.4.3 线程池生命周期

1. RUNNING:运行状态,能够接受新的任务,也能处理阻塞队列中的任务,当线程池被创建时处于该状态;

2. SHUTDOWN:关闭状态,此时不能够接收新的任务,但仍能处理阻塞队列中的任务,当调用线程池的shutdown()方法时,进入该状态;

3. STOP:处于此状态既不能接收新的任务,也会中断正在处理的任务,线程池处于running或者shutdown状态时,调用shutdowNow()方法进入该状态;

4. TIDYING:所有的任务都已经结束,此时处于收尾工作,当进入该状态时会调用钩子函数terminated(),此函数在ThreadPoolExecutor 类中是空的,如果需要进行相应处理,需要自己重载

 terminated()方法。

5. TERMINATED:terminated()方法执行完后进入该状态,线程池彻底终止。

5.4.4 线程池分类

  • newFixedThreadPool固定线程池

newFixedThreadPool: 固定线程池,核心线程数和最大线程数固定相等,而空闲存活时间为0毫秒,说明此参数也无意义,工作队列为最大为Integer.MAX_VALUE大小的阻塞队列。


当执行任务时,如果线程都很忙,就会丢到工作队列等有空闲线程时再执行,队列满就执行默认的拒绝策略。

  • newCachedThreadPool带缓冲线程池

newCachedThreadPool带缓冲线程池,从构造看核心线程数为0,最大线程数为Integer最大值大小,超过0个的空闲线程在60秒后销毁,SynchronousQueue这是一个直接提交的队列,意味着每个新任务都会有线程来执行,如果线程池有可用线程则执行任务,没有的话就创建一个来执行,线程池中的线程数不确定,一般建议执行速度较快较小的线程,不然这个最大线程池边界过大容易造成内存溢出。

  • newSingleThreadExecutor创建一个单线程化的Executor

newSingleThreadExecutor创建一个单线程化的Executor,核心线程数和最大线程数均为1,空闲线程存活0毫秒同样无意思,意味着每次只执行一个线程,多余的先存储到工作队列,一个一个执行,保证了线程的顺序执行。


如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

  • newScheduledThreadPool调度线程池

newScheduledThreadPool调度线程池,即按一定的周期执行任务,即定时任务,对ThreadPoolExecutor进行了包装而已

  • newSingleThreadScheduledExecutor创建一个单线程执行程序

newSingleThreadScheduledExecutor创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。线程池中最多执行1个线程,之后提交的线程活动将会排在队列中以此执行并且可定时或者延迟执行线程活动。

5.4.5 线程池案例

public class ThreadPoolTest {
    public static void main(String[] args) {
        // 创建一个固定线程池 核心线程数和最大线程数都是5
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 10; i++) {
            // 需要有返回值时实现Callable接口,并使用线程池的submit方法提交任务即可
            Runnable r = new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + ">>>执行任务……");
                }
            };
            executorService.execute(r);
        }

        // 线程池不终止那么main线程也不会结束
        try {
            Thread.sleep(5000);
            executorService.shutdown();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值