面试||并发多线程

一、简述线程、进程、程序的基本概念?

程序
程序,是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。

进程是系统进行资源分配和调度的基本单位
进程,是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。

线程 CPU 分配的基本单位
线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

另外,Java 线程是重量级的,每个线程默认使用 1024KB 的内存,所以一个 Java 进程是无法开启大量线程的

三者之间的关系
线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。
从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。

线程有什么优缺点?
1)好处
使用多线程可以把程序中占据时间长的任务放到后台去处理,如图片、视屏的下载。
发挥多核处理器的优势,并发执行让系统运行的更快、更流畅,用户体验更好。
2)坏处
大量的线程降低代码的可读性。
更多的线程需要更多的内存空间。
当多个线程对同一个资源出现争夺时候要注意线程安全的问题。

守护线程?它和非守护线程有什么区别?
Java 中的线程分为两种:守护线程(Daemon)和用户线程(User)。

任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(boolean on) 设置。true 则把该线程设置为守护线程,反之则为用户线程。
Thread.setDaemon(boolean on) 方法,必须在Thread.start() 方法之前调用,否则运行时会抛出异常。
唯一的区别是:
程序运行完毕,JVM 会等待非守护线程完成后关闭,但是 JVM 不会等待守护线程。

判断虚拟机(JVM)何时离开,Daemon 是为其他线程提供服务,如果全部的 User Thread 已经撤离,Daemon 没有可服务的线程,JVM 撤离。

扩展:Thread Dump 打印出来的线程信息,含有 daemon
字样的线程即为守护进程。可能会有:服务守护进程、编译守护进程、Windows 下的监听 Ctrl + break
的守护进程、Finalizer 守护进程、引用处理守护进程、GC 守护进程。

二、什么是多线程上下文切换?

多线程会共同使用一组计算机上的 CPU ,而线程数大于给程序分配的 CPU 数量时,为了让各个线程都有执行的机会,就需要轮转使用 CPU 。不同的线程切换使用 CPU 发生的切换数据等,就是上下文切换

在上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。在程序中,上下文切换过程中的“页码”信息是保存在进程控制块(PCB)中的。PCB还经常被称作“切换桢”(switchframe)。“页码”信息会一直保存到CPU的内存中,直到他们被再次使用。

上下文切换是存储和恢复 CPU 状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。

Java 中用到的线程调度算法是什么?

假设计算机只有一个 CPU ,则在任意时刻只能执行一条机器指令,每个线程只有获得 CPU 的使用权才能执行指令。

所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得 CPU 的使用权,分别执行各自的任务。
在运行池中,会有多个处于就绪状态的线程在等待 CPU ,Java 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权。
有两种调度模型:分时调度模型抢占式调度模型
分时调度模型是指让所有的线程轮流获得 CPU 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。
Java 虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用 CPU ,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用 CPU 。处于运行状态的线程会一直运行,直至它不得不放弃 CPU 。

什么是线程饥饿?

饥饿,一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态
Java 中导致饥饿的原因:
高优先级线程吞噬所有的低优先级线程的 CPU 时间。
线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。

你对线程优先级的理解是什么?

每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。
我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个 int 变量(从1-10),1 代表最低优先级,10 代表最高优先级。
Java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级。

三、线程的生命周期?

五个状态
新建(new): 当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。例如:Thread t1 = new Thread() 。
可运行(runnable): 线程对象创建后,其他线程(比如 main 线程)调用了该对象的 start 方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 cpu 的使用权。例如:t1.start() 。
运行(running): 线程获得 CPU 资源正在执行任务(#run() 方法),此时除非此线程自动放弃 CPU 资源或者有优先级更高的线程进入,线程将一直运行到结束。
死亡(dead): 当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。自然终止:正常运行完 #run()方法,终止。异常终止:调用 #stop() 方法,让一个线程终止运行。
堵塞(blocked): 由于某种原因导致正在运行的线程让出 CPU 并暂停自己的执行,即进入堵塞状态。直到线程进入可运行(runnable)状态,才有机会再次获得 CPU 资源,转到运行(running)状态。阻塞的情况有三种:正在睡眠:调用 #sleep(long t) 方法,可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过去可进入可运行(runnable)状态。正在等待:调用 #wait() 方法。调用 notify() 方法,回到就绪状态。被另一个线程所阻塞:调用 #suspend() 方法。调用 #resume() 方法,就可以恢复。
在这里插入图片描述
在这里插入图片描述

如何结束一个一直运行的线程?

一般来说,有两种方式:
方式一,使用退出标志,这个 flag 变量要多线程可见。
在这种方式中,之所以引入共享变量,是因为该变量可以被多个执行相同任务的线程用来作为是否中断的信号,通知中断线程的执行。
方式二,使用 interrupt 方法,结合 isInterrupted 方法一起使用。

如果一个线程由于等待某些事件的发生而被阻塞,又该怎样停止该线程呢?这种情况经常会发生,比如当一个线程由于需要等候键盘输入而被阻塞,或者调用Thread#join() 方法,或者 Thread#sleep(…) 方法,在网络中调用ServerSocket#accept() 方法,或者调用了DatagramSocket#receive()方法时,都有可能导致线程阻塞,使线程处于处于不可运行状态时。即使主程序中将该线程的共享变量设置为 true,但该线程此时根本无法检查循环标志,当然也就无法立即中断。

这里我们给出的建议是,不要使用 Thread#stop()· 方法,而是使用 Thread 提供的#interrupt() 方法,因为该方法虽然不会中断一个正在运行的线程,但是它可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态,退出堵塞代码。
所以,方式一和方式二,并不是冲突的两种方式,而是可能根据实际场景下,进行结合。

interrupted 和 isInterrupted 方法的区别?

1)interrupt 方法
Thread#interrupt() 方法,用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。
注意:线程中断仅仅是置线程的中断状态位不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出 InterruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。
2)interrupted
Thread#interrupted() 静态方法,查询当前线程的中断状态,并且清除原状态。如果一个线程被中断了,第一次调用 #interrupted() 方法则返回 true ,第二次和后面的就返回 false 了。
3)isInterrupted
Thread#isInterrupted() 方法,查询指定线程的中断状态不会清除原状态

一个线程如果出现了运行时异常会怎么样?
如果这个异常没有被捕获的话,这个线程就停止执行了
另外重要的一点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放

四、创建线程的方式及实现

方式一,继承 Thread 类创建线程类。
方式二,通过 Runnable 接口创建线程类。
方式三,通过 Callable 和 Future 创建线程。

五、wait + notify 实现通知机制

利用 Object 类的 wait 和 notify方法实现线程阻塞。
首先,wait、notify 方法是针对对象的,调用任意对象的 wait 方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的 notify 方法则将随机解除该对象阻塞的线程,但它需要重新获取改对象的锁,直到获取成功才能往下执行。
其次,wait、notify 方法必须在 synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调用 wait、notify 方法的对象是同一个,如此一来在调用 wait 之前当前线程就已经成功获取某对象的锁,执行 wait 阻塞后当前线程就将之前获取的对象锁释放。
线程间协作方式总结及使用示例

Thread类的 sleep 方法和对象的 wait 方法都可以让线程暂停执行,它们有什么区别?

sleep 方法,是线程类 Thread 的静态方法。调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态)
wait 方法,是 Object 类的方法。调用对象的 #wait() 方法,会导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的 #notify() 方法(或#notifyAll()方法)时,才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。

为什么 wait, notify 和 notifyAll 这三方法不在 Thread 类里面?

一个很明显的原因是 Java 提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。
由于 wait,notify 和 notifyAll 方法都是锁级别的操作,所以把它们定义在 Object 类中,因为锁属于对象。

为什么 wait 和 notify 方法要在同步块中调用?

Java API 强制要求这样做,如果你不这么做,你的代码会抛出 IllegalMonitorStateException 异常。
还有一个原因是为了避免 wait 和 notify 之间产生竞态条件

为什么你应该在循环中检查等待条件?

处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。所以,我们不能写 if (condition) 而应该是 while (condition) ,特别是 CAS 竞争的时候。

线程的 sleep 方法和 yield 方法有什么区别?

两者都不释放锁
sleep 方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会。yield 方法只会给相同优先级或更高优先级的线程以运行的机会。
线程执行 sleep 方法后转入阻塞(blocked)状态,而执行 yield 方法后转入就绪(ready)状态
sleep 方法声明抛出 InterruptedException 异常,而 yield 方法没有声明任何异常。
sleep 方法比 yield 方法(跟操作系统 CPU 调度相关)具有更好的可移植性。

什么叫线程安全?

线程安全,是编程中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
Servlet 是线程安全吗?
Servlet 不是线程安全的,Servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。

Struts2 是线程安全吗?
Struts2 的 Action 是多实例多线程的,是线程安全的,每个请求过来都会 new 一个新的 Action 分配给这个请求,请求完成后销毁。

SpringMVC 是线程安全吗?
不是的,和 Servlet 类似的处理流程。

单例模式的线程安全性?
单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法:
饿汉式单例模式的写法:线程安全
懒汉式单例模式的写法:非线程安全
双检锁单例模式的写法:线程安全

多线程同步和互斥有几种实现方法,都是什么?

1)线程同步
线程同步,是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。

线程间的同步方法,大体可分为两类:用户模式和内核模式。顾名思义:

  • 内核模式,就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态。内核模式下的方法有:
    事件
    信号量
    互斥量
  • 用户模式,就是不需要切换到内核态,只在用户态完成操作。用户模式下的方法有:
    原子操作(例如一个单一的全局变量)
    临界区

2)线程互斥
线程互斥,是指对于共享的进程系统资源,在各单个线程访问时的排它性。
当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。
线程互斥可以看成是一种特殊的线程同步。

如何在两个线程间共享数据

在两个线程间共享变量,即可实现共享。
一般来说,共享变量要求变量本身是线程安全的,然后在线程内使用的时候,如果有对共享变量的复合操作,那么也得保证复合操作的线程安全性。
1,如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,卖票系统就可以这么做。
2,如果每个线程执行的代码不同,这时候需要用不同的Runnable对象,例如,设计4个线程。其中两个线程每次对j增加1,另外两个线程对j每次减1,银行存取款

什么是 ThreadLocal 变量?

ThreadLocal ,是 Java 里一种特殊的变量。每个线程都有一个 ThreadLocal 就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。
它是为创建代价高昂的对象获取线程安全的好方法,比如你可以用 ThreadLocal 让 SimpleDateFormat 变成线程安全的,因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。

  • 首先,通过复用减少了代价高昂的对象的创建个数。
  • 其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全
    所以,ThreadLocal 很适合实现线程级的单例。 《Java并发编程:深入剖析ThreadLocal》

什么是 InheritableThreadLocal ?
InheritableThreadLocal 类,是 ThreadLocal 类的子类。ThreadLocal 中每个线程拥有它自己的值,与 ThreadLocal 不同的是,InheritableThreadLocal 允许一个线程以及该线程创建的所有子线程都可以访问它保存的值

有哪些多线程开发良好的实践?

1、给线程命名。

这样可以方便找 bug 或追踪。给线程起一个和它要完成的任务相关的名字,所有的主要框架甚至JDK都遵循这个最佳实践。

2、最小化同步范围。

锁花费的代价高昂且上下文切换更耗费时间空间,试试最低限度的使用同步和锁,缩小临界区。因此相对于同步方法同步块拥有对锁的绝对控制权。

3、优先使用 volatile ,而不是 synchronized 。

4、尽可能使用更高层次的并发工具而非 wait 和 notify 方法来实现线程通信。

首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger这些同步类简化了编码操作,而用 wait 和 notify 很难实现对复杂控制流的控制。
其次,这些类是由最好的企业编写和维护在后续的 JDK 中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。

5、优先使用并发容器,而非同步容器。

这是另外一个容易遵循且受益巨大的最佳实践,并发容器比同步容器的可扩展性更好,所以在并发编程时使用并发集合效果更好。如果下一次你需要用到 Map ,我们应该首先想到用 ConcurrentHashMap 类。

6、考虑使用线程池。

同步和异步有何异同,在什么情况下分别使用他们?

如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。
当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。

Java 锁

synchronized 的原理是什么?

Java 8 并发篇 - 冷静分析 Synchronized
JVM源码分析之synchronized实现
synchronized是 Java 内置的关键字,它提供了一种独占的加锁方式,是重量级锁。
synchronized的获取和释放锁由JVM实现,用户不需要显示的释放锁,非常方便。
synchronized 可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。
然而,synchronized 也有一定的局限性。

  • 当线程尝试获取锁的时候,如果获取不到锁会一直阻塞。
  • 如果获取锁的线程进入休眠或者阻塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待。

Java 中每一个对象都可以作为锁,这是 synchronized 实现同步的基础:
普通同步方法,锁是当前实例对象
静态同步方法,锁是当前类的 class 对象
同步方法块,锁是括号里面的对象

Java 对象头、Monitor
Java 对象头和 Monitor 是实现 synchronized 的基础。
synchronized 用的锁是存在Java对象头里的。
什么是 Java 对象头呢? Hotspot 虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。

  • Mark Word 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。
  • Klass Point 是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

Monitor

  • 互斥: 一个 Monitor 锁在同一时刻只能被一个线程占用,其他线程无法占用。
  • 信号机制( signal ): 占用 Monitor 锁失败的线程会暂时放弃竞争并等待某个谓词成真(条件变量),但该条件成立后,当前线程会通过释放锁通知正在等待这个条件变量的其他线程,让其可以重新竞争锁。

锁优化

简单来说,在 JVM 中 monitorenter 和 monitorexit 字节码依赖于底层的操作系统的Mutex Lock 来实现的,但是由于使用 Mutex Lock 需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的。然而,在现实中的大部分情况下,同步方法是运行在单线程环境(无锁竞争环境),如果每次都调用 Mutex Lock 那么将严重的影响程序的性能。

JDK 1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

自旋锁
由来:线程的阻塞和唤醒,需要 CPU 从用户态转为核心态。频繁的阻塞和唤醒对 CPU 来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时,我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间。为了这一段很短的时间,频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。
定义:所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。执行一段无意义的循环即可(自旋)。

  • 适应自旋锁:自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

锁消除
由来:为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制。但是,在有些情况下,JVM检测到不可能存在共享数据竞争,这时JVM会对这些同步锁进行锁消除。

锁粗化
锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
我们知道在使用同步锁的时候,需要让同步块的作用范围尽可能小:仅在共享数据的实际作用域中才进行同步。这样做的目的,是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
在大多数的情况下,上述观点是正确的。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。

锁的升级
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。它们会随着竞争的激烈而逐渐升级。注意,锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

  • 重量级锁通过对象内部的监视器(Monitor)实现。其中,Monitor 的本质是,依赖于底层操作系统的 Mutex Lock 实现。操作系统实现线程之间的切换,需要从用户态到内核态的切换,切换成本非常高。
  • 轻量级锁的主要目的,是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁,导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁。(CAS操作)
  • 偏向锁主要目的是:为了在无多线程竞争的情况下,尽量减少不必要的轻量级锁执行路径。

在这里插入图片描述
三种锁之间的转换图:
在这里插入图片描述
在这里插入图片描述
同步方法和同步块,哪个是更好的选择?
同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。
同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁。

在监视器(Monitor)内部,是如何做线程同步的?
监视器和锁在 Java 虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。

volatile 实现原理

Volatile 是轻量级的 synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
volatile 有什么用?
volatile 保证内存可见性禁止指令重排。同时,volatile 可以提供部分原子性。
简单来说,volatile 用于多线程环境下的单次操作(单次读或者单次写)。

volatile 变量和 atomic 变量有什么不同?
volatile 变量,可以确保先行关系,即写操作会发生在后续的读操作之前,但它并不能保证原子性。
AtomicInteger 类提供的 atomic 方法,可以让这种操作具有原子性。例如 #getAndIncrement() 方法,会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。

可以创建 volatile 数组吗?
Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。
同理,对于 Java POJO 类,使用 volatile 修饰,只能保证这个引用的可见性,不能保证其内部的属性

volatile 能使得一个非原子操作变成原子操作吗?
一种实践是用 volatile 修饰 long 和 double 变量,使其能按原子类型来读写。double 和 long 都是64位宽,因此对这两种类型的读是分为两部分的,第一次读取第一个 32 位,然后再读剩下的 32 位,这个过程不是原子的,但 Java 中 volatile 型的 long 或 double 变量的读写是原子的。

volatile 和 synchronized 的区别?

  1. volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取。synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  2. volatile 仅能使用在变量级别。synchronized 则可以使用在变量、方法、和类级别的。
  3. volatile 仅能实现变量的修改可见性,不能保证原子性。而synchronized 则可以保证变量的修改可见性和原子性。
  4. volatile 不会造成线程的阻塞。synchronized 可能会造成线程的阻塞。
  5. volatile 标记的变量不会被编译器优化。synchronized标记的变量可以被编译器优化。

什么场景下可以使用 volatile 替换 synchronized ?

  1. 只需要保证共享资源的可见性的时候可以使用 volatile 替代,synchronized 保证可操作的原子性一致性和可见性。
  2. volatile 适用于新值不依赖于旧值的情形。
  3. 1 写 N 读。
  4. 不与其他变量构成不变性条件时候使用 volatile 。

什么是死锁、活锁?

死锁,是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

产生死锁的必要条件:

  • 互斥条件:所谓互斥就是进程在某一时间内独占资源。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

死锁的解决方法:

  • 撤消陷于死锁的全部进程。
  • 逐个撤消陷于死锁的进程,直到死锁不存在。
  • 从陷于死锁的进程中逐个强迫放弃所占用的资源,直至死锁消失。
  • 从另外一些进程那里强行剥夺足够数量的资源分配给死锁进程,以解除死锁状态。

什么是活锁?
活锁,任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。

死锁与活锁的区别?
活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”,而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
实际上,死锁就是悲观锁可能产生的结果,而活锁是乐观锁可能产生的结果

什么是悲观锁、乐观锁?
悲观锁

  • 悲观锁,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
  • 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
    再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。

乐观锁

  • 乐观锁,顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。
  • 乐观锁的实现方式:
    • 使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。
    • Java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

Java Lock 接口

虽然 Lock 也翻译成锁,但是和上面的 「Java 锁」 分开,它更多强调的是 synchronized 和 volatile 关键字带来的重量级和轻量级锁。而 Lock 是 Java 锁接口,提供了更多灵活的功能。
在这里插入图片描述
Java AQS
java.util.concurrent.locks.AbstractQueuedSynchronizer 抽象类,简称 AQS ,是一个用于构建锁和同步容器的队列同步器。事实上concurrent 包内许多类都是基于 AQS 构建。例如 ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,等。AQS 解决了在实现同步容器时设计的大量细节问题。
AQS 使用一个 FIFO 的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态 waitStatus 。

什么是 Java Lock 接口?
java.util.concurrent.locks.Lock 接口,比 synchronized 提供更具拓展行的锁操作。它允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。它的优势有:

  • 可以使锁更公平。
  • 可以使线程在等待锁的时候响应中断。
  • 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间。
  • 可以在不同的范围,以不同的顺序获取和释放锁。

什么是可重入锁(ReentrantLock)?

可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码块。
synchronized、ReentrantLock 都是可重入的锁,可重入锁相对来说简化了并发编程的开发。
ReenTrantLock 的实现是一种自旋锁,通过循环调用 CAS 操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。

synchronized 和 ReentrantLock 异同?
相同点

  • 都实现了多线程同步和内存可见性语义。
  • 都是可重入锁。

不同点

  • 同步实现机制不同
    • synchronized 通过 Java 对象头锁标记和 Monitor 对象实现同步。
    • ReentrantLock 通过CAS、AQS(AbstractQueuedSynchronizer)和 LockSupport(用于阻塞和解除阻塞)实现同步。
  • 可见性实现机制不同
    • synchronized 依赖 JVM 内存模型保证包含共享变量的多线程内存可见性。
    • ReentrantLock 通过 ASQ 的 volatile state 保证包含共享变量的多线程内存可见性。
  • 使用方式不同
    • synchronized 可以修饰实例方法(锁住实例对象)、静态方法(锁住类对象)、代码块(显示指定锁对象)。
    • ReentrantLock 显示调用 tryLock 和 lock 方法,需要在 finally 块中释放锁。
  • 功能丰富程度不同
    • synchronized 不可设置等待时间、不可被中断(interrupted)。
    • ReentrantLock 提供有限时间等候锁(设置过期时间)、可中断锁(lockInterruptibly)、condition(提供 await、condition(提供 await、signal 等方法)等丰富功能
  • 锁类型不同
    • synchronized 只支持非公平锁。
    • ReentrantLock 提供公平锁和非公平锁实现。当然,在大部分情况下,非公平锁是高效的选择。

ReadWriteLock 是什么?

ReadWriteLock ,读写锁,用来提升并发程序性能的锁分离技术的 Lock 实现类。可以用于 “多读少写” 的场景,读写锁支持多个读操作并发执行,写操作只能由一个线程来操作

ReadWriteLock 对向数据结构相对不频繁地写入,但是有多个任务要经常读取这个数据结构的这类情况进行了优化。ReadWriteLock 使得你可以同时有多个读取者,只要它们都不试图写入即可。如果写锁已经被其他任务持有,那么任何读取者都不能访问,直至这个写锁被释放为止。
ReadWriteLock 对程序性能的提高主要受制于如下几个因素:

  • 数据被读取的频率与被修改的频率相比较的结果。
  • 读取和写入的时间
  • 有多少线程竞争
  • 是否在多处理机器上运行

三个线程按顺序循环打印 abc 三个字母,比如 abcabcabc

Java 内存模型

java并发内存模型以及内存操作规则(八种原子操作)

Java 虚拟机规范中试图定义一种 Java 内存模型(Java Memory Model,JMM)来屏蔽掉各层硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。(不要和JVM内存区域搞混)

Java 内存模型规定了所有的变量都存储在主内存(Main Memory)中。每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间的变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的关系如下图:
在这里插入图片描述
两个线程之间是如何通信的呢?
线程之间的通信方式,目前有共享内存消息传递两种。
1)共享内存
在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。典型的共享内存通信方式,就是通过共享对象进行通信。

  • 首先,线程 A 把本地内存 A 更新过得共享变量刷新到主内存中去。
  • 然后,线程 B 到主内存中去读取线程 A 之前更新过的共享变量。

2)消息传递
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。在 Java 中典型的消息传递方式,就是 #wait() 和 #notify() ,或者 BlockingQueue

为什么代码会重排序?
在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:

  • 在单线程环境下不能改变程序运行的结果。
  • 存在数据依赖关系的不允许重排序
    需要注意的是:重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义。

什么是内存模型的 happens-before 呢?
在这里插入图片描述
什么是内存屏障?
内存屏障,又称内存栅栏,是一组处理器指令,用于实现对内存操作的顺序限制。

对主存的一次访问一般花费硬件的数百次时钟周期。处理器通过缓存(caching)能够从数量级上降低内存延迟的成本这些缓存为了性能重新排列待定内存操作的顺序。也就是说,程序的读写操作不一定会按照它要求处理器的顺序执行。当数据是不可变的,同时/或者数据限制在线程范围内,这些优化是无害的。如果把这些优化与对称多处理(symmetric multi-processing)和共享可变状态(shared mutable state)结合,那么就是一场噩梦。

当基于共享可变状态的内存操作被重新排序时,程序可能行为不定。一个线程写入的数据可能被其他线程可见,原因是数据写入的顺序不一致。适当的放置内存屏障,通过强制处理器顺序执行待定的内存操作来避免这个问题。

Java 并发容器

什么是并发容器的实现?
何为同步容器?可以简单地理解为通过 synchronized来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。
并发容器,使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性。

  • 例如在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁。在这种锁机制下,允许任意数量的读线程并发地访问 map ,并且执行读操作的线程和写操作的线程也可以并发的访问 map ,同时允许一定数量的写操作线程并发地修改 map ,所以它可以在并发环境下实现更高的吞吐量。
  • 再例如,CopyOnWriteArrayList 。

ConcurrentHashMap

SynchronizedMap 和 ConcurrentHashMap 有什么区别?
SynchronizedMap

  • 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为 map 。
    ConcurrentHashMap
  • 使用分段锁来保证在多线程下的性能。ConcurrentHashMap 中则是一次锁住一个segment。ConcurrentHashMap 默认将 hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提升是显而易见的。【注意,这块是 JDK7 的实现。在 JDK8 中,具体的实现已经改变】
    另外 ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中,当 iterator 被创建后集合再发生改变就不再是抛出 ConcurrentModificationException 异常,取而代之的是在改变时 new 新的数据从而不影响原有的数据,iterator 完成后再将头指针替换为新的数据 ,这样 iterator 线程可以使用原来老的数据,而写线程也可以并发的完成改变。

Java 中 ConcurrentHashMap 的并发度是什么?
在 JDK8 前,ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16 ,这样在多线程情况下就能避免争用。
在 JDK8 后,它摒弃了 Segment(锁段)的概念,而是启用了一种全新的方式实现,利用 CAS 算法。同时加入了更多的辅助变量来提高并发度,

CopyOnWriteArrayList 可以用于什么应用场景?
CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModificationException 异常。在 CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。

由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 ygc 或者 fgc 。
不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然 CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求
CopyOnWriteArrayList 透露的思想:

  • 读写分离,读和写分开
  • 最终一致性
  • 使用另外开辟空间的思路,来解决并发冲突
  • CopyOnWriteArrayList 适用于读操作远远多于写操作的场景。例如,缓存

Java 阻塞队列

什么是阻塞队列?有什么适用场景?
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:

  • 在队列为空时,获取元素的线程会等待队列变为非空。
  • 当队列满时,存储元素的线程会等待队列可用。

阻塞队列常用于生产者和消费者的场景:

  • 生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程
  • 阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

Java 提供了哪些阻塞队列的实现?
【最常用】ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。

  • 此队列按照先进先出(FIFO)的原则对元素进行排序,但是默认情况下不保证线程公平的访问队列,即如果队列满了,那么被阻塞在外面的线程对队列访问的顺序是不能保证线程公平(即先阻塞,先插入)的。

LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。

  • 此队列按照先出先进的原则对元素进行排序

PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。

DelayQueue支持延时获取元素的无界阻塞队列,即可以指定多久才能从队列中获取当前元素。

SynchronousQueue:一个不存储元素的阻塞队列,直接新建一个线程来执行新来的任务。

  • 每一个 put 必须等待一个 take 操作,否则不能继续添加元素。并且他支持公平访问队列

LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。

  • 相对于其他阻塞队列,多了 tryTransfer 和 transfer 方法。
  • transfer 方法:如果当前有消费者正在等待接收元素(take 或者待时间限制的 poll 方法),transfer 可以把生产者传入的元素立刻传给消费者。如果没有消费者等待接收元素,则将元素放在队列的 tail 节点,并等到该元素被消费者消费了才返回。
  • tryTransfer 方法:用来试探生产者传入的元素能否直接传给消费者。如果没有消费者在等待,则返回 false 。和上述方法的区别是该方法无论消费者是否接收,方法立即返回。而 transfer 方法是必须等到消费者消费了才返回。

LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列

  • 优势在于多线程入队时,减少一半的竞争。

Java 原子操作类

什么是原子操作?
原子操作(Atomic Operation),意为”不可被中断的一个或一系列操作”。

处理器使用基于对缓存加锁或总线加锁的方式,来实现多处理器之间的原子操作。
在 Java 中,可以通过循环 CAS 的方式来实现原子操作。CAS操作 —— Compare & Set ,或是 Compare & Swap ,现在几乎所有的 CPU 指令都支持 CAS 的原子操作。
原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。

CAS 操作有什么缺点?
1)ABA 问题
比如说一个线程 one 从内存位置 V 中取出 A ,这时候另一个线程 two 也从内存中取出 A ,并且 two 进行了一些操作变成了 B ,然后 two 又将 V 位置的数据变成 A ,这时候线程 one 进行 CAS 操作发现内存中仍然是 A ,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。
从 Java5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。
2)循环时间长开销大
对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized 。
3)只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。

Java 线程池

在这里插入图片描述

什么是 Executor 框架?

Executor 框架,是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。

无限制的创建线程,会引起应用程序内存溢出。所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用 Executor 框架,可以非常方便的创建一个线程池。

为什么使用 Executor 框架?
每次执行任务创建线程 new Thread() 比较消耗性能,创建一个线程是比较耗时、耗资源的。
调用 new Thread() 创建的线程缺乏管理,被称为野线程,而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。
接使用 new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。

在 Java 中 Executor 和 Executors 的区别?
Executors 是 Executor 的工具类,不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
Executor 接口对象,能执行我们的线程任务
ExecutorService 接口,继承了 Executor 接口,并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
使用 ThreadPoolExecutor ,可以创建自定义线程池。
Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使用 #get() 方法,获取计算的结果。

讲讲线程池的实现原理

提交一个任务到线程池中,线程池的处理流程如下:
1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
在这里插入图片描述

创建线程池的几种方式?

Java 类库提供一个灵活的线程池以及一些有用的默认配置,我们可以通过Executors 的静态方法来创建线程池。
Executors 创建的线程池,分成普通任务线程池,和定时任务线程池。

普通任务线程池
1、newFixedThreadPool(int nThreads) 方法,创建一个固定长度的线程池。
每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化。
当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
2、newCachedThreadPool() 方法,创建一个可缓存的线程池。
如果线程池的规模超过了处理需求,将自动回收空闲线程。
当需求增加时,则可以自动添加新线程。线程池的规模不存在任何限制
3、newSingleThreadExecutor() 方法,创建一个单线程的线程池。
它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它。
它的特点是,能确保依照任务在队列中的顺序来串行执行

定时任务线程池
4、newScheduledThreadPool(int corePoolSize) 方法,创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似 Timer 。

如何使用 ThreadPoolExecutor 创建线程池?
Executors 提供了创建线程池的常用模板,实际场景下,我们可能需要自动以更灵活的线程池,此时就需要使用 ThreadPoolExecutor 类。

// ThreadPoolExecutor.java

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

corePoolSize 参数,核心线程数大小,当线程数 < corePoolSize ,会创建线程执行任务。
maximumPoolSize 参数,最大线程数, 当线程数 >= corePoolSize 的时候,会把任务放入 workQueue 队列中。
keepAliveTime 参数,保持存活时间,当线程数大于 corePoolSize 的空闲线程能保持的最大时间。
unit 参数,时间单位。
workQueue 参数,保存任务的阻塞队列。
handler 参数,超过阻塞队列的大小时,使用的拒绝策略。
threadFactory 参数,创建线程的工厂。

.任务缓存队列及排队策略
在前面我们多次提到了任务缓存队列,即workQueue,它用来存放等待执行的任务。
workQueue的类型为BlockingQueue,通常可以取下面三种类型:
1)ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
2)LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
3)synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。

任务拒绝策略
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy由调用线程处理该任务

线程池的关闭方式有几种?
ThreadPoolExecutor 提供了两个方法,用于线程池的关闭,分别是:

  • #shutdown() 方法,不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。
  • #shutdownNow() 方法,立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。

线程池中 submit 和 execute 方法有什么区别?
两个方法都可以向线程池提交任务。

  • #execute(…) 方法,返回类型是 void ,它定义在 Executor 接口中。
  • #submit(…) 方法,可以返回持有计算结果的 Future 对象,它定义在 ExecutorService 接口中,它扩展了 Executor 接口,其它线程池类像 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 都有这些方法。

什么是 Callable、Future、FutureTask ?
1)Callable
Callable 接口,类似于 Runnable ,从名字就可以看出来了,但是Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。
简单来说,可以认为是带有回调的 Runnable 。

2)Future
Future 接口,表示异步任务,是还没有完成的任务给出的未来结果。所以说 Callable 用于产生结果,Future 用于获取结果

3)FutureTask
在 Java 并发程序中,FutureTask 表示一个可以取消的异步运算。
它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。
一个 FutureTask 对象,可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是继承了 Runnable 接口,所以它可以提交给 Executor 来执行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值