进程
进程是是应用程序运行的载体,是程序的一次执行过程,是临时的、有生命周期的,由程序、数据和进程控制块三部分组成,进程之间相互独立。
进程的三种基本状态
- 就绪(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()中断线程的,线程不一定会被中断