面试题:并行和并发有什么区别?
现在都是多核CPU,在多核CPU下
并发是同一时间应对多件事情的能力,多个线程轮流使用一个或多个CPU
并行是同一时间动手做多件事情的能力,4核CPU同时执行4个线程
面试题:创建线程的方式有哪些?
继承Thread类
实现runnable接口
实现Callable接口
线程池创建线程(项目中使用方式)
面试题:runnable 和 callable 有什么区别?
Runnable 接口run方法没有返回值
Callable接口call方法有返回值,需要FutureTask获取结果
Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛
面试题:线程包括哪些状态?
新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待( WAITING )、时间等待(TIMED_WALTING)、终止(TERMINATED)
面试题:线程状态之间是如何变化的?
1、创建线程对象是新建状态
2、调用了start()方法转变为可执行状态
3、线程获取到了CPU的执行权,执行结束是终止状态
在可执行状态的过程中,如果没有获取CPU的执行权,可能会切换其他状态
如果没有获取锁(synchronized或lock)进入阻塞状态,获得锁再切换为可执行状态
如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可切换为可执行状态
如果线程调用了sleep(50)方法,进入计时等待状态,到时间后可切换为可执行状态
面试题:新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?可以使用线程中的join方法解决。t.join(),阻塞调用此方法的线程进入timed_waiting直到线程t执行完成后,此线程再继续执行
面试题:notify()和 notifyAll()有什么区别?notifyAll:唤醒所有wait的线程;notify:只随机唤醒一个 wait 线程
面试题:在java中wait和sleep方法的不同?
共同点:wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态。
不同点
1.方法归属不同,sleep(long) 是 Thread 的静态方法。而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有
2.醒来时机不同,执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去,它们都可以被打断唤醒
3. 锁特性不同(重点)
wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)
而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)
面试题:synchronized关键字的底层原理
Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】
它的底层由monitor实现的,monitor是jvm级别的对象( C++实现),线程获得锁需要使用对象(锁)关联monitor一个对象锁是通过对象头MarkWord 里面(ptr_to_heavyweight_monitor)记录了monitor的地址。
ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针,占30位
在monitor内部有三个属性,分别是owner、entrylist、waitset
其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程
面试题:Monitor实现的锁属于重量级锁,你了解过锁升级吗?Monitor 被翻译为监视器,是由jvm提供,c++语言实现
Owner:存储当前获取锁的线程的,只能有一个线程可以获取
EntryList:关联没有抢到锁的线程,处于Blocked状态的线程
WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程
Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式;一但锁发生竞争都会升级为重量级锁! | ||
重量级锁 | 一个线程持有锁 | 底层使用Monitor实现,里面涉及到了用户态和内核态的切换、进程上下文切换,成本较高。 |
monitor内部有三个属性 | ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针,占30位 在monitor内部有三个属性,分别是owner、entrylist、waitset 其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程 | |
轻量级锁 | 不同线程交替持有锁 | 线程加锁的时间是错开的,没有竞争,可以使用轻量级锁。轻量级锁修改了对象头锁标志(每次修改都是CAS操作)相对重量级锁性能提升很多。 |
加锁过程 | 1.在线程栈中创建一个Lock Record,将其Object reference字段指向锁对象。 | |
2.通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。 | ||
3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。 | ||
4.如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。 | ||
解锁过程 | 1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。 | |
2.如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。 | ||
3.如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁。 | ||
偏向锁 | 一段很长时间内都只被一个线程使用锁 | 在第一次获得锁时,会有一个CAS操作,之后该线程再获得锁只需要判断markword中是否是自己线程ID 即可。 |
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。 | ||
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。 |
JMM(Java Memory Model)Java内存模型
JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存);线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存。
关键字 volatile (volatile 关键字虽然拥有多个线程之间的可见性,但是却不具备同步性--不具备原子性),可以算是个轻量级的synchronized, 性能要比synchronized 强很多,不会造成阻塞(在很多的开源架构里,比如netty 的底层代码就大量的使用volatile。)一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
① 保证线程间的可见性
② 禁止进行指令重排序
volatile 用于只针对于多个线程可见的变量操作,并不能代替synchronized 的同步操作。
volatile 关键字只具有可见性,没有原子性,要实现原子性建议使用atomic类的系列对象。
atomic类只能保证本身的方法原子性,并不能保证多次操作的原子性。
AtomicInteger 原子整形类
private static AtomicInteger count = new AtomicInteger(0);
count++;
count.incrementAndGet();//将加后的结果返回
count.addAndGet(10); //在相加的时候指定步长
什么是AQS?(AbstractQueuedSynchronizer) | |||||
AbstractQueuedSynchronizer是多线程中的队列同步器。是一种锁机制,它是做为一个基础框架使用的,像ReentrantLock(阻塞式锁)、CountDownLatch(倒计时锁)、Semaphore(信号量)都是基于AQS实现的 | |||||
AQS内部维护了一个先进先出的双向队列,队列中存储的排队的线程 在AQS内部还有一个属性state,这个state就相当于是一个资源,默认是0(无锁状态),如果队列中的有一个线程修改成功了state为1,则当前线程就相等于获取了资源 在对state修改的时候使用的cas操作,保证多个线程修改的情况下原子性。 | |||||
![]() | |||||
AQS与synchronized的区别? | |||||
主要区别 | synchronized是关键字,C++语言实现;悲观锁(自动释放锁);锁竞争激烈都是重量级锁 | ||||
AQS由 java语言实现;悲观锁(手动开启和关闭);锁竞争激烈的情况下提供了很多种解决方案 | |||||
ReentrantLock的实现原理 | |||||
ReentrantLock支持重入锁,同一个线程调用 lock 方法获取了锁之后,再次调用 lock,是不会再阻塞;ReentrantLock主要利用CAS+AQS队列来实现。支持公平锁和非公平锁,在提供的构造器的中无参默认是非公平锁,也可以传参设置为公平锁。 | |||||
![]() | |||||
线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功 | |||||
假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部 当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁。 | |||||
CountDownLatch | |||||
概念 | CountDownLatch本身是基于共享锁实现的,是为了解决某些操作只能在一组操作全部执行完成后才能执行的情景;CountDown是倒数计数,所以CountDownLatch的用法通常是设定一个大于0的值,该值即代表需要等待的总任务数,每完成一个任务后,将总任务数减一,直到最后该值为0,说明所有等待的任务都执行完了,门闩此时就被打开,后面的任务可以继续执行。 | ||||
CountDownLatch主要是通过AQS的共享锁机制实现的,所以其必然有一个内部类sync继承AQS并实现tryAcquireShared(获取共享锁)和tryReleaseShared(释放共享锁)方法 | |||||
共享模式和独占模式的区别是什么? | 当调用CountDownLatch.countDown()方法时,只是CAS加失败重试的机制,这里不存在对资源的获取操作,所以不会体现共享模式; | ||||
当调用CountDownLatch.await方法时,如果state不为0,那么线程将阻塞,其实这里和独占的ReentrantLock认为state不为0类似,也并体现不出共享性; | |||||
但是当state为0时,CountDownLatch中所有在await方法上阻塞的线程都将获取成功,而ReentrantLock中只有一个线程可以在lock上获取成功,这里变体现了独占和共享模式的区别。 | |||||
总结一下如下:↓ | |||||
独占模式 | 当state为0时,下一个获取成功的线程将独占state状态,其他在等待这个条件(state=0)的线程无法获取成功。 | ||||
共享模式 | 当state为0时,所有在等待这个条件(state=0)的线程都将获取成功。 | ||||
Semaphore 信号量,可以限制执行的线程数量 | |||||
使用场景 | 通常用于那些资源有明确访问数量限制的场景,常用于限流 | ||||
使用步骤 | Semaphore sp=new Semaphore (3) | 创建对象; 给定一个容量3;初始化了3个信号量,最多同时可以有3个线程去执行,其他的线程想要执行,必须要等信号量中的线程释放才能执行! | |||
sp.acquire() | 请求一个信号量,这时候的信号量个数减一 | ||||
一旦没有可用的信号量,即信号量个数变为负数时,再次请求的时候就会阻塞,直到其他线程释放了信号量 | |||||
sp.release() | 释放一个信号量,此时信号量个数加一 | ||||
死锁:一个线程需要同时获取多把锁,这时就容易发生死锁,线程t1持有A的锁等待获取B锁,线程t2持有B的锁等待获取A的锁。当程序出现了死锁现象,我们可以使用jdk自带的工具jstack -l pid |
synchronized和Lock有什么区别 ? | |
语法层面 | synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现 |
Lock 是接口,源码由 jdk 提供,用 java 语言实现 | |
使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁 | |
功能层面 | 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能 |
Lock 提供了许多 synchronized 不具备的功能,例如公平锁、可打断、可超时、多条件变量 | |
Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock(读写锁) | |
性能层面 | 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖 |
在竞争激烈时,Lock 的实现通常会提供更好的性能。 |
线程池的核心参数ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,workQueue,threadFactory,handler) | |||
参数1 | corePoolSize | int | 核心线程池大小 |
参数2 | maximumPoolSize | int | 最大线程数目 = (核心线程+救急线程的最大数目) |
参数3 | keepAliveTime | long | 生存时间 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放 |
参数4 | unit | TimeUnit | 救急线程的生存时间单位,如秒、毫秒等 |
参数5 | workQueue | BlockingQueue<Runnable> | 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务 |
参数6 | threadFactory | ThreadFactory | 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等 |
参数7 | handler | RejectedExecutionHandler | 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略 |
自定义的线程池 | |||
1.在使用有界队列时 | |||
若有新的任务需要执行,如果线程池实际线程数小于corePoolSize 则优先创建线程,若大于corePoolSize 则会将任务加入队列 | |||
若队列已满,则在总线程数不大于maximumPoolSize的前提下创建新的线程,若线程数大于maximumPoolSize,则执行拒绝策略或其他自定义方式。 | |||
SynchronousQueue:直接提交队列 | SynchronousQueue没有容量,所以实际上提交的任务不会被添加到任务队列,总是将新任务提交给线程执行, | ||
如果没有空闲的线程,则尝试创建新的线程,如果线程数量已经达到最大值(maximumPoolSize),则执行拒绝策略。 | |||
2.无界的任务队列时,LinkedBlockingQueue | |||
LinkedBlockingQueue与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在入队失败的情况。当有新的任务到来,系统的线程数小于corePoolSize时,则新建线程执行任务。 | |||
当达到corePoolSize后,就不会继续增加。若后续仍有新的任务加入,而没有空闲的线程资源,则任务直接进入队列等待。若任务创建和处理的速度差异很大,无界队列会保持快速增长,直到系统内存资源耗尽。 | |||
3.JDK 拒绝策略 :线程池默认的处理策略是AbortPolicy! | |||
AbortPolicy | 直接抛出RejectedExecutionException异常,系统正常工作 | ||
CallerRunsPolicy | 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。 | ||
DiscardOldestPolicy | 丢弃最老的一个请求,尝试再次提交当前任务。 | ||
DiscardPolicy | 丢弃无法处理的任务,不给予任何处理 | ||
如果需要自定义拒绝策略可以实现RejectedExecutionHandler接口 | |||
线程池中有哪些常见的阻塞队列 | |||
ArrayBlockingQueue | 基于数组结构的有界阻塞队列,FIFO。 | ||
强制有界;底层是数组;提前初始化 Node 数组;Node需要是提前创建好的;一把锁 | |||
LinkedBlockingQueue | 基于链表结构的有界阻塞队列,FIFO。 | ||
默认无界,支持有界;底层是单向链表;是懒惰的,创建节点的时候添加数据;入队会生成新 Node;两把锁(头尾) | |||
DelayedWorkQueue | 是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的。 | ||
SynchronousQueue | 不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。 | ||
如何确定核心线程数 | |||
IO密集型任务 | 一般来说:文件读写、DB读写、网络请求等 核心线程数大小设置为2N+1 (N为cpu的核数) | ||
CPU密集型任务 | 一般来说:计算型代码、Bitmap转换、Gson转换等 核心线程数大小设置为N+1 | ||
线程池的种类有哪些 | |||
newFixedThreadPool | 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待 | ||
核心线程数与最大线程数一样,没有救急线程 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE 适用于任务量已知,相对耗时的任务 | |||
newSingleThreadExecutor | 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任 务,保证所有任务按照指定顺序(FIFO)执行 | ||
核心线程数和最大线程数都是1 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE 适用于按照顺序执行的任务 | |||
newCachedThreadPool | 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程 | ||
核心线程数为0 最大线程数是Integer.MAX_VALUE 阻塞队列为SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。 适合任务数比较密集,但每个任务执行时间较短的情况 | |||
newScheduledThreadPool | 可以执行延迟任务的线程池,支持定时及周期性任务执行 | ||