线程是个涉及技术点模块多的知识点
我们将分为:Java线程基本概念、线程安全、多线程、线程池进行逐步分析。
1、基本概念:
一个基本的CPU执行单元 & 程序执行流的最小单元
1、比进程更小的可独立运行的基本单位,可理解为:轻量级进程
2、组成:线程ID + 程序计数器 + 寄存器集合 + 堆栈
3、线程自己不拥有系统资源,与其他线程共享进程所拥有的全部资源。
作用:减少程序在并发执行时所付出的时空开销,提高操作系统的并发性能。
Java中线程的状态分为6种:
(1)初始(NEW):新创建了一个线程对象,但还没有调用start()方法
(2)运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线 程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用 权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
(3)阻塞(BLOCKED):表示线程阻塞于锁。
(4)等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
(5)超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
(6)终止(TERMINATED):表示该线程已经执行完毕。
线程分类
1、守护线程
(1)概念:守护用户线程的线程,即在程序运行时为其他线程提供一种通用服务
(2)现实例子: 垃圾回收线程
(3)实现方式:
thread.setDaemon(true);
2、非守护线程(用户线程)
(1)主线程(UI线程)
定义:Android系统在程序启动时会自动启动一条主线程
作用:处理四大组件与用户进行交互的事情(如UI、界面交互相关)
注意:因为用户随时会与界面发生交互,因此主线程任何时候都必须保持很高的响应速度,所以主线程不允许进行耗时操作,否则会出现ANR
(2)定义:手动创建的线程
作用:耗时的操作(网络请求、I/O操作等)
守护线程与非守护线程之间的区别
虚拟机是否已退出:
当所有用户线程结束时,因为没有守护的必要,所以守护线程也会终止,虚拟机也同样退出;
反过来,只要任何用户线程还在运行,守护线程就不会终止,虚拟机就不会退出
线程优先级
setPriority(int grade)
2、 线程安全
线程安全应该是每个应用程序的根基,一个线程不安全应用程序是达不到用户需求的,也是企业面试重点考察的问题。要讨论
线程安全,避不开的就是锁的概念,下面我们在三个方向来介绍一下。
(1)volatile
这个关键字我们应该挺熟悉得吧,我们在写单例的时候经常会用到,但是有的同学并不知道这个关键字的作用。
1、线程修改了变量的值时, 变量的新值对其他线程是立即可见的。 换句话说, 就是不同线程对这个变量进行操作时具有可见性。即该关键字保证了可见性
2、禁止使用指令重排序。这里提到了重排序, 那么什么是重排序呢? 重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。volatile关键字禁止指令重排序有两个含
义: 一个是当程序执行到volatile变量的操作时, 在其前面的操作已经全部执行完毕, 并且结果会对后面的
操作可见, 在其后面的操作还没有进行; 在进行指令优化时, 在volatile变量之前的语句不能在volatile变量后面执行; 同样, 在volatile变量之后的语句也不能在volatile变量前面执行。即该关键字保证了时序性
(2)synchronized
synchronized可以用在方法上也可以使用在代码块中,其中方法是实例方法和静态方法分别锁的是该类的实例对象和该类的对象。而使用在代码块中也可以分为三种,具体的可以看上面的表格。这里的需要注意的是:如果锁的是类对象的话,尽管new多个实例对象,但他们仍然是属于同一个类依然会被锁住,即线程之间保证同步关系。
但是我在百度面试的时候,一个大佬说也可以锁字节码,目前还不太了解,后续补充。。。
记得是这么给我出的题,问哪个线程先执行,会锁住吗?锁的是什么?
void main(){
private static synchronized Thread threadA = new Thread();
private static synchronized Thread threadB = new Thread();
threadA .start();
threadB .start();
}
一般synchronized会配合Object.wait() / Object.notify() Object.notifyAll()使用
synchronized(obj){
.... //1
obj.wait();//2
obj.wait(long millis);//2
....//3
}
这里有个面试经常遇到的问题:
1、java中的sleep()和wait()的区别
对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
在调用sleep()方法的过程中,线程不会释放对象锁。而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备
2、java中的notify()和notifyAll()的区别
notify():使一个等待状态的线程唤醒,注意并不能确切唤醒等待状态线程,是由JVM决定且不按优先级。
notifyAll():使所有等待状态的线程唤醒,注意并不是给所有线程上锁,而是让它们竞争。
获取对象锁进入运行状态。
补充:
并发编程中三大性质:原子性,有序性,可见性
其实整个并发编程所遇到的问题可以说是以下三个问题的变种。
a、原子性问题:
由Java内存模型提供的8个原子性操作所支持,Long和Double的读写大部分商业虚拟机上已实现为原子性操作,更大范围的原子性操作,Java内存模型还提供了lock和unlock操作来支持,在字节码层次提供了monitorenter和monitorexit来隐式的使用这两个操作,反映到java代码中就是同步代码块了 synchronize。
lock(锁定):作用于主内存中的变量,它把一个变量标识为一个线程独占的状态;
unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便后面的load动作使用;
load(载入):作用于工作内存中的变量,它把read操作从主内存中得到的变量值放入工作内存中的变量副本
use(使用):作用于工作内存中的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作;
assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;
store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送给主内存中以便随后的write操作使用;
write(操作):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
b、可见性问题
线程不与主内存进行直接交互,而是把主内存的实例变量拷贝一份到线程的工作内存中进行操作,然后再同步给主内存。之所以这样做,是因为工作内存大都由高速缓存、寄存器这类比主内存存取速度更快的内存担当,以便弥补CPU速度与主内存存取速度不在一个数量级的差距。
注:当线程操作某个对象时,执行顺序如下:
1 从主存复制变量到当前工作内存(read -> load)
2 执行代码改变共享变量的值(use -> assign)
3 用工作内存的数据刷新主存相关内容(store -> write)
所以单个线程与线程的工作内存之间就有了相互的隔离效果,专业术语称之为“可见性问题”
c、时序性问题
线程在引用变量时不能直接从主内存引用,如果工作内存中内有该变量,则会从主内存拷贝一个副本到工作内 存中,即read -> load ,完成后线程会引用该副本。当同一个线程再度引用该字段时,有可能重新从主内存获取变量副本(read -> load -> use),也有可能直接引用原来的副本(use),也就是说read、load、use 顺序可以有JVM实现系统决定。这个时候线程与线程之间操作的先后顺序,就会决定你程序对主内存最后的修改是不是正确的,专业术语称之为“时序性问题”。
Java提供了volatile和synchronize两个关键字来保证线程之间操作的有序性,synchronize是由“一个变量在同一时刻只允许一条线成对其进行lock操作”。
(3)条件对象Condition
该接口定义了类似Object的监视器方法,与Lock配合可以实现等待/通知模式
主要方法
await()相当于wait()方法
await(long time, TimeUnit unit)相当于wait(long)方法
signal()相当于notify()方法
signalAll()相当于notifyAll()方法
await()
和signal()
同样需要在lock.lock()
之后调用,否则会报监视器出错。
public class ThreadCondition {
public static void main(String[] args) {
try {
ThreadA threadA = new ThreadA();
threadA.start();
Thread.sleep(3000);
threadA.singal();
} catch (InterruptedException e) {
}
}
static class ThreadA extends Thread {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void run() {
try {
lock.lock();
System.out.println("ThreadA begin await " + System.currentTimeMillis());
condition.await();
System.out.println("ThreadA end await " + System.currentTimeMillis());
} catch (InterruptedException e) {
} finally {
lock.unlock();
}
}
public void singal() {
lock.lock();
System.out.println("ThreadA begin signal " + System.currentTimeMillis());
condition.signal();
System.out.println("ThreadA end signal " + System.currentTimeMillis());
lock.unlock();
}
}
}
注:ReentrantLock
可以实现线程之间同步互斥
3、多线程
多线程首先应对应以下原则:
不要阻塞UI线程(即主线程):单线程会导致主线程阻塞,然后出现ANR错误:主线程被阻塞超过5s则会出现错误
不要在UI线程之外更新UI组件
实现多线程的方式有哪些呢?
4、线程池
通常来说我们说到线程池第一时间想到的就是它:ExecutorService,它是一个接口,其实如果要从真正意义上来说,它可以叫做线程池的服务,因为它提供了众多接口api来控制线程池中的线程,而真正意义上的线程池就是:ThreadPoolExecutor,它实现了ExecutorService接口,并封装了一系列的api使得它具有线程池的特性,其中包括工作队列、核心线程数、最大线程数等。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//...
}
下面一一来说明这些参数的作用:
corePoolSize:线程池中的核心线程数量 maximumPoolSize:线程池中的最大线程数量
keepAliveTime:这个就是上面说到的“保持活动时间“,上面只是大概说明了一下它的作用,不过它起作用必须在一个前提下,就是当线程池中的线程数量超过了corePoolSize时,它表示多余的空闲线程的存活时间,即:多余的空闲线程在超过keepAliveTime时间内没有任务的话则被销毁。而这个主要应用在缓存线程池中
unit:它是一个枚举类型,表示keepAliveTime的单位,常用的如:TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)
workQueue:任务队列,主要用来存储已经提交但未被执行的任务,不同的线程池采用的排队策略不一样,稍后再讲
threadFactory:线程工厂,用来创建线程池中的线程,通常用默认的即可
handler:通常叫做拒绝策略,1、在线程池已经关闭的情况下 2、任务太多导致最大线程数和任务队列已经饱和,无法再接收新的任务。在上面两种情况下,只要满足其中一种时,在使用execute()来提交新的任务时将会拒绝,而默认的拒绝策略是抛一个RejectedExecutionException异常
我们重点关注一下workQueue这个参数,因为针对不同的线程池传入的workQueue可以输出有差异的工厂实现类下面我们会讲到。
LinkedBlockingQueue:无界的队列
SynchronousQueue:直接提交的队列
DelayedWorkQueue:等待队列ArrayBlockingQueue(有界的队列)
PriorityBlockingQueue(优先级队列)
但是我们发现ThreadPoolExecutor创建会配置很多参数,官方也不推荐直接创建ThreadPoolExecutor,而是推荐使用Executors的工厂方法来创建线程池,Executors类是官方提供的一个工厂类,它里面封装好了众多功能不一样的线程池,从而使得我们创建线程池非常的简便,主要提供了如下五种功能不一样的线程池:
1、newFixedThreadPool() :
作用:该方法返回一个固定线程数量的线程池,该线程池中的线程数量始终不变,即不会再创建新的线程,也不会销毁已经创建好的线程,自始自终都是那几个固定的线程在工作,所以该线程池可以控制线程的最大并发数。
栗子:假如有一个新任务提交时,线程池中如果有空闲的线程则立即使用空闲线程来处理任务,如果没有,则会把这个新任务存在一个任务队列中,一旦有线程空闲了,则按FIFO方式处理任务队列中的任务。
2、newCachedThreadPool() :
作用:该方法返回一个可以根据实际情况调整线程池中线程的数量的线程池。即该线程池中的线程数量不确定,是根据实际情况动态调整的。
栗子:假如该线程池中的所有线程都正在工作,而此时有新任务提交,那么将会创建新的线程去处理该任务,而此时假如之前有一些线程完成了任务,现在又有新任务提交,那么将不会创建新线程去处理,而是复用空闲的线程去处理新任务。那么此时有人有疑问了,那这样来说该线程池的线程岂不是会越集越多?其实并不会,因为线程池中的线程都有一个“保持活动时间”的参数,通过配置它,如果线程池中的空闲线程的空闲时间超过该“保存活动时间”则立刻停止该线程,而该线程池默认的“保持活动时间”为60s。
3、newSingleThreadExecutor() :
作用:该方法返回一个只有一个线程的线程池,即每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待这一个线程空闲,当这个线程空闲了再按FIFO方式顺序执行任务队列中的任务。
4、newScheduledThreadPool() : 作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。
5、newSingleThreadScheduledExecutor() :
作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。只不过和上面的区别是该线程池大小为1,而上面的可以指定线程池的大小。
优化线程池ThreadPoolExecutor
虽说线程池极大改善了系统的性能,不过创建线程池也是需要资源的,所以线程池内线程数量的大小也会影响系统的性能,大了反而浪费资源,小了反而影响系统的吞吐量,所以我们创建线程池需要把握一个度才能合理的发挥它的优点,通常来说我们要考虑的因素有CPU的数量、内存的大小、并发请求的数量等因素,按需调整。
通常核心线程数可以设为CPU数量+1,而最大线程数可以设为CPU的数量*2+1。
获取CPU数量的方法为:
Runtime.getRuntime().availableProcessors();
我们的线程池怎么暂停呢?
主要是shutdown()和shutdownNow()的区
1、shutdown()方法在终止前允许执行以前提交的任务。
2、shutdownNow()方法则是阻止正在任务队列中等待任务的启动并试图停止当前正在执行的任务。
如果我们要自定义自己的ThreadPoolExecutor我们可以利用ThreadPoolExecutor向外提供的三个接口,这三个接口分别是:
beforeExecute() - 任务执行前执行的方法
afterExecute() -任务执行结束后执行的方法
terminated() -线程池关闭后执行的方法
来自定义自己的实现。
面试美团的时候就被问到AsyncTask的实现,我们都知道AsyncTask内部实现其实就是Thread+Handler但是被问到andler是为了处理线程之间的通信,而这个Thread到底是指什么呢?当时回答的不是很令人满意
通过AsyncTask源码可以得知,其实这个Thread是线程池,AsyncTask内部实现了两个线程池,分别是:串行线程池和固定线程数量的线程池。而这个固定线程数量则是通过CPU的数量决定的。
在默认情况下,我们大都通过AsyncTask::execute()来执行任务的,
,而execute()内部则是调用executeOnExecutor(sDefaultExecutor, params)方法执行的,第一个参数就是指定处理该任务的线程池,而默认情况下AsyncTask是传入串行线程池(在这里不讲版本的变化),也就是任务只能单个的按顺序执行,而我们要是想让AsyncTask并行的处理任务,大家都知道调用AsyncTask::executeOnExecutor(sDefaultExecutor, params)方法传入这个参数即可:AsyncTask.THREAD_POOL_EXECUTOR。
而这个参数的意义在于为任务指定了一个固定线程数量的线程池去处理,从而达到了并行处理的功能,我们可以在源码中看到AsyncTask.THREAD_POOL_EXECUTOR这个参数就是一个固定线程数量的线程池:
public static final Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
暂时就总结了这么多,有遗漏后续再补。方便之后面试复习