Java多线程
1.简述java内存模型
Java内存模型即Java Memory Model,简称JMM,其规范了Java虚拟机与计算机内存时如何协同工作的,java内存模型定义了程序中各种变量的访问规则。
- java内存模型定义了程序中各种变量的访问规则。其规定所有变量都存储在主内存,线程均有自己的工作内存。
- 工作内存中保存被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作空间进行,不能直接读写主内存数据。
- 操作完成后,线程的工作内存通过缓存一致性协议将操作完的数据刷回主存。
2.简述as-if-serial
编译器等会对原始的程序进行指令重排序和优化。但不管怎么重排序,其结果和用户原始程序输出预定结果一致。
3.简述happens-before
JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
- 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么JMM允许这种重排序。
4.简述happens-before八大原则
程序次序规则: 一个线程内写在前面的操作先行发生于后面的。
锁定规则: unlock 操作先行发生于后面对同一个锁的 lock 操作。
volatile 规则: 对 volatile 变量的写操作先行发生于后面的读操作。
线程启动规则: 线程的 start 方法先行发生于线程的每个动作。
线程中断规则: 对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
线程终止规则: 线程中所有操作先行发生于对线程的终止检测。
对象终结规则: 对象的初始化先行发生于 finalize 方法。
传递性规则: 如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作C
5.as-if-serial和happens-before的区别
as-if-serial 保证单线程程序的执行结果不变
happens-before 保证正确同步的多线程程序的执行结果不变。
6.简述原子性操作
一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行,这就是原子性操作。
7.简述线程的可见性
可见性指当一个线程修改了共享变量时,其他线程能够立即得知修改。volatile,synchronized,final都能保证可见性。
8.简述java中的volatile关键字的作用
- 保证变量对所有线程的可见性。 当一条线程修改了变量值,新值对于其他线程来说是立即可以得知的。
- 禁止指令重排序优化。 使用 volatile 变量进行写操作,汇编指令带有 lock 前缀,相当于一个内存屏障,编译器不会将后面的指令重排到内存屏障之前。
9.java线程的实现方式
- 继承Thread类,重写run方法,然后创建类的实例调用start方法启动多线程,注意调用run方法不能启动多线程,调run方法将视为普通方法等上一个方法执行完再执行。
- 实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target
- 通过Callable,FutureTask实现。和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大:call()方法可以有返回值,可以声明抛出异常。
- 使用线程池
Java创建线程的三种方式及对比 - 简书 (jianshu.com)
10.简述java线程的状态
线程状态有New, RUNNABLE, BLOCK, WAITING, TIMED_WAITING, THERMINATED
NEW: 新建状态,线程被创建且未启动,此时还未调用 start 方法。
RUNNABLE: 运行状态。其表示线程正在JVM中执行,但是这个执行,不一定真的在跑,也可能在排队等CPU。
BLOCKED: 阻塞状态。线程等待获取锁,锁还没获得。
WAITING: 等待状态。线程内run方法运行完语句Object.wait()/Thread.join()进入该状态。
TIMED_WAITING: 限期等待。在一定时间之后跳出状态。调用Thread.sleep(long) Object.wait(long),Thread.join(long)进入状态。其中这些参数代表等待的时间。
TERMINATED: 结束状态。线程调用完run方法进入该状态。
11.sleep() 和 wait() 的区别
- sleep是线程中的方法,但是wait是Object中的方法。
- sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
- sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
- sleep不需要被唤醒(休眠之后推出阻塞,他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态),但是wait需要(不指定时间需要被别人中断)。
12.简述java中线程通信的方式
使用volatile关键字,用volatile修饰成员变量,告知程序任何对该变量的访问均需要从内存中获取,而对它的改变必须同步刷新回共享内存,保证线程对变量访问的可见性。
使用synchronize关键字,保证多个线程互斥访问临界区,保证线程安全,实现线程间的通信。
使用方法wait和notify/notifyAll。
等等
13.简述线程池以及线程池的各项参数
线程池的核心思想是允许我们重复使用线程而不是频繁的创建新的线程。其好处主要在于:
- 提高线程利用率,避免频繁的创建与销毁线程而造成系统的开销。
- 可限制线程的最大数量,防止无限创建线程,造成的系统负担重以及内存溢出的问题
线程池的主要参数:
corePoolSize:核心线程数
常驻核心线程数,超过该值后如果线程空闲就会被销毁,没有超过保留。
maximumPoolSize:最大线程数
线程池所允许最大同时进行的线程数,超过该值则执行拒绝策略。
keepAliveTime:线程空闲时间
线程空闲时间达到该值后会被销毁,直到只剩下corePoolSize个线程位置,避免浪费内存资源
workQueue:工作队列。
若有新的任务来请求线程,当前执行的线程已经达到常驻核心线程数则将该任务加入工作队列中。如果工作队列满了,线程数小于最大线程数则创建新线程,如果工作队列满了,线程也达到了最大线程数则执行拒绝策略。
threadFactory:线程工厂
用来生产一组相同任务的线程
handler:拒绝策略。
AbortPolicy:丢弃任务并抛出异常
CallerRunsPolicy: 调用者运行策略,线程池中没办法运行,那么就由提交任务的这个线程运行
DiscardOldestPolicy 抛弃队列里等待最久的任务并把当前任务加入队列
DiscardPolicy 表示直接抛弃当前任务但不抛出异常。
14.线程池创建方法以及线程池的种类
- 一类是通过
ThreadPoolExecutor
创建的线程池; - 另一个类是通过
Executors
创建的线程池。
线程池的种类
- Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
- Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
- Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
- Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
- Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
- Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
- ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置,后面会详细讲。
15.简述Executor
Executor框架是在Java5中引入的,可以通过该框架来控制线程的启动,执行,关闭,简化并发编程。把任务提交和执行解耦,要执行任务的人只需要把任务描述清楚提交即可,任务的执行提交人不需要去关心。产生线程池的函数ThreadPoolExecutor也是Executor的具体实现类。
16.简述Executor的继承关系
Executor: 一个接口,其定义了一个接收Runnable对象的方法executor,该方法接收一个Runable实例执行这个任务
ExecutorService: Executor的子类接口,其定义了一个接收Callable对象的方法,返回 Future 对象,同时提供execute方法。
Executors: 实现ExecutorService接口的静态工厂类,提供了一系列工厂方法用于创建线程池。
AbstractExecutorService: 抽象类,提供 ExecutorService 执行方法的默认实现。
ThreadPoolExecutor: 继承AbstractExecutorService,用于创建线程池
17.简述线程池的状态
Running: 能接受新提交的任务,也可以处理阻塞队列的任务。
Shutdown: 不再接受新提交的任务,但可以处理存量任务,线程池处于running时调用shutdown方法,会进入该状态。
Stop: 不接受新任务,不处理存量任务,调用shutdownnow进入该状态。
Tidying: 所有任务已经终止了,worker_count(有效线程数)为0。
Terminated: 线程池彻底终止。在tidying模式下调用terminated方法会进入该状态。
18.简述阻塞队列
阻塞队列是生产者消费者的实现具体组件之一。生产者往阻塞队列添加元素,消费者往阻塞队列移除元素。
当阻塞队列为空时,从队列中获取元素的操作将会被阻塞,当阻塞队列满了,往队列添加元素的操作将会被阻塞。具体实现有:
ArrayBlockingQueue:底层是由数组组成的有界阻塞队列。
LinkedBlockingQueue:底层是由链表组成的有界阻塞队列。
PriorityBlockingQueue:阻塞优先队列。
DelayQueue:创建元素时可以指定多久才能从队列中获取当前元素
SynchronousQueue:不存储元素的阻塞队列,每一个存储必须等待一个取出操作
LinkedTransferQueue:与LinkedBlockingQueue相比多一个transfer方法,即如果当前有消费者正
等待接收元素,可以把生产者传入的元素立刻传输给消费者。
LinkedBlockingDeque:双向阻塞队列。
19.谈一谈ThreadLocal
简介:
ThreadLocal 中填充的的是当前线程的局部变量,该变量对其他线程而言是封闭且隔离的,ThreadLocal为变量在每个线程中创建了一个副本,这样每个线程都可以访问自己内部的副本变量。
作用:在多线程环境下可以防止自己的变量被其他线程篡改。
使用场景:
- 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
- 线程间数据隔离
- 进行事务操作,用于存储线程事务信息。
- 数据库连接,Session会话管理。
使用方法:
ThreadLoacl 有一个静态内部类 ThreadLocalMap,其 Key 是ThreadLocal 对象,值是 Entry 对象,ThreadLocalMap是每个线程私有的。set 给ThreadLocalMap设置值。get 获取ThreadLocalMap。
存在的问题:
对于线程池,由于线程池会重用 Thread 对象,因此与 Thread 绑定的 ThreadLocal 也会被重用,
造成一系列问题。
内存泄漏。由于 ThreadLocal 是弱引用,但 Entry 的 value 是强引用,因此当 ThreadLocal 被垃圾
回收后,value 依旧不会被释放,产生内存泄漏。
解决方案: 用完ThreadLocal之后,记得调用remove方法,清理数据。
20.聊聊你对java并发包下unsafe类的理解
Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大。
21.JAVA中的乐观锁与CAS算法
乐观锁:对于乐观锁,开发者认为数据发送时发生并发冲突的概率不大,所以读操作前不上锁。到了写操作时才会进行判断,数据在此期间是否被其他线程修改。如果发生修改,那就返回写入失败;如果没有被修改,那就执行修改操作,返回修改成功。
CAS(Compare And Swap)算法:
- 该算法认为不同线程对变量的操作时产生竞争的情况比较少。
- 该算法的核心是对当前读取变量值 E 和内存中的变量旧值 V 进行比较。
- 如果相等,就代表其他线程没有对该变量进行修改,就将变量值更新为新值 N。
- 如果不等,就认为在读取值 E 到比较阶段,有其他线程对变量进行过修改,不进行任何操作。
22.ABA问题及解决方法简述
ABA问题: CAS 算法是基于值来做比较的,如果当前有两个线程,一个线程将变量值从 A 改为 B ,再由 B 改回为A ,当前线程开始执行 CAS 算法时,就很容易认为值没有变化,误认为读取数据到执行 CAS 算法的期间,没有线程修改过数据。
解决: AtomicStampReference类在cas的基础上增加了一个标记stamp版本号戳。
23.简述常见的Atomic类
基本数据类型的原子类有:
AtomicInteger 原子更新整形
AtomicLong 原子更新长整型
AtomicBoolean 原子更新布尔类型
Atomic数组类型有:
AtomicIntegerArray 原子更新整形数组里的元素
AtomicLongArray 原子更新长整型数组里的元素
AtomicReferenceArray 原子更新引用类型数组里的元素。
Atomic引用类型有
AtomicReference 原子更新引用类型
AtomicMarkableReference 原子更新带有标记位的引用类型,可以绑定一个 boolean 标记
AtomicStampedReference 原子更新带有版本号的引用类型
FieldUpdater类型:
AtomicIntegerFieldUpdater 原子更新整形字段的更新器
AtomicLongFieldUpdater 原子更新长整形字段的更新器
AtomicReferenceFieldUpdater 原子更新引用类型字段的更新器
24.简述Atomic类基本实现原理
以AtomicIntger 为例:
方法getAndIncrement():以原子方式将当前的值加1
具体实现为:
在 for 死循环中取得 AtomicInteger 里存储的数值
对 AtomicInteger 当前的值加 1
调用 compareAndSet 方法进行原子更新
先检查当前数值是否等于 expect(检查原始值是否被修改过)
如果等于则说明当前值没有被其他线程修改,则将值更新为 next,
如果不是会更新失败返回 false,程序会进入 for 循环重新进行 compareAndSet 操作。
25.简述CountDownLatch(闭锁)
countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,调用countDown方法,计数器的值就减1,当计数器的值为0时,表示所有线程都执行完毕,然后在等待的线程就可以恢复工作了。
只能一次性使用,不能reset。
26.简述CyclicBarrier
CyclicBarrier 主要功能和countDownLatch类似,也是通过一个计数器,使一个线程等待其他线程各自执行完毕后再执行。但是其可以重复使用(reset)。
27.简述Semaphore
Semaphore即信号量。可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。
Semaphore 的构造方法参数接收一个 int 值,设置一个计数器,表示可用的许可数量即最大并发数。使用 acquire 方法获得一个许可证,计数器减一,使用 release 方法归还许可,计数器加一。如果此时计数器值为0,线程进入休眠。
28.简述Exchanger
Exchanger类可用于两个线程之间交换信息。
可简单地将Exchanger对象理解为一个包含两个格子的容器,通过exchanger方法可以向两个格子中填充信息。
线程通过exchange 方法交换数据,第一个线程执行 exchange 方法后会阻塞等待第二个线程执行该方法。
当两个线程都到达同步点时这两个线程就可以交换数据当两个格子中的均被填充时,该对象会自动将两个格子的信息交换,然后返回给线程,从而实现两个线程的信息交换。
29.简述ConcurrentHashMap
JDK7采用分段锁技术。首先将数据分成 Segment 数据段,然后给每一个数据段配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。
get 除读到空值不需要加锁。该方法先经过一次再散列,再用这个散列值通过散列运算定位到Segment,最后通过散列算法定位到元素。
put 须加锁,首先定位到 Segment,然后进行插入操作,第一步判断是否需要对 Segment 里的HashEntry 数组进行扩容,第二步定位添加元素的位置,然后将其放入数组。
JDK8的改进
取消分段锁机制,采用CAS算法进行值的设置,如果CAS失败再使用 synchronized 加锁添加元素
引入红黑树结构,当某个槽内的元素个数超过8且 Node数组 容量大于 64 时,链表转为红黑树。
使用了更加优化的方式统计集合内的元素数量。
30.Synchronized底层实现原理
synchronized在JVM编译后会产生monitorenter 和 monitorexit 这两个字节码指令.
对象都有一个监视器ObjectMonitor,这个监视器内部有很多属性,比如当前等待线程数、计数器、当前所属线程等;其中计数器属性就是用来记录是否已被线程占有,方法执行到monitorenter时,计数器+1,执行到monitorexit时,计数器-1,线程就是通过这个计数器来判断当前锁对象是否已被占用(0为未占用,此时可以获取锁).
31.Synchronized关键字的使用方法
- synchronized 方法:在方法声明中加入 synchronized关键字来声明。
- synchronized 块: 通过 synchronized关键字来声明synchronized 代码块。
32.简述java偏向锁
java偏向锁是java6引入的一项多线程优化,其实是无锁竞争下可重入锁的简单实现。
它会偏向于第一个访问锁的线程,如果上次访问到本次访问中,没有其他的线程使用过这把锁,则偏向锁不需要触发同步机制。
实现原理:
锁对象头中有一个ThreadId字段,如果该字段为空,则第一次获取锁时将自身写入到这个字段中,将锁头内是否偏向锁的状态置为1
下一次获取锁时直接比较自身线程Id与ThreadId是否一致,如果一致则认为该线程已经获取了锁,直接使用。
如果不一致且发生竞争则锁升级,根据现有的ThreadId通知之前进程将ThreadId置空。两个线程采用CAS机制来执行切换。
如果CAS也失败则失败的进入自旋,如果自旋失败则进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己
33.简述轻量级锁
轻量级锁是为了在没有竞争的前提下减少重量级锁出现并导致的性能消耗。
其申请流程为:
如果同步对象没有被锁定,虚拟机将在当前线程的栈帧中建立一个锁记录空间,存储锁对象目前Mark Word 的拷贝。
虚拟机使用 CAS 尝试把对象的 Mark Word 更新为指向锁记录的指针
如果更新成功即代表该线程拥有了锁,锁标志位将转变为 00,表示处于轻量级锁定状态。
如果更新失败就意味着至少存在一条线程与当前线程竞争。虚拟机检查对象的 Mark Word 是否指向当前线程的栈帧
如果指向当前线程的栈帧,说明当前线程已经拥有了锁,直接进入同步块继续执行
如果不是则说明锁对象已经被其他线程抢占。
如果出现两条以上线程争用同一个锁,轻量级锁就不再有效,将膨胀为重量级锁,锁标志状态变为10此时Mark Word 存储的就是指向重量级锁的指针,后面等待锁的线程也必须阻塞。
34.锁优化策略有哪些
自适应自旋锁,锁消除,锁粗化,锁升级等
自旋锁:线程获取锁失败后,不放弃 CPU ,不停的重试获取锁。
自适应自旋锁:自旋次数不再人为设定,通常由前一次在同一个锁上自旋时间以及锁拥有者的状态决定。
锁粗化:扩大加锁范围,避免反复的加锁和解锁。
锁消除:一种更为彻底的优化,在编译时,java编译器对运行上下文进行扫描,去除不可能存在共享资源竞争的锁。
36.简述公平锁和非公平锁
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
- 优点:所有的线程都能得到资源,不会饿死在队列中。
- 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
- 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
- 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
37.简述Lock和ReentrantLock
Lock 是 java并发包的顶层接口。
可重入锁 ReentrantLock 是 Lock 最常见的实现,与 synchronized 一样可重入。ReentrantLock 在默认情况下是非公平的,可以通过构造方法指定公平锁。一旦使用了公平锁,性能会下降。
公平锁 是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
36.简述AQS
AQS(AbstractQuenedSynchronizer)抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。
**其核心思想为:**请求获得共享资源(volatile int state)时,如果共享资源空闲,则将请求线程设置为有效工作线程,并将共享资源设置为锁定状态,如果共享资源被占用,则将该线程放入CLH队列中自旋。CLH队列为虚拟双向队列,即不存在队列实例,只存在节点之间的关系。
AQS的资源共享方式:
独占式:独占,只有一个线程能执行,如ReentrantLock
共享:多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
AQS的实现原理:
AQS采用模板方法模式,使用者继承AbstractQueuedSynchronizer并重写指定方法。自定义同步器在实现时只需要实现共享资源的获取与释放方式即可,线程等待队列的维护AQS已经在底层实现好了。
主要实现的方法:
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
37.synchronized和lock的区别
-
Synchronized是关键字,内置语言实现,即其定义的层次在JVM上,Lock是接口,需要具体实现类,比如ReentrantLock。
-
Synchronized在线程发生异常时JVM会自动释放锁。Lock不会,所以需要在finally语句段中实现释放锁逻辑。
-
Synchronized可重入 不可中断 非公平。Lock不限制都可。
-
Java多线程
1.简述java内存模型
Java内存模型即Java Memory Model,简称JMM,其规范了Java虚拟机与计算机内存时如何协同工作的,java内存模型定义了程序中各种变量的访问规则。
- java内存模型定义了程序中各种变量的访问规则。其规定所有变量都存储在主内存,线程均有自己的工作内存。
- 工作内存中保存被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作空间进行,不能直接读写主内存数据。
- 操作完成后,线程的工作内存通过缓存一致性协议将操作完的数据刷回主存。
2.简述as-if-serial
编译器等会对原始的程序进行指令重排序和优化。但不管怎么重排序,其结果和用户原始程序输出预定结果一致。
3.简述happens-before
JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
- 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么JMM允许这种重排序。
4.简述happens-before八大原则
程序次序规则: 一个线程内写在前面的操作先行发生于后面的。
锁定规则: unlock 操作先行发生于后面对同一个锁的 lock 操作。
volatile 规则: 对 volatile 变量的写操作先行发生于后面的读操作。
线程启动规则: 线程的 start 方法先行发生于线程的每个动作。
线程中断规则: 对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
线程终止规则: 线程中所有操作先行发生于对线程的终止检测。
对象终结规则: 对象的初始化先行发生于 finalize 方法。
传递性规则: 如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作C
5.as-if-serial和happens-before的区别
as-if-serial 保证单线程程序的执行结果不变
happens-before 保证正确同步的多线程程序的执行结果不变。
6.简述原子性操作
一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行,这就是原子性操作。
7.简述线程的可见性
可见性指当一个线程修改了共享变量时,其他线程能够立即得知修改。volatile,synchronized,final都能保证可见性。
8.简述java中的volatile关键字的作用
- 保证变量对所有线程的可见性。 当一条线程修改了变量值,新值对于其他线程来说是立即可以得知的。
- 禁止指令重排序优化。 使用 volatile 变量进行写操作,汇编指令带有 lock 前缀,相当于一个内存屏障,编译器不会将后面的指令重排到内存屏障之前。
9.java线程的实现方式
- 继承Thread类,重写run方法,然后创建类的实例调用start方法启动多线程,注意调用run方法不能启动多线程,调run方法将视为普通方法等上一个方法执行完再执行。
- 实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target
- 通过Callable,FutureTask实现。和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大:call()方法可以有返回值,可以声明抛出异常。
- 使用线程池
Java创建线程的三种方式及对比 - 简书 (jianshu.com)
10.简述java线程的状态
线程状态有New, RUNNABLE, BLOCK, WAITING, TIMED_WAITING, THERMINATED
NEW: 新建状态,线程被创建且未启动,此时还未调用 start 方法。
RUNNABLE: 运行状态。其表示线程正在JVM中执行,但是这个执行,不一定真的在跑,也可能在排队等CPU。
BLOCKED: 阻塞状态。线程等待获取锁,锁还没获得。
WAITING: 等待状态。线程内run方法运行完语句Object.wait()/Thread.join()进入该状态。
TIMED_WAITING: 限期等待。在一定时间之后跳出状态。调用Thread.sleep(long) Object.wait(long),Thread.join(long)进入状态。其中这些参数代表等待的时间。
TERMINATED: 结束状态。线程调用完run方法进入该状态。
11.sleep() 和 wait() 的区别
- sleep是线程中的方法,但是wait是Object中的方法。
- sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
- sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
- sleep不需要被唤醒(休眠之后推出阻塞,他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态),但是wait需要(不指定时间需要被别人中断)。
12.简述java中线程通信的方式
使用volatile关键字,用volatile修饰成员变量,告知程序任何对该变量的访问均需要从内存中获取,而对它的改变必须同步刷新回共享内存,保证线程对变量访问的可见性。
使用synchronize关键字,保证多个线程互斥访问临界区,保证线程安全,实现线程间的通信。
使用方法wait和notify/notifyAll。
等等
13.简述线程池以及线程池的各项参数
线程池的核心思想是允许我们重复使用线程而不是频繁的创建新的线程。其好处主要在于:
- 提高线程利用率,避免频繁的创建与销毁线程而造成系统的开销。
- 可限制线程的最大数量,防止无限创建线程,造成的系统负担重以及内存溢出的问题
线程池的主要参数:
corePoolSize:核心线程数
常驻核心线程数,超过该值后如果线程空闲就会被销毁,没有超过保留。
maximumPoolSize:最大线程数
线程池所允许最大同时进行的线程数,超过该值则执行拒绝策略。
keepAliveTime:线程空闲时间
线程空闲时间达到该值后会被销毁,直到只剩下corePoolSize个线程位置,避免浪费内存资源
workQueue:工作队列。
若有新的任务来请求线程,当前执行的线程已经达到常驻核心线程数则将该任务加入工作队列中。如果工作队列满了,线程数小于最大线程数则创建新线程,如果工作队列满了,线程也达到了最大线程数则执行拒绝策略。
threadFactory:线程工厂
用来生产一组相同任务的线程
handler:拒绝策略。
AbortPolicy:丢弃任务并抛出异常
CallerRunsPolicy: 调用者运行策略,线程池中没办法运行,那么就由提交任务的这个线程运行
DiscardOldestPolicy 抛弃队列里等待最久的任务并把当前任务加入队列
DiscardPolicy 表示直接抛弃当前任务但不抛出异常。
14.线程池创建方法以及线程池的种类
- 一类是通过
ThreadPoolExecutor
创建的线程池; - 另一个类是通过
Executors
创建的线程池。
线程池的种类
- Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
- Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
- Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
- Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
- Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
- Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
- ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置,后面会详细讲。
15.简述Executor
Executor框架是在Java5中引入的,可以通过该框架来控制线程的启动,执行,关闭,简化并发编程。把任务提交和执行解耦,要执行任务的人只需要把任务描述清楚提交即可,任务的执行提交人不需要去关心。产生线程池的函数ThreadPoolExecutor也是Executor的具体实现类。
16.简述Executor的继承关系
Executor: 一个接口,其定义了一个接收Runnable对象的方法executor,该方法接收一个Runable实例执行这个任务
ExecutorService: Executor的子类接口,其定义了一个接收Callable对象的方法,返回 Future 对象,同时提供execute方法。
Executors: 实现ExecutorService接口的静态工厂类,提供了一系列工厂方法用于创建线程池。
AbstractExecutorService: 抽象类,提供 ExecutorService 执行方法的默认实现。
ThreadPoolExecutor: 继承AbstractExecutorService,用于创建线程池
17.简述线程池的状态
Running: 能接受新提交的任务,也可以处理阻塞队列的任务。
Shutdown: 不再接受新提交的任务,但可以处理存量任务,线程池处于running时调用shutdown方法,会进入该状态。
Stop: 不接受新任务,不处理存量任务,调用shutdownnow进入该状态。
Tidying: 所有任务已经终止了,worker_count(有效线程数)为0。
Terminated: 线程池彻底终止。在tidying模式下调用terminated方法会进入该状态。
18.简述阻塞队列
阻塞队列是生产者消费者的实现具体组件之一。生产者往阻塞队列添加元素,消费者往阻塞队列移除元素。
当阻塞队列为空时,从队列中获取元素的操作将会被阻塞,当阻塞队列满了,往队列添加元素的操作将会被阻塞。具体实现有:
ArrayBlockingQueue:底层是由数组组成的有界阻塞队列。
LinkedBlockingQueue:底层是由链表组成的有界阻塞队列。
PriorityBlockingQueue:阻塞优先队列。
DelayQueue:创建元素时可以指定多久才能从队列中获取当前元素
SynchronousQueue:不存储元素的阻塞队列,每一个存储必须等待一个取出操作
LinkedTransferQueue:与LinkedBlockingQueue相比多一个transfer方法,即如果当前有消费者正
等待接收元素,可以把生产者传入的元素立刻传输给消费者。
LinkedBlockingDeque:双向阻塞队列。
19.谈一谈ThreadLocal
简介:
ThreadLocal 中填充的的是当前线程的局部变量,该变量对其他线程而言是封闭且隔离的,ThreadLocal为变量在每个线程中创建了一个副本,这样每个线程都可以访问自己内部的副本变量。
作用:在多线程环境下可以防止自己的变量被其他线程篡改。
使用场景:
- 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
- 线程间数据隔离
- 进行事务操作,用于存储线程事务信息。
- 数据库连接,Session会话管理。
使用方法:
ThreadLoacl 有一个静态内部类 ThreadLocalMap,其 Key 是ThreadLocal 对象,值是 Entry 对象,ThreadLocalMap是每个线程私有的。set 给ThreadLocalMap设置值。get 获取ThreadLocalMap。
存在的问题:
对于线程池,由于线程池会重用 Thread 对象,因此与 Thread 绑定的 ThreadLocal 也会被重用,
造成一系列问题。
内存泄漏。由于 ThreadLocal 是弱引用,但 Entry 的 value 是强引用,因此当 ThreadLocal 被垃圾
回收后,value 依旧不会被释放,产生内存泄漏。
解决方案: 用完ThreadLocal之后,记得调用remove方法,清理数据。
20.聊聊你对java并发包下unsafe类的理解
Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大。
21.JAVA中的乐观锁与CAS算法
乐观锁:对于乐观锁,开发者认为数据发送时发生并发冲突的概率不大,所以读操作前不上锁。到了写操作时才会进行判断,数据在此期间是否被其他线程修改。如果发生修改,那就返回写入失败;如果没有被修改,那就执行修改操作,返回修改成功。
CAS(Compare And Swap)算法:
- 该算法认为不同线程对变量的操作时产生竞争的情况比较少。
- 该算法的核心是对当前读取变量值 E 和内存中的变量旧值 V 进行比较。
- 如果相等,就代表其他线程没有对该变量进行修改,就将变量值更新为新值 N。
- 如果不等,就认为在读取值 E 到比较阶段,有其他线程对变量进行过修改,不进行任何操作。
22.ABA问题及解决方法简述
ABA问题: CAS 算法是基于值来做比较的,如果当前有两个线程,一个线程将变量值从 A 改为 B ,再由 B 改回为A ,当前线程开始执行 CAS 算法时,就很容易认为值没有变化,误认为读取数据到执行 CAS 算法的期间,没有线程修改过数据。
解决: AtomicStampReference类在cas的基础上增加了一个标记stamp版本号戳。
23.简述常见的Atomic类
基本数据类型的原子类有:
AtomicInteger 原子更新整形
AtomicLong 原子更新长整型
AtomicBoolean 原子更新布尔类型
Atomic数组类型有:
AtomicIntegerArray 原子更新整形数组里的元素
AtomicLongArray 原子更新长整型数组里的元素
AtomicReferenceArray 原子更新引用类型数组里的元素。
Atomic引用类型有
AtomicReference 原子更新引用类型
AtomicMarkableReference 原子更新带有标记位的引用类型,可以绑定一个 boolean 标记
AtomicStampedReference 原子更新带有版本号的引用类型
FieldUpdater类型:
AtomicIntegerFieldUpdater 原子更新整形字段的更新器
AtomicLongFieldUpdater 原子更新长整形字段的更新器
AtomicReferenceFieldUpdater 原子更新引用类型字段的更新器
24.简述Atomic类基本实现原理
以AtomicIntger 为例:
方法getAndIncrement():以原子方式将当前的值加1
具体实现为:
在 for 死循环中取得 AtomicInteger 里存储的数值
对 AtomicInteger 当前的值加 1
调用 compareAndSet 方法进行原子更新
先检查当前数值是否等于 expect(检查原始值是否被修改过)
如果等于则说明当前值没有被其他线程修改,则将值更新为 next,
如果不是会更新失败返回 false,程序会进入 for 循环重新进行 compareAndSet 操作。
25.简述CountDownLatch(闭锁)
countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,调用countDown方法,计数器的值就减1,当计数器的值为0时,表示所有线程都执行完毕,然后在等待的线程就可以恢复工作了。
只能一次性使用,不能reset。
26.简述CyclicBarrier
CyclicBarrier 主要功能和countDownLatch类似,也是通过一个计数器,使一个线程等待其他线程各自执行完毕后再执行。但是其可以重复使用(reset)。
27.简述Semaphore
Semaphore即信号量。可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。
Semaphore 的构造方法参数接收一个 int 值,设置一个计数器,表示可用的许可数量即最大并发数。使用 acquire 方法获得一个许可证,计数器减一,使用 release 方法归还许可,计数器加一。如果此时计数器值为0,线程进入休眠。
28.简述Exchanger
Exchanger类可用于两个线程之间交换信息。
可简单地将Exchanger对象理解为一个包含两个格子的容器,通过exchanger方法可以向两个格子中填充信息。
线程通过exchange 方法交换数据,第一个线程执行 exchange 方法后会阻塞等待第二个线程执行该方法。
当两个线程都到达同步点时这两个线程就可以交换数据当两个格子中的均被填充时,该对象会自动将两个格子的信息交换,然后返回给线程,从而实现两个线程的信息交换。
29.简述ConcurrentHashMap
JDK7采用分段锁技术。首先将数据分成 Segment 数据段,然后给每一个数据段配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。
get 除读到空值不需要加锁。该方法先经过一次再散列,再用这个散列值通过散列运算定位到Segment,最后通过散列算法定位到元素。
put 须加锁,首先定位到 Segment,然后进行插入操作,第一步判断是否需要对 Segment 里的HashEntry 数组进行扩容,第二步定位添加元素的位置,然后将其放入数组。
JDK8的改进
取消分段锁机制,采用CAS算法进行值的设置,如果CAS失败再使用 synchronized 加锁添加元素
引入红黑树结构,当某个槽内的元素个数超过8且 Node数组 容量大于 64 时,链表转为红黑树。
使用了更加优化的方式统计集合内的元素数量。
30.Synchronized底层实现原理
synchronized在JVM编译后会产生monitorenter 和 monitorexit 这两个字节码指令.
对象都有一个监视器ObjectMonitor,这个监视器内部有很多属性,比如当前等待线程数、计数器、当前所属线程等;其中计数器属性就是用来记录是否已被线程占有,方法执行到monitorenter时,计数器+1,执行到monitorexit时,计数器-1,线程就是通过这个计数器来判断当前锁对象是否已被占用(0为未占用,此时可以获取锁).
31.Synchronized关键字的使用方法
- synchronized 方法:在方法声明中加入 synchronized关键字来声明。
- **synchronized 块:**通过 synchronized关键字来声明synchronized 代码块。
32.简述java偏向锁
java偏向锁是java6引入的一项多线程优化,其实是无锁竞争下可重入锁的简单实现。
它会偏向于第一个访问锁的线程,如果上次访问到本次访问中,没有其他的线程使用过这把锁,则偏向锁不需要触发同步机制。
实现原理:
锁对象头中有一个ThreadId字段,如果该字段为空,则第一次获取锁时将自身写入到这个字段中,将锁头内是否偏向锁的状态置为1
下一次获取锁时直接比较自身线程Id与ThreadId是否一致,如果一致则认为该线程已经获取了锁,直接使用。
如果不一致且发生竞争则锁升级,根据现有的ThreadId通知之前进程将ThreadId置空。两个线程采用CAS机制来执行切换。
如果CAS也失败则失败的进入自旋,如果自旋失败则进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己
33.简述轻量级锁
轻量级锁是为了在没有竞争的前提下减少重量级锁出现并导致的性能消耗。
其申请流程为:
如果同步对象没有被锁定,虚拟机将在当前线程的栈帧中建立一个锁记录空间,存储锁对象目前Mark Word 的拷贝。
虚拟机使用 CAS 尝试把对象的 Mark Word 更新为指向锁记录的指针
如果更新成功即代表该线程拥有了锁,锁标志位将转变为 00,表示处于轻量级锁定状态。
如果更新失败就意味着至少存在一条线程与当前线程竞争。虚拟机检查对象的 Mark Word 是否指向当前线程的栈帧
如果指向当前线程的栈帧,说明当前线程已经拥有了锁,直接进入同步块继续执行
如果不是则说明锁对象已经被其他线程抢占。
如果出现两条以上线程争用同一个锁,轻量级锁就不再有效,将膨胀为重量级锁,锁标志状态变为10此时Mark Word 存储的就是指向重量级锁的指针,后面等待锁的线程也必须阻塞。
34.锁优化策略有哪些
自适应自旋锁,锁消除,锁粗化,锁升级等
自旋锁:线程获取锁失败后,不放弃 CPU ,不停的重试获取锁。
自适应自旋锁:自旋次数不再人为设定,通常由前一次在同一个锁上自旋时间以及锁拥有者的状态决定。
锁粗化:扩大加锁范围,避免反复的加锁和解锁。
锁消除:一种更为彻底的优化,在编译时,java编译器对运行上下文进行扫描,去除不可能存在共享资源竞争的锁。
36.简述公平锁和非公平锁
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
- 优点:所有的线程都能得到资源,不会饿死在队列中。
- 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
- 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
- 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
37.简述Lock和ReentrantLock
Lock 是 java并发包的顶层接口。
可重入锁 ReentrantLock 是 Lock 最常见的实现,与 synchronized 一样可重入。ReentrantLock 在默认情况下是非公平的,可以通过构造方法指定公平锁。一旦使用了公平锁,性能会下降。
公平锁 是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
36.简述AQS
AQS(AbstractQuenedSynchronizer)抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。
其核心思想为: 请求获得共享资源(volatile int state)时,如果共享资源空闲,则将请求线程设置为有效工作线程,并将共享资源设置为锁定状态,如果共享资源被占用,则将该线程放入CLH队列中自旋。CLH队列为虚拟双向队列,即不存在队列实例,只存在节点之间的关系。
AQS的资源共享方式:
独占式:独占,只有一个线程能执行,如ReentrantLock
共享:多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
AQS的实现原理:
AQS采用模板方法模式,使用者继承AbstractQueuedSynchronizer并重写指定方法。自定义同步器在实现时只需要实现共享资源的获取与释放方式即可,线程等待队列的维护AQS已经在底层实现好了。
主要实现的方法:
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。37.synchronized和lock的区别
- Synchronized是关键字,内置语言实现,即其定义的层次在JVM上,Lock是接口,需要具体实现类,比如ReentrantLock。
- Synchronized在线程发生异常时JVM会自动释放锁。Lock不会,所以需要在finally语句段中实现释放锁逻辑。
- lock的使用更加灵活,常常可以提高效率,例如lock可以使用读锁。