多线程技术概述
目录
进程
- 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,每个进程在运行时互不干扰.
线程
- 是进程中的一个独立的执行路径,即某个应用程序的一个执行途径,进程的各个线程共享一个内存空间,线程之间可以自由切换,并发执行.且一个进程至少有一个线程,如果一个线程都没有了,那么这个程序就不再运行.
- 线程实际上是在进程基础上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程.
线程调度
CPU就相当于一个脑子,一台电脑一般只有一个CPU,某一刻只能干一件事情,就需要通过线程调度实现线程切换,因为CPU的运行速度特别快,所以感知不到线程的切换,就实现了多个程序并存.
两种线程调度方法:分时调度 , 抢占式调度
-
分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
-
抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),
Java使用的为抢占式调度。
CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核心而言,某个时刻,
只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时
刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的 使
用率更高。
同步与异步
- 同步:排队执行 , 效率低但是安全.
- 异步:同时执行 , 效率高但是数据不安全.
并发与并行
- 并发:指两个或多个事件在同一个时间段内发生。
- 并行:指两个或多个事件在同一时刻发生(同时发生)。
Java实现多线程
常用实现多线程的方法:
-
继承Thread类,重写run()方法.
//主线程 public static void main(String[] args){ MyThread m = new MyThread; //开始这个线程 m.start(); //执行代码 } public class MyThread extends Thread(){ @Override public void run(){ //执行代码 } }
这里的run方法就是线程要执行的任务方法,这个执行路径的触发方式,不是调用run()方法,而是通过thread对象的start()来启动任务.
每个线程都有自己的栈空间, 共用一份堆内存. 由一个线程调用的方法,那么这个方法也会执行在这个线程里面.
-
实现Runnable接口,重写run()方法
//主线程 public static void main(String[] args){ //1. 创建一个任务对象 MyRunnable r = new MyRunnable(); //2. 创建一个线程,并为其 分配一个任务 Thread t = new Thread(r); //3. 执行这个线程 t.start(); //执行代码 } public class MyRunnable extends Runnable(){ @Override public void run(){ //执行代码 } }
实现Runnable与继承Thread相比有如下优势:
- 通过创建任务,然后给线程分配任务的方式来实现的多线程. 更适合多个线程同时执行相同任务的情况.
- 可以避免单继承所带来的局限性(因为在Java中只允许单继承,不允许多继承, 而接口可以多实现).
- 任务与线程本身是分离的(因为实现Runnable接口是通过创建任务,再给线程分配任务的方式实现的),提高了程序的健壮性.
- 后续学习的线程池技术, 接受Runnable类型的任务, 不接收Thread类型的线程.
线程阻塞
线程阻塞可以理解为所有消耗时间的操作,比如说线程休眠(sleep), 以及线程在执行某一命令时耗费的时间(比如说常见的文件读取, 在文件读取完之前,线程就会停在这里, 不会往下执行), 所以又被称为耗时操作.
线程中断
一个线程是一个独立的执行路径, 它是否应该结束, 应该由其自身决定.
通过interrupt()让线程中断只是给这个线程做了一个中断标记,并不能直接让线程中断,需要通过try-catch去获取这个中断标记,获取到这个中断标记之后,需要在catch块里面去结束这个任务.
可以直接通过return;结束这个任务,后续可以在catch里面将该释放的资源全部释放, 然后让线程死亡.
守护线程
线程分为守护线程和用户线程
- 用户线程: 直接创建的线程都是用户线程, 当一个进程不包含任何的存活的用户线程时, 进程结束.
- 守护线程: 守护用户线程的, 当最后一个用户线程结束时, 所有守护线程自动死亡.
- 可以通过setDaemon(boolean on)方法设置用户线程或者守护线程, 通过isDaemon()判断线程是否为守护线程.
线程安全问题
多个线程在同时运行时, 很容易发生线程不安全的问题.
-
解决方法: 排队执行
-
同步代码块
格式:
synchronized(锁对象){ //任务代码 }
Java中任何对象都可以作为锁对象, 相当于给这个对象打上一个标记.
多个线程只有使用同一个锁对象才能正确实现排队执行, 不可以每一个线程使用自己的锁对象.
-
同步方法
格式:
@Override public void run(){ } public synchronized 返回值类型 方法名(){ //任务代码 }
同步代码块和同步方法都属于隐式锁
-
显示锁 Lock 子类 ReentrantLock
格式:
private Lock l = new ReentrantLock() @Override public void run(){ l.lock; //任务代码 l.unlock; }
整理显示锁和隐式锁的区别?
-
公平锁和非公平锁
在创建显示锁时传入一个boolean类型的参数, 如果传入true,则就是公平锁
公平锁是指先到先得, 排队执行;
非公平锁是指一旦解开这个锁,各个线程就会同时去抢这个锁,谁抢到就是谁的.未声明公平锁的全都是非公平锁.
-
多线程通信问题
当多个线程并发执行时, 在默认情况下CPU是随机切换线程的,每个线程执行的次序都是随机的。
如果我们需要多个线程共同完成一个任务,并且希望他们有规律的执行(有可能是多线程间交替执行,有可能是当某个线程达到某个条件后才让其他线程执行),就需要线程之间协调通信。
我认为处理线程安全的同步锁机制,也算是线程间的通信。
- 一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。而且synchronized的锁对象和调用wait()、notify/notifyAll() 方法的对象必须是同一个对象。
- 由于 wait()、notify/notifyAll() 在synchronized 代码块执行,说明当前线程一定是获取了锁的。
- wait()使当前线程阻塞,当线程执行wait()方法的时候,会释放当前的锁,然后让出CPU,进入等待状态。
- 只有当线程执行 notify/notifyAll() 方法的时候,才会唤醒一个或多个正处于等待状态的线程,然后当前线程继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,才会再次释放锁。
- notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll() 后立即退出同步代码块或者调用wait(),以唤醒其他线程让其获得锁
线程的六种状态
-
NEW
线程刚被创建, 尚未启动的线程的线程状态。
-
RUNNABLE
正在运行的线程的线程状态, 处于可运行状态的线程正在Java虚拟机中执行,但它可能正在等待来自操作系统的其他资源,例如处理器。
-
BLOCKED
线程的线程状态被阻塞等待监视器锁定。 排队执行时等待时的状态.
-
WAITING
等待线程的线程状态, 无限期等待另一个线程执行特定操作的线程处于此状态。
-
TIMED_WAITING
正在等待另一个线程执行最多指定等待时间的操作的线程处于此状态。 中途可以被唤醒.
-
TERMINATED
已退出的线程处于此状态。 此时线程死亡.
有返回值的线程Callable
Callable接口类似于Runnable ,因为它们都是为其实例可能由另一个线程执行的类而设计的。 但是, Runnable不会返回结果,也不能抛出已检查的异常。
-
格式
//1.编写类实现Callable接口, 实现call方法 class XXX implements Callable<T>{ @Override public <T> call() throws Exception{ return T; } } //2.创建FutureTask对象, 并传入第一步编写的Callable类对象 FutureTask<Integar> future = new FutureTask<>(Callable c) //3.通过Thread启动线程 new Thread(future).start();
-
可以通过get()方法获取这个返回值, 进而判断这个是否运行完, 运行完之后主线程才开始运行.
-
通过isDone()判断子线程是否执行完毕
-
通过cancel()方法取消子线程.
Runnable 与 Callable的相同点
- 都是接口
- 都可以编写多线程程序
- 都采用Thread.start()启动线程
Runnable 与 Callable的不同点
- Runnable没有返回值;Callable可以返回执行结果
- Callable接口的call()允许抛出异常;Runnable的run()不能抛出
Callable获取返回值
Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执
行,如果不调用不会阻塞。
线程池Executors
概念
线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。
工作机制
- 在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。
- 一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
####使用线程池的原因
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程
就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容
器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
- 降低资源消耗。
- 提高响应速度。
- 提高线程的可管理性。
####四种常见线程池ExecutorService
-
缓存线程池(长度无限制)
任务加入后的执行流程:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在,则创建线程,并放入线程池,然后使用
ExecutorService service = Executors.newCachedThreadPool(); //向线程池中 加入 新的任务 service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } });
-
定长线程池(长度是指定的数值)
执行流程:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
- 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
ExecutorService service = Executors.newFixedThreadPool(2); service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } });
-
单线程线程池(只有一个线程)
效果与定长线程池 创建时传入数值1 效果一致。
执行流程:
- 判断线程池的那个线程是否空闲
- 空闲则使用
- 不空闲,则等待池中的单个线程空闲后使用
ExecutorService service = Executors.newSingleThreadExecutor(); service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } });
-
周期性任务定长线程池
执行流程:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
- 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
周期性任务执行时:
定时执行, 当某个时机触发时, 自动执行某任务 .
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
/**
* 定时执行
* 参数1. runnable类型的任务
* 参数2. 时长数字
* 参数3. 时长数字的单位
*/
/*service.schedule(new Runnable() {
@Override
public void run() {
System.out.println("俩人相视一笑~ 嘿嘿嘿");
}
},5,TimeUnit.SECONDS);
*/
/**
* 周期执行
* 参数1. runnable类型的任务
* 参数2. 时长数字(延迟执行的时长)
* 参数3. 周期时长(每次执行的间隔时间)
* 参数4. 时长数字的单位
*/
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("俩人相视一笑~ 嘿嘿嘿");
}
},5,2,TimeUnit.SECONDS);
Lambda表达式
函数式编程思想:不关注过程,只注重结果,与面向对象通过创建对象调用方法解决问题不同,它是直接调用方法去实现。
//例1
public static void main(String[] args){
//面向对象思想,调用对象实现
Thread t = new Thread(new Runnable(){
@Override
public void run(){
//执行代码
}
});
t.start();
//函数式编程思想
Thread t = new Thread(() ->{
执行代码
});
t.start();
}
//例2
就相当于去掉匿名内部类的类的部分,只保留方法参数部分和方法体。