首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Java并发编程系列之二线程基础

Java并发编程系列之二线程基础

原创
作者头像
程序员田同学
发布于 2022-04-07 07:48:39
发布于 2022-04-07 07:48:39
41000
代码可运行
举报
文章被收录于专栏:java-springjava-spring
运行总次数:0
代码可运行

上篇文章对并发的理论基础进行了回顾,主要是为什么使用多线程、多线程会引发什么问题及引发的原因,和怎么使用Java中的多线程去解决这些问题。

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

推荐读者先行查看并发编程的理论知识,以便可以丝滑入戏。

并发编程系列之一并发理论基础

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

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

一、线程生命周期

线程的几种状态转换

image-20220407104430832
image-20220407104430832

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),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他状态。

理解线程的五种状态,在调用多线程的方法时,能清楚的知道当前处于哪个状态。

我们举一个简单的实例来说明每个状态。

代码语言:txt
AI代码解释
复制
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() 方法来启动线程。

代码语言:java
AI代码解释
复制
public class MyRunnable implements Runnable {

    public void run() {

        // 需要执行多线程的业务逻辑

    }

}

    
代码语言:java
AI代码解释
复制
public static void main(String[] args) {

    MyRunnable myRunnable = new MyRunnable();

    Thread thread = new Thread(myRunnable);

    thread.start();

}

     

2、 实现 Callable 接口

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

代码语言:java
AI代码解释
复制
public class MyCallable implements Callable<Integer> {

    public Integer call() {

        return 123;

    }

}

  
代码语言:java
AI代码解释
复制
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() 方法。

代码语言:java
AI代码解释
复制
public class MyThread extends Thread {

    public void run() {

        // ...

    }

}

  

         
代码语言:java
AI代码解释
复制
public static void main(String[] args) {

    MyThread mt = new MyThread();

    mt.start();

}

  

       

4、实现接口 VS 继承 Thread

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

  • Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
  • 类可能只要求可执行就行,继承整个 Thread 类开销过大。

三、线程控制

线程在使用过程中能对其灵活的控制,包含线程睡眠和线程让步等。

在学习线程的一些控制方法前,有一个必须要了解的前置知识,在线程中分为守护进程和非守护进程。

1、Daemon

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

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

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

main() 属于非守护线程。

非守护线程可以转换为守护进程。

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

代码语言:java
AI代码解释
复制
public static void main(String[] args) {

    Thread thread = new Thread(new MyRunnable());

    thread.setDaemon(true);

}

  

2、sleep()

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

sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。

代码语言:java
AI代码解释
复制
public void run() {

    try {

        Thread.sleep(3000);

    } catch (InterruptedException e) {

        e.printStackTrace();

    }

}

      

3、yield()

对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。

代码语言:java
AI代码解释
复制
public void run() {

    Thread.yield();

}

  

4、join()

一旦这个线程执行了这个方法,只有这个线程处于死亡状态其他线程才能执行。

代码语言:txt
AI代码解释
复制
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

**①. 同步一个代码块**

代码语言:java
AI代码解释
复制
public void func() {

    synchronized (this) {

        // ...

    }

}

  

      

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

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

代码语言:java
AI代码解释
复制
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());

}        
代码语言:html
AI代码解释
复制
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

  

       

对于以下代码,两个线程调用了不同对象的同步代码块,因此这两个线程就不需要同步。从输出结果可以看出,两个线程交叉执行。

代码语言:java
AI代码解释
复制
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());

}

        
代码语言:html
AI代码解释
复制
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9  

     

**②. 同步一个方法**

代码语言:java
AI代码解释
复制
public synchronized void func () {

    // ...

}

         

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

**③. 同步一个类**

代码语言:java
AI代码解释
复制
public void func() {

    synchronized (SynchronizedExample.class) {

        // ...

    }

}  

       

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

代码语言:java
AI代码解释
复制
public class SynchronizedExample {



    public void func2() {

        synchronized (SynchronizedExample.class) {

            for (int i = 0; i < 10; i++) {

                System.out.print(i + " ");

            }

        }

    }

}

  
代码语言:java
AI代码解释
复制
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());

}

  

    

**④. 同步一个静态方法**

代码语言:java
AI代码解释
复制
public synchronized static void fun() {

    // ...

}

  

    

作用于整个类。

2、ReentrantLock

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

代码语言:java
AI代码解释
复制
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(); // 确保释放锁,从而避免发生死锁。

        }

    }

}

   
代码语言:java
AI代码解释
复制
public static void main(String[] args) {

    LockExample lockExample = new LockExample();

    ExecutorService executorService = Executors.newCachedThreadPool();

    executorService.execute(() -> lockExample.func());

    executorService.execute(() -> lockExample.func());

}

     
代码语言:html
AI代码解释
复制
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类中有四个构造方法。

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

代码语言:txt
AI代码解释
复制
    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种静态属性:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  TimeUnit.DAYS;               //天
  TimeUnit.HOURS;             //小时
  TimeUnit.MINUTES;           //分钟
  TimeUnit.SECONDS;           //秒
  TimeUnit.MILLISECONDS;      //毫秒
  TimeUnit.MICROSECONDS;      //微妙
  TimeUnit.NANOSECONDS;       //纳秒
  • workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,观察传入的workQueue 都是默认,即最大可添加Integer.MAX_VALUE个任务,所有在使用过程中要避免使用默认线程池。这里的阻塞队列有以下几种选择:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  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是同步队列,因此会在池中寻找可用线程来执行,若有可以线程则执行,若没有可用线程则创建一个线程来执行该任务;若池中线程空闲时间超过指定时间,则该线程会被销毁。
  • 适用:执行很多短期的异步任务
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  /**
   * 1.创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程

   * 2.当任务数增加时,此线程池又可以智能的添加新线程来处理任务

   * 3.此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小

   */
  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>() 无界阻塞队列
  • 通俗:创建可容纳固定数量线程的池子,每个线程的存活时间是无限的,当池子满了就不再添加线程了;如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列)
  • 适用:执行长期任务
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
   /**
\* 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>() 无解阻塞队列
  • 通俗:创建只有一个线程的线程池,当该线程正繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列)
  • 适用:按顺序执行任务的场景
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  /**  *创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(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队列中,这是一种按照超时时间排序的队列结构
  • 适用:执行周期性任务
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制

  /**
   * 创建一个定长线程池,支持定时及周期性任务执行。延迟执行
   */
  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中的使用

代码语言:txt
AI代码解释
复制
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类中提供的几个静态方法来创建线程池:

代码语言:txt
AI代码解释
复制
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的容器中。

代码语言:txt
AI代码解释
复制
/\*\*

 \* 线程池配置

 \*

 \* @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内部会自动调用多线程的拒绝策略、线程初始化等方法。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
干货|微信小程序线上线下推广的方法和销售话术小技巧分享
一款小程序在被开发后,若想获得广泛的用户群体,就得靠一些巧妙地话术宣传和推广措施。那么问题来了,微信小程序推广和话术有哪些呢?具体要怎么操作?别着急,请听小编来跟大家一一道来。 小程序推销话术小技巧
速成应用小程序开发平台
2018/06/17
3.7K0
微信 iOS 版更新:表情包选择栏大变样,小程序更像一个 App 了
【本文授权转载自小程序报道第一媒体知晓程序(微信号 zxcx0101)。关注「知晓程序」,让你更知小程序。】 9 月 17 日,期待已久的微信 7.0.7 for iOS 正式版上线了。 与一周前更新的微信 7.0.7 for Android 内测版相似,此次更新并没有新功能上线,更多是一些细节上的改变。或许是为了与 Android 版本号同步,iOS 版微信的更新跳过了 7.0.6 版本号,从 7.0.5 直接来到了 7.0.7。 为了帮助大家更好地了解此次更新情况,我们将微信 7.0.7 for
腾讯大讲堂
2019/09/19
8910
微信 iOS 版更新:表情包选择栏大变样,小程序更像一个 App 了
微信公众平台开发[2] —— 微信端分享功能
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011415782/article/details/51870790
泥豆芽儿 MT
2018/09/11
5.7K0
微信公众平台开发[2] —— 微信端分享功能
独立开发 一个社交 APP 的架构分享 (已实现)
根据文章内容总结摘要。
林冠宏-指尖下的幽灵
2018/01/03
5.1K0
张小龙发布2018微信全新计划(内附演讲全文)
2018 年我们希望能做一些新的尝试,我认为探索线下的精彩生活,这是我们下一步想要尝试的一个方向。
腾讯大讲堂
2018/01/15
3.1K4
张小龙发布2018微信全新计划(内附演讲全文)
薛青:融合通信企业实践案例分享
企业微信的口号叫做“连接成就智慧企业”, 曾经还有一个口号叫做“让每一家企业都有自己的微信”,企业微信是从微信系统中发展出来的。
腾讯云开发者社区技术沙龙
2018/10/16
1.8K0
薛青:融合通信企业实践案例分享
为什么人脸识别系统总是认错黑人?
2019 年 1 月的一个周六,两名警察来到一家酒店。几十分钟前,他们接到报警,说有一名黑人男子在礼品店里偷吃零食,被店员逮了个正着,你们赶紧来看看呀。
浅黑科技
2021/03/03
1.6K0
为什么人脸识别系统总是认错黑人?
互联网时代的女性主义特征
当前互联网上女性的影响越来越大。从表面上看,女性在网上更喜欢社交、购物等等,但这些只是表面现象,而且都是在说女性本身在怎么样。
晓吾
2020/04/08
1.5K0
互联网时代的女性主义特征
《增长黑客》节选与笔记[通俗易懂]
这本书涉及了很多具体又贴合现实的互联网产品问题,即使你是非专业人士,也应该读一读,了解开发者是如何把你当猫耍的,以便你更好地认识一些套路,解锁,为选择手机软件或者云端应用擦亮眼睛!
全栈程序员站长
2022/08/27
7.8K0
免费使用满血版DeepSeek R1搭建服从性超强的AI女友+个人法律顾问,新手小白也可以【图文喂饭级教程】
家人们,谁懂啊!最近我沉迷探索各种 AI 大模型,一心想找个 “得力助手” 帮我处理繁重的工作。听说 deepseek 很厉害,兴致勃勃跑去官网体验,结果现实给我泼了盆冷水。官网那叫一个卡顿,点一下页面,转半天圈没反应,“服务器繁忙,请稍后再试” 的提示反复弹出,就像个赶不走的 “小恶魔”,一次次把我的期待狠狠碾碎😫 这使用体验,简直糟糕透顶,我都怀疑自己是不是在和一个世纪前的老旧系统打交道。
小白的大数据之旅
2025/03/14
7290
免费使用满血版DeepSeek R1搭建服从性超强的AI女友+个人法律顾问,新手小白也可以【图文喂饭级教程】
夸克“凶猛”:一场手机镜头背后的狂野冒险
浅友们好~我是史中,我的日常生活是开撩五湖四海的科技大牛,我会尝试各种姿势,把他们的无边脑洞和温情故事讲给你听。如果你想和我做朋友,不妨加微信(shizhongmax)。
浅黑科技
2022/11/11
1.8K0
夸克“凶猛”:一场手机镜头背后的狂野冒险
龙哥风向标 20230411~20230418 GPT拆解
盈利点:利用Midjourney的提示词样式衣服商机,可以开展国内的衣服定制业务,提供定制化的提示词样式衣服,包括MJ提示词样式和其丨他样式,同时可以考虑提供定制情侣衫、班服等服务。
ApacheCN_飞龙
2024/01/31
3860
从原理到策略算法再到架构产品看推荐系统 | 附Spark实践案例
作者 | HCY崇远 01 前言 本文源自于前阵子连续更新的推荐系统系列,前段时间给朋友整理一个关于推荐系统相关的知识教学体系,刚好自身业务中,预计明年初随着业务规模增长,估摸着又要启动推荐相关的项目了,所以也是趁机把相关的知识结构梳理了一遍。这这里重新做整理,并额外做了一些增减,让整体逻辑会更通顺一点。 整个文章的结构逻辑,先从推荐系统的基础知识结构讲起,然后由浅入深过渡到几个推荐策略算法上,并且为每个推荐策略算法提供一些简单的入门Spark案例代码,再从策略过渡到系统层级,包括数据架构、策略组合
CSDN技术头条
2018/02/06
2K0
从原理到策略算法再到架构产品看推荐系统 | 附Spark实践案例
JavaSE 编写第一个程序
介绍 JavaSE 基础的基本语法知识,不会包含特别难以理解或更深层次的内容,通俗易懂。本人是实战派,看着大幅篇章晦涩的理论,但是没有多少实践证明的书籍就头疼;同时如果知识东一点、西一点,跳跃性太大,不成体系,也比较麻烦。
全栈程序员站长
2022/09/14
6.7K0
JavaSE 编写第一个程序
【超长好文】网络安全从业者面试指南
多年来筛选了数以千记的简历,为何很多人连面试的机会都没有?参与了数以百记的应聘者的面试,为何如此多的人没有通过最终面试?能力当然是最重要的,可我却见过很多能力不比已经入职的同事差却应聘失败的人,到底该如何做?希望这篇安全从业者面试指南能够帮到你,让你少走一些弯路。
星尘安全
2024/10/01
2190
【超长好文】网络安全从业者面试指南
扎克伯格舌战群儒实录全文大放送!
源 / 新浪科技 文 / 新浪科技编译组 导读: 美国东部时间4月10日,Facebook首席执行官马克·扎克伯格出席了美国参议院司法和商务委员会的联合听证会,讨论数据隐私和Facebook社交网站上虚假信息等问题。 扎克伯格在质询中再次对Facebook数据泄露事件表示道歉,并回答参议员关于Facebook数据隐私、假新闻、对大选的影响、垄断等问题。 Grassley:司法和商务、科学和运输委员会将下达听证通知。我们欢迎今天参加Facebook社交媒体隐私泄露和滥用数据听证会的每一个人。
顶级程序员
2018/04/26
9530
扎克伯格舌战群儒实录全文大放送!
大教堂与集市(最新译本)
Linux有一套令人感到吃惊的软件工程理论,为了验证这些理论,我专门在fetchmail项目中加以应用,效果之好让我感到惊讶。本文将讨论这些理论,并对比两种完全不同的开发模式:绝大多数商业公司所采用的“大教堂”模式和Linux世界采用的“集市”模式。两种模式的根本不同在于他们对软件排错有着完全对立的认识。我从Linux的经验出发,证实了这样一个命题:“只要眼睛多,bug容易捉”,这和那些由利己个体组成的自纠错系统有着异曲同工之妙。文末,我探讨了在这种观念的影响下,软件可能拥有的未来。
麒思妙想
2021/04/09
2.3K0
大教堂与集市(最新译本)
人工智障 2 : 你看到的AI与智能无关
两年前,写了一篇文章《为什么现在的人工智能助理都像人工智障》,当时主要是怼“智能助理们”。这次呢则是表达 “我不是针对谁,只是现在所有的深度学习都搞不定对话AI”,以及“你看都这样了,那该怎么做AI产品”。
Fayson
2019/11/28
1.3K0
初中数学课程与信息技术的整合[通俗易懂]
2.1 基本工具介绍 2 2.1.1滑动的梯子上的猫 2 2.1.2智能画笔挥洒自如 7 2.1.3选了再做谋而后动 9 2.1.4公式输入即打即现 10 2.1.5动态测量功能多多 15 2.2文本命令应有尽有 18 2.2.1点可不简单 18 2.2.2直线面面观 22 2.2.3圆和圆弧很重要 23 2.2.4圆锥曲线条件多 24 2.2.5函数曲线最有用 25 2.2.6图形变换功能强 26 2.2.7对象组分合遮盖 28 2.2.8文本含变量表格 28 2.2.9测量招数真不少 31 2.2.10动画轨迹和跟踪 32 2.2.11对象属性有奥妙 38 2.3平面几何 40 2.3.1动态几何暗藏玄机 40 2.3.2动点定值眼见为实 42 2.3.3图案组合美不胜收 50 2.3.4课件制作初步体验 58 2.4代数运算 68 2.4.1符号计算力量大 68 2.4.2因式分解渊源长 70 2.4.3赋值语句真方便 72 2.4.4定义函数编程快 74 2.4.5复数联通数与形 77
全栈程序员站长
2022/08/26
1.5K0
如何做职业规划并进行求职准备(持续更新)「建议收藏」
总结:就现在情况,大学我不考研,安心求职 考研=我要“它”+我现在就要 我不要“它”:测试是个实践性很强的工作,测试招聘学士学位占比低,研究型的测试研究生学历比起小本并不能带来太大优势 我现在不要:不可否认,学历可以突破职业瓶颈,所以我要考研,但是是在很多年以后,而不是现在。(等以后进入管理阶层,有了丰富的经验,考取工商管理MBA,得到的相关的文凭技能人脉会更加有价值)
全栈程序员站长
2022/11/01
3.3K0
如何做职业规划并进行求职准备(持续更新)「建议收藏」
推荐阅读
相关推荐
干货|微信小程序线上线下推广的方法和销售话术小技巧分享
更多 >
LV.2
悦阳网络全栈工程师
目录
  • 一、线程生命周期
    • 1、新建(New)
    • 2、就绪
    • 3、运行(Runnable)
    • 4、阻塞(Blocking)
    • 5、死亡(Terminated)
  • 二、创建线程的三种方式
    • 1、实现 Runnable 接口
    • 2、 实现 Callable 接口
    • 3、继承 Thread 类
    • 4、实现接口 VS 继承 Thread
  • 三、线程控制
    • 1、Daemon
    • 2、sleep()
    • 3、yield()
    • 4、join()
    • 5、wait()\notify()
  • 四、线程同步
    • 1、synchronized
    • 2、ReentrantLock
    • 3、比较
    • 4、使用选择
  • 五、线程池
    • 1、核心线程类
    • 2、线程执行流程
    • 3、四种线程池及使用场景
    • 5、使用实例
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档