Java多线程与并发之面试常问题

 

JAVA多线程与并发


进程与线程的区别

进程是资源分配的最小单位,线程是CPU调度的最小单位java

  • 全部与进程相关的资源,都被记录在PCB(进程控制块)中
  • 进程是抢占处理机的调度单位;线程属于某个进程,共享其资源
  • 线程只由堆栈寄存器、程序计数器和TCB(线程控制块)组成

总结:c++

  • 线程不能看作独立应用,而进程可看作独立应用
  • 进程有独立的地址空间,相互不影响,线程只是进程的不一样执行路径
  • 线程没有独立的地址空间,多进程的程序比多线程的程序健壮
  • 进程的开销比线程大,切换代价高

Java进程和线程的关系

  • Java对操做系统的功能进行封装,包括进程和线程
  • 运行一个程序会产生一个进程,进程包含至少一个线程
  • 每一个进程对应一个JVM实例,多个线程共享JVM里的堆
  • Java采用单线程编程模型,程序会自动建立主线程
  • 主线程能够建立子线程,原则上要后于子线程完成执行

start和run的区别

  • 调用start()方法会建立一个新的子线程并启动
  • run()方法只是Thread的一个普通方法的调用(注:仍是在主线程里面执行)

Thread和Runnable

  • Thread是实现了Runnable接口的类,使得run支持多线程
    public class Thread implements Runnable
  • 由于类的单一继承原则,推荐多使用Runnable接口

如何给run()方法传参

实现方式有三种算法

  • 构造函数传参
  • 成员变量传参
  • 回调函数传参

如何实现处理线程的返回值

实现的方式主要有三种:编程

  • 主线程等待法
  • 使用Thread类的join()阻塞当前线程以等待子线程处理完毕
  • 经过Callable接口实现:经过Future Or 线程池获取

Java线程的六个状态

  • 新建(New):建立后还没有启动的线程的状态
  • 运行(Runnable):包含Running 和Ready
  • 无限期等待(Waiting):不会被分配CPU执行时间,须要显示被唤醒
  • 限期等待(Timed Waiting):在必定时间后会由系统自动唤醒
  • 阻塞(Blocked):等待获取排它锁
  • 结束(Terminated):已终止线程的状态,线程已经结束执行

Sleep和wait的区别

  • sleep是Thread的方法,wait是Object类中定义的方法
  • sleep()方法能够在任何地方使用
  • wait()方法只能在synchronized方法或synchronized块中使用
  • Thread.sleep只会让出CPU,不会致使锁行为的改变
  • Object.wait不只让出CPU,还会释放已经占有的同步资源锁

notify和notifyAll的区别

锁池EntryList:假设线程A已经得到了某个对象(不是类)的锁,而其余线程B,C想要调用这个对象的某个synchronized方法(或者块),因为B,C线程在进入对象的synchronized方法以前必须得到该对象锁的拥有权,而恰巧该对象的锁恰好被线程A所占用,此时B,C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方就是锁池。数组

等待池WaitSet:假设线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁,同时线程A就进入到了该对象的等待池中,进入到等待池中的线程不会去竞争该对象的锁。缓存

  • notifyAll会让全部出于等待池WaitSet的线程所有进入锁池EntryList去竞争获取锁的机会
  • notify只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会

Yield与join的区别

当调用Thread.yeild()函数时,会给线程调度器一个当前线程愿意让出CPU使用的暗示,可是线程调度器可能会忽略这个暗示。并不会让出当前线程的锁。安全

  • yield是一个静态的原生(native)方法
  • yield不能保证是的当前正在运行的线程迅速转换到可运行的状态,仅能从运行态转换到可运行态,而不能是等待或阻塞。

join方法可使得一个线程在另外一个线程结束后再执行。当前线程将阻塞直到这个线程实例完成了再执行。多线程

  • join方法可设置超时,使得join()方法的影响在特定超时后无效,如,join(50)。注:join(0),并非等待0秒,而是等待无限时间,等价join()。
  • join方法必须在线程start()方法调用以后才有意义
  • join方法的原理,就是调用了相应线程的wait方法

如何中断线程

已经被抛弃的方法:并发

  • 经过调用stop()方法中止线层(缘由:不安全,会释放掉锁)
  • 经过调用suspend()和resume()方法

目前使用的方法:app

  • 调用interrput(),通知线程应该中断了:1.若是线程出于被阻塞的状态,那么线程将当即退出被阻塞状态,并抛出一个InterruputedException异常。2.若是线程出于正常的活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。
  • 须要被调用的线程配合中断:1.在正常运行任务时,常常检查本线程的中断标志位,若是被设置了中断标志就自行中止线程。2.若是线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。

Synchronized

线程安全出现的缘由:

  • 存在共享数据(也称为临界资源)
  • 存在多线程共同操做这些共享数据

解决线程安全的根本办法:同一时刻有且只有一个线程在操做共享数据,其余线程必须等到该线程处理完数据以后再对共享数据进行操做,引入了互斥锁

互斥锁的特性:

  • 互斥性:即在同一个时间只容许一个线程持有某个对象锁,经过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对须要同步的代码块(复合操做)进行访问。互斥性也成为操做的原子性。
  • 可见性:必须确保在锁被释放以前,对共享变量所作的修改,对于随后得到该锁的另外一个线程是可见的(即在得到锁时应得到最新的共享变量的值),不然另外一个线程多是在本地缓存的某个副本上继续操做,从而引发不一致性。
  • synchronized锁的不是代码,是对象。

根据获取的锁分类:

  • 获取对象锁:一、同步代码块(synchronized(this),synchronized(类实例对象)),锁是小括号()中的实例对象。二、同步非静态方法(synchronized method),锁是当前对象的实例对象。
  • 获取类锁:一、同步代码块(synchronized(类.class)),锁是小括号()中的类对象(class对象)。二、同步静态方法(synchronized static method),锁是当前对象的类对象(class对象)。
  • 有线程访问对象的同步代码块时,另外的线程能够访问该对象的非同步代码块
  • 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另外一个线程访问对象的同步方法,会被阻塞
  • 类锁和对象锁互补干扰

synchronized底层实现原理:

  • Monitor:每一个java对象天生自带了一把看不见的锁(c++实现)
  • Monitor锁的竞争、获取与释放
  • 自旋锁:原因,一、许多状况下,共享数据的锁定状态持续时间较短,切换线程不值得。二、经过让线程执行忙循环等待锁的释放,不让出CPU。三、若锁被其余线程长时间占用,会带来许多性能上的开销
  • 自适应自旋锁:一、自旋的次数不固定。二、由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
  • 锁消除:JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁
  • 锁粗化:经过扩大锁的范围,避免反复的加锁和解锁

锁的内存语义:

  • 当线程释放锁时,Java内存模型会把该线程对应的本地内存中的共享变量刷新到主内存中去;
  • 而当线程得到锁时,Java内存模型会把该线程对应的本地内存置为无效,从而使得监视器保护的临界区代码必须从主内存中读取共享变量。

synchronized的四种状态:

  • 无锁
  • 偏向锁:减小同一线程获取锁的代价,大多数状况下,锁不存在多线程竞争,老是由同一线程多层次得到。核心思想:若是一个线程得到了锁,那么锁就进入偏向模式,此时MarkWord的结构也变为偏向锁结构,当该线程再次请求锁时,无需任何同步操做,即获取锁的过程只须要检查Markword的锁标记位为偏向锁以及当前线程Id等于Markword的ThreadId 便可,这样就省去了大量有关锁申请的操做。不适合锁竞争比较激烈的多线程场合
  • 轻量级锁:由偏向锁升级来的,偏向锁运行在一个线程进入同步块的状况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。适应场景:线程交替执行同步代码块。若存在同一时间访问同一锁的状况,就致使轻量级锁膨胀为重量级锁
  • 重量级锁

AQS:

AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架,AbstractQueuedSynchronizer中对state的操做是原子的,且不能被继承。全部的同步机制的实现均依赖于对改变量的原子操做。为了实现不一样的同步机制,咱们须要建立一个非共有的(non-public internal)扩展了AQS类的内部辅助类来实现相应的同步逻辑,AbstractQueuedSynchronizer并不实现任何同步接口,它提供了一些能够被具体实现类直接调用的一些原子操做方法来重写相应的同步逻辑。AQS同时提供了互斥模式(exclusive)和共享模式(shared)两种不一样的同步逻辑。

ReentrantLock:

  • jdk1.5后引入了ReentrantLock(再入锁),位于java.util.concurrent.locks包
  • 和CountDownLatch、FutrueTask、Semaphore同样基于AQS实现
  • 可以实现比synchronized更细粒度的控制,如控制fairness
  • 调用lock()以后,必须调用unlock()释放锁
  • 性能未必比synchronized高,而且也是可重入的
  • ReentrantLock公平性的设置:参数为true时,一、倾向于将锁赋予等待时间最久的线程。二、公平锁:获取锁的顺序按前后调用lock方法的顺序。三、synchronized并非公平性锁

synchronized和ReentrantLock的区别:

  • synchronized是关键字,ReentrantLock是类
  • ReentrantLock能够对获取锁的等待时间进行设置,避免死锁
  • ReentrantLock能够获取各类锁的信息
  • ReentrantLock能够灵活的实现多路通知

synchronized和volatile的区别

  • volatile本质是告诉JVM当前变量在寄存器(工做内存)中的值是不肯定的,须要从主存中读取;synchronized则是锁定当前变量,只有当前线程能够访问该变量,其余线程被阻塞直到该线程完成变量操做为止
  • volatile仅能使用在变量级别;synchronized则可使用在变量、方法和类级别
  • volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则能够保证变量修改的可见性和原子性
  • volatile不会形成现成的阻塞;synchronized可能形成线程的阻塞
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可被编译器优化

线程间的通信方式

本质上有两大类:共享内存机制和消息通讯机制。

  • 同步:多个线程经过synchronized关键字这种方式来实现线程间的通讯。如:线程A须要等待线程B执行完method方法后,线程A才能执行这个方法,以此实现线程A,B之间的通信。
  • while轮询的方式(不建议使用):线程A不断地改变条件,线程B不停地经过while语句检测某个条件(这个条件与线程A的操做有关)是否成立 ,从而实现了线程间的通讯。缺点:浪费资源,线程B会不停的while
  • wait/notify机制:线程A须要线程B完成某任务在执行时,线程A调用wait()方法,进入等待池中,等待线程B的唤醒。线程B完成某任务后(这个任务线程A所须要的),调用notify将其唤醒。优势:比起while轮询方法,更加的节约资源。缺点:通知过早,会打乱程序的执行逻辑。即线程B先于线程A占用CPU,可是此时线程A并为执行
  • 管道通讯:就是使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通讯

Java内存模型

java内存模型(即Java Memory Model,简称JMM)自己是一种抽象的概念,并不真实的存在,它描述一组规范或者规则,经过这组规范定义了程序中各个变量的访问方式。

JMM中的主内存:

  • 存储Java实例对象
  • 包括成员变量、类信息、常量、静态变量等
  • 数据共享的区域,多线程并发操做时会引起一系列的安全问题

JMM中的工做内存:

  • 存储当前方法的全部本地变量信息,本地变量对其余线程不可见
  • 字节码行号指示器、Native方法信息
  • 属于线程私有数据区域,不存在线层安全问题

JMM与java内存区域划分是不一样的概念层次:

  • JMM描述的是一组规则,围绕原子性,有序性,可见性展开
  • 类似点:存在共享区域和私有区域

JMM如何解决可见性问题:

  • 在单线程环境下不能改变程序运行的结果
  • 存在数据依赖关系的不容许重排序
  • 没法经过happens-before原则推导出来的,才能进行指令重排序
  • 若是操做A happens-before 操做B,那么操做A在内存上所作的操做对操做B来讲都是可见的

happens-before

  • 程序次序原则:一个线程内,按照代码顺序,书写在前面的操做先行发生于书写在后面的操做
  • 锁定规则:一个unLock操做先行发生于后面对同一个锁的lock操做
  • volatile变量规则:对一个变量的写操做先行发生于后面对这个读操做
  • 传递规则:若是操做A先行发生于操做B,操做B先行发生于操做C,则A先行发生于C
  • 线程启动原则:Thread对象的start()方法先行发生于此线程的每个动做
  • 线程中断原则:对线程interrupt()方法调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结原则:线程中全部的操做都先行发生于线程的终止检测,咱们能够经过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结原则:一个对象的初始化完成先行发生于他的finalize()方法的开始

CAS

compare and swap:

  • 包含三个操做数,内存位置(V),预期原值(A)和新值(B)
  • J.U.C的atomic包提供了经常使用的原子性数据类型以及引用、数组等相关类型和更新操做工具,是不少线程安全程序的首选
  • Unsafe类虽然提供CAS服务,但因可以操纵任意内存地址读写而有隐患
  • Java9之后,可使用Variable Handle API 来代替Unsafe
  • 缺点:若循环时间长,则开销很大,只能保证一个共享变量的原子操做,ABA问题(解决:经过版原本解决ABA问题,AtomicStampedReference)

JAVA线程池

为何要使用线程池

  • 下降资源消耗
  • 提升线程的可管理性
  • 提升响应速度;

J.U.C的三个Executor接口

  • Executor:运行新任务的简单接口,将任务提交和任务执行细节解耦
  • ExecutorService:具有管理执行器和任务生命周期的方法,提交任务机制更完善
  • ScheduledExecutorService:支持Future和按期执行任务

线程池的状态:

  • RUNNING:能接受新提交的任务,而且也能处理阻塞队列中的任务
  • SHUTDOWN;再也不接受新提交的任务,但能够处理存量任务
  • STOP:再也不接受新提交的任务,也不处理存量任务
  • TIDYING:全部的任务都已经终止
  • TERMINATED:teriminated()方法执行完后进入该状态

线程池的大小如何选定:

  • CPU密集型:线程数=按照核数或者核数+1 设定
  • I/O密集型:线程数 = CPU核数 *(1+平均等待时间/平均工做时间)

线程池的参数

  • corePoolSize:线程池中的核心线程数,当提交一个任务时,线程池建立一个新线程执行任务,直到当前线程数等于corePoolSize;若是当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;若是执行了线程池的prestartAllCoreThreads()方法,线程池会提早建立并启动全部核心线程。
  • maximumPoolSize:线程池中容许的最大线程数。若是当前阻塞队列满了,且继续提交任务,则建立新的线程执行任务,前提是当前线程数小于maximumPoolSize;
  • keepAliveTime:线程空闲时的存活时间,即当线程没有任务执行时,继续存活的时间;默认状况下,该参数只在线程数大于corePoolSize时才有用;
  • unit:keepAliveTime的单位;
  • workQueue:用来保存等待被执行的任务的阻塞队列,且任务必须实现Runable接口,在JDK中提供了以下阻塞队列:
    一、ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务;
    二、LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量一般要高于ArrayBlockingQuene;
    三、SynchronousQuene:一个不存储元素的阻塞队列,每一个插入操做必须等到另外一个线程调用移除操做,不然插入操做一直处于阻塞状态,吞吐量一般要高于LinkedBlockingQuene;
    四、priorityBlockingQuene:具备优先级的无界阻塞队列;
  • threadFactory:建立线程的工厂,经过自定义的线程工厂能够给每一个新建的线程设置一个具备识别度的线程名。
  • handler:线程池的饱和策略,当阻塞队列满了,且没有空闲的工做线程,若是继续提交任务,必须采起一种策略处理该任务,线程池提供了4种策略:
    一、AbortPolicy:直接抛出异常,默认策略;
    二、CallerRunsPolicy:用调用者所在的线程来执行任务;
    三、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
    四、DiscardPolicy:直接丢弃任务;
    固然也能够根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。
  • Exectors:工厂类提供了线程池的初始化接口
    • newFixedThreadPool(int nThreads) 指定工做线程数量的线程池
    • newCachedThreadPool()处理大量短期工做任务的线程池。1:试图缓存线程并重用,当无缓存线程可用时,就会建立新的工做线程;2:若是线程闲置的时间超过阈值,则会被终止并移除缓存;三、系统长时间闲置的时候,不会消耗什么资源
    • newSingleThreadExcutor()建立惟一的工做者线程来执行任务,若是线程异常结束,会有另外一个线程取代它
    • newSingleThreadScheduledExecutor()与newScheduledThreadPool(int corePoolSize)定时或者周期性的工做制度,二者的区别在于单一工做线程仍是多个线程
    • newWorkStealingPool()内部会构建ForkJoinPool,利用working-stealing算法,并行地处理任务,不保证处理顺序

线程池的任务提交:线程池框架提供了两种方式提交任务,根据不一样的业务需求选择不一样的方式。

  • Executor.execute():经过Executor.execute()方法提交的任务,必须实现Runnable接口,该方式提交的任务不能获取返回值,所以没法判断任务是否执行成功。
  • ExecutorService.submit():经过ExecutorService.submit()方法提交的任务,能够获取任务执行完的返回值。

线程池任务的执行:具体的执行流程以下:

  • 一、workerCountOf方法根据ctl的低29位,获得线程池的当前线程数,若是线程数小于corePoolSize,则执行addWorker方法建立新的线程执行任务;不然执行步骤(2);
  • 二、若是线程池处于RUNNING状态,且把提交的任务成功放入阻塞队列中,则执行步骤(3),不然执行步骤(4);
  • 三、再次检查线程池的状态,若是线程池没有RUNNING,且成功从阻塞队列中删除任务,则执行reject方法处理任务;
  • 四、执行addWorker方法建立新的线程执行任务,若是addWoker执行失败,则执行reject方法处理任务;

addWoker方法实现的前半部分:

一、判断线程池的状态,若是线程池的状态值大于或等SHUTDOWN,则不处理提交的任务,直接返回;

二、经过参数core判断当前须要建立的线程是否为核心线程,若是core为true,且当前线程数小于corePoolSize,则跳出循环,开始建立新的线程,具体实现以下:

线程池的工做线程经过Woker类实现,在ReentrantLock锁的保证下,把Woker实例插入到HashSet后,并启动Woker中的线程,其中Worker类设计以下:

  • 一、继承了AQS类,能够方便的实现工做线程的停止操做;
  • 二、实现了Runnable接口,能够将自身做为一个任务在工做线程中执行;
  • 三、当前提交的任务firstTask做为参数传入Worker的构造方法;

runWorker方法是线程池的核心:

  • 一、线程启动以后,经过unlock方法释放锁,设置AQS的state为0,表示运行中断;
  • 二、获取第一个任务firstTask,执行任务的run方法,不过在执行任务以前,会进行加锁操做,任务执行完会释放锁;
  • 三、在执行任务的先后,能够根据业务场景自定义beforeExecute和afterExecute方法;
  • 四、firstTask执行完成以后,经过getTask方法从阻塞队列中获取等待的任务,若是队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源;

getTask实现:

  • 一、workQueue.take:若是阻塞队列为空,当前线程会被挂起等待;当队列中有任务加入时,线程被唤醒,take方法返回任务,并执行;
  • 二、workQueue.poll:若是在keepAliveTime时间内,阻塞队列仍是没有任务,则返回null;
    因此,线程池中实现的线程能够一直执行由用户提交的任务。

Future和Callable实现:在实际业务场景中,Future和Callable基本是成对出现的,Callable负责产生结果,Future负责获取结果。

  • 一、Callable接口相似于Runnable,只是Runnable没有返回值。
  • 二、Callable任务除了返回正常结果以外,若是发生异常,该异常也会被返回,即Future能够拿到异步执行任务各类结果;
  • 三、Future.get方法会致使主线程阻塞,直到Callable任务执行完成;

协程

协程(Coroutine)这个词其实有不少叫法,好比有的人喜欢称为纤程(Fiber),或者绿色线程(GreenThread)。其实究其本质,对于协程最直观的解释是线程的线程。虽然读上去有点拗口,但本质上就是这样。

协程的核心在于调度那块由他来负责解决,遇到阻塞操做,马上放弃掉,而且记录当前栈上的数据,阻塞完后马上再找一个线程恢复栈并把阻塞的结果放到这个线程上去跑,这样看上去好像跟写同步代码没有任何差异,这整个流程能够称为coroutine,而跑在由coroutine负责调度的线程称为Fiber。

早期,在JVM上实现协程通常会使用kilim,不过这个工具已经好久不更新了,如今经常使用的工具是Quasar,而本文章会所有基于Quasar来介绍。

相关文章
相关标签/搜索