目录
Java 进程和线程 ---- (下)
线程池
创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数
有限。为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作
线程。
从
JDK1.5
开始,
Java API
提供了
Executor
框架可以创建不同的线程池。比如单线程池,每次处理一个任务;数目固
定的线程池或者是缓存线程池(一个适合很多生存期短的任务的程序的可扩展线程池)。
new Thread
的弊端
- 每次new Thread新建对象性能差。
- 线程缺乏统一管理,可能无限制新建线程,相互之间竞争及可能占用过多系统资源导致死机或oom(内存溢出)。
-
缺乏更多功能,如定时执行、定期执行、线程中断。
OutOfMemoryError即OOM的可能原因?
- 数据库的cursor没有及时关闭
- 未关闭InputStream outputStream
- Bitmap 使用后未调用recycle()
- static等关键字
- 非静态内部类持有外部类的引用context泄露
- 流量/数据量峰值:应用程序在设计之初均有用户量和数据量的限制,某一时刻,当用户数量或数据量突然达到一个峰值,并且这个峰值已经超过了设计之初预期的阈值,那么以前正常的功能将会停止,并触发java.lang.OutOfMemoryError: Java heap space异常。
- 内存泄漏:特定的编程错误会导致你的应用程序不停的消耗更多的内存,每次使用有内存泄漏风险的功能就会留下一些不能被回收的对象到堆空间中,随着时间的推移,泄漏的对象会消耗所有的堆空间,最终触发java.lang.OutOfMemoryError: Java heap space错误。
四种线程池的好处
重用存在的线程,减少对象创建、消亡的开销,性能佳可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞
提供定时执行、定期执行、单线程、并发数控制等功能。
线程池的工作原理
- 线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则执行第二步;
- 线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里进行等待。如果工作队列满了,则执行第三步;
- 线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
- 一般创建线程对象时不建议使用extends Thread方法, 单根继承。
- 一般没有返回结果时建议使用Runnable接口,如果有返回值一般建议使用Callable接口。
- 如果使用线程比较频繁建议使用线程池。
守护线程(Daemon Thread)
在Java中有两类线程:用户线程 (User Thread)、守护线程 (Daemon Thread)。
所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。
如何将一个子线程设置为守护线程?在一个线程调用
start
启动之前,调用方法
thread.setDaemon(true);
就可以将
thread
线程设置为守护线程
。
守护线程一般应该是一个独立的线程,它的
run()
方法是一个无限循环。
System . out . println ( " 开始程序 ......." );Thread t = new Thread (){public void run (){while ( true )System . out . println ( "dddd....." );}};t . setDaemon ( true );t . start ();System . out . println ( " 结束程序 ......" );
线程组
Thread线程类中toString方法的定义
ThreadGroup group = getThreadGroup(); //获取当前线程对象所属的线程组
currentThread()方法返回正在被执行的线程的信息。
package xiancheng;
public class D11 {
public static void main(String[] args) {
DD a = new DD();
//a.start();
a.run();//
}
}
class DD extends Thread {
public DD() {
System.out.println("构造方法的打印" + Thread.currentThread());
}
public void run() {
System.out.println("run方法的打印" + Thread.currentThread());
}
}
/*
直接调用run方法
构造方法的打印Thread[main,5,main]
run方法的打印Thread[main,5,main]
*/
/*
用star()方法启动
构造方法的打印Thread[main,5,main]
run方法的打印Thread[Thread-0,5,main]
*/
- 所有线程都隶属于一个线程组。那可以是一个默认线程组【main】,亦可是一个创建线程时明确指定的组说明;
- 在创建之初,线程被限制到一个组里,而且不能改变到一个不同的组;
- 若创建多个线程而不指定一个组,它们就会与创建它的线程属于同一个组。
package xiancheng;
public class D12 implements Runnable {
public static void main(String[] args) {
ThreadGroup a = new ThreadGroup("创建的线程组");// 创建线程组
Thread b = new Thread(a, "线程1");// 定义线程时指定对应的线程组
System.out.println(b.getThreadGroup());// 获取对象所属的线程组
}
public void run() {
}
}
//输出结果:
//java.lang.ThreadGroup[name=创建的线程组,maxpri=10]
主要通途:可以通过线程组,对线程组中一组线程进行统一管理,而不是一个一个的管理。注意显示线程时,如果线程显示为
null
表示线程已经消亡(执行结束)。在具体开发中很少使用。
ThreadPoolExecutor
ThreadPoolExecutor
是线程池框架的一个核心类
- 线程池通过线程复用机制,并对线程进行统一管理;
- 降低系统资源消耗。通过复用已存在的线程,降低线程创建和销毁造成的消耗;
- 提高响应速度。当有任务到达时,无需等待新线程的创建便能立即执行;
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗大量系统资源,还会降低系统的稳定性,使用线程池可以进行对线程进行统一的分配、调优和监控。
线程池的运行状态总共有5
种,其值和含义分别如下:
- RUNNING: 高3位为111,接受新任务并处理阻塞队列中的任务
- SHUTDOWN: 高3位为000,不接受新任务但会处理阻塞队列中的任务
- STOP:高3位为001,不会接受新任务,也不会处理阻塞队列中的任务,并且中断正在运行的任务
- TIDYING: 高3位为010,所有任务都已终止,工作线程数量为0,线程池将转化到TIDYING状态,即将要执行 terminated()结束钩子方法
- TERMINATED: 高3位为011,terminated()方法已经执行结束
构造器中各个参数的含义:
1.corePoolSize
线程池中的核心线程数。当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于
corePoolSize
;如果当前线程数为
corePoolSize
,继续提交的任务被保存到阻塞队列中,等待被执行。
2.maximumPoolSize
线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线
程数小于
maximumPoolSize
。
3.keepAliveTime
线程空闲时的存活时间。默认情况下,只有当线程池中的线程数大于
corePoolSize
时,
keepAliveTime
才会起作
用,如果一个线程空闲的时间达到
keepAliveTime
,则会终止,直到线程池中的线程数不超过
corePoolSize
。但是
如果调用了
allowCoreThreadTimeOut(boolean)
方法,
keepAliveTime
参数也会起作用,直到线程池中的线程数为
0
。
4.unit
keepAliveTime
参数的时间单位。
5.workQueue
任务缓存队列,用来存放等待执行的任务。如果当前线程数为
corePoolSize
,继续提交的任务就会被保存到任务缓
存队列中,等待被执行。
一般来说,这里的
BlockingQueue
有以下三种选择:
- SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。因此,如果线程池中始终没有空闲线程(任务提交的平均速度快于被处理的速 度),可能出现无限制的线程增长。
- LinkedBlockingQueue:基于链表结构的阻塞队列,如果不设置初始化容量,其容量Integer.MAX_VALUE,即为无界队列。因此,如果线程池中线程数达到了corePoolSize,且始终没有空闲线程(任务提交的平均速度快于被处理的速度),任务缓存队列可能出现无限制的增长。
- ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务。
6.threadFactory
线程工厂,创建新线程时使用的线程工厂。
7.handler
任务拒绝策略,当阻塞队列满了,且线程池中的线程数达到
maximumPoolSize
,如果继续提交任务,就会采取任
务拒绝策略处理该任务,线程池提供了
4
种任务拒绝策略:
- AbortPolicy:丢弃任务并抛出RejectedExecutionException异常,默认策略;
- CallerRunsPolicy:由调用execute方法的线程执行该任务;
- DiscardPolicy:丢弃任务,但是不抛出异常;
- DiscardOldestPolicy:丢弃阻塞队列最前面的任务,然后重新尝试执行任务(重复此过程)。
当然也可以根据应用场景实现
RejectedExecutionHandler
接口自定义饱和策略,如记录日志或持久化存储不能处理的任务。
Executors
创建线程池
用来创建一个可以无限扩大的线程池,适用于服务器负载较轻,执行很多短期异步任务。
- newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收重用时则新建线程。
因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于可以预测线程数量的业务中,或者服务器负载较重,对当前线程数量进行限制。
- newFixedThreadPool 创建一个固定大小的定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景
- newScheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行。
适用于需要保证顺序执行各个任务,并且在任意时间点,不会有多个线程是活动的场景
- newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
- FIFO: 全称First in, First out,先进先出 LIFO: 全称Last in, First out,后进先出。
适用于大耗时的操作,可以并行来执行
- newWorkStealingPool:创建一个拥有多个任务队列的线程池,可以减少连接数创建当前可用cpu数量的线程来并行执行
提交任务的方式
线程池框架提供了两种方式提交任务,
submit()
和
execute()
,通过
submit()
方法提交的任务可以返回任务执行的结果,通过
execute()
方法提交的任务不能获取任务执行的结果。
关闭线程池
shutdownNow
:对正在执行的任务全部发出
interrupt()
,停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表
shutdown
:当调用
shutdown
后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务
volatile关键字
volatile是java提供的一种同步手段,只不过它是轻量级的同步
- 因为volatile只能保证多线程的内存可见性,不能保证多线程的执行原子性。
- 最彻底的同步要保证有序性、可见性和原子性的synchronized
- 任何被volatile修饰的变量,都不拷贝副本到工作内存,任何修改都及时写在主存。因此对于volatile修饰的变量的修改,所有线程马上就能看到,但是volatile不能保证对变量的修改是原子的
- 如果一段代码被认为是Atomic,则表示执行该段代码时是不会被中断的(即原子性)硬件原理和电位有关。
volatile
存在的意义是,任何线程对
a
的修改,都会马上被其他线程读取到,因为直接操作主存,没有线程对工作内存和主存的同步。所以,
volatile
的使用场景是有限的,在有限的一些情形下可以使用
volatile
变量替代锁。
要使
volatile
变量提供理想的线程安全
,
必须同时满足两个条件
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
volatile特性
- 保证可见性:当写一个 volatile 变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存,使其他线程立即可见;
- 保证有序性:当变量被修饰为 volatile 时,JMM 会禁止读写该变量前后语句的大部分重排序优化,以保证变量赋值操作的顺序与程序中的执行顺序一致;
- 部分原子性:对任意单个 volatile 变量的读/写具有原子性,但类似于 volatile++ 这种复合操作不具有原子性。
Java
内存模型规定和指引
Java
程序在不同的内存架构、
CPU
和操作系统间有确定性地行为。它在多线程的情况下尤其重要。
Java
内存模型对一个线程所做的变动能被其它线程可见提供了保证,它们之间是先行发生关系。这个关系
定义了一些规则让程序员在并发编程时思路更清晰。
1
、线程内的代码能够按先后顺序执行,这被称为程序次序规则。
2
、对于同一个锁,一个解锁操作一定要发生在时间上后发生的另一个锁定操作之前,也叫做管程锁定规则。
3
、前一个对
volatile
的写操作在后一个
volatile
的读操作之前,也叫
volatile
变量规则。
4
、一个线程内的任何操作必需在这个线程的
start()
调用之后,也叫作线程启动规则。
5
、一个线程的所有操作都会在线程终止之前,线程终止规则。
6
、一个对象的终结操作必需在这个对象构造完成之后,也叫对象终结规则。
7
、可传递性。如果操作
A
先行发生于操作
B
,操作
B
先行发生于操作
C
,那就可以得出操作
A
先行发生于操作
C
的结论。
线程状态切换
线程变化的
5
状态转换:
1
、新建状态
(New)
:新创建了一个线程对象。
new Thread()
2
、就绪状态
(Runnable)
:线程对象创建后,其它线程调用了该对象的
start()
方法。只能针对处于新建状态的线程对象调用
start
方法,否则
IllegalThreadStateException
该状态的线程位于可执行线程池中,变得可执行,等待获取
CPU
的使用权。
3
、执行状态
(Running)
:就绪状态的线程获取了
CPU
。执行程序代码。注意在一个多处理器的机器上会有多个
线程并行执行,
现在大部分桌面和服务器操作系统都采用时间片轮转法的抢占式调度策略,在选择下一个执行线程时系统会考虑线
程的优先级。
调用yield方法可以让运行状态的线程转入就绪
4
、堵塞状态
(Blocked)
:堵塞状态是线程由于某种原因放弃
CPU
使用权。临时停止执行。直到线程进入就绪状态,才有机会转到执行状态。线程切换是由底层平台控制的,具有一定的随机性。
堵塞的情况分三种:
(
一
)
、等待堵塞:执行的线程执行
wait()
方法,
JVM
会把该线程放入等待池中。
(
二
)
、同步堵塞:执行的线程在获取对象的同步锁时,若该同步锁被别的线程占用。则
JVM
会把该线程放入锁池中。
(
三
)
、其它堵塞:执行的线程执行
sleep()
或
join()
方法,或者发出了
I/O
请求时。
JVM
会把该线程置为堵塞状态。当
sleep()
状态超时、
join()
等待线程终止或者超时、或者
I/O
处理完成时。线程又一次转入就绪状态。
5
、死亡状态
(Dead)
:线程运行完了或者因异常退出了
run()
方法,该线程结束生命周期。
当主线程结束时,其它线程不受任何影响,并不会随之结束,一旦子线程启动后则拥有和主线程相同的地位,并不受主线程的影响;
注意:不要试图对一个已经死亡的线程调用
start
方法使其重新启动,该线程将不可再次作为线程执行,否则异常。
- 在Java里面没有办法强制启动一个线程,它是被线程调度器控制着且Java没有公布相关的API ;
- System.gc()或者Runtime.getRuntime().gc()可以通知进行垃圾回收,但是垃圾回收是一个低优先级线程,具体运行时间无法预知 ;
- 不要用stop方法来停止一个线程。因为stop方法太极端,会出现同步问题,使数据不一致。所以可以考虑通过设置标志,通过return, break,异常等手段来控制流程自然停止。
- suspend()方法用于暂停线程的执行,该方法容易导致死锁,因为该线程在暂停的时候仍然占有该资源,这会导致其他需要该资源的线程与该线程产生环路等待,从而造成死锁 resume()方法用于恢复线程的执行。
- suspend() 和 resume() 方法:两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。
- sleep方法使得当前线程休眠休眠时间的准确性依赖于系统时钟和CPU调度机制。如果需要可以通过调用interrupt() 方法来唤醒休眠线程
- 不释放已获取的锁资源,如果sleep方法在同步上下文中调用,那么其他线程是无法进入到当前同步块或者同步方法中的。
- wait(Object中定义的)wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。直到其他线程调用此对象的 notify() 方法或 notifyAll()方法,当前线程被唤醒,进入就绪状态 notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。
- wait(long timeout)让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的notify()方法或 notifyAll() 方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。 让当前线程进入等待状态,当别的其他线程调用notify()或者notifyAll()方法时,当前线程进入就绪状态。
- wait方法必须在同步上下文中调用,例如:同步方法块或者同步方法中,这也就意味着如果你想要调用wait方法,前提是必须获取对象上的锁资源;
- 当wait方法调用时,当前线程将会释放已获取的对象锁资源,并进入等待队列,其他线程就可以尝试获取对象上的锁资源。
- wait方法定义在Object类中,作用于对象本身 sleep方法定义在Thread中,作用于当前线程。Java程序中wait 和 sleep都会造成某种形式的暂停,它们可以满足不同的需要。wait()方法用于线程间通信,如果等待条件为真且其它线程被唤醒时它会释放锁,而sleep()方法仅仅释放CPU资源或者让当前线程停止执行一段时间,但不会释放锁。
join
方法
主要作用是同步,它可以使得线程之间的并行执行变为串行执行。在
A
线程中调用了
B
线程的
join()
方法时,表示只有当
B
线程执行完毕时,
A
线程才能继续执行。调用这个方法的线程将被阻塞
方法
join(long)
的功能在内部是使用
wait(long)
方法来实现的,所以
join(long)
方法具有释放锁的特点。但是
sleep(long)
不释放锁。