线程、线程池


 

进程

进程是是应用程序运行的载体,是程序的一次执行过程,是临时的、有生命周期的,由程序、数据和进程控制块三部分组成,进程之间相互独立。
 

进程的三种基本状态
  • 就绪(ready)状态:进程准备就绪,获得CPU时间片后可立即运行。就绪的进行放在就绪队列中,操作系统按指定的调度策略分配cpu时间片。
  • 运行(running)状态:进程获取到cpu的时间片,开始执行代码。时间片用完后,回到就绪状态。
  • 阻塞(blocked )状态:进程暂停执行,等待某一事件的发生,比如等待IO完成、用户输入等。

在这里插入图片描述
这三种只是进程的基本状态,进程并不止这三种状态。

 

为什么不使用多进程而是使用多线程
  • 线程更加轻量级,启动、消亡所需时间短,需要的系统资源比进程少得多,且可以共享进程的内存空间
  • 多进程的运行不可预期,且测试困难

 

并发编程

并发、并行的区别
  • 并发:同时存在、交替执行
  • 并行:同时存在,同时执行。同时执行依赖多核cpu。

一个进程中可以有多条线程,如果是单核cpu,只能交替执行;如果是多核cpu,每个核可以执行1条线程,提高执行效率。
 
多线程在多核cpu下才有优势,在单核cpu下有线程上下文切换的时间开销,执行效率反而不如单线程。

 

什么时候适合使用多线程
  • 任务会阻塞线程,导致后续的代码不能尽快执行,eg. 文件IO、大量运算
  • 任务执行时间过长,可以划分为互补干扰的子任务,eg. 多线程分段下载
  • 间断性任务,eg. 定时统计
  • 任务本身需要协作执行,eg. 生产者/消费者模式的线程协作

 

多线程需要考虑的问题
  • 硬件资源:cpu核心数、内存、带宽等
  • 软件资源:数据库最大连接数、操作系统允许打开的socket最大数量等
  • 线程安全
  • 线程协作、线程通信

 

频繁切换线程上下文,会带来一定的性能开销,如何减少上下文切换的开销
  • 线程数不宜太多,避免创建不需要的线程
  • 无锁并发编程。未获取到锁会引起线程切换,可以用一些办法避免使用锁,减少线程间上下文的切换,eg. 将数组、有序集合按照hashCode取模进行分段,不同的线程处理不同段的数据;使用CAS算法

 

线程

线程是程序执行的最小单元,一个进程可以有一个或多个线程(至少有一个),同一进程中的多个线程共享所在进程的内存空间。
 

进程、线程的区别
  • 进程是操作系统分配资源的基本单位,操作系统以进程为单位分配资源,线程是程序执行的最小单元
  • 进程之间相互独立,有独立的内存空间,进程内的线程没有独立的内存空间,共用所在进程的内存空间
  • 线程上下文切换比进程上下文切换快得多

 

线程的6种状态

1、New 新建
已创建线程,但尚未调用start()启动线程

 

2、RUNNABLE 可运行
Java把就绪(ready)、运行(running)两种状态合并为一种状态:可运行(runnable)。调用了start()启动线程后,线程就处于可运行状态(就绪);线程获取到时间片开始执行代码,就处于运行(running)状态。

操作系统只给就绪队列中(就绪状态)的线程分配时间片。

 

3、BLOCKED 阻塞
正在执行的线程需要等待特定事件的发生、完成,就会进入阻塞状态,常见的有:①等待获取锁,②等待IO完成。线程进入阻塞状态后会让出cpu使用权,等待的事件发生、完成后线程进入就绪状态,进入调度队列,等待操作系统分配时间片。

 

4、WAITING (无限期)等待
正在执行的线程中调用某些方法会让线程进入无限期等待状态,进入无限期等待状态的线程会让出cpu使用权,被其它线程显式唤醒后,进入就绪状态。

进入无限期等待唤醒
Object.wait()Object.notify() 或 Object.notifyAll()
Thread.join()被调用的线程执行完毕
LockSupport.park()LockSupport.unpark(currentThread)

 

5、TIMED_WAITING 限时等待
在正在执行的线程中调用某些方法会让线程进入限时等待状态,进入限时等待状态的线程会让出cpu使用权,在指定时间后会自动被唤醒,也可以中途被唤醒,唤醒后进入就绪状态。

进入限时等待唤醒
Thread.sleep(time)sleep时间结束
Object.wait(time)wait时间结束,或者调用Object.notify() / notifyAll()
LockSupport.parkNanos(time)/parkUntil(time)park时间结束,或者调用LockSupport.unpark(currentThread)

 

6、TERMINATED 消亡
线程执行完毕,或者线程执行时发生异常、错误无法继续执行,会进入消亡状态。

 

线程的生命周期

在这里插入图片描述

 

创建线程的4种方式

1、继承Thread类,重写run()方法

public class MyThread extends Thread{

    @Override
    public void run() {
        //...
    }

}
MyThread myThread = new MyThread();
myThread.start();

 

2、实现Runnable接口,重写run()方法

public class MyRunnable implements Runnable{

    @Override
    public void run() {
        //...
    }

}
//需要借助Thread类包装为线程
Thread thread = new Thread(new MyRunnable());
thread.start();

Runnable是函数式接口,不复用时可写成lambda表达式

Thread thread = new Thread(() -> {
    //...
});

 

3、使用Callable接口+Future接口

/**
 * 泛型指定call()方法的返回值类型
 */
class MyCallable implements Callable<String> {

    /**
     * run()返回的是void,与run()方法不同,call()可以有返回值
     */
    @Override
    public String call() throws Exception {
        //...
        return null;
    }

}
//使用Future进一步包装Callable,用于获取任务状态、返回值
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
//依然需要使用Thread类包装为线程
new Thread(futureTask).start();

 

4、使用线程池创建线程

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        6,
        20,
        10, TimeUnit.MINUTES,
        new LinkedBlockingQueue<Runnable>(100),
        new ThreadPoolExecutor.AbortPolicy()
);

//参数是Runnable接口的实例,返回值void
threadPoolExecutor.execute(new MyRunnable());
//参数可以是Callable接口或Runable接口的实例,返回值是Future
Future<String> future = threadPoolExecutor.submit(new MyCallable());

 

总结

  • 如果不复用Runable、Callable接口,可直接写成lambda表达式
  • Runnable、Callable接口方式都需要借助Thread类包装为线程
  • java只支持单继承,第一种方式直接继承Thread后不能再继承其它类,所以一般不用第一种。需要获取任务状态、返回值时,使用Callable方式,不需要获取任务状态、返回值时,使用Runable方式。

 

start()、run()的区别

  • start()会在jvm中创建一条新线程,分配线程所需的资源,并调用run()方法执行run()方法中的代码,实现了多线程
  • run()只是一个普通方法,直接调用run()方法只是在当前线程中执行run()方法中的代码,并不会创建新线程,不能实现多线程

 

用户线程、守护线程

线程分为用户线程、守护线程2类,未指定线程类型时,默认为用户线程,main线程默认为用户线程。

守护线程是用户线程的守护者,常见的守护线程比如gc线程。守护线程不可控,尽量少用守护线程。

只要还有用户线程在执行,程序就不会终止;如果程序中没有用户线程在执行,只有守护线程在执行,则jvm直接退出,程序终止运行。

 

线程常用方法
线程参数
Thread thread = new Thread();
Thread thread = Thread.currentThread();  //获取当前线程


thread.getId(); //线程id
thread.getName(); //线程名
thread.getPriority(); //线程优先级
thread.getState(); //线程状态
thread.isDaemon();  //是否是守护线程


thread.setName("my_thread"); //设置线程名,默认主线程是main、其它线程以Thread-n的形式命名
thread.setPriority(5); //设置线程优先级,可使用[1,10]上的整数,也可以使用常量,默认5,优先级高的线程优先分配时间片
thread.setPriority(Thread.MIN_PRIORITY); //1
thread.setPriority(Thread.NORM_PRIORITY); //5
thread.setPriority(Thread.MAX_PRIORITY); //10
thread.setDaemon(false); //是否作为守护线程,默认false 作为用户线程


thread.start(); //启动线程

不同的操作系统,对线程优先级的支持有差异,不要过度依赖于线程优先级保证线程的执行顺序,设置优先级时尽量使用预定义的优先级常量(1、5、10)。

 

join()

Thread的实例方法,在一个线程中加入另一条已启动的线程,可用于线程间通信

Thread1 thread1 = new Thread1();
thread1.start();

thread1.join();  //在当前线程中加入thread1

先执行加入的线程thread1,等加入的线程执行完毕,才会继续执行当前线程

 

yield()、sleep()
Thread.yield();  //线程让步,当前线程让出cpu的使用权,进入就绪状态

Thread.sleep(500); //线程休眠,当前线程休眠指定ms,会让出cpu的使用权,但不会释放锁,指定时间后自动苏醒,进入就绪状态

都是Thread类的静态方法,操作的都是当前线程,都会让出cpu的使用权。

yield()是不可靠的,只是提示线程调度器当前线程愿意让出cpu使用权,由线程调度器决定是否让出cpu使用权,不是一定会让出。

 

wait()、notify()、notifyAll()

均是Object类的实例方法,常用于线程间通信。

这三个方法使用的前提是已经获取到了互斥锁,所以这三个方法都只能在同步代码块中使用,且操作的要是同一把锁(锁对象的内存地址相同)

public class Thread1 extends Thread {
    public static Object lock = new Object(); //锁。可以把要使用的公共资源直接作为锁,也可以把一个Object对象作为锁,获取到锁后,才可以操作公共资源。要是同一把锁(地址相同)

    @Override
    public void run() {
        //....
        
        //同步代码块
        synchronized (lock){  //获取锁,获取到锁后自动执行里面的代码
            //.....  //操作资源

            lock.notify();  //如果同步代码块中:释放锁后还有代码要执行,则应该使用notify()、notifyAll()进行等待
            // lock.notifyAll();

            try {
                lock.wait(); //让出cpu使用权,释放锁,进入无限期等待,如果要继续运行,需要在其它线程中显式唤醒
         //lock.wait(10000);  //限时等待,指定时间后自动唤醒进入ready状态
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            //.....  //如果同步代码块中、wait()之后还有代码,唤醒后进入ready状态,获取到时间片后、需要重新获取锁才能继续执行后面的代码
        }
        
        //.....  //如果同步代码块后还有代码,唤醒后进入ready状态,获取时间片后就可以继续执行,无需获取锁
        
    }

}

wait()释放锁后,锁分配给等待这个锁的哪个线程?

等待池WaitSet中存放等待锁的线程,锁池EntryList是针对对象锁而言的,把对象作为锁,锁被释放后进入锁池中。
 

notify()、notifyAll()的区别

  • notify()是在等待池中等待该锁的所有线程中,随机选择一个线程来获取锁
  • notifyAll()是让等待池中等待该锁的所有线程,都来竞争锁

 

park()、parkUntil()、parkNanos()、unpark()

均为LockSupport类的静态方法

//将当前线程挂起,让出cpu使用权,进入无限期等待状态
LockSupport.park();

//参数是long型的时间戳,将当前线程挂起,让出cpu使用权,进入限时等待状态,到达指定时间自动唤醒,进入就绪状态
LockSupport.parkUntil(100000000000L);  //ms
LockSupport.parkNanos(100000000000L);   //ns(纳秒)

//unpark()可以解处指定线程park()、parkUntil()、parkNanos()的挂起,将线程恢复到运行(就绪)状态。参数是Thread实例,指定要解除挂起的线程
LockSupport.unpark(thread1); 

park()、parkUntil()、parkNanos()都会让出cpu使用权,但不释放锁。
 

与wait()、notify()相比,LockSupport更加灵活

  • LockSupport的方法不要求写在同步代码块中,线程间不需要使用同步锁,实现了线程间的解耦
  • unpark()方法可以先于park()方法调用,不用担心执行的先后顺序

 

interrupt() 线程中断

Thread类的实例方法

  • 如果对处于等待、限时等待的线程使用,会中断等待、限时等待状态,进入就绪状态。sleep()、wait()等方法都是放在try中的,如果在等待期间,在其它线程中使用interrupt()中断,会抛出中断异常,并立即被唤醒,进入ready状态。
  • 如果对正在运行的线程使用,会给该线程设置一个中断标志,由该线程自己决定在哪个适合的代码位置暂停,往往不会立刻停止,需要判断是否已暂停

① isInterrupted()  Thread类的实例方法,判断指定线程是否已中断
② interrupted()  Thread类的静态方法,判断当前线程是否已中断,并清除之前设置的中断标志
 
用interrupt()中断线程是不可靠的,可以设置一个标志位(boolean型的变量),在一些位置判断标志位的值,为false就return中断执行。多线程常用于批量操作,可以将标志位是否为true作为循环条件。

 

使线程进入等待状态、限时等待状态的方法对比
方法类型使用要求用途
sleep()Thread类的静态方法不释放锁无限制线程内控制
wait()Object的实例方法释放锁只能在同步代码块中使用、要使用同一把锁线程间通信
park()系列均为LockSupport类的静态方法不释放锁无限制线程内控制,unpark()可用于线程间通信

均可用interrupt()唤醒

 

线程协作

常见的线程协作模式是生产者/消费者。

一个线程作为生产者,生产要处理数据,将数据放到仓库中;可使用juc的LinkedBlockingQueue作为仓库,队列,自然是先进先出的;一个线程作为消费者,消费|处理仓库中的数据。LinkedBlockingQueue是阻塞队列,取不到元素(队列为空)时会一直阻塞直到有元素可取。

 

线程死锁

多条线程彼此都在等待对方持有的锁,永远无法继续往下执行。

 

线程池

为什么要使用线程池
  • 使用预先创建好的线程执行任务,不用临时创建线程、分配线程所需资源,减少了时间开销,提高了性能。
  • 线程池可以统一管理线程,无需手动创建、启动线程

 

线程池的5种状态

在这里插入图片描述

  • RUNNING:运行状态,可以接受任务、执行线程。
  • SHUTDOWN:运行状态的线程池执行shutdown()后会转换到SHUTDOWN状态,不再接受新任务,只中断所有闲置线程,不会中断正在执行的线程,不会移除队列中的任务,即会等待正在执行的任务、队列中的任务执行完毕。
  • STOP:运行状态的线程池执行shutdownNow()后会会转换到STOP状态,不再接受新任务,中断所有工作线程(包括正在执行的线程),并移除队列中的所有任务。
  • TIDYING:处于SHUTDOWN或STOP状态的线程池,线程池中的工作线程数为0后,会变为TIDYING状态。
  • TERMINATED: 线程池处于TIDYING状态后,会自动调用terminated()方法,变成TERMINATED状态。

 

线程池创建

juc提供的Executor可以快速创建线程池,比如

//线程池容量为1,不能扩容
ExecutorService es = Executors.newSingleThreadExecutor();  

//指定线程池容量,不能扩容
ExecutorService es = Executors.newFixedThreadPool(10);  

//指定核心线程数,线程池容量不够时自动扩容。可定时执行
ExecutorService es = Executors.newScheduledThreadPool(10);  

//线程池容量不够时自动扩容。会自动回收空闲线程
ExecutorService es = Executors.newCachedThreadPool();  

Executors提供的方法只能设置线程池的个别参数,不能设置阻塞队列的相关参数,使用的阻塞队列的容量默认是Integer.MAX_VALUE,队列中可能存在大量任务,导致内存占用大,引起频繁GC,甚至OOM。

对于线程池这种资源占用大的东西,一定要手动设置参数,一般不用Executors创建线程池,而是用ThreadPoolExecutor直接创建,手动控制线程池的各项参数。

//ThreadPoolExecutor提供了多个重载的构造方法,全参的构造方法如下
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        6,  //corePoolSize 核心线程数
        20,  //maximumPoolSize 最大线程数
        10, TimeUnit.MINUTES,  //keepAliveTime 空闲线程的存活时间
        new LinkedBlockingQueue<Runnable>(100),  //workQueue 阻塞队列
        //Executors.defaultThreadFactory(),  //threadFactory 创建线程使用的工厂
        new ThreadPoolExecutor.AbortPolicy()  //handler 使用的拒绝策略
);

可以自定义的线程池,比如继承ThreadPoolExecutor重写里面的方法,给线程池增加监控功能。

 

线程池参数
  • corePoolSize:核心线程数,建议设置为接近cpu核心数左右。开始接收到任务时才创建核心线程,并非创建线程池时就创建核心线程。
  • maximumPoolSize:最大线程数。当核心线程都在使用,且队列已满时,才检查是否达到了最大线程数,未达到最大线程数则创建新线程来执行任务。
  • keepAliveTime:空闲线程的存活时间。
  • workQueue:阻塞队列,可以使用有界队列、无界队列,没有空闲线程时会将任务放到队列中。
  • threadFactory:创建线程使用的工厂,缺省时默认为Executors.defaultThreadFactory(),也可以使用自定义的线程工厂。
  • handler:拒绝策略。指定当队列已满时,如何处理新提交的任务,缺省时默认使用AbortPolicy。

一共6个,但threadFactory一般都是使用默认的、不用指定,指定其它五个即可。

 

线程池的拒绝策略

jdk提供了4种拒绝策略,均为ThreadPoolExecutor的静态内部类

  • AbortPolicy:默认策略,直接抛出异常
  • CallerRunsPolicy:用调用者(提交者)所在的线程来执行
  • DiscardPolicy:直接丢弃任务
  • DiscardOldestPolicy:丢弃队列中的第一个任务(最靠前的任务)

也可以使用自定义的拒绝策略

/**
 * 自定义的线程池拒绝策略
 * 实现RejectedExecutionHandler接口,重写rejectedExecution()方法即可
 */
class MyPolicy implements RejectedExecutionHandler {

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        //通过线程池本身可以拿到很多参数
        BlockingQueue<Runnable> queue = e.getQueue();
        //...
    }

}

 

线程池、Future常用方法
//提交任务,返回void
threadPoolExecutor.execute(runnable);
//提交任务,返回Future
Future<?> future = threadPoolExecutor.submit(runnable);


boolean isDone = future.isDone();

//取消任务。参数指定如果任务已经开始执行,是否中断;返回值只是表示任务是否能被移除,并非是移除结果
boolean canCancel = future.cancel(false);

if (canCancel) {
    //查询任务是否移除成功
    boolean isCancelled = future.isCancelled();
}


//移除队列中的指定任务。如果该任务还在队列中(尚未开始执行),则从队列中移除该任务。remove()只能移除Runnable型任务
threadPoolExecutor.remove(runnable);

//移除对队列中所有任务
threadPoolExecutor.purge();


//终止线程池
threadPoolExecutor.shutdown();
//立刻终止线程池
threadPoolExecutor.shutdownNow();

 
execute()、submit()的区别

  • execute()参数只能是Runnable接口的实例,返回值是void,用于不需要获取任务状态、返回值的情形;
  • submit()参数可以是Callable接口或Runable接口的实例,返回值是Future,用于需要获取任务状态、返回值的情形;

 

shutdown()、shutdownNow()的区别

  • shutdown()不再接受新任务,只中断所有闲置线程,不会中断正在执行的线程,不会移除队列中的任务,即会等待正在执行的任务、队列中的任务执行完毕。
  • shutdownNow()不再接受新任务,中断所有工作线程(包括正在执行的线程),并移除队列中的所有任务。

shutdown()、shutdownNow()都是使用interrupt()中断线程的,线程不一定会被中断

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值