1 java多线程(非常重要)
1.1. 线程
线程和进程的区别?
线程是CPU调度的最小单位,一个进程中可以包含多个线程,在Android中,一个进程通常是一个App,App中会有一个主线程,主线程可以用来操作界面元素,如果有耗时的操作,必须开启子线程执行,不然会出现ANR,除此以外,进程间的数据是独立的,线程间的数据可以共享。
java多线程实现方式主要有:
-
继承Thread
优点 : 方便传参,可以在子类添加成员变量,通过方法设置参数或构造函数传参。
缺点:
1.因为Java不支持多继承,所以继承了Thread类以后,就无法继承其他类。
2.每次都要新建一个类,不支持通过线程池操作,创建和销毁线程对资源的开销比较大。
3.从代码结构上讲,为了启动一个线程任务,都要创建一个类,耦合性太高。
4.无法获取线程任务的返回结果。Thread syncTask = new Thread() { @Override public void run() { // 执行耗时操作 } }; syncTask.start();//启动线程
-
实现Runnable
优点 : 此方式可以继承其他类。也可以使用线程池管理,节约资源。创建线程代码的耦合性较低。推荐使用此种方式创建线程。
缺点: 不方便传参,只能使用主线程中用final修饰的变量。其次是无法获取线程任务的返回结果。//写法1:集成Runnable接口定义任务类 public class ThreadTask implements Runnable { @Override public void run() { while(true) { System.out.println(Thread.currentThread().getName()+" is running..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } //在其他地方使用 new Thread(new ThreadTask ()).start(); //写法2:匿名内部类写法 new Thread(new Runnable() { @Override public void run() { //做操作 } }).start();
-
实现Callable
此种方式创建线程底层源码也是使用实现Runnable接口的方式实现的,所以不是一种新的创建线程的方式,只是在实现Runnable接口方式创建线程的基础上,同时实现了Future接口,实现有返回值的创建线程。Runnable 与 Callable的区别:
1. Runnable是在JDK1.0的时候提出的多线程的实现接口,而Callable是在JDK1.5之后提出的; 2. Runnable 接口之中只提供了一个run()方法,并且没有返回值; 3. Callable接口提供有call(),可以有返回值;
扩展:
Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果; 当不调用此方法时,主线程不会阻塞 public class CallableImpl implements Callable<String> { public CallableImpl(String acceptStr) { this.acceptStr = acceptStr; } private String acceptStr; @Override public String call() throws Exception { // 任务阻塞 1 秒 Thread.sleep(1000); return this.acceptStr + " append some chars and return it!"; } public static void main(String[] args) throws ExecutionException, InterruptedException { Callable<String> callable = new CallableImpl("my callable test!"); FutureTask<String> task = new FutureTask<>(callable); long beginTime = System.currentTimeMillis(); // 创建线程 new Thread(task).start(); // 调用get()阻塞主线程,反之,线程不会阻塞 String result = task.get(); long endTime = System.currentTimeMillis(); System.out.println("hello : " + result); System.out.println("cast : " + (endTime - beginTime) / 1000 + " second!"); } } //执行结果 hello : my callable test! append some chars and return it! cast : 1 second!
总结:
根据Oracle提供的JAVA官方文档的说明,Java创建线程的方法只有两种方式,即继承Thread类和实现Runnable接口。其他所有创建线程的方式,底层都是使用这两种方式中的一种实现的,比如通过线程池、通过匿名类、通过lambda表达式、通过Callable接口等等,全是通过这两种方式中的一种实现的。所以我们在掌握线程创建的时候,必须要掌握的只有这两种,通过文章中优缺点的分析,这两种方法中,最为推荐的就是实现Runnable接口的方式去创建线程。
1.2. 线程的状态有哪些?
Java中定义线程的状态有6种,可以查看Thread类的State枚举:
public static enum State
{
NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED;
private State() {}
}
- 初始(NEW):新创建了一个线程对象,还没调用start方法;
- 运行(RUNNABLE):java线程中将就绪(ready)和运行中(running)统称为运行(RUNNABLE)。线程创建后调用了该对象的start方法,此时处于就绪状态,当获得CPU时间片后变为运行中状态;
- 阻塞(BLOCKED):表现线程阻塞于锁;
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断);
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定时间后自行返回;
- 终止(TERMINATED):表示该线程已经执行完毕。
状态详细说明:
-
初始状态(NEW)
实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。 -
就绪状态(RUNNABLE之READY)
就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
调用线程的start()方法,此线程进入就绪状态。
当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
锁池里的线程拿到对象锁后,进入就绪状态。运行中状态(RUNNABLE之RUNNING)
线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一的一种方式。 -
阻塞状态(BLOCKED)
阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。 -
等待(WAITING)
处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。 -
超时等待(TIMED_WAITING)
处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。 -
终止状态(TERMINATED)
当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
1.3. 线程的状态转换及控制
主要由这几个方法来控制:sleep、join、yield、wait、notify以及notifyAll。
wait() / notify() / notifyAll()
wait(),notify(),notifyAll() 是定义在Object类的实例方法,用于控制线程状态,三个方法都必须在synchronized 同步关键字所限定的作用域中调用(只能在同步控制方法或者同步控制块中使用),否则会报错 java.lang.IllegalMonitorStateException。
join() / sleep() / yield()
join()
如果线程A调用了线程B的join方法,线程A将被阻塞,等待线程B执行完毕后线程A才会被执行。这里需要注意一点的是,join方法必须在线程B的start方法调用之后调用才有意义。join方法的主要作用就是实现线程间的同步,它可以使线程之间的并行执行变为串行执行。
sleep()
当线程A调用了 sleep方法,则线程A将被阻塞,直到指定睡眠的时间到达后,线程A才会重新被唤起,进入就绪状态。
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
try {
Thread.sleep(1000); // 阻塞当前线程1s
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
yield() 当线程A调用了yield方法,它可以暂时放弃处理器,但是线程A不会被阻塞,而是进入就绪状态。执行了yield方法的线程什么时候会继续运行由线程调度器来决定。
public class YieldThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
// 主动放弃
Thread.yield();
}
}
}
sleep方法和wait方法的区别是什么?
wait方法既释放cpu,又释放锁。 sleep方法只释放cpu,但是不释放锁。
sleep 方法是Thread类的一个静态方法,其作用是使运行中的线程暂时停止指定的毫秒数,从而该线程进入阻塞状态并让出处理器,将执行的机会让给其他线程。但是这个过程中监控状态始终保持,当sleep的时间到了之后线程会自动恢复。
wait 方法是Object类的方法,它是用来实现线程同步的。当调用某个对象的wait方法后,当前线程会被阻塞并释放同步锁,直到其他线程调用了该对象的 notify 方法或者 notifyAll 方法来唤醒该线程。所以 wait 方法和 notify(或notifyAll)应当成对出现以保证线程间的协调运行。
1.4. Java如何正确停止线程
注意:Java中线程的stop()、suspend()、resume()三个方法都已经被弃用,所以不再使用stop()方法停止线程。
我们只能调用线程的interrupt()方法通知系统停止线程,并不能强制停止线程。线程能否停止,何时停止,取决于系统。
1.5 线程池(非常重要)
线程池的地位十分重要,基本上涉及到跨线程的框架都使用到了线程池,比如说OkHttp、RxJava、LiveData以及协程等。
与新建一个线程相比,线程池的特点?
- 节省开销: 线程池中的线程可以重复利用。
- 速度快:任务来了就能开始,省去创建线程的时间。
- 线程可控:线程数量可空和任务可控。
- 功能强大:可以定时和重复执行任务。
ExecutorService简介
通常来说我们说到线程池第一时间想到的就是它:ExecutorService,它是一个接口,其实如果要从真正意义上来说,它可以叫做线程池的服务,因为它提供了众多接口api来控制线程池中的线程,而真正意义上的线程池就是:ThreadPoolExecutor,它实现了ExecutorService接口,并封装了一系列的api使得它具有线程池的特性,其中包括工作队列、核心线程数、最大线程数等。
线程池(ThreadPoolExecutor)中的几个参数是什么意思?
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {//...}
参数解释如下(重要):
corePoolSize:核心线程数量,不会释放。
maximumPoolSize:允许使用的最大线程池数量,非核心线程数量,闲置时会释放。
keepAliveTime:闲置线程允许的最大闲置时间。它起作用必须在一个前提下,就是当线程池中的线程数量超过了corePoolSize时,它表示多余的空闲线程的存活时间,即:多余的空闲线程在超过keepAliveTime时间内没有任务的话则被销毁。而这个主要应用在缓存线程池中
unit:闲置时间的单位。
workQueue:阻塞队列,用来存储已经提交但未被执行的任务,不同的阻塞队列有不同的特性。
threadFactory:线程工厂,用来创建线程池中的线程,通常用默认的即可
handler:通常叫做拒绝策略,1、在线程池已经关闭的情况下 2、任务太多导致最大线程数和任务队列已经饱和,无法再接收新的任务 。在上面两种情况下,只要满足其中一种时,在使用execute()来提交新的任务时将会拒绝,而默认的拒绝策略是抛一个RejectedExecutionException异常
上面的参数理解起来都比较简单,不过workQueue这个任务队列却要再次说明一下,它是一个BlockingQueue<Runnable>对象,而泛型则限定它是用来存放Runnable对象的,刚刚上面讲了,不同的线程池它的任务队列实现肯定是不一样的,所以,保证不同线程池有着不同的功能的核心就是这个workQueue的实现了,细心的会发现在刚刚的用来创建线程池的工厂方法中,针对不同的线程池传入的workQueue也不一样,五种线程池分别用的是什么BlockingQueue:
1、newFixedThreadPool()—>LinkedBlockingQueue 无界的队列
2、newSingleThreadExecutor()—>LinkedBlockingQueue 无界的队列
3、newCachedThreadPool()—>SynchronousQueue 直接提交的队列
4、newScheduledThreadPool()—>DelayedWorkQueue 等待队列
5、newSingleThreadScheduledExecutor()—>DelayedWorkQueue 等待队列
线程池中用到的三种阻塞队列:
-
LinkedBlockingQueue:无界的队列
它的容量是 Integer.MAX_VALUE,为 231 -1 ,是一个非常大的值,可以认为是无界队列。FixedThreadPool 和 SingleThreadExecutor 线程池的线程数是固定的,所以没有办法增加特别多的线程来处理任务,这时就需要 LinkedBlockingQueue 这样一个没有容量限制的阻塞队列来存放任务。
-
SynchronousQueue:直接提交的队列
如果不希望任务在队列中等待而是希望将任务直接移交给工作线程,可使用SynchronousQueue作为等待队列。SynchronousQueue不是一个真正的队列,而是一种线程之间移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接收这个元素。只有在使用无界线程池或者有饱和策略时才建议使用该队列。
-
DelayedWorkQueue:等待队列
它对应的线程池分别是 ScheduledThreadPool 和 SingleThreadScheduledExecutor,这两种线程池的最大特点就是可以延迟执行任务,比如说一定时间后执行任务或是每隔一定的时间执行一次任务。
DelayedWorkQueue 的特点是内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构(堆的应用之一就是 优先级队列)。之所以线程池 ScheduledThreadPool 和 SingleThreadScheduledExecutor 选择 DelayedWorkQueue,是因为它们本身正是基于时间执行任务的,而延迟队列正好可以把任务按时间进行排序,方便任务的执行。
线程池的种类有哪些:五种功能不一样的线程池:
这样创建线程池的话,我们需要配置一堆东西,非常麻烦。所以,官方也不推荐使用这种方法来创建线程池,而是推荐使用Executors的工厂方法来创建线程池,Executors类是官方提供的一个工厂类,它里面封装好了众多功能不一样的线程池(但底层实现还是通过ThreadPoolExecutor),从而使得我们创建线程池非常的简便,主要提供了如下五种功能不一样的线程池:
newCachedThreadPool() :返回一个可以根据实际情况调整线程池中线程的数量的线程池。即该线程池中的线程数量不确定,是根据实际情况动态调整的。
newFixedThreadPool() :线程池只能存放指定数量的线程池,线程不会释放,可重复利用。
newSingleThreadExecutor() :单线程的线程池。即每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待这一个线程空闲,当这个线程空闲了再按FIFO方式顺序执行任务队列中的任务。
newScheduledThreadPool() :可定时和重复执行的线程池。
newSingleThreadScheduledExecutor():同上。和上面的区别是该线程池大小为1,而上面的可以指定线程池的大小。
通过Executors的工厂方法来获取:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
ScheduledExecutorService singleThreadScheduledPool = Executors.newSingleThreadScheduledExecutor();
通过Executors的工厂方法来创建线程池极其简便,其实它的内部还是通过new ThreadPoolExecutor(…)的方式创建线程池的,我们看一下这些工厂方法的内部实现:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,