跳至主要內容

Java线程&线程池

holic-x...大约 12 分钟碎片化碎片化

Java线程&线程池

1.Java线程

线程生命周期

​ 生命周期:新建=》就绪=》启动=》阻塞=》死亡

image-20240401231438038

1.新建(New):创建后未启动(未调用start()方法)的线程处于这种状态

2.就绪(Ready):调用start()方法,进入就绪状态。等待CPU调度执行

3.运行(Running):该状态可能正在CPU执行,也可能在等待CPU执行

​ 注意: 线程要想进入运行状态执行,首先必须处于就绪状态中

4.无限期等待(Waiting):不会分配CPU执行时间,要等待被其他线程显式唤醒。Object.wait()方法可让其无限等待

5.限期等待(Timed Waiting): 不会分配CPU执行时间,在一定时间之后它们会由系统自动唤醒。Object.wait(long millis)方法让线程进入限期等待状态

6.阻塞(Blocked):线程在获取排他锁失败(因为锁被其它线程所占用),进入同步阻塞状态

7.死亡(Dead):线程执行完了或者因异常退出了run()方法,该线程生命周期结束

创建线程

线程的创建有三种方式:继承Thread、实现Runnable、实现Callable接口

  • 继承Thread类,重写run方法,调用start方法启动线程
class MyThread1 extends Thread {
    @Override
    public void run() {
        while(true){
            System.out.println("方式1创建线程......");
        }
    }
}

public class CreateThreadDemo1 {
    public static void main(String[] args) {
        MyThread1 mt = new MyThread1();
        mt.start();
    }
}
  • 实现Runnable接口,重写run方法,通过Thread调用start方法启动线程
class MyThread2 implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("方式2创建线程......");
        }
    }
}

public class CreateThreadDemo2 {
    public static void main(String[] args) {
        MyThread2 mt = new MyThread2();
        Thread thread = new Thread(mt);
        thread.start();
    }
}
  • 实现Callable接口,重写call方法(从任务中产生返回值),借助FutureTask对象创建间Thread对象启动线程,调用get方法获取线程结束之后的结果
class MyThread3 implements Callable<String> {
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 10; i++) {
            System.out.println("方式3:借助Callable创建线程" + i);
        }
        return "call线程";
    }
}

public class CreateThreadDemo3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 线程开启之后需要执行里面的call方法
        MyThread3 mt = new MyThread3();
        // 构建FutureTask对象
        FutureTask<String> ft = new FutureTask<>(mt);
        //创建线程对象
        Thread t = new Thread(ft);
        // 开启线程
        t.start();
        System.out.println(ft.get());
    }
}

测试类

当线程调用start方法之后会进入就绪ready状态(而非运行run状态),线程内部依赖jvm的调度,当线程调用start方法之后,jvm会判定这个线程是否可执行,但至于什么时候执行则取决于jvm内部的调度,由CPU自行决定要指定的线程

public class ThreadTest {
    /**
     * 1>分别以两种方式测试线程创建,分析其生命周期
     * a.继承Thread类
     * b.实现Runnable接口
     *
     * 2>线程的5个状态分析
     * 新建     new
     * 就绪     ready
     * 运行     run
     * 阻塞     wait
     * 死亡     terminal(结束)
     */
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 方式1创建线程:继承Thread
        MyThread1 mt = new MyThread1();
        mt.start();

        // 方式2创建线程:实现Runnable接口(可通过Lambda表达式创建并启动线程)
        new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    System.out.println("方式2创建线程:借助Lambda表达式构建");
                }
            }
        }).start();

        /**
         * 执行结果分析:线程的启动主要CPU内部的调度(涉及CPU时间片的概念)
         * 此处执行的结果显示,每次执行的线程顺序均有所不同,
         * 且每个线程是交替执行的,一个线程执行一段时间之后便将CPU交付给下一个线程执行以此类推
         */
    }
}

🔖线程设置

常用的线程方法

方法名说明
void setName(String name)设置线程名称
String getName()返回线程名称
Thread currentThread()返回对当前正在执行的线程对象的引用

线程是一个可调操作系统的对象,他的start()、stop()、yield()、sleep(long var0)...等线程方法都是调用操作系统执行

public class Thread implements Runnable {
    private long tid; 线程的ID, 由线程自己分配
    private volatile String name; 线程的名称, 可以手动设置(setName())
    private int priority; 线程的优先级, 默认5, 最小1, 最大10
    private ThreadGroup group; 线程所属的线程组
    private ClassLoader contextClassLoader; 次线程上下文ClassLoader
    private boolean daemon = false; 是否是守护线程
    private volatile int threadStatus = 0; 线程的状态. 对应静态枚举类Stete
 
    public void run(){} 线程的功能方法
    public synchronized void start(){} 线程准备就绪,等待程序调用run()方法. 异步操作, 调用顺序不代表实际启动的顺序.
    public final void stop(){} 终止当前线程, 不建议使用
    public static native void sleep(long var0){} 指定毫秒数让正在执行的线程暂停, 具体取决于系统定时器和调度程序的精度和准确性
    public final void setPriority(int var1){} 设置优先级, 不一定有用, 影响线程执行优先级的因素很多
    public final void setDaemon(boolean var1){} 设置守护线程标识
    public static native void yield(){} 放弃当前CPU执行资源,放弃时间不确定
    public final native boolean isAlive(){} 判断线程是否处于活动状态. 活动状态: 启动未终止
    public final void resume(){} 恢复线程, 不建议使用
    public final void suspend(){} 暂停线程, 不建议使用
    public void interrupt(){} 中断线程
    public static boolean interrupted(){} 测试当前线程是否中断, 并且具备清除状态的功能
    public boolean isInterrupted(){} 测试当前线程是否中断
    public final void join(){} 等待调用的这个线程终止
    public Thread.State getState(){} 获取线程状态
}

🚀不同创建方式的对比

线程有几种创建方式? 这几种创建方式有和优缺点有何区别?

  • 采用实现Runnable接口的方式

    线程仅仅是实现了Runnable接口,还可以继承其他类

    在这种方式下,可以多个线程共享一个Target对象,非常适合多个线程共同处理同一份资源,从而将cpu代码、数据分开,较好的体现了面向对象的思想

    劣势:编程稍微复杂,如果要访问当前线程,必须使用Thread.currentThread()方法

  • 采用继承Thread类的方式

    劣势:因为线程继承了Thread类,所有不能再继承其他父类

    优势:编程比较简单,如果需要访问当前线程,直接使用this对象即可

  • 采用实现Callable接口的方式

    ​ 劣势:编程相对复杂,不能直接使用Thread类中的方法(需借助第三方对象进行构建)

    ​ 优势:扩展性强,实现该接口的同时还可继承其他的类

👀线程常用方法

1.Thread.sleep(long millis):当前线程调用此方法,进入阻塞,不释放对象锁,线程自动唤醒进入可运行状态

2.Thread.yield():当前线程调用此方法,放弃获取的cpu时间片,由运行状态变可运行状态,让OS再次选择线程。实际中无法保证yield()达到让步目的,Thread.yield()不会导致阻塞

3.Thread.join()/thread.join(long millis):当前线程里调用其它线程的join方法,当前线程阻塞,但不释放对象锁,待别的线程执行结束,当前线程进入可运行状态

4.Obj.wait():当前线程调用释放对象锁(需先拿锁),进入等待队列

5.Obj.notify():唤醒在此对象监视器上等待的单个线程,选择是任意性的

6.Obj.notifyAll():唤醒在此对象监视器上等待的所有线程

2.线程池

线程池简介

为什么引入线程池?

1.降低资源消耗:重复利用已创建的线程,降低线程创建和销毁的损耗

2.提高响应速度:务到来时能立即执行

3.提高线程的可管理性:使用线程池可统一的分配、调优和监控

4.提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。如延时定时线程池ScheduledThreadPoolExecutor,允许任务延期执行或定期执行

线程池设计

设计说明
Executor顶层接口,提供思想,将任务提交和任务执行进行解耦
ExecutorService接口继承 Executor,扩展了生命周期管理的方法、返回 Futrue 的方法、批量提交任务的方法
AbstractExecutorService抽象类继承 ExecutorService 接口,对 ExecutorService 相关方法提供了默认实现,用 RunnableFuture 的实现类 FutureTask 包装 Runnable 任务,交给 execute() 方法执行,然后可以从该 FutureTask 阻塞获取执行结果,并且对批量任务的提交做了编排
ThreadPoolExecutor继承 AbstractExecutorService,维护自身的生命周期,管理线程和任务

线程池的运行状态

image-20240401233126872

Running:能接受新提交的任务,并且也能处理阻塞队列里的任务

Shutdown:关闭状态,不再接受新提交的任务,但可以继续处理阻塞队列里的任务

Stop:不能接受新任务,也不能处理队列里的任务,会中断正在处理任务的线程

Tidying:所有任务都终止,workerCount(有效线程数)为0

Terminated:在terminated()方法执行后进入此状态。处于SHUTDOWN或STOP状态,且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态

ThreadPoolExecutor 常用方法:

submit():执行 Callable

execute():执行 Runnable

shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,不接受新任务

shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务

isShutdown():判断线程是否终止

getActiveCount():正在运行的线程数

getCorePoolSize():获取核心线程数

getMaximumPoolSize():获取最大线程数

getQueue():获取线程池中的任务队列

allowCoreThreadTimeOut(boolean):设置空闲时是否回收核心线程

线程池参数及配置

(1)7大参数

1.核心线程数(corePoolSize)

​ 线程池中的核心线程数量(最少的线程个数),即使这些线程处理空闲状态,也不被销毁,除非设置了 allowCoreThreadTimeOut; 默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程

2.任务队列(workQueue)

​ 任务队列,当核心线程全部繁忙时,由 execute/submit 方法提交的 Runnable 任务存放到该任务队列中,等待被核心线程来执行

3.最大线程数(maximumPoolSize)

​ 线程池中允许的最大线程数,当核心线程全部繁忙且任务队列存满之后,线程池会临时追加线程,直到总线程数达到 maximumPoolSize

4.线程空闲超时时间(keepAliveTime)

如果一个线程处于空闲状态,并且当前的线程数量大于 corePoolSize,那么在指定时间后,这个空闲线程会被销毁

5.时间单位(unit)

​ keepAliveTime 的时间单位(天、小时、分、秒......),java.util.concurrent.TimeUnit

6.线程工厂(threadFactory)

​ 用于创建线程,可默认也可以自定义实现,可以用来指定线程名,是否为daemon线程等

7.拒绝策略(handler)

​ 核心线程 corePoolSize 正在执行任务、任务队列workQueue 已满、并且线程池中的线程数达到 maximumPoolSize 时,就需"拒绝"掉新提交过来的任务

(2)拒绝策略

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

CallerRunsPolicy:直接运行这个任务的run方法,但并非是由线程池的线程处理,而是交由任务的调用线程处理

DiscardPolicy:直接丢弃任务,不抛出任何异常

DiscardOldestPolicy:将当前处于等待队列列头的等待任务强行取出,然后再试图将当前被拒绝的任务提交到线程池执行

(3)7个阻塞队列

ArrayBlockingQueue:数组有界,FIFO排序,支持公平锁和非公平锁

LikedBlockingQueue:链表有界,FIFO排序,默认长度为Integer.MAX_VALUE

PrioriBlockQueue:线程优先级排序无界队列,默认自然排序,也可实现compareTo()方法排序

DelayQueue:延时获取无界,可指定多久后才可以获取元素

SynchronousQueue:不储存元素,每次put必须等待take,否则不能添加元素,支持公平锁和非公平锁

LinkedTransferQueue:链表无界,多了transfer()、tryTransfer()在队尾插元素方法

LinkedBlockingDeque:链表双向,头尾都可添加移除元素,可降低锁的竞争

(4)Executors提供的线程池
FixedThreadPool(n)
  数量固定的线程池,超出的任务会在队列中等待空闲的线程,可用于控制程序的最大并发数
CachedThreadPool()
  短时间内处理大量工作的线程池,会根据任务数量产生对应的线程,并试图缓存线程以便重复使用,如果限制 60 秒没被使用,则会被移除缓存
SingleThreadExecutor()
  创建一个单线程线程池
ScheduledThreadPool(n)
  创建一个数量固定的线程池,支持执行定时性或周期性任务
SingleThreadScheduledExecutor()
  此线程池就是单线程的 newScheduledThreadPool
WorkStealingPool(n)
  Java8 新增创建线程池的方法,创建时如果不设置任何参数,则以当前机器处理器个数作为线程个数,此线程池会并行处理任务,不能保证执行顺序
(5)线程池大小的选择

​ CPU 密集型: 任务需要大量使用 CPU 进行运算,但是没有阻塞,CPU一直高速运行;该情况下应尽可能减少 CPU 对线程的切换,所以要使线程数尽可能少;

​ 线程数 = CPU 核心数 + 1;

​ IO 密集型: 任务需要进行大量的 IO 操作,那么就会有大量的阻塞等待,CPU大部分时间是在阻塞,该情况下应尽可能减少阻塞所带来的消耗,所以要使线程数尽可能多;

​ 线程数 = CPU 核心数 * 2;

3.Java线程安全

java线程相关题目open in new window

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3