Java

大约 11 分钟

Java

Java Documentationopen in new window

OpenJDKopen in new window

深入理解Java虚拟机open in new window

HollisChuang's Blogopen in new window

1. 如何保证线程安全

线程安全:当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的

1.1 互斥同步(阻塞同步)

  1. synchronized

  2. Lock: 例如ReentrantLock

synchronizedReentrantLock的区别:

  • synchronized是在Java语法层面的同步,ReentrantLock是Java语言层面,释放锁要确保在finally块中释放锁

  • 性能上差不太多,但是两者都可满足需要时优先使用synchronized

  • ReentrantLock相比synchronized相比增加了一些高级功能:

等待可中断: 当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情

公平锁: 多个线程在等待同一个锁时,必须按照申请锁的时间顺序来一次获得锁,ReentrantLock使用公平锁将会导致性能下降

锁绑定多个条件: 一个ReentrantLock对象可以同时绑定多个Condition对象,需要多次调用newCondition()synchronized需要额外添加锁

1.2 无锁编程(非阻塞同步)

不管风险,先进行操作,如果没有其他线程争用共享数据,那操作就直接成功;如果共享的数据被争用产生了冲突,最常用的补偿措施是不断地重试,直到出现没有竞争的共享数据为止

基于CAS的操作:

  • 使用Unsafe.compareAndSet方法,
  • 基于CASj.u.c包中的原子类(AtomicInteger等)
  • 使用j.u.c中的线程安全的并发集合,如ConcurrentHashMapConcurrentSkipListMapCopyOnWriteArrayList

CAS操作的ABA问题

如果一个变量V初次读取的时候是A值,其他线程把它修改为B,又修改回为A,那CAS操作就会误认为它从来没有修改过

如何解决: 使用版本或者时间戳,例如AtomicStampedReference,但是大部分情况下ABA问题不会影响程序并发的正确性,如果需要解决,传统的互斥同步可能会比原子类更高效

1.3 无同步方案

同步与线程安全两者没有必然的联系

线程本地存储: 通过ThreadLocal实现

每一个线程的Thread对象中都有一个ThreadLocalMap对象,这个对象存储了一组以ThreadLocal.threadLocalHashCode为键,以本地线程变量为值的K-V键值对,ThreadLocal就是当前线程的ThreadLocalMap的访问入口,每一个ThreadLocal对象都包含一个唯一的threadLocalHashCode值,使用这个值就可以在线程K-V值对中找回对应的本地线程变量

2. Unsafe如果获取,能做哪些操作

https://spectred.github.io/java/magic/unsafe.htmlopen in new window

可以通过反射来获取Unsafe实例,可以实例化一个类、修改私有属性值、CAS操作、使用堆外内存和锁的park/unpark操作

3. lambda是如何实现的

Lambda 底层实现分析open in new window

Java Lambdas : How it works in JVM & is it OOP?open in new window

Java 8 Lambdas - A Peek Under the Hoodopen in new window

lambda引导方法动态生成一个匿名类字节码

4. volatile 关键字

再有人问你Java内存模型是什么,就把这篇文章发给他。open in new window

深入理解Java中的volatile关键字open in new window

再有人问你volatile是什么,把这篇文章也发给他。open in new window

4.1 先从Java内存模型(JMMJava Memory Model)说起

Java内存模型规定所有的变量都存储在主内存,每条线程有自己的工作内存。

线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。

不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成

4.2 volatile有哪些作用

当一个变量被定义成volatile之后会具备可见性有序性两项特性

  • 可见性

    当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。但是不能保证原子性,不是线程安全的

    synchronizedfinal关键字也保证了可见性

    • synchronized: 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中
    • final: 被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有吧this的引用传递出去,那么其他线程就可以看见final字段的值
  • 有序性 (禁止指令重排序优化)

    有序性即程序执行的顺序按照代码的先后顺序执行

    如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的

    synchronized也可以保证有序性: 通过 一个变量在同一个时刻只允许一条线程对其进行lock操作规则获得

4.3 volatile如何实现可见性和有序性的(实现的原理是什么)?

可见性: 被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新

有序性: 禁止指令重排序优化,内存屏障

4.4 什么是Happens-Before(先行发生)原则

先行发生是Java内存模型中定义的两项操作之间的偏序关系

  • volatile变量规则: 被其修饰的变量的写操作先发生于读操作
  • 程序次序规则: 在一个线程内,按照控制流顺序,前面的操作先发生在书写在后边的操作
  • 线程启动规则、线程终止规则、线程中断规则、对象终结规则、传递性

5. 锁优化有哪些技术?(自旋,锁消除,锁膨胀)

5.1 自旋锁与自适应自旋

自旋: 如果有两个或两个以上的线程同时执行,让后面请求锁的线程"稍等一会",但不放弃处理器的执行时间,看看持有锁的线程是否很快会释放锁。为了让线程等待,需要让线程执行一个忙循环(自旋)

如果锁被占用的时间很长,自旋的线程只会白白消耗处理器资源,带来性能的浪费,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式挂起线程。自旋次数默认是10次,通过-XX:PreBlockSpin来指定

自适应自旋: 自旋的时间不固定,而是由上一次在同一个锁上的自旋时间和锁的拥有者状态来决定的

如果上一次很快获得锁,那么再次获锁时可以等上很多次;如果上一次很少成功获锁,那么再次获锁时有可能直接忽略掉自旋,避免处理器资源浪费

5.2 锁消除

锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除

锁消除的主要判断依据来源于逃逸分析的数据支持,如果判断代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把他们当做是栈上的数据进行对待,认为是线程私有的,同步加锁也就无需再进行

5.3 锁膨胀(锁升级)

锁膨胀是通过对象头中的MarkWord的锁标志位实现的

当使用synchronized时,(如果没有使用,处于无锁状态),

  • 偏向锁(01): 锁会偏向第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要进行同步,

  • 轻量锁(00): 偏向锁时被另一个线程访问,升级为轻量锁,此线程会自旋获锁,不会阻塞

  • 重量锁(10): 轻量锁时,自旋的线程自旋一定次数后还没有获锁,进入阻塞升级为重量锁,阻塞其他线程,性能降低

6. Java中线程有哪些状态,各个状态间是如何转换的

java.lang.Thread.State中定义了6种线程状态,在任意一个时间点,一个线程只能有且只有其中的一种状态,并且可以通过特定的方法在不同状态之间转换

  • NEW 新建

    创建后尚未启动的线程处于这种状态

  • RUNNABLE 运行

    Thread#start()包括操作系统线程状态中的RunningReady,处于此状态的线程有可能正在执行,也有可能正在等待着操作系统为它分配执行时间

  • WAITING 无限期等待

    处于这中状态的线程不会被分配处理器执行时间,他们要等待被其他线程显示唤醒,Object::wait(),Thread.join(),LockSupport::park()

  • TIMED_WAITING 限期等待

    处于这种状态的线程也不会被分配处理器执行时间,不过无需等待被其他线程显式唤醒,在一定时间之后他们会由系统自动唤醒

Thread.sleep, Object.wait with timeout, Thread.join with timeout,LockSupport.parkNanos, LockSupport.parkUntil

  • BLOCKED 阻塞

    线程被阻塞,synchronized

  • TERMINATED 结束

    线程已结束执行

7. 线程池的参数,工作流程,有哪些拒绝策略,如何回收线程

java.util.concurrent.ThreadPoolExecutor

  • corePoolSize核心线程数,

  • maximumPoolSize最大线程数,

  • keepAliveTime+unit: 如果一个线程处于闲置(idle)状态并且当前的线程数量大于核心线程数,在指定时间后这个线程会被销毁

  • workQueue 在执行任务之前保存任务的队列

  • threadFactory 创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等

  • rejectedExecutionHandler 拒绝策略,有4种: 抛出异常,直接丢弃(不抛异常),丢弃任务队列中等待时间最长的,谁提交任务谁来执行这个任务

工作流程

如果当前线程数量小于核心线程就创建核心线程,如果大于核心线程数就放到工作队列中,如果工作队列满了并且小于最大线程数,就创建非核心线程,如果大于最大线程数就采取拒绝策略

如何回收线程

线程池中的线程分为核心线程和非核心线程,核心线程常驻线程池,当工作任务队列满时,将会创建非核心线程来处理任务,当任务处理完成后,在一定时间内空闲的线程需要被回收,需要用到工作任务队列-阻塞队列中的workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)方法,如果返回null则可以进行回收(调用Thread#interrupt())

默认情况下只会回收非核心线程,当allowCoreThreadTimeOuttrue时也会回收核心线程,一般不要回收核心线程

8. 并行流Stream.parallelStream可能有哪些问题

Stream.parallelStream默认是通过ForkJoinPool.commonPool线程池来实现的,将流分为多个子流到不同的CPU中处理然后合并处理结果

可能产生的问题: 共享资源的竞争,线程安全,死锁,线程切换,事务等

9. 什么是内存泄露,什么情况会导致内存泄露,如何解决

当对象已经不再被使用,但是垃圾回收期不能回收的时候,产生内存泄露

未引用对象将会被垃圾回收,而引用对象却不会,未引用对象是无用的对象,无用的对象并不都是未引用对象,有一些无用对象有可能是引用对象,这部分是内存泄露的来源

如果对象A引用对象B。A的生命周期比B的生命周期要长,当B在程序中不再被使用的时候,A仍然引用着B,在这种情况下,垃圾回收器是不会回收对象B的,可能造成内存不足的问题,因为A可能不止引用着B,还可能引用其他生命周期比A短的对象,造成了大量无用对象不能被回收,且占据内存资源,同样B也可能引用其他对象,这些被B对象引用着的对象也不能被垃圾回收器回收,所有的无用对象消耗大量内存

怎样阻止内存泄露

  • 使用List Map等集合或者大对象时,使用完成后赋值为null
  • 避免死循环创建或对集合添加元素
  • 及时关闭打开的文件等