面试整理——Java并发编程

Java并发编程

一. 多线程

1.1 线程

问:什么时候用线程?

  • 需要频繁创建和销毁
  • 需要频繁切换
  • 多核环境
  • 并行操作
  • 相比稳定安全更需要速度

问:创建多线程的方式,以及线程的状态转换?

Java中有:

  • 继承Thread类,重写run(),创建线程对象。
  • 实现Runnable接口,重写run(),创建线程对象。
  • 实现Callable接口,重写call(),创建实例对象并作为构造器参数创建FutureTask对象,再使用FutureTask对象作为构造器参数创建线程对象。
  • 使用Executor框架创建线程池。

线程状态

状态 名称 示例 介绍
New 初始状态 new Thread(runnable) 线程还未开始运行,处于创建状态
Runnable 运行状态 thread.start() 可运行但未必在运行,取决于OS分配的运行时间,即就绪和运行两状态的合并。
Blocked 阻塞状态 请求内部的对象锁,但锁被其他线程持有
Waiting 等待状态 Object.wait() / Thread.join() / 等待Lock或Condition 当线程等待另一个线程通知调度器某个条件时,处于此状态
Timed waiting 超时等待 Object.wait() / Thread.join() / Thread.sleep() / Lock.tryLock() / Condition.await() 调用有些有超时参数的方法时,可以指定时间自行返回
Terminated 终止状态 不要使用stop() 要么因为run方法正常结束而终止,要么因为没有捕获的异常终止了run方法而结束

一个经历了所有状态的线程可能会经历以下流程:线程创建后调用start()方法开始,状态由NEW->RUNNABLE。当线程执行wait()后,线程进入WAITING状态。处于WAITING状态的线程需要等待其他线程的通知才可以恢复RUNNABLE状态,而TIME_WAITING则是在WAITING上增加了时间限制,在超时后会自动返回RUNNABLE状态。当线程调用同步方法时,在无法获得锁的情况下,线程进入BLOCKED状态。线程在执行run()方法后将进入终止状态。

问:终止线程的方法?

  • 使用退出标志(如volatile布尔变量),表示线程正常退出;
  • 通过interrupt()中断,run()内通过isInterrupted()判断是否中断,若处于阻塞状态就无法检测中断状态,会抛出异常InterruptedException。

问:谈一下线程切换,并引申到Java阻塞,运行?

线程切换就是指CPU从一个进程/线程切换到另一个进程/线程(上下文指某个时间节点CPU寄存器和计数器的内容)。大概过程可以简单的理解为:挂起当前线程(存储当前上下文),恢复一个线程(找到一个合适的上下文并恢复到寄存器),跳转到程序计数器指向的位置(即线程被中断时的代码行)。

阻塞就是指线程执行到某一阶段时,需要获取某些资源才能继续执行,但此时这些资源被其他线程占用,所以当前线程需要处于等待状态。

问:线程的中断机制?

Java的线程中有一个中断标识位,表示是否有中断请求,并在Thread中提供了interrupt()来中断线程,以及interrupted()和isInterrupted()来判断当前中断状态,前者会将中断复位。

中断并不意味着线程会立即终止,中断线程可以任意处理,通常会把中断请求当作终止请求,由线程根据情况在合适的时机终止。

处于阻塞状态的线程会抛出异常InterruptedException,且抛出异常的方法大多会先重置中断标识位再抛出异常。

问:怎么实现所有线程在等待某个事件的发生才会去执行?

  • 读写锁:主线程先获取写锁,所有子线程获取读锁,等事件发生时主线程释放写锁。
  • CountDownLatch:初始值设置为1,所有子线程调用 await() 等待,等事件发生时调用 countDown() 方法计数减为0。
  • Semaphore:初始值设为N,主线程先调用 acquire(N) 申请N个信号量,其他线程调用 acquire() 阻塞等待,等事件发生时主线程同时释放N个信号量。
  • CyclicBarrier

问:如何实现控制线程在某段时间内完成,不完成就撤销?

问:线程回调?

问:RPC 框架?设计一个 RPC,怎么实现

用到了回调这块的东西

问:线程间的通信可以怎么做?线程间相互通知你会怎么做,代码要怎么实现?

答了synchronized和Object的wait和notify,强调了notify是随机,All是全部,然后问All怎么个全部法,全部唤醒都开始执行不就不安全了吗?


1.2 ThreadLocal

问:谈一下ThreadLocal?应用场景?原理?与 Thread 类的关系?

应用:

  • 用来解决数据库连接,存放connection对象,不同线程存放各自session;
  • 解决simpleDateFormat线程安全问题;
  • 会出现内存泄漏,显式remove..不要与线程池配合,因为worker往往是不会退出的;

原理:线程中创建副本,访问自己内部的副本变量,内部实现是其内部类名叫ThreadLocalMap的成员变量threadLocals,key为本身,value为实际存值的变量副本

与 Thread 类的关系:

问:static 能不能修饰 ThreadLocal?

能,且通常将ThreadLocal变量声明为私有静态,这样做有好处也有坏处,主要是为了避免重复的创建TSO(thread specific object,即与线程相关的变量),坏处是可能导致内存泄漏。

如果声明ThreadLocal为某个类的实例变量,每创建一个类的实例就会创建新的ThreadLocal实例,导致同一个线程可能访问到同一个TSO类的不同实例,也因为重复创建相同的对象导致浪费。

因为类对ThreadLocal的静态引用,导致最终生成了对ThreadLocal的一条可达引用链路,使ThreadLocal实例不会被垃圾回收,即产生了内存泄漏。

所以使用ThreadLocal需要手动回收这部分内存,可以remove或使ThreadLocal变量=null。

问:谈一下ThreadLocal的内存泄漏问题?

如果是强引用,设置tl=null,但是key的引用依然指向ThreadLocal对象,所以会有内存泄漏,而使用弱引用则不会; 但是还是会有内存泄漏存在,ThreadLocal被回收,key的值变成null,导致整个value再也无法被访问到; 解决办法:在使用结束时,调用ThreadLocal.remove来释放其value的引用;

问:如果我们要获取父线程的ThreadLocal值呢?

ThreadLocal是不具备继承性的,所以是无法获取到的,但是我们可以用InteritableThreadLocal来实现这个功能。InteritableThreadLocal继承来ThreadLocal,重写了createdMap方法,已经对应的get和set方法,不是在利用了threadLocals,而是interitableThreadLocals变量。

这个变量会在线程初始化的时候(调用init方法),会判断父线程的interitableThreadLocals变量是否为空,如果不为空,则把放入子线程中,但是其实这玩意没啥鸟用,当父线程创建完子线程后,如果改变父线程内容是同步不到子线程的。。。同样,如果在子线程创建完后,再去赋值,也是没啥鸟用的


1.3 Thread

问:Thread类里有哪些常用方法?

问:interrupt/isInterrupted/interrupt区别?

  • interrupt() 调用该方法的线程的状态为将被置为”中断”状态(set操作)
  • isinterrupted() 是作用于调用该方法的线程对象所对应的线程的中断信号是true还是false(get操作)。例如我们可以在A线程中去调用B线程对象的isInterrupted方法,查看的是A
  • interrupted()是静态方法:内部实现是调用的当前线程的isInterrupted(),并且会重置当前线程的中断状态(getandset)

问:谈一下start和run方法的区别?

答:start方法启动多线程,但并不一定直接运行,而是先进入可运行状态,等待调度分配。run方法是线程体,包含了要执行的线程的内容,当run方法执行结束,线程随即终止,调用run方法并不能用来启动线程。

问:sleep 和 wait 有什么区别?

sleep属于线程类,wait属于object类;sleep不释放锁

问:sleep 和 yeild 方法有什么区别?


1.4 线程池

问:项目的用户量多吗?有用过线程池吗?用到多线程的优势在哪?

  • 以前的项目用户量不大,但有学习和练习过线程池。
  • 多线程意味着能同时执行多个任务,利用多核处理器,提高并发性能,加快程序执行速度,提高系统的吞吐量和响应能力。
  • 线程池则是用来管理和限制系统中执行线程的数量(统一管理,避免资源耗尽),重复的利用已创建的线程,防止内存的浪费(降低资源消耗),减少创建和销毁线程的次数,(提高响应速度)。

线程池优先把任务放入队列,而不是创建更多线程,适合CPU密集型任务,线程过多反而导致上下文切换等影响效率。对于IO密集的场景,一些线程池会优先创建线程,让单位时间内执行更多的任务。

池化技术:用空间换时间,把创建耗时的资源统一管理起来。

问:ThreadPoolExecutor 初始化参数?

  • corePoolSize(线程池基本大小):控制执行线程的数目,超过此值就不再创建基本线程。
  • maximumPoolSize(线程池最大数量):若队列满了,会继续创建新的线程,直到达到线程池最大数量,当然若任务队列没有界限此参数就失效了。
  • ThreadFactory(线程工厂):用于设置创建线程的工厂,一般用于给线程统一命名。
  • runnableTaskQueue(任务队列):保存等待执行的任务阻塞队列,有多种选择:数组先进先出队列、链表先进先出队列、不存储元素的阻塞队列、含优先级的无限阻塞队列。注意:不要使用无界队列,任务堆积会占用内存空间,一旦占满就会频繁GC,导致服务不可用。
  • RejectedExecutionHandler(饱和策略):队列和线程池都满了后,处于饱和状态时的新任务处理策略。默认是AbortPolicy直接抛出异常,还有直接丢弃、丢弃最近任务等等。
  • keepAliveTime(线程活动保持时间):工作线程空闲后能够存活的时间,当任务很多且执行时间较短时,可以调高此值,提高线程的利用率。
  • TimeUnit(时间单位):可选的单位有天(DAYS)、小时(HOURS)、分钟 (MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)。

问:线程状态?

线程池有5种状态:running,showdown,stop,Tidying,TERMINATED。

  • running:线程池处于运行状态,可以接受任务,执行任务,创建线程默认就是这个状态了
  • showdown:调用showdown()函数,不会接受新任务,但是会慢慢处理完堆积的任务。
  • stop:调用showdownnow()函数,不会接受新任务,不处理已有的任务,会中断现有的任务。
  • Tidying:当线程池状态为showdown或者stop,任务数量为0,就会变为tidying。这个时候会调用钩子函数terminated()。
  • ERMINATED:terminated()执行完成。

在线程池中,用了一个原子类来记录线程池的信息,用了int的高3位表示状态,后面的29位表示线程池中线程的个数。

问:corepoolSize 怎么设置,maxpoolsize 怎么设置,keep-alive 各种的?

问:阻塞队列在生产中的设置?

一般设置为 0,防止用户阻塞

问:Java中的线程池是如何实现的?讲一下线程池的原理?

  • 线程中线程被抽象为静态内部类Worker,是基于AQS实现的存放在HashSet中;
  • 要被执行的线程存放在BlockingQueue中;
  • 基本思想就是从workQueue中取出要执行的任务,放在worker中处理;

问:如果线程池中的一个线程运行时出现了异常,会发生什么?

如果提交任务的时候使用了submit,则返回的feature里会存有异常信息,但是如果数execute则会打印出异常栈。但是不会给其他线程造成影响。之后线程池会删除该线程,会新增加一个worker。

问:讲一下线程池的原理?

  • 提交一个任务,线程池里存活的核心线程数小于corePoolSize时,线程池会创建一个核心线程去处理提交的任务
  • 如果线程池核心线程数已满,即线程数已经等于corePoolSize,一个新提交的任务,会被放进任务队列workQueue排队等待执行。
  • 当线程池里面存活的线程数已经等于corePoolSize了,并且任务队列workQueue也满,判断线程数是否达到maximumPoolSize,即最大线程数是否已满,如果没到达,创建非核心线程执行提交的任务。
  • 如果当前的线程数达到了maximumPoolSize,还有新的任务过来的话,直接采用拒绝策略处理。

问:如何实现一个线程池,Java中线程池如何进行配置?线程池的线程数怎么确定?

如果是IO操作为主怎么确定?如果计算型操作又怎么确定?跳表的查询过程是怎么样的,查询和插入的时间复杂度?

问:如何实现一个线程池,Java中线程池如何进行配置?线程池的核心参数记得吗?线程池的工作流程记得吗?

问:线程池里如何知道线程执行完了没有?线程阻塞怎么办?怎么保证所有线程执行完之后继续往下处理?怎么让一个线程等另一个线程执行结束?

答了CountdownLatch和CyclicBarrier,之后提示可以利用Thread的join方法

问:Java 的信号灯?

问:Executors 静态方法?

问:拒绝策略?

  • AbortPolicy直接抛出异常阻止线程运行;
  • CallerRunsPolicy如果被丢弃的线程任务未关闭,则执行该线程;
  • DiscardOldestPolicy移除队列最早线程尝试提交当前任务
  • DiscardPolicy丢弃当前任务,不做处理

问:newFixedThreadPool (固定数目线程的线程池)?

  • 阻塞队列为无界队列LinkedBlockingQueue
  • 适用于处理CPU密集型的任务,适用执行长期的任务

问:newCachedThreadPool(可缓存线程的线程池)?

  • 阻塞队列是SynchronousQueue
  • 适用于并发执行大量短期的小任务

问:newSingleThreadExecutor(单线程的线程池)?

  • 阻塞队列是LinkedBlockingQueue
  • 适用于串行执行任务的场景,一个任务一个任务地执行

问:newScheduledThreadPool(定时及周期执行的线程池)?

  • 阻塞队列是DelayedWorkQueue
  • 周期性执行任务的场景,需要限制线程数量的场景

1.5 线程安全

问:谈一下对线程安全的理解?用什么方法保证线程的安全?

问:如何线程安全的实现一个计数器?

问:请写一个线程安全的单例模式?

问:单例模式,饿汉式,懒汉式,线程安全的做法,两次判断 instance 是否为空,每次判断的作用是什么?

问:ABC 三个线程如何保证顺序执行?

问:生产者消费者模式的实现方式?


二. 锁

2.1 锁

问:讲讲你知道的锁?锁的几种特性?

  • 独占/排他与共享:独占/排他即同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁。共享则表示同一时刻多个线程可以同时访问。
  • 公平与非公平:先对锁进行获取请求的线程一定先被满足,这是公平锁;反之则是非公平锁。
  • 可重入:支持重进入,表示锁能够支持一个线程对资源重复加锁。

问:公平锁与非公平锁?

  1. 公平锁指在分配锁前检查是否有线程在排队等待获取该锁,优先分配排队时间最长的线程,非公平直接尝试获取锁 。
  2. 公平锁需多维护一个锁线程队列,大量的线程切换,效率低;默认非公平。

问:独占锁与共享锁?

  1. ReentrantLock为独占锁(悲观加锁策略) 。
  2. ReentrantReadWriteLock中读锁为共享锁,写锁为独占锁。
  3. JDK1.8 邮戳锁(StampedLock), 不可重入锁读的过程中也允许获取写锁后写入。这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁, 乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。

问:可重入锁概念?

  1. 可重入锁是指同一个线程可以多次获取同一把锁,不会因为之前已经获取过还没释放而阻塞;
  2. ReentrantLock、ReentrantReadWriteLock和synchronized都是可重入锁。
  3. 可重入锁的一个优点是可一定程度避免死锁。

问:讲讲4种锁状态?

即重量级锁的四种状态,为了提高效率,尽量避免使用重量级锁:

  • 无锁:是否偏向-0,锁标志-01。
  • 偏向锁:是否偏向-1,锁标志-01。会偏向第一个访问锁的线程,当一个线程访问同步代码块获得锁时,会在对象头和栈帧记录里存储锁偏向的线程ID,当这个线程再次进入同步代码块时,就不需要CAS操作来加锁了,只要测试一下对象头里是否存储着指向当前线程的偏向锁,如果偏向锁未启动,new出的对象是普通对象(即无锁,有稍微竞争会成轻量级锁),如果启动,new出的对象是匿名偏向(偏向锁) 对象头主要包括两部分数据:Mark Word(标记字段, 存储对象自身的运行时数据)、class Pointer(类型指针, 是对象指向它的类元数据的指针)。
  • 轻量级锁(自旋锁) :锁标志-00。
    1. 在把线程进行阻塞操作之前先让线程自旋等待一段时间,可能在等待期间其他线程已经解锁,这时就无需再让线程执行阻塞操作,避免了用户态到内核态的切换。(自适应自旋时间为一个线程上下文切换的时间)
    2. 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁
    3. 自旋锁底层是通过指向线程栈中Lock Record的指针来实现的
  • 重量级锁:锁标志-10。

问:轻量级锁与偏向锁的区别?

  1. 偏向锁对象头 Mark Word 中存放线程ID,轻量级锁则是栈帧中锁记录的指针。
  2. 偏向锁是在无竞争场景下完全消除同步,连CAS也不执行;轻量级锁是通过CAS来避免进入开销较大的互斥操作。
  3. 偏向锁遇到线程竞争会升级为轻量级锁,前者适用于只有一个线程访问同步块,后者则是线程竞争不激烈的场景。

问:自旋锁升级到重量级锁条件?

  1. 某线程自旋次数超过10次;
  2. 等待的自旋线程超过了系统core数的一半;

问:讲讲读写锁?优点?实现方式?

读写锁即分离了读锁和写锁,同一时刻允许多个线程访问,提高了读操作间的并发性。

优点有:

  • 保证了读操作间的并发。
  • 保证写操作对读操作的可见性。
  • 简化读写交互场景的编程方式。

并发包中提供了读写锁 ReentrantReanWriteLock ,和ReentrantLock相似,同样基于AQS,但是读写锁是基于共享资源的,不是互斥,关键在于state的处理,读写锁把高16为记为读状态,低16位记为写状态,从而分开了读写操作,读读情况其实就是读锁重入,读写/写读/写写都是互斥的,只要判断低16位就好了。读状态是所有线程获取读锁次数的总和,每个线程各自的获取次数则存在ThreadLocal中。

问:乐观锁和悲观锁的区别?优缺点?使用场景?JDK中涉及到乐观锁和悲观锁的内容?这两种锁在Java和MySQL分别是怎么实现的?MySql的悲观锁怎么防止并发?

乐观锁和悲观锁的区别:如字面意思,乐观锁指总是假设最好的情况,每次操作都不会上锁,而是更新时判断是否在此期间有被其他线程修改;悲观锁则相反,总假设最坏的情况,共享资源每次只给一个线程使用。

优缺点:两种锁适用于不同的场景,当共享资源竞争激烈时,乐观锁产生大量的更新失败,导致一些操作会浪费CPU资源如自旋;而资源竞争不那么激烈时,悲观锁所要进行的线程阻塞切换、等待唤醒等需要额外浪费CPU资源。

使用场景:乐观锁常见于多读少写的应用场景;悲观锁被应用于传统关系型数据库,如行锁,表锁,读锁,写锁等,适用于多写少读的场景。

悲观锁实现方式:

  • JDK:synchronizedRetreentLock 等。
  • MySQL:行锁,表锁,读锁,写锁等

乐观锁实现方式:(CAS,版本号机制)

  • JDK:CAS,JVM中的CAS操作通过处理器提供的 CMPXCHG 指令来实现原子操作。
  • MySQL:MVCC-多版本并发控制。

MySql的悲观锁怎么防止并发:TODO

问:死锁产生的原因?如何预防?死锁的条件,怎么解除死锁,怎么观测死锁?

死锁产生的原因是线程之间相互等待,导致程序瘫痪。

如何预防死锁:一般是允许前三个必要条件,通过合理的分配算法确保不会形成封闭等待链。

死锁产生有四个必要条件:

  1. 互斥条件:资源同时只能被一个线程占有,线程间互斥等待。
  2. 不可剥夺条件:线程获得资源后不能被夺走,只能主动释放。
  3. 请求和保持条件:线程请求的资源被其他线程占据,此线程被阻塞但不会释放已有的资源。
  4. 循环等待条件:存在线程间的循环等待链路,前一个线程请求资源被下个线程持有。

处理死锁的方法:

  1. 避免一个线程同时获得多个锁。
  2. 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
  3. 尝试使用定时锁,使用 lock.tryLock(timeout) 来代替内部锁机制。
  4. 对于数据库锁,加锁和解锁必须在同一个数据库连接里,避免解锁失败情况。

检测死锁的方法:

  • 通过JStack工具生成JVM当前的线程快照,通过线程快照定位线程停顿的原因,死锁一般是因为线程互相等待对方持有的对象。
  • 通过JConsole监控工具,检测线程死锁。

问:加锁会带来哪些性能问题。如何解决?

锁的开销来自于三部分:

  1. 上下文切换,挂起当前线程(存储当前上下文),恢复一个线程(找到一个合适的上下文并恢复到寄存器),跳转到程序计数器指向的位置(即线程被中断时的代码行)。
  2. 调度器开销,唤醒或休眠线程。
  3. 多核环境的跨处理器调度开销。

如何解决这些性能开销:

  1. 减少线程切换,如通过CAS尝试短时间内请求锁的自旋锁。
  2. 减少锁的冲突次数,比如读写锁分离,哈希表分段加锁等。
  3. 在较少竞争的环境下尽量避免使用锁,如 synchronized 对锁分等级,CAS + volatile 等

问:偏向锁、轻量级锁、自旋锁等优化?

问:事务有哪些特性?

问:怎么理解原子性?


2.2 volatile

问:volatile的作用是什么?可见性?volatile 的实现原理?

volatile变量
(1):变量可见性

(2):防止指令重排序

(3):保障变量单次读,写操作的原子性,但不能保证i++这种操作的原子性,因为本质是读,写两次操作

问:volatile 的实现原理?及内存屏障相关?

问:volatile如何保证线程间可见和避免指令重排?

volatile可见性是有指令原子性保证的,在jmm中定义了8类原子性指令,比如write,store,read,load。而volatile就要求write-store,load-read成为一个原子性操作,这样子可以确保在读取的时候都是从主内存读入,写入的时候会同步到主内存中(准确来说也是内存屏障),指令重排则是由内存屏障来保证的,由两个内存屏障:

  • 一个是编译器屏障:阻止编译器重排,保证编译程序时在优化屏障之前的指令不会在优化屏障之后执行。
  • 第二个是cpu屏障:sfence保证写入,lfence保证读取,lock类似于锁的方式。java多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个lock指令,就是增加一个完全的内存屏障指令。

2.3 synchronized

问:synchronized 使用方式及实现原理,以及锁优化?

synchronized实现原理?

contentionList(请求锁线程队列) entryList(有资格的候选者队列) waitSet(wait方法后阻塞队列) onDeck(竞争候选者) ower(竞争到锁线程) !ower(执行成功释放锁后状态); Synchronized 是非公平锁。

Synchronized 在线程进入 ContentionList 时,等待的线程会先尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源。

底层是由一对monitorenter和monitorexit指令实现的(监视器锁)

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程:

  • 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
  • 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
  • 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

问:synchronized 在静态方法和普通方法的区别?

问:synchronized 和 ReentranLock的区别?

  • 都是可重入锁; R是显示获取和释放锁,s是隐式;
  • R更灵活可以知道有没有成功获取锁,可以定义读写锁,是api级别,s是JVM级别;
  • R可以定义公平锁;Lock是接口,s是java中的关键字

问:synchronized 和 lock 有什么区别?

问:HashTable,同步锁,synchronized 关键字 1.6 之后提升了什么,怎么提升的这些?


2.4 Lock

问:Lock 接口有哪些实现类,使用场景是什么?

问:讲讲ReentrantLock实现原理?

ReentrantLock原理(CAS+AQS)

CAS+AQS队列来实现
(1):先通过CAS尝试获取锁, 如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起;

(2): 当锁被释放之后, 排在队首的线程会被唤醒CAS再次尝试获取锁,

(3):如果是非公平锁, 同时还有另一个线程进来尝试获取可能会让这个线程抢到锁;

(4):如果是公平锁, 会排到队尾,由队首的线程获取到锁。

问:ReentrantLock 是如何实现可重入性的 ?

内部自定义了同步器 Sync,加锁的时候通过CAS 算法 ,将线程对象放到一个双向链表 中,每次获取锁的时候 ,看下当前维 护的那个线程ID和当前请求的线程ID是否一样,一样就可重入了;

问:ReentrantLock如何避免死锁?

  • 响应中断lockInterruptibly()
  • 可轮询锁tryLock()
  • 定时锁tryLock(long time)

问:tryLock 和 lock 和 lockInterruptibly 的区别?

(1):tryLock 能获得锁就返回 true,不能就立即返回 false,

(2):tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false

(3):lock 能获得锁就返回 true,不能的话一直等待获得锁

(4):lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程, lock 不会抛出异常,而 lockInterruptibly 会抛出异常。


2.5 Condition

问:讲一下Condition?

Condition接口主要是和Lock配合实现等待通知模式,类似于对象监视器方法和synchronized的组合。其定义了一系列等待和唤醒方法,Condition对象依赖于Lock对象,一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)。等待时将节点从同步队列移动到等待队列,需要把当前线程构建成一个新的等待结点。通知时则不需要新建节点,可以看作直接把等待节点移动到同步队列。

问:对象监视器方法和Condition的异同 ?

任意一个Java对象,都拥有一组监视器方法(定义在 java.lang.Object 上),主要包括 wait()wait(long timeout)notify() 以及 notifyAll() 方法,这些方法与 synchronized 同步关键字配合,可以实现等待/通知模式

Condition 接口也提供了类似 Object 的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。

Object的监视器方法与Condition接口的对比


2.6 AQS

问:讲讲AQS?实现原理?讲讲AQS怎么实现的Fair和NoFair?

AQS即队列同步器(AbstractQueuedSynchronizer),是构建锁和其他同步组件的基础框架,锁是面向使用者的,同步器则是面向锁的实现者。

实现原理:AQS底层是一个双向链表实现的FIFO同步队列,通过整型成员变量state来判断锁的同步状态,基于模板方法模式来设计,包含独占式和共享式的获取同步状态,各种锁自定义实现AQS。

对于非可重入锁状态不是0则去阻塞;对于可重入锁如果是0则执行,非0则判断当前线程是否是获取到这个锁的线程,是的话把state状态+1,比如重入5次,那么state=5。 而在释放锁的时候,同样需要释放5次直到state=0其他线程才有资格获得锁。

问:AQS两种资源共享方式?

  • Exclusive:独占,只有一个线程能执行,如ReentrantLock。
  • Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier。

问:CAS了解么? CAS 有什么缺陷,如何解决?还了解其他同步机制么?

CAS,Compare and Set,比较并交换,常用来实现乐观锁。

实现原理:内存值,预期值,修改的新值,当内存值等于预期值时,将内存值修改为新值,否则什么都不做。

CAS的缺陷有三个:

  1. ABA问题,即A变为B再变为A也能满足判断条件。
  2. 如果CAS失败,自旋会给CPU带来压力,死循环。
  3. 只能保证对一个变量的原子性操作。

解决方案分别是:

  1. 引入版本号或修改次数,1A->2B->3A。
  2. 使用JVM提供的pause指令。
  3. 用锁将多个共享变量合并为一个来操作,使用 AtomicReference 类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

CAS在java中的应用:
(1):Atomic系列

还有信号量机制,自旋锁等。

问:CAS和synchronized有什么区别?都用synchronized不行么?

synchronized是获取对象锁,属于重量级锁,虽然有进行优化,但还是有一定的性能开销。CAS则并未利用锁,其机制比较适合读多写少的场景。

如果都采用synchronized,在并发编程时不能获得令人满意的执行效率。


2.7 CountDownLatch

问:用过CountDownLatch么?什么场景下用的?

问:什么是信号量Semaphore?应用场景?

信号量是一种固定资源的限制的一种并发工具包,基于AQS实现的,在构造的时候会设置一个值,代表着资源数量。信号量主要是应用于是用于多个共享资源的互斥使用,和用于并发线程数的控制(druid的数据库连接数,就是用这个实现的),信号量也分公平和非公平的情况,基本方式和reentrantLock差不多,在请求资源调用task时,会用自旋的方式减1,如果成功,则获取成功了,如果失败,导致资源数变为了0,就会加入队列里面去等待。调用release的时候会加一,补充资源,并唤醒等待队列。

Semaphore 应用

  • acquire() release() 可用于对象池,资源池的构建,比如静态全局对象池,数据库连接池;
  • 可创建计数为1的S,作为互斥锁(二元信号量)

问:用过CyclicBarrier吗? 和 countdownlatch 的区别?

个人理解 赛马和点火箭

  • con用于主线程等待其他子线程任务都执行完毕后再执行,cyc用于一组线程相互等待大家都达到某个状态后,再同时执行;
  • CountDownLatch是不可重用的,CyclicBarrier可重用

CountDownLatch是等待其他线程执行到某一个点的时候,在继续执行逻辑(子线程不会被阻塞,会继续执行),只能被使用一次。最常见的就是join形式,主线程等待子线程执行完任务,在用主线程去获取结果的方式(当然不一定),内部是用计数器相减实现的(没错,又特么是AQS),AQS的state承担了计数器的作用,初始化的时候,使用CAS赋值,主线程调用await()则被加入共享线程等待队列里面,子线程调用countDown的时候,使用自旋的方式,减1,知道为0,就触发唤醒。

CyclicBarrier回环屏障,主要是等待一组线程到底同一个状态的时候,放闸。CyclicBarrier还可以传递一个Runnable对象,可以到放闸的时候,执行这个任务。CyclicBarrier是可循环的,当调用await的时候如果count变成0了则会重置状态,如何重置呢,CyclicBarrier新增了一个字段parties,用来保存初始值,当count变为0的时候,就重新赋值。还有一个不同点,CyclicBarrier不是基于AQS的,而是基于RentrantLock实现的。存放的等待队列是用了条件变量的方式。


2.8 锁的应用

问:ConcurrentHashMap的get需要加锁么,为什么?

问:Hashtable 是怎么加锁的 ?


三. 内存模型

问:Java内存模型?为什么要有工作内存和主内存?

为什么要有工作内存和主内存:计算机随着技术发展CPU与内存速度差距越来越大,于是出现高速缓存技术存放CPU常用数据,其速度与CPU接近,多核多线程每个核都有自己的缓存区,所以面临着缓存一致性问题。Java内存模型主要为了解决这个问题。

所有的实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。可以将它们看作共享变量。而局部变量,方法定义参数和异常处理器参数这些则不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。

Java内存模型定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM(Java内存模型)的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

即三个规则:

  • 所有的数据都在主内存中。
  • 每个线程都保留一份共享变量的副本。线程对变量的所有操作都必须在这个副本内存中进行,而不能直接读写主内存。
  • 不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。