实现多线程:
- 继承Thead类;
- 实现Runnable接口;
- 实现Callable接口通过FutureTask包装类来创建Thread线程;
- 使用ExecutorService、Callable、Future实现有返回结果的多线程。
线程池:
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
- 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能理解执行;
- 提高线程的客观理性:使用线程池可以对系统资源进行统一分配,调优和监控。
常用的线程池:
- newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务单提交顺序执行;
- newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,知道线程达到线程池的最大大小;
- newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做现职,线程池大小完全依赖于操作系统(或JVM)能够创建的最大线程大小;
- newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求;
- newSingleThreadExecutor:创建一个单线程的线程池,此线程池支持定时以及周期性执行任务的需求。
线程生命周期:
- 新建:使用new创建一个线程,该线程处于新建状态,此时仅由JVM为其分配内存,并初始化成员变量的值;
- 就绪:调用了start()方法后该线程就处于就绪状态,JVM会为其创建方法调用栈和程序计数器,等待调度运行;
- 运行:处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体时该线程处于运行状态;
- 阻塞:线程因为某种原因放弃了cpu使用权,也让出了cpu时间片,暂时停止运行,直到线程进入可运行状态才有机会再次获得cpu时间片转到运行状态;
- 死亡
等待阻塞 | 同步阻塞 | 其他阻塞 |
运行的线程执行o.wait()方法,JVM会把该线程放入等待队列中 | 运行的线程在获取对象同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中 | 运行的线程执行Thead.sleep()或t.join()方法,或者发出I/O请求时,JVM会把该线程置为阻塞状态,当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行状态 |
正常结束 | 异结束 | 调用stop |
run()或call()方法执行完成,线程正常结束 | 线程抛出一个未捕获的Exception或Error | 直接调用该线程的stop()方法来结束该线程,但是容易导致死锁 |
线程终止:
正常运行结束 | 程序运行结束,线程自动结束 |
使用退出标志退出线程 | 一般run()方法执行完就会结束,有些需要再外部某些条件满足的时候才能关闭 |
interrupt方法结束线程 |
|
stop方法终止线程 | thread.stop()强制终止线程,会导致该线程所持有的所有锁的突然释放(不可控),被保护的数据会呈现不一致性,不推荐使用。 |
notify() | notifyAll() |
可能导致死锁 | 不会导致死锁 |
只能唤醒一个处于wait状态的线程 | 可以唤醒所有处于wait状态的线程 |
wait()应配合while使用,不能用if,不满足时需要notify唤醒另外的线程 |
sleep() | wait() |
属于Thread类 | 属于Object类 |
使用时导致程序暂停执行指定的时间,让出cpu给其他线程使用,指定时间到了会自动恢复运行状态。使用时线程不会释放对象锁。 | 使用时会导致线程放弃对象锁,进入等待次对象的等待锁定池,只有针对此对象调用notify()后该线程才进入对象锁定尺中准备,获取对象锁进入运行状态。 |
start() | 被用来启动新创建的线程,内部调用了run()方法 |
run() | 调用时只会在原来的线程中调用,没有新的线程启动 |
interrupted | isInterrupted |
会将中断状态清除 | 不清除中断状态 |
submit() | execute() |
返回类型是void | 可以返回持有计算结果的Future对象 |
定义在Executor接口中 | 定义在ExecutorService接口中 |
可以向线程池提交任务 | 可以向线程池提交任务 |
synchronized关键字:
- 为了解决多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或代码块在任意时刻只能有一个线程执行。
synchronized关键字使用方法:
- 修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁;
- 修饰静态方法:给当前类加锁,会作用域类的所有对象实例。访问静态synchronized方法占用的锁是当前类的锁,访问非静态synchronized方法占用的锁是当前实例对象锁;
- 修饰代码块:制定加锁对象,给定对象加锁,进入同步代码库前要获得给定对象的锁;
volatile关键字:
- 保证内存可见性。即一个线程修改了某个变量的值,这新值对 其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。
- 禁止进行指令重排序。
- 用于多线程环境下的单次操作(单次读或者单次写)。
volatile | synchronized |
只能使用在变量上 | 可以使用在变量、方法和类 |
仅能实现变量的修改可见性,并不能保证原子性 | 可以保证变量的修改可见性和原子性 |
不会造成线程的阻塞 | 可能造成线程阻塞 |
不会被编辑器优化 | 可以被编辑器优化 |
Thread类中的yield方法:
- 使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。可以暂停当前正在执行的线程对象,让其他有相同优先级的线程执行。
- 是一个静态方法而且只保证当前线程放弃CPU占用而不能保证其他线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。
Executors 框架
- Executor 框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。无限制的创建线程会引起应用程序内存溢出,所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用 Executors 框架可以非常方便的创建一个线程池。
- 调用 new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程 之间的频繁交替也会消耗很多系统资源。
- 直接使用 new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、 定时定期执行、线程中断等都不便实现。
Executors 框架优点 :
- 能复用已存在并空闲的线程从而减少线程对象的创建从而减少了消亡线程的开销。
- 可有效控制最大并发线程数,提高系统资源使用率,同时避免过多资源竞争。
- 框架中已经有定时、定期、单线程、并发数控制等功能。
- 使用线程池框架 Executor 能更好的管理线程、提供系统资源使用率
Callable 和 Future:
- Callable 被线程执行后产生返回值,Future 拿到异步执行任务的返回值。
- Callable 接口类似于 Runnable,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 被线程执行后,可以返回值,这个返回值可以被 Future 拿到。Callable 是带有回调的 Runnable。Future 接口表示异步任务,是还没有完成的任务给出的未来结果。
Java 中用到的线程调度算法:
- 计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得 CPU 的使用权才能执行指令。
- 多线程的并发运行,是指各个线程轮流获得 CPU 的使用权,分别执行各自的任务。
- 在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机的一项任务就是负责线程的调度。
- 线程调度是指按照特定机制为多个线程分配 CPU 的使用权。
线程调度模型:
- 分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片。
- java 虚拟机采用抢占式调度模型,优先让可运行池中优先级高的线程占用 CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用 CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。
wait | sleep |
会释放CPU 执行权和占有的锁 | 仅释放 CPU 使用权,一直持有锁 |
用于线程间交互 | 用于暂停执行 |
Java 中线程的状态:
- 初始态:new 创建一个 Thread 对象,但还未调用 start()启动线程时,线程处于初始态;
- 运行态:runnable 在 Java 中,运行态包括就绪态和运行态;
- 就绪态:该状态下的线程已经获得执行所需的所有资源,只要 CPU 分配执行权就能运行。所有就绪态的线程存放在就绪队列中;
- 运行态:获得 CPU 执行权,正在执行的线程。由于一个 CPU 同一时刻只能执行一条线程,因此每个 CPU 每个时刻只有一条运行态的线程。
- 阻塞态:当一条正在执行的线程请求某一资源失败时,就会进入阻塞态。而在 Java 中,阻 塞态专指请求锁失败时进入的状态。由一个阻塞队列存放所有阻塞态的线程。处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列,等待执行;
- 等待态:当前线程中调用 wait、join、park 函数时,当前线程就会进入等待态。也有一个等待队列存放所有等待态的线程。线程处于等待态表示它需要等待其他线程的指示才能继续运行。进入等待态的线程会释放 CPU 执行权,并释放资源(如:锁) ;
- 超时等待态:当运行中的线程调用 sleep(time)、wait、join、parkNanos、parkUntil 时,就会进入该状态;它和等待态一样,并不是因为请求不到资源,而是主动进入,并且进入后需要其他线程唤醒;进入该状态后释放 CPU 执行权和占有的资源。与等待态的区别:到了超时时间后自动进入阻塞队列,开始竞争锁;线程被放入超时等待队列,与 yield 相比,它会使线程较长时间得不到运行。
- 终止态:线程执行结束后的状态。
wait、notify:
- wait 和 notify必须配套使用,即必须使用同一把锁调用; 必须放在一个同步块中调用 。
- wait 和 notify 的对象必须是他们所处同步块的锁对象。
线程安全:
- 多线程访问统一代码,不会产生不确定的结果。
- 某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
Servlet和SpringMVC 的 Controller | Struts2 的 action |
不是线程安全 | 线程安全 |
单实例多线程 | 多实例多线程 |
多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的 | 每个请求过来都会 new一 个新的 action 分配给这个请求,请求完成后销毁 |
需要考虑线程安全问题,可以提升不用处理太多的 gc,可以使用 ThreadLocal 来处理多线程的问题 | 不用考虑线程安全问题 |
确保线程安全:
- 同步
- 使用原子类
- 实现并发锁
- 使用 volatile 关键字
- 使用不变类和线程安全类
线程优先级:
- 每一个线程都是有优先级的,高优先级的线程在运行时会具有优先权, 但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。
- 我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线 程前执行。线程优先级是一个 int 变量(从 1-10),1 代表最低优先级,10 代表最高优先级。
- java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,一般无需设置。
线程调度器(Thread Scheduler):
- 是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。
- 一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。
- 线程调度并不受到 Java 虚拟机控制,由应用程序控制。
时间分片(Time Slicing ):
- 是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。
- 分配 CPU 时间可以基于线程优先级或者线程等待的时间。