javaSE——多线程

一、线程

1、概念

线程是指进程中的一个执行流程,一个进程可以由多个线程组成,即一个进程中可以同时运行多个不同的线程,它们分别执行不同的任务。当进程内的多个线程同时运行时,这种运行方式成为并发运行。

2、线程和进程的区别

  • 每个进程都有独立的代码和存储空间(进程上下文),进程切换的开销大。

  • 线程没有独立的存储空间,而是和所属进程中其他的线程共享代码和存储空间,但每个线程有独立的运行栈和程序计数器,因此线程切换的开销较小。

  • 多进程——在操作系统中能同时运行多个任务(程序),也称多任务。

  • 多线程——在同一应用程序中有多个顺序流同时执行。

二、线程的创建

1、继承Thread类创建线程

Thread存放在java.lang类库里,但编译器会自动加载此类库,而run()方法是定义在Thread里面的一个方法,所以线程的程序代码编写在run()里面

激活线程的语法

class 类名 extends Thread    //从Thread类扩展出子类
{
    属性
    方法
    修饰符 run(){
        以线程处理的程序;
    }
}

步骤:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体

  2. 创建Thread子类的实例,即创建了线程对象

  3. 调用线程对象的sttart()方法来启动该线程

2、实现Runnable接口创建线程

步骤:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体

  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象

  3. 调用线程对象的start()方法来启动该线程

3、使用Callable和Future创建进程

Callable接口包含一个方法call,实现该方法来定义一个任务,这个任务具有返回值V。该方法可以抛出异常。

java5提供了Future接口来代表Callable接口里的call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runnable接口,所以这样可以作为Thread的target。

创建并启动有返回值的线程的步骤

  • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法没有返回值,再创建Callable实现类的实例。(从java8开始,可以直接使用Lambda表达式创建Callable对象)。

  • 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

  • 使用FutureTask作为Thread对象的target创建并启动新线程。

  • 调用FutureTask对象的get方法来获得子线程执行结束后的返回值。

4、创建线程的三种方法对比

采用实现 Runnable、Callable 接口的方式创建多线程的优缺点

  1. 线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。

  2. 在这种方式下,多个线程可以共享同一个 target 对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

  3. 劣势是,编程稍稍复杂,如果需要访问当前线程,则必须使用 Thread.currentThread(方法。

采用继承 Thread 类的方式创建多线程的优缺点:

  1. 劣势是,因为线程类已经继承了 Thread 类,所以不能再继承其他父类。

  2. 优势是,编写简单,如果需要访问当前线程,则无须使用 Thread.currentThread(方法,直接使用this 即可获得当前线程。

三、线程的生命周期

1、新建和就绪状态

当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的 Java 对象一样,仅仅由 Java 虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何的动态特征,程序也不会执行线程的执行体。

当线程对象调用了 start() 方法之后,该线程处于 就绪状态, Java 虚拟机会为其创建方法调用栈和程序计数器,处于这种状态中的线程并没有开始运行,只是表示该线程可以运行了,至于该线程何时开始运行,取决于 JVM 里线程调度器的调度

注意:如果直接调用线程对象的 run() 方法,系统把线程对象当成一个普通对象,而 run() 方法也是一个普通方法,而不是一个线程执行体。

2、运行和阻塞状态

处于就绪状态的线程获得了CPU,开始执行 run() 方法的线程执行体,则该线程处于运行状态,如果计算机只有一个 CPU ,那么在任何时刻只有一个线程处于运行状态,当然,在一个多处理器的机器上,将会有多个线程并行执行;当线程数大于处理器数时,依然会存在多个线程在同一个 CPU 上轮换的现象。

当发生如下情况时,线程将会进入阻塞状态。

  • 线程调用 sleep() 方法主动放弃所占用的处理器资源

  • 线程调用了一个阻塞式 IO 方法,在该方法返回之前,该线程被阻塞。

  • 线程试图获得一个同步监视器,但该同步监视器正在被其他线程所持有

  • 线程在等待某个通知 (notify)

  • 程序调用了线程的 suspend() 方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法

3、线程死亡

线程会以如下三种方式结束,结束后就处于死亡状态。

  • run() 或 call() 方法执行完成,线程正常结束。

  • 线程抛出一个未捕获的 Exception 或 Error

  • 直接调用该线程的 stop() 方法来结束该线程——该方法容易导致死锁,通常不推荐

注意:当主线程接收时,其他线程不受任何影响,并不会随之结束。一旦子线程启动来后,它就拥有和主线程相同的地位,它不会受主线程的影响。

线程对象的 isAlive() 方法,测试某个线程是否已经死亡

  1. 线程处于就绪、运行、阻塞、三种状态,该方法返回 true

  2. 线程处于新建、死亡两种状态,该方法返回 false

注意:不要试图对一个死亡的线程调用 start() 方法使它重新启动,死亡就是死亡,该线程将不可再次作为线程执行(对新建状态的线程两次调用 start() 方法也是错误的)

四、控制线程

1、join线程

在自己的线程中加入别的线程,也就是调用别的线程的 join()方法,使得自己的线程阻塞,会等待加入的线程结束后才继续执行。

join()方法的三种重载形式

  • join():等待被 join 的线程执行完成

  • join(long millis):等待被 join 的线程的时间最长为 mills 毫秒。如果在 millis 毫秒内被 join 的线程还没有执行结束,就不再等待。

  • join(long millis, int nanos):等待被 join 的线程的时间最长为 mills 毫秒加 nanos 毫微秒。

2、后台线程

setDaemon() 后台线程是一种在后台运行的,任务是为其他的线程提供服务的线程。JVM 的垃圾回收线程就是一种后台线程。

  • 前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程

  • 设置后台线程 setDaemon()必须要在 start()方法前调用,否则报错

  • setDaemon():用于设置线程为后台线程。

  • isDaemon():用于判断指定线程是否是后台线程。

3、线程睡眠

sleep()使当前线程暂停一段时间,进入阻塞状态,时间结束后直接进入就绪态。

以下代码中,定义实现了Runnable接口的类,重写run()方法,在while死循环中调用sleep()方法让当前线程睡眠1s,当线程调用interrupt方法时发生打断线程的异常,catch捕获并使用break跳出循环

4、线程让步

当线程调用Thread类的yield()静态方法时,线程让出cpu资源,从运行态转为阻塞态。

下面代码的run()方法中,当i可以被3整除时调用yield()方法让出cpu。从运行结果可以看出,当线程Thraed1执行到可以被3整除时,让出线程给Thread2执行,反之,Thread2让出线程给Thread1执行。

5、改变线程优先级

每一个线程都有一定的优先级,其中有三个常量:

    MAX_PRIORITY:值为 10

    MIN_PRIORITY:值为 1

    NORM_PRIORITY:值为 5

也可以自己设置不同的优先级,范围是 1~10 之间的整数。

子线程的优先级都与创建的它的父线程的优先级相同,其中 main 方法默认是普通优先级为 5。

可通过 setPriority()和 getPriority()方法设置或获取当前线程的优先级。

五、线程同步

1、线程同步

同步是使用synchronized关键字定义的代码块,但同步时需要设置一个对象锁,一般会给当前对象this上锁

2、同步方法

如果在一个方法中使用了synchronized定义,则此方法就是同步方法

3、同步锁(Lock)

Lock对象提供了比 synchronized 方法和 synchronized 代码块更广泛的锁定操作,Lock 允许实现更灵活的结构,可以具有差别很大的属性,并且支持多个相关的 Condition 对象。

Lock 是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象。

某些锁可能允许对共享资源并发访问,如 ReadWriteLock(读写锁),Lock、ReadWriteLock 是 Java 5提供的两个根接口,并为 Lock 提供ReentrantLock(可重入锁)实现类,为 ReadWriteLock 提供了ReentrantReadWriteLock 实现类。

Java 8 新增了新型的 StampedLock 类,在大多数场景中它可以替代传统的 ReentrantReadWriteLock。ReentrantReadWriteLock 为读写操作提供了三种锁模式:

  1. Writing

  2. ReadingOptimistic

  3. Reading。

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

地加锁、释放锁,通常使用 ReentrantLock 的代码格式如下:

class X
{
    //定义对象
       private final ReentrantLock Lock = new ReentrantLock();
    //定义需要保证线程安全的方法
    public void m(){
        //加锁
        lock.lock();
                try{
                    //需要保证线程安全的代码
                    //……method body
                }
                //使用finally块来保证释放锁
                finally
                {
                    lock.unlock();
                }
        }    
}

使用ReentrantLock对象来进行同步,加锁和释放锁出现在不同的作用范围内时,通常建议使用finally块来确保在必要时释放锁。通过使用ReentrantLock对象,我们可以把Account类改为如下形式,它依然是线程安全的:

public class Account {
    // 定义锁对象
    private  final ReentrantLock lock = new ReentrantLock();
    // 封装账户编号、账户余额两个Field
    private String accountNo;
    private double balance;

    public Account() {
    }
    // 构造器
    public Account(String accountNo, double balance){
        this.accountNo=accountNo;
        this.balance=balance;
    }
    public String getAccountNo(){
        return this.accountNo;
    }

    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }
    // 因为账户余额不允许随便修改,所以只为balance提供getter方法
    public double getBalance() {
        return this.balance;
    }
    // 提供一个线程安全的draw()方法来完成取钱操作
    public void draw(double drawAmount) {
        lock.lock();
        try {
            // 账户余额大于取钱数目
            if (balance >= drawAmount) {
                // 吐出钞票
                System.out.println(Thread.currentThread().getName()
                        + "取钱成功!吐出钞票:" + drawAmount);
                try {
                    Thread.sleep(1);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
                // 修改余额
                balance -= drawAmount;
                System.out.println("\t余额为: " + balance);
            } else {
                System.out.println(Thread.currentThread().getName()
                        + "取钱失败!余额不足!");
            }
        }finally {
            lock.unlock();
        }
    }
    public int hashCode() {
        return accountNo.hashCode();
    }
    public boolean equals(Object obj) {
        if(this == obj)
            return true;
        if (obj != null && obj.getClass()==Account.class) {
            Account target = (Account)obj;
            return target.getAccountNo().equals(accountNo);
        }
        return false;
    }
}

六、线程通信

生产者-消费者问题:

生产者将生产的产品放到仓库中,而消费者从仓库中取走产品。仓库一次存放固定数量的产品。如果仓库满了,生产者停止生产,等待消费者消费产品;如果仓库不满,生产者继续生产;如果仓库是空的,消费者停止消费,等仓库有产品再继续消费。

public class ProducerConsumer{
     public static void main(String[] args) {
         Stack s = new Stack();      //创建对象s
         Producer p = new Producer(s);        //创建生产者对象
         Consumer c = new Consumer(s);        //创建消费者对象
         new Thread(p).start();         //创建生产者进程1      
         new Thread(p).start();         //创建生产者进程2
         new Thread(p).start();         //创建生产者进程3
         new Thread(c).start();         //创建消费者进程
     }
}
//Rabbit类(产品)
class Rabbit{
     int id;           //类的成员变量
     Rabbit(int id){
         this.id = id;
     }
     public String toString() {
         return "玩具:"+id;
     }
}
//栈(存放玩具兔的仓库)
class Stack{
     int index = 0;
     Rabbit[] rabbitArray = new Rabbit[6];         //存放玩具兔的数组
     public synchronized void push(Rabbit wt) {         //玩具兔放入数组的方法
         while(index == rabbitArray.length) {
              try {
                  this.wait();            //栈满,等待消费者消费
              }catch(InterruptedException e) {
                  e.printStackTrace();
              }
         }
         this.notifyAll();            //唤醒所有生产者进程
         rabbitArray[index] = wt;        //将玩具兔放入栈
         index++;
     }
     public synchronized Rabbit pop() {        //将玩具兔取走的方法
         while(index == 0) {         //如果栈空
              try {
                  this.wait();        //等待生产玩具兔
              }catch(InterruptedException e) {
                  e.printStackTrace();
              }
         }
         this.notifyAll();         //栈不空唤醒所有消费者线程
         index--;
         return rabbitArray[index];
     }
}
//生产者类
class Producer implements Runnable{
     Stack st = null;
     Producer(Stack st){
         this.st = st;          //构造方法,为类的成员变量ss赋值
     }
     public void run() {                  //线程体
         for(int i = 0;i<20;i++) {            //循环生产20个玩具
              Rabbit r = new Rabbit(i);            //创建玩具兔类
              st.push(r);               //将生产的玩具兔放入栈
              //输出生产了玩具r,默认调用了玩具兔类的toString()
              System.out.println("生产-"+r);
              try {
                  Thread.sleep((int)(Math.random()*200));     //生产一个玩具兔后睡眠
              }catch(InterruptedException e) {
                  e.printStackTrace();
              }
         }
     }
}
//消费者类
class Consumer implements Runnable{
     Stack st = null;
     Consumer(Stack st){                  //构造方法,为类的成员变量ss赋值
         this.st = st;
     }
     public void run() {
         for(int i =0;i<20;i++) {        //循环消费,即取走20个玩具兔
              Rabbit r = st.pop();        //从栈中取走一个玩具兔
              System.out.println("消费-"+r);
              try {
                  Thread.sleep((int)(Math.random()*200));
              }catch(InterruptedException e) {
                  e.printStackTrace();
              }
         }
     }
}

七、线程池

1、简介

线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务

2、优点

  • 线程和任务分离,提升线程重用性;

  • 控制线程并发数量,降低服务器压力,统一管理所有线程; 

  • 提升系统响应速度,假如创建线程用的时间为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那么使用线程池就免去了T1和T3的时间;

3、ThreadPoolExecutor源码

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}
 
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         threadFactory, defaultHandler);
}
 
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), handler);
}
 
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.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
  • corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。

  • maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。

  • keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。

  • unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。

  • workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。

  • threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。

  • handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。

线程池的使用流程

// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE,
                                             MAXIMUM_POOL_SIZE,
                                             KEEP_ALIVE,
                                             TimeUnit.SECONDS,
                                             sPoolWorkQueue,
                                             sThreadFactory);
// 向线程池提交任务
threadPool.execute(new Runnable() {
    @Override
    public void run() {
        ... // 线程执行的任务
    }
});
// 关闭线程池
threadPool.shutdown(); // 设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程
threadPool.shutdownNow(); // 设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表

4、线程池的工作原理

5、线程池的参数

(1)、任务队列(workQueue)

任务队列是基于阻塞队列实现的,即采用生产者消费者模式,在 Java 中需要实现 BlockingQueue 接口。但 Java 已经为我们提供了 7 种阻塞队列的实现:

  • ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)。

  • LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE。

  • PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现 Comparable 接口也可以提供 Comparator 来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务。

  • DelayQueue:类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来。

  • SynchronousQueue: 一个不存储元素的阻塞队列,消费者线程调用 take() 方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用 put() 方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。

  • LinkedBlockingDeque: 使用双向队列实现的有界双端阻塞队列。双端意味着可以像普通队列一样 FIFO(先进先出),也可以像栈一样 FILO(先进后出)。

  • LinkedTransferQueue: 它是ConcurrentLinkedQueue、LinkedBlockingQueue 和 SynchronousQueue 的结合体,但是把它用在 ThreadPoolExecutor 中,和 LinkedBlockingQueue 行为一致,但是是无界的阻塞队列。

注意有界队列和无界队列的区别:如果使用有界队列,当队列饱和时并超过最大线程数时就会执行拒绝策略;而如果使用无界队列,因为任务队列永远都可以添加任务,所以设置 maximumPoolSize 没有任何意义。

(2)、线程工厂(threadFactory)

线程工厂指定创建线程的方式,需要实现 ThreadFactory 接口,并实现 newThread(Runnable r) 方法。该参数可以不用指定,Executors 框架已经为我们实现了一个默认的线程工厂:

/**
 * The default thread factory.
 */
private static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;
 
    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                              Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }
 
    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

(3)、拒绝策略(handler)

当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现 RejectedExecutionHandler 接口,并实现 rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法。不过 Executors 框架已经为我们实现了 4 种拒绝策略:

  • AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。

  • CallerRunsPolicy:由调用线程处理该任务。

  • DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。

  • DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。

6、功能线程池

(1)、定长线程池(FixedThreadPool)

特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。

应用场景:控制线程最大并发数。

// 1. 创建定长线程池对象 & 设置线程池线程数量固定为3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
fixedThreadPool.execute(task);

(2)、定时线程池(ScheduledThreadPool )

特点:核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为延时阻塞队列。

应用场景:执行定时或周期性的任务。

// 1. 创建 定时线程池对象 & 设置线程池线程数量固定为5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务

(3)、可缓存线程池(CachedThreadPool)

特点:无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。

应用场景:执行大量、耗时少的任务。

// 1. 创建可缓存线程池对象
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
cachedThreadPool.execute(task);

(4)、单线程化线程池(SingleThreadExecutor)

特点:只有 1 个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。

应用场景:不适合并发但可能引起 IO 阻塞性及影响 UI 线程响应的操作,如数据库操作、文件操作等。

// 1. 创建单线程化线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
singleThreadExecutor.execute(task);

(5)、对比

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DF10F-0001A

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值