关于后端:Java并发编程系列之二线程基础

6次阅读

共计 17205 个字符,预计需要花费 44 分钟才能阅读完成。

上篇文章对并发的实践根底进行了回顾,次要是为什么应用多线程、多线程会引发什么问题及引发的起因,和怎么应用 Java 中的多线程去解决这些问题。

正所谓,知其然知其所以然,这是学习一个常识遵循的准则。

举荐读者后行查看并发编程的理论知识,以便能够丝滑入戏。

并发编程系列之一并发实践根底

本篇文章重点在于 Java 中怎么去应用多线程,和多线程的一些相干概念和操作,及怎么优化多线程。

在 Java 中每个对象都有其生命周期,线程同样不例外,也有其生命周期。

一、线程生命周期

线程的几种状态转换

1、新建(New)

新创建了一个线程对象,但还没有调用 start()办法。

2、就绪

当线程对象调用了 start()办法后,该线程就进入就绪状态。处于就绪状态的线程位于线程队列中,此时它只是具备了运行的条件,是否取得 CPU 的使用权并开始运行,还须要期待零碎的调度。

3、运行(Runnable)

如果处于就绪状态的线程取得了 CPU 的使用权,并开始执行 run()办法中的线程执行体,则该线程处于运行状态。

一个线程启动后,它可能不会始终处于运行状态,当运行状态的线程应用完零碎调配的工夫后,零碎就会剥夺该线程占用的 CPU 资源,让其余线程取得执行的机会。须要留神的是,

只有处于就绪状态的线程才可能转换到运行状态。

4、阻塞(Blocking)

期待获取一个排它锁,如果其线程开释了锁就会完结此状态。

①无限期期待(Waiting)

期待其它线程显式地唤醒,否则不会被调配 CPU 工夫片。

进入办法 退出办法
没有设置 Timeout 参数的 Object.wait() 办法 Object.notify() / Object.notifyAll()
没有设置 Timeout 参数的 Thread.join() 办法 被调用的线程执行结束
LockSupport.park() 办法

②限期期待(Timed Waiting)

无需期待其它线程显式地唤醒,在肯定工夫之后会被零碎主动唤醒。

调用 Thread.sleep() 办法使线程进入限期期待状态时,经常用“使一个线程睡眠”进行形容。

调用 Object.wait() 办法使线程进入限期期待或者无限期期待时,经常用“挂起一个线程”进行形容。

睡眠和挂起是用来形容行为,而阻塞和期待用来形容状态。

阻塞和期待的区别在于,阻塞是被动的,它是在期待获取一个排它锁。而期待是被动的,通过调用 Thread.sleep() 和 Object.wait() 等办法进入。

进入办法 退出办法
Thread.sleep() 办法 工夫完结
设置了 Timeout 参数的 Object.wait() 办法 工夫完结 / Object.notify() / Object.notifyAll()
设置了 Timeout 参数的 Thread.join() 办法 工夫完结 / 被调用的线程执行结束
LockSupport.parkNanos() 办法
LockSupport.parkUntil() 办法

5、死亡(Terminated)

如果线程调用 stop()办法或 nun()办法失常执行结束,或者线程抛出一个未捕捉的异样 (Exception) 谬误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再领有运行的资格,也不能再转换到其余状态。

了解线程的五种状态,在调用多线程的办法时,能分明的晓得以后处于哪个状态。

咱们举一个简略的实例来阐明每个状态。

public class MyThread extends Thread {
    
    // 运行状态
    public void run() {// ...}
    
    public static void main(String[] args) {MyThread mt = new MyThread(); //1、新建状态
    mt.start(); // 就绪状态}
}
     

在线程管制章节有一些办法,如 sleep()\join()办法,这些办法会让线程处于阻塞状态。

理解了线程的生成周期当前,接下来咱们就须要把握在 Java 中怎么应用多线程。

在 Java 中有三种形式实现多线程。

二、创立线程的三种形式

有三种应用线程的办法:

  • 实现 Runnable 接口;
  • 实现 Callable 接口;
  • 继承 Thread 类。

实现 Runnable 和 Callable 接口的类只能当做一个能够在线程中运行的工作,不是真正意义上的线程,因而最初还须要通过 Thread 来调用。能够说工作是通过线程驱动从而执行的。

1、实现 Runnable 接口

须要实现 run() 办法。

通过 Thread 调用 start() 办法来启动线程。

public class MyRunnable implements Runnable {public void run() {// 须要执行多线程的业务逻辑}
}
    
public static void main(String[] args) {MyRunnable myRunnable = new MyRunnable();
    Thread thread = new Thread(myRunnable);
    thread.start();}
     

2、实现 Callable 接口

与 Runnable 相比,Callable 能够有返回值,返回值通过 FutureTask 进行封装。

public class MyCallable implements Callable<Integer> {public Integer call() {return 123;}
}
  
public static void main(String[] args) throws ExecutionException, InterruptedException {MyCallable mc = new MyCallable();
    FutureTask<Integer> ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
    thread.start();
    System.out.println(ft.get());
}
  

3、继承 Thread 类

同样也是须要实现 run() 办法,因为 Thread 类也实现了 Runable 接口。

当调用 start() 办法启动一个线程时,虚构机会将该线程放入就绪队列中期待被调度,当一个线程被调度时会执行该线程的 run() 办法。

public class MyThread extends Thread {public void run() {// ...}
}
  
         
public static void main(String[] args) {MyThread mt = new MyThread();
    mt.start();}
  
       

4、实现接口 VS 继承 Thread

实现接口会更好一些,因为:

  • Java 不反对多重继承,因而继承了 Thread 类就无奈继承其它类,然而能够实现多个接口;
  • 类可能只要求可执行就行,继承整个 Thread 类开销过大。

三、线程管制

线程在应用过程中能对其灵便的管制,蕴含线程睡眠和线程退让等。

在学习线程的一些管制办法前,有一个必须要理解的前置常识,在线程中分为守护过程和非守护过程。

1、Daemon

守护线程是程序运行时在后盾提供服务的线程,不属于程序中不可或缺的局部。

当所有非守护线程完结时,程序也就终止,同时会杀死所有守护线程。

垃圾回收线程就是一个经典的守护线程,当咱们的程序中不再有任何运行的 Thread, 程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是 JVM 上仅剩的线程时,垃圾回收线程会主动来到。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

main() 属于非守护线程。

非守护线程能够转换为守护过程。

应用 setDaemon() 办法将一个线程设置为守护线程。

public static void main(String[] args) {Thread thread = new Thread(new MyRunnable());
    thread.setDaemon(true);
}
  

2、sleep()

Thread.sleep(millisec) 办法会休眠以后正在执行的线程,millisec 单位为毫秒。

sleep() 可能会抛出 InterruptedException,因为异样不能跨线程流传回 main() 中,因而必须在本地进行解决。线程中抛出的其它异样也同样须要在本地进行解决。

public void run() {
    try {Thread.sleep(3000);
    } catch (InterruptedException e) {e.printStackTrace();
    }
}
      

3、yield()

对静态方法 Thread.yield() 的调用申明了以后线程曾经实现了生命周期中最重要的局部,能够切换给其它线程来执行。该办法只是对线程调度器的一个倡议,而且也只是倡议具备雷同优先级的其它线程能够运行。

public void run() {Thread.yield();
}
  

4、join()

一旦这个线程执行了这个办法,只有这个线程处于死亡状态其余线程能力执行。

public class MyThread extends Thread {
11
12     public MyThread() {13}
14
15     public MyThread(String name) {16         super(name);
17     }
18
19     @Override
20     public void run() {21         for (int i = 0; i < 10; i++) {22             System.out.println(getName() + ":" + i);
23         }
24     }
25
26     public static void main(String[] args) {
27         // 1. 创立 MyThread 类的对象
28         MyThread myThread1 = new MyThread("线程 1");
29         MyThread myThread2 = new MyThread("线程 2");
30         MyThread myThread3 = new MyThread("线程 3");
31
32         // 2. 启动线程
33         myThread1.start();
34         try {
35             // 期待 myThread1 线程死亡, 只有当该线程死亡之后能力继续执行其它线程
36             myThread1.join();
37         } catch (InterruptedException e) {38             e.printStackTrace();
39         }
40         myThread2.start();
41         myThread3.start();
42
43     }
44 }

5、wait()\notify()

wait\notify\notifyAll 操作都是属于 Object 类提供的办法,即所有的对象都具备该办法,他们是的一对的,调用的时候不能离开呦。

wait(): 调用 wait 办法的线程,以后持有锁的该线程期待,直至该对象的另一个持锁线程调用 notify/notifyAll 操作。
wait(long timeOut)、wait(long timeOut,int nanos)

线程状态转换是,当 wait 被唤醒或超时,并不是间接进入到运行或者就绪状态,而是先进入到 Block 状态,抢锁胜利后,能力进入到可运行状态。

wait 办法在调用进入阻塞之前会开释锁,而 sleep 或 join 是不会开释锁的

notify(): 告诉持有该对象锁的所有线程中的的随便一个线程被唤醒

notifyAll(): 告诉持有该对象锁的所有线程被同时唤醒

咱们形象的做一个比喻:

如果把多线程比喻成一个运动员,跑道就是 CPU 每次只能容许一个运动员进入跑道,运动员的后勤保障就是守护过程,通过 setDaemon()办法,运动员就转业为了后勤人员。

执行 sleep()就是提前设定一个工夫,让运动员劳动会。wait()办法是运动员无限期的睡着,直到教练杀进去一脚踹醒(执行 notify 办法)运动员才会唤醒。

yield()会把跑道让给别的运动员。

join()办法会让运动员领有最高的跑道权限,我不跑完,谁都不能进来。

四、线程同步

Java 容许并发管制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不精确,相互之间产生抵触,因而退出同步锁以防止在该线程没有实现操作之前,被其余线程的调用,从而保障了该变量的唯一性和准确性。

Java 提供了两种锁机制来管制多个线程对共享资源的互斥拜访,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。

1、synchronized

①. 同步一个代码块

public void func() {synchronized (this) {// ...}
}
  
      

它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。

对于以下代码,应用 ExecutorService 执行了两个线程,因为调用的是同一个对象的同步代码块,因而这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须期待。

public class SynchronizedExample {public void func1() {synchronized (this) {for (int i = 0; i < 10; i++) {System.out.print(i + " ");
            }
        }
    }
}
  
public static void main(String[] args) {SynchronizedExample e1 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e1.func1());
}        
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
  
       

对于以下代码,两个线程调用了不同对象的同步代码块,因而这两个线程就不须要同步。从输入后果能够看出,两个线程穿插执行。

public static void main(String[] args) {SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e2.func1());
}
        
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9  
     

②. 同步一个办法

public synchronized void func () {// ...}
         

它和同步代码块一样,作用于同一个对象。

③. 同步一个类

public void func() {synchronized (SynchronizedExample.class) {// ...}
}  
       

作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。

public class SynchronizedExample {public void func2() {synchronized (SynchronizedExample.class) {for (int i = 0; i < 10; i++) {System.out.print(i + " ");
            }
        }
    }
}
  
public static void main(String[] args) {SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func2());
    executorService.execute(() -> e2.func2());
}
  
    

④. 同步一个静态方法

public synchronized static void fun() {// ...}
  
    

作用于整个类。

2、ReentrantLock

ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。

public class LockExample {private Lock lock = new ReentrantLock();

    public void func() {lock.lock();
        try {for (int i = 0; i < 10; i++) {System.out.print(i + " ");
            }
        } finally {lock.unlock(); // 确保开释锁,从而防止产生死锁。}
    }
}
   
public static void main(String[] args) {LockExample lockExample = new LockExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> lockExample.func());
    executorService.execute(() -> lockExample.func());
}
     
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9    

3、比拟

①. 锁的实现 **

synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。

②. 性能

新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。

③. 期待可中断

当持有锁的线程长期不开释锁的时候,正在期待的线程能够抉择放弃期待,改为解决其余事件。

ReentrantLock 可中断,而 synchronized 不行。

④. 偏心锁

偏心锁是指多个线程在期待同一个锁时,必须依照申请锁的工夫程序来顺次取得锁。

synchronized 中的锁是非偏心的,ReentrantLock 默认状况下也是非偏心的,然而也能够是偏心的。

⑤. 锁绑定多个条件

一个 ReentrantLock 能够同时绑定多个 Condition 对象。

4、应用抉择

除非须要应用 ReentrantLock 的高级性能,否则优先应用 synchronized。

这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地反对它,而 ReentrantLock 不是所有的 JDK 版本都反对。

并且应用 synchronized 不必放心没有开释锁而导致死锁问题,因为 JVM 会确保锁的开释。

如果并发的线程数量很多,并且每个线程都是执行一个工夫很短的工作就完结了,这样频繁创立线程就会大大降低零碎的效率,因为频繁创立线程和销毁线程须要工夫。

线程池就利用而生。

五、线程池

线程池围绕着一个外围的类 java.uitl.concurrent.ThreadPoolExecutor,咱们将它作为一个切入点揭开线程池的面纱。

1、外围线程类

java.uitl.concurrent.ThreadPoolExecutor 类是线程池中最外围的一个类,因而如果要透彻地理解 Java 中的线程池,必须先理解这个类。上面咱们来看一下 ThreadPoolExecutor 类的具体实现源码。

在 ThreadPoolExecutor 类中有四个构造方法。

其中三个最终都是调用了上面这个构造方法,限于篇幅就不在贴其余三个源码了,读者能够进行求证。

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

上面解释下一下结构器中各个参数的含意:

  • corePoolSize:外围池的大小,这个参数跟前面讲述的线程池的实现原理有十分大的关系。在创立了线程池后,默认状况下,线程池中并没有任何线程,而是期待有工作到来才创立线程去执行工作,除非调用了 prestartAllCoreThreads()或者 prestartCoreThread()办法,从这 2 个办法的名字就能够看出,是预创立线程的意思,即在没有工作到来之前就创立 corePoolSize 个线程或者一个线程。默认状况下,在创立了线程池后,线程池中的线程数为 0,当有工作来之后,就会创立一个线程去执行工作,当线程池中的线程数目达到 corePoolSize 后,就会把达到的工作放到缓存队列当中;
  • maximumPoolSize:线程池最大线程数,这个参数也是一个十分重要的参数,它示意在线程池中最多能创立多少个线程;
  • keepAliveTime:示意线程没有工作执行时最多放弃多久工夫会终止。默认状况下,只有当线程池中的线程数大于 corePoolSize 时,keepAliveTime 才会起作用,直到线程池中的线程数不大于 corePoolSize,即当线程池中的线程数大于 corePoolSize 时,如果一个线程闲暇的工夫达到 keepAliveTime,则会终止,直到线程池中的线程数不超过 corePoolSize。然而如果调用了 allowCoreThreadTimeOut(boolean)办法,在线程池中的线程数不大于 corePoolSize 时,keepAliveTime 参数也会起作用,直到线程池中的线程数为 0;
  • unit:参数 keepAliveTime 的工夫单位,有 7 种取值,在 TimeUnit 类中有 7 种动态属性:

    TimeUnit.DAYS;               // 天
    TimeUnit.HOURS;             // 小时
    TimeUnit.MINUTES;           // 分钟
    TimeUnit.SECONDS;           // 秒
    TimeUnit.MILLISECONDS;      // 毫秒
    TimeUnit.MICROSECONDS;      // 奥妙
    TimeUnit.NANOSECONDS;       // 纳秒
  • workQueue:一个阻塞队列,用来存储期待执行的工作,这个参数的抉择也很重要,会对线程池的运行过程产生重大影响,一般来说,察看传入的 workQueue 都是默认,即最大可增加 Integer.MAX_VALUE 个工作,所有在应用过程中要防止应用默认线程池。这里的阻塞队列有以下几种抉择:

    ArrayBlockingQueue;
    LinkedBlockingQueue;
    SynchronousQueue;
    
    ArrayBlockingQueue 和 PriorityBlockingQueue 应用较少,个别应用 LinkedBlockingQueue 和 Synchronous。线程池的排队策略与 BlockingQueue 无关。
  • threadFactory:线程工厂,次要用来创立线程;
  • handler:示意当回绝解决工作时的策略,有以下四种取值:
ThreadPoolExecutor.AbortPolicy: 抛弃工作并抛出 RejectedExecutionException 异样。ThreadPoolExecutor.DiscardPolicy:也是抛弃工作,然而不抛出异样。ThreadPoolExecutor.DiscardOldestPolicy:抛弃队列最后面的工作,而后从新尝试执行工作(反复此过程)ThreadPoolExecutor.CallerRunsPolicy:由调用线程解决该工作 

以上对结构的七个参数进行了介绍,那么这些参数是怎么起作用的呢,咱们接着看线程池的执行流程。

2、线程执行流程

  1. 当线程池小于 corePoolSize 时,新提交工作将创立一个新线程执行工作,即便此时线程池中存在闲暇线程。
  2. 当线程池达到 corePoolSize 时,新提交工作将被放入 workQueue 中,期待线程池中任务调度执行
  3. 当 workQueue 已满,且 maximumPoolSize>corePoolSize 时,新提交工作会创立新线程执行工作
  4. 当提交工作数超过 maximumPoolSize 时,新提交工作由 RejectedExecutionHandler 解决
  5. 当线程池中超过 corePoolSize 线程,闲暇工夫达到 keepAliveTime 时,开释闲暇线程
  6. 当设置 allowCoreThreadTimeOut(true)时,该参数默认 false,线程池中 corePoolSize 线程闲暇工夫达到 keepAliveTime 也将敞开

3、四种线程池及应用场景

Java 通过 Executors 提供四种线程池,别离为

  1. newSingleThreadExecutor 创立一个单线程化的线程池,它只会用惟一的工作线程来执行工作,保障所有工作依照指定程序 (FIFO, LIFO, 优先级) 执行。
  2. newFixedThreadPool 创立一个定长线程池,可控制线程最大并发数,超出的线程会在队列中期待。
  3. newScheduledThreadPool 创立一个可定期或者延时执行工作的定长线程池,反对定时及周期性工作执行。
  4. newCachedThreadPool 创立一个可缓存线程池,如果线程池长度超过解决须要,可灵便回收闲暇线程,若无可回收,则新建线程。

newCachedThreadPool:

  • 底层:返回 ThreadPoolExecutor 实例,corePoolSize 为 0;maximumPoolSize 为 Integer.MAX_VALUE;keepAliveTime 为 60L;工夫单位 TimeUnit.SECONDS;workQueue 为 SynchronousQueue(同步队列)
  • 艰深:当有新工作到来,则插入到 SynchronousQueue 中,因为 SynchronousQueue 是同步队列,因而会在池中寻找可用线程来执行,若有能够线程则执行,若没有可用线程则创立一个线程来执行该工作;若池中线程闲暇工夫超过指定工夫,则该线程会被销毁。
  • 实用:执行很多短期的异步工作

    /**
     * 1. 创立一个可缓存的线程池。如果线程池的大小超过了解决工作所须要的线程,那么就会回收局部闲暇(60 秒不执行工作)的线程 <br>
     * 2. 当工作数减少时,此线程池又能够智能的增加新线程来解决工作 <br>
     * 3. 此线程池不会对线程池大小做限度,线程池大小齐全依赖于操作系统(或者说 JVM)可能创立的最大线程大小 <br>
     */
    public static void cacheThreadPool() {ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
            for (int i = 1; i <= 10; i++) {
                final int ii = i;
                try {Thread.sleep(ii * 1);
                } catch (InterruptedException e) {e.printStackTrace();
                }
                cachedThreadPool.execute(()->out.println("线程名称:" + Thread.currentThread().getName() + ",执行" + ii));
            }
    }
    -----output------
    线程名称:pool-1-thread-1,执行 1
    线程名称:pool-1-thread-1,执行 2
    线程名称:pool-1-thread-1,执行 3
    线程名称:pool-1-thread-1,执行 4
    线程名称:pool-1-thread-1,执行 5
    线程名称:pool-1-thread-1,执行 6
    线程名称:pool-1-thread-1,执行 7
    线程名称:pool-1-thread-1,执行 8
    线程名称:pool-1-thread-1,执行 9
    线程名称:pool-1-thread-1,执行 10

newFixedThreadPool:

  • 底层:返回 ThreadPoolExecutor 实例,接管参数为所设定线程数量 n,corePoolSize 和 maximumPoolSize 均为 n;keepAliveTime 为 0L;工夫单位 TimeUnit.MILLISECONDS;WorkQueue 为:new LinkedBlockingQueue<Runnable>() 无界阻塞队列
  • 艰深:创立可包容固定数量线程的池子,每个线程的存活工夫是有限的,当池子满了就不再增加线程了;如果池中的所有线程均在忙碌状态,对于新工作会进入阻塞队列中(无界的阻塞队列)
  • 实用:执行长期工作

     /**
      * 1. 创立固定大小的线程池。每次提交一个工作就创立一个线程,直到线程达到线程池的最大大小 <br>
      * 2. 线程池的大小一旦达到最大值就会放弃不变,如果某个线程因为执行异样而完结,那么线程池会补充一个新线程 <br>
      * 3. 因为线程池大小为 3,每个工作输入 index 后 sleep 2 秒,所以每两秒打印 3 个数字,和线程名称 <br>
      */
    public static void fixTheadPoolTest() {ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
            for (int i = 0; i < 10; i++) {
                final int ii = i;
                fixedThreadPool.execute(() -> {out.println("线程名称:" + Thread.currentThread().getName() + ",执行" + ii);
                    try {Thread.sleep(2000);
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                });
            }
    }
    ------output-------
    线程名称:pool-1-thread-3,执行 2
    线程名称:pool-1-thread-1,执行 0
    线程名称:pool-1-thread-2,执行 3
    线程名称:pool-1-thread-3,执行 4
    线程名称:pool-1-thread-1,执行 5
    线程名称:pool-1-thread-2,执行 6
    线程名称:pool-1-thread-3,执行 7
    线程名称:pool-1-thread-1,执行 8
    线程名称:pool-1-thread-3,执行 9 

newSingleThreadExecutor:

  • 底层:FinalizableDelegatedExecutorService 包装的 ThreadPoolExecutor 实例,corePoolSize 为 1;maximumPoolSize 为 1;keepAliveTime 为 0L;工夫单位 TimeUnit.MILLISECONDS;workQueue 为:new LinkedBlockingQueue<Runnable>() 无解阻塞队列
  • 艰深:创立只有一个线程的线程池,当该线程正忙碌时,对于新工作会进入阻塞队列中(无界的阻塞队列)
  • 实用:按程序执行工作的场景
/**  * 创立一个单线程化的线程池,它只会用惟一的工作线程来执行工作,保障所有工作依照指定程序 (FIFO, LIFO, 优先级) 执行
 */
public static void singleTheadPoolTest() {ExecutorService pool = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int ii = i;
            pool.execute(() -> out.println(Thread.currentThread().getName() + "=>" + ii));
        }
}

-----output-------

线程名称:pool-1-thread-1,执行 0
线程名称:pool-1-thread-1,执行 1
线程名称:pool-1-thread-1,执行 2
线程名称:pool-1-thread-1,执行 3
线程名称:pool-1-thread-1,执行 4
线程名称:pool-1-thread-1,执行 5
线程名称:pool-1-thread-1,执行 6
线程名称:pool-1-thread-1,执行 7
线程名称:pool-1-thread-1,执行 8
线程名称:pool-1-thread-1,执行 9

NewScheduledThreadPool:

  • 底层:创立 ScheduledThreadPoolExecutor 实例,该对象继承了 ThreadPoolExecutor,corePoolSize 为传递来的参数,maximumPoolSize 为 Integer.MAX_VALUE;keepAliveTime 为 0;工夫单位 TimeUnit.NANOSECONDS;workQueue 为:new DelayedWorkQueue() 一个按超时工夫升序排序的队列
  • 艰深:创立一个固定大小的线程池,线程池内线程存活工夫无限度,线程池能够反对定时及周期性工作执行,如果所有线程均处于忙碌状态,对于新工作会进入 DelayedWorkQueue 队列中,这是一种依照超时工夫排序的队列构造
  • 实用:执行周期性工作
/**
 * 创立一个定长线程池,反对定时及周期性工作执行。提早执行
 */
public static void sceduleThreadPool() {ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
        Runnable r1 = () -> out.println("线程名称:" + Thread.currentThread().getName() + ",执行:3 秒后执行");
        scheduledThreadPool.schedule(r1, 3, TimeUnit.SECONDS);
        Runnable r2 = () -> out.println("线程名称:" + Thread.currentThread().getName() + ",执行: 提早 2 秒后每 3 秒执行一次");
        scheduledThreadPool.scheduleAtFixedRate(r2, 2, 3, TimeUnit.SECONDS);
        Runnable r3 = () -> out.println("线程名称:" + Thread.currentThread().getName() + ",执行: 一般工作");
        for (int i = 0; i < 5; i++) {scheduledThreadPool.execute(r3);
        }
}
----output------
线程名称:pool-1-thread-1,执行: 一般工作
线程名称:pool-1-thread-5,执行: 一般工作
线程名称:pool-1-thread-4,执行: 一般工作
线程名称:pool-1-thread-3,执行: 一般工作
线程名称:pool-1-thread-2,执行: 一般工作
线程名称:pool-1-thread-1,执行: 提早 2 秒后每 3 秒执行一次
线程名称:pool-1-thread-5,执行:3 秒后执行
线程名称:pool-1-thread-4,执行: 提早 2 秒后每 3 秒执行一次
线程名称:pool-1-thread-4,执行: 提早 2 秒后每 3 秒执行一次
线程名称:pool-1-thread-4,执行: 提早 2 秒后每 3 秒执行一次
线程名称:pool-1-thread-4,执行: 提早 2 秒后每 3 秒执行一次

5、应用实例

在 ThreadPoolTaskExecutor 的原理章节中,有一系列的办法,如果咱们手动调用这些线程池办法实现办法是极其简单的。

①、在 java 中的应用

public class Test {public static void main(String[] args) {   
         ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,
                 new ArrayBlockingQueue<Runnable>(5));
          
         for(int i=0;i<15;i++){MyTask myTask = new MyTask(i);
             executor.execute(myTask);
             System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中期待执行的工作数目:"+
             executor.getQueue().size()+",已执行玩别的工作数目:"+executor.getCompletedTaskCount());
         }
         executor.shutdown();}
}
 
 
class MyTask implements Runnable {
    private int taskNum;
     
    public MyTask(int num) {this.taskNum = num;}
     
    @Override
    public void run() {System.out.println("正在执行 task"+taskNum);
        try {Thread.currentThread().sleep(4000);
        } catch (InterruptedException e) {e.printStackTrace();
        }
        System.out.println("task"+taskNum+"执行结束");
    }
}

从执行后果能够看出,当线程池中线程的数目大于 5 时,便将工作放入工作缓存队列外面,当工作缓存队列满了之后,便创立新的线程。如果下面程序中,将 for 循环中改成执行 20 个工作,就会抛出工作回绝异样了。

不过在 java doc 中,并不提倡咱们间接应用 ThreadPoolExecutor,而是应用 Executors 类中提供的几个静态方法来创立线程池:

Executors.newCachedThreadPool();    // 创立一个缓冲池,缓冲池容量大小为 Integer.MAX_VALUE
Executors.newSingleThreadExecutor();  // 创立容量为 1 的缓冲池
Executors.newFixedThreadPool(int);  // 创立固定容量大小的缓冲池

从它们的具体实现来看,它们实际上也是调用了 ThreadPoolExecutor,只不过参数都已配置好了。

newFixedThreadPool 创立的线程池 corePoolSize 和 maximumPoolSize 值是相等的,它应用的 LinkedBlockingQueue;

newSingleThreadExecutor 将 corePoolSize 和 maximumPoolSize 都设置为 1,也应用的 LinkedBlockingQueue;

newCachedThreadPool 将 corePoolSize 设置为 0,将 maximumPoolSize 设置为 Integer.MAX_VALUE,应用的 SynchronousQueue,也就是说来了工作就创立线程运行,当线程闲暇超过 60 秒,就销毁线程。

理论中,如果 Executors 提供的三个静态方法能满足要求,就尽量应用它提供的三个办法,因为本人去手动配置 ThreadPoolExecutor 的参数有点麻烦,要依据理论工作的类型和数量来进行配置。

另外,如果 ThreadPoolExecutor 达不到要求,能够本人继承 ThreadPoolExecutor 类进行重写。

②、在 Spring 中应用

以下为 Java 线程池在 Spring 中的应用,ThreadPoolTaskExecutor 一个对象注入到 Spring 的容器中。

/**
 * 线程池配置
 *
 * @author tcy
 **/
@Configuration
public class ThreadPoolConfig {
  // 外围线程池大小
  private final int corePoolSize = 50;

  // 最大可创立的线程数
  private final int maxPoolSize = 200;

  // 队列最大长度
  private final int queueCapacity = 1000;

  // 线程池保护线程所容许的闲暇工夫
  private final int keepAliveSeconds = 300;

  @Bean(name = "threadPoolTaskExecutor")
  public ThreadPoolTaskExecutor threadPoolTaskExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setMaxPoolSize(maxPoolSize);
    executor.setCorePoolSize(corePoolSize);
    executor.setQueueCapacity(queueCapacity);
    executor.setKeepAliveSeconds(keepAliveSeconds);
    // 线程池对回绝工作 (无线程可用) 的解决策略
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    return executor;
  }

在办法或者类上加 @Async 注解,表明该办法或类为多线程办法,Spirng 外部会主动调用多线程的回绝策略、线程初始化等办法。

正文完
 0