多线程(五)

目录

一. 死锁

二. 线程池

1. 使用线程池的优点

2. 线程池执行任务的流程

3. 线程池的使用

4. JDK内置的四大线程池


 

 

一. 死锁


产生原因:对并发资源的加锁成“环”。

例:

class Pen{}

class Book{}

public class Test {
    private static Pen pen = new Pen();
    private static Book book = new Book();
    public static void main(String[] args)  {
        //笔线程
        Thread thread = new Thread(()->{
            synchronized (pen){
                System.out.println("我有笔,把你的本子给我。");
                synchronized (book){
                    System.out.println("把你的本子给我");
                }
            }
        });
        //本子线程
        Thread thread1 = new Thread(()->{
            synchronized (book) {
                System.out.println("我有本子,把你的笔给我。");
                synchronized (pen) {
                    System.out.println("把你的笔给我");
                }
            }
        });
        thread.start();
        thread1.start();
    }
}

产生死锁,无法进一步执行。

 

二. 线程池


1. 使用线程池的优点

  • 降低资源的消耗:通过重复利用已创建的线程降低线程创建与销毁带来的损耗。
  • 提高响应速度:当新任务到达时,任务不需要等待线程创建就可以立即执行。
  • 提高线程的可管理性:使用线程池可以统一进行线程的分配、调度与监控。

补充:线程池的继承关系

 

2. 线程池执行任务的流程

当一个Runnable或Callable对象到达线程池时,执行策略如下:

第一步:首先判断核心线程池中的线程是否都在执行任务;

              如果是,再次查看核心线程池是否已满,如果未满,创建新的线程并执行任务。

              如果核心线程池有空闲线程,则将任务分配给空闲线程执行。否则执行第二步。

第二步:判断工作队列(BlockingQueue)是否已满;

              如果工作队列没有满,将提交任务存储到工作队列中等待核心池的调度;

              否则,若工作队列已满,则执行第三步。

第三步:判断当前线程池中的线程数是否已达到最大值maxiumSize;

              若已达到最大值,将任务交给饱和策略处理;

              否则,继续创建新线程执行任务。

 

3. 线程池的使用

3.1 手工创建线程池

通过创建ThreadPoolExecutor来创建线程池。

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

  I. corePoolSize(核心池的大小):当提交任务到线程池时,线程池会创建一个新的线程来执行任务,即使核心线程池中有其他空闲线程也会创建新线程,一直到线程数达到了核心池的大小为止

调用prestartAllCoreThreads(),线程池会提前创建并启动所有核心线程。

 

  II. workQueue(工作队列):用于保存等待执行任务的阻塞队列。

可以选择以下几个阻塞队列:

  • ArrayBlockingQueue:基于数组结构的有界阻塞队列,此队列按照FIFO原则对元素进行排序。
  • LinkedBlockingQueue:基于链表结构的阻塞队列,按照FIFO排序元素,吞吐量高于ArrayBlockingQueue。(内置的固定大小的线程池采用此队列)
  • synchronousQueue:一个不存储元素的阻塞队列(无界队列),每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,通常吞吐量比LinkedBlockingQueue还要高。(内置的缓存线程池采用此队列)
  • PriorityBlockingQueue:具有优先级的无界阻塞队列。(优先级队列的底层数据结构是堆)

 

  III. maxiumPoolSize(线程池最大线程数量):线程池允许创建的最大线程数。

如果队列已满并且已创建的线程数小于此参数,则线程池会再创建新的线程执行任务。否则,调用饱和策略处理。

如果采用无界队列,此参数无意义。

 

  IV. keepAliveTime(线程保持活动时间):线程池的工作线程空闲后,保持存活的时间。

若任务很多,并且每个任务执行的时间较短,可以跳大此参数来提高线程利用率。

 

  V. TimeUnit(IV参数的时间单位)

 

  VI. RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态。此时采用饱和策略来处理任务,默认采用AbortPolicy。

JDK一共内置四个饱和策略。

  • AbortPolicy,表示无法处理新任务抛出异常,JDK默认采用此策略。
  • CallerRunsPolicy,等待调用者线程空闲后运行任务。(调用者线程:调用线程池的线程)
  • DiscardOldestPolicy,丢弃阻塞队列中最近的一个任务,并执行当前任务。
  • DiscardPolicy,不处理,直接将新任务丢弃,也不报错。

 

线程池的简单使用:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 调用Runnable接口实现线程池的简单使用
 */

class MyThread implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i <10; i++){
            System.out.println(Thread.currentThread().getName()+"、"+i);
        }
    }
}

public class Test {
    public static void main(String[] args)  {
        MyThread myThread = new MyThread();
        ExecutorService executorService =
                new ThreadPoolExecutor(3,5,2000,
                        TimeUnit.MICROSECONDS,new LinkedBlockingQueue<>());
        for (int i = 0; i < 5; i++){
            executorService.execute(myThread);
        }
        //线程执行完后一定要关闭
        executorService.shutdown();

    }
}

部分截图:

我们可以清晰的看到,是三个线程在交替执行,因为核心线程池的大小设置为3。

 

import java.util.concurrent.*;

/**
 * 调用Callable接口实现线程池的简单使用
 */

class MyThread implements Callable{

    @Override
    public Object call() throws Exception {
        for (int i = 0; i < 10; i++){
            System.out.println(Thread.currentThread().getName()+"、"+i);
        }
        return Thread.currentThread().getName()+"执行结束";
    }
}

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyThread myThread = new MyThread();
        ExecutorService executorService =
                new ThreadPoolExecutor(3,5,2000,TimeUnit.MICROSECONDS,
                        new LinkedBlockingQueue<>());
        for (int i = 0; i < 5; i++){
            Future future = executorService.submit(myThread);
            System.out.println(future.get());
        }
        //关闭
        executorService.shutdown();
    }
}

根据结果我们可以看出,通常是一个线程执行完毕后才开始下一个线程的执行,是为什么呢?

因为Future类的get()方法会阻塞其他线程,一直等到当前Callable线程执行完毕拿到返回值为止。

 

4. JDK内置的四大线程池

普通调度池(ExecutorService)

       I. 创建无大小限制的线程池:Executors.newCachedThreadPool()

            适用于很多短期任务的小程序,负载较轻的服务器。

        II. 创建固定大小线程池:Executors.newFixedThreadPool(int nThreads)

            适用于为了满足资源管理的需求而需要限制当前线程数量的应用场合,适用于负载比较重的服务器。

        III. 单线程池:Executors.newSingleThreadPool()

            适用于需要保证顺序的执行各个任务并且在任意时间点不会有多个线程活动的场景。

定时调度池(ScheduledExecutorService)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值