金九银十快到了,即将进入找工作的高峰期,最新整理的最全多线程并发面试题和答案总结,希望对想进BAT的同学有帮助,由于篇幅较长,建议收藏后细看~,需要更全面的Word文档的请点主页私信【学习】领取免费的哦
本文目录多线程与并发面试问题概览:
本文目录
java中你知道哪些锁?
问题回答
- 乐观锁/悲观锁
- 共享锁/独享锁
- 公平锁/非公平锁
- 互斥锁/读写锁
- 可重入锁
- 自旋锁
- 分段锁
- 偏向锁/轻量级锁/重量级锁
锁的分类
Java线程的状态或者生命周期?
问题回答
- Java的线程状态被定义在公共枚举类java.lang.Thread.state中。一种有六种状态
- 新建(NEW):表示线程新建出来还没有被启动的状态,比如:Thread t = new MyThread();
- 就绪/运行(RUNNABLE):该状态包含了经典线程模型的两种状态:就绪(Ready)、运行(Running):
- 阻塞(BLOCKED):通常与锁有关系,表示线程正在获取有锁控制的资源,比如进入synchronized代码块,获取ReentryLock等;发起阻塞式IO也会阻塞,比如字符流字节流操作。
- 等待(WAITING):线程在等待某种资源就绪。
- 超时等待(TIMED_WAIT):线程进入条件和等待类似,但是它调用的是带有超时时间的方法。
- 终止(TERMINATED):线程正常退出或异常退出后,就处于终结状态。也可以叫线程的死亡。
看下源码
在Java.lang.Thread里,有个内部枚举: State,一个线程在某一时刻可以是State里的一种状态
State状态
看图理解
线程的状态
哪些情况或者方法可以进入等待状态?
- 当一个线程执行了Object.wait()的时候,它一定在等待另一个线程执行Object.notify()或者Object.notifyAll()。
- 一个线程thread,其在主线程中被执行了thread.join()的时候,主线程即会等待该线程执行完成。
- 当一个线程执行了LockSupport.park()的时候,其在等待执行LockSupport.unpark(thread)。
哪些情况或者方法可以进入超时等待状态?
问题回答
该状态不同于WAITING,它可以在指定的时间后自行返回
- Object.wait(long)
- Thread.join(long)
- LockSupport.parkNanos()
- LockSupport.parkUntil()
- Thread.sleep(long)
synchronized 与lock区别?
问题回答
- lock是一个接口,而synchronized是java的一个关键字
- synchronized异常会释放锁,lock异常不会释放,所以一般try catch包起来,finally中写入unlock,避免死锁。
- Lock可以提高多个线程进行读操作的效率
- synchronized关键字,可以放代码块,实例方法,静态方法,类上
- lock一般使用ReentrantLock类作为锁,配合lock()和unlock()方法。在finally块中写unlock()以防死锁。
- jdk1.6之前synchronized低效。jdk1.6之后synchronized高效。
synchronized 与ReentrantLock区别?
问题回答
- synchronized依赖JVM实现,ReentrantLock是JDK实现的。synchronized是内置锁,只要在代码开始的地方加synchronized,代码结束会自动释放。Lock必须手动加锁,手动释放锁。
- ReenTrantLock比synchronized增加了一些高级功能。synchronized代码量少,自动化,但扩展性低,不够灵活;ReentrantLock扩展性好,灵活,但代码量相对多。
- 两者都是可重入锁。都是互斥锁。
- synchronized是非公平锁,ReentrantLock可以指定是公平锁还是非公平锁。
synchronized 与ThreadLocal区别?
问题回答
- 都是为了解决多线程中相同变量的访问冲突问题。
- Synchronized同步机制,提供一份变量,让不同的线程排队访问。
- ThreadLocal关键字,为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
- ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
看代码
ThreadLocal
synchronized
synchronized 与volatile区别?
问题回答
- volatile是一个类型修饰符(type specifier)。
- volatile,它能够使变量在值发生改变时能尽快地让其他线程知道。
- 关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且只能修改变量,而synchronized可以修饰方法,以及代码块。
- 多线程访问volatile不会发生阻塞,而synchronized会出现阻塞
- volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步
- 关键字volatile解决的下变量在多线程之间的可见性;而synchronized解决的是多线程之间资源同步问题
Thread类中的start()和run()方法有什么区别?
- 通过调用线程类的start()方法来启动一个线程,使线程处于就绪状态,即可以被JVM来调度执行,在调度过程中,JVM通过调用线程类的run()方法来完成实际的业务逻辑,当run()方法结束后,此线程就会终止。
- 如果直接调用线程类的run()方法,会被当作一个普通的函数调用,程序中仍然只有主线程这一个线程。即start()方法能够异步地调用run()方法,但是直接调用run()方法却是同步的,无法达到多线程的目的。
- 因此,只有通过调用线程类的start()方法才能达到多线程的目的。
事务的隔离级别及引发的问题?
问题回答
- 4个隔离级别:读未提交、读已提交、可重复读、串行化
- 分别怎么理解呢?
- 读未提交(READ UNCOMMITTED),事务中的修改,即使没有提交,对其它事务也是可见的。
- 读已提交(READ COMMITTED),一个事务能读取已经提交的事务所做的修改,不能读取未提交的事务所做的修改。也就是事务未提交之前,对其他事务不可见。
- 可重复读(REPEATABLE READ),保证在同一个事务中多次读取同样数据的结果是一样的。
- 串行化(SERIALIZABLE),强制事务串行执行。
3.读已提交是sql server的默认隔离级别。
可重复读是mysql的默认隔离级别。
简要回答
- 4个隔离级别,读未提交、读已提交、可重复读、可串行化。
- 读未提交(READ UNCOMMITTED),事务提交与否都可见,引发脏读、不可重复读、幻读。
- 读已提交(READ COMMITTED),已提交的事务可见,引发不可重复读、幻读。
- 可重复读(REPEATABLE READ),多次读取,数据一致,引发幻读。
- 串行化(SERIALIZABLE),串行执行。
事务隔离级别和引发的问题
大多数数据库的默认隔离级别为: Read Commited,如Sql Server , Oracle。
少数数据库默认的隔离级别为Repeatable Read, 如MySQL InnoDB存储引擎。
理解脏读、不可重复读、幻读
脏读:读到未提交的数据。
脏读
不可重复读:重点是修改,同样的条件, 你读取过的数据, 再次读取出来发现值不一样了。
不可重复读
幻读:重点在于新增或者删除,同样的条件, 第1次和第2次读出来的记录数不一样。
幻读
简单理解4个隔离级别
- 读未提交,比如事务A和事务B同时进行,事务A在整个执行阶段,会将某数据的值从1开始一直加到10,然后进行事务提交。此时,事务B能够读取事务A操作过程中的未提交的数据(1、2、3、4、5、6...10)。
- 读已提交,事务A在整个执行阶段,会将某数据的值从1开始一直加到10,然后进行事务提交。此时,事务B只能读取到最终的10。
- 可重复读,事务B开始读取到的是某个值是0,事务A对值进行修改提交多次,事务B读取到的依然是0。多次读取,结果一致。
- 串行化,是最严格的事务隔离级别,它要求所有事务被串行执行,一个事务没有结束,另外的事务没法继续。
案列演示
读未提交
读未提交
读已提交
读已提交
可重复读
可重复读
串行化
串行化1
什么是线程安全,java如何保证线程安全?
问题回答
- 在多线程环境中,能永远保证程序的正确性。执行结果不存在二义性。说白了,运行多少次结果都是一致的。
- 换种说法,当多个线程访问某一个类(对象或方法)时,这个类始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。
- 使用synchronized关键字和使用锁。
介绍一下线程池?结果
问题回答
- 线程池就是预先创建一些线程,它们的集合称为线程池。
- 线程池可以很好地提高性能,在系统启动时即创建大量空闲的线程,程序将一个task给到线程池,线程池就会启动一条线程来执行这个任务,执行结束后,该线程不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。
- 线程的创建和销毁比较消耗时间,线程池可以避免这个问题。
- Executors是jdk1.5之后的一个新类,提供了一些静态方法,帮助我们方便的生成一些常见的线程池
- newSingleThreadExecutor:创建一个单线程化的Executor。
- newFixedThreadPool:创建一个固定大小的线程池。
- newCachedThreadPool:创建一个可缓存的线程池
- newScheduleThreadPool:创建一个定长的线程池,可以周期性执行任务。
我们还可以使用ThreadPoolExecutor自己定义线程池,弄懂它的构造参数即可
- int corePoolSize,//核心池的大小
- int maximumPoolSize,//线程池最大线程数
- long keepAliveTime,//保持时间/额外线程的存活时间
- TimeUnit unit,//时间单位
- BlockingQueue workQueue,//任务队列
- ThreadFactory threadFactory,//线程工厂
- RejectedExecutionHandler handler //异常的捕捉器
简要回答
- 线程池就是预先创建一些线程
- 线程池可以很好地提高性能
- 线程池可以避免线程的频繁创建和销毁
- Executors可以创建常见的4种线程(单线程池、固定大小的、可缓存的、可周期性执行任务的)。
- 可以通过ThreadPoolExecutor自己定义线程池。
看看Excutors中的方法
Excutors中的方法
看看ThreadPoolExecutor的构造函数
ThreadPoolExecutor的构造函数
常见的线程池有哪些?
问题回答
- Executors是jdk1.5之后的一个新类,提供了一些静态方法,帮助我们方便的生成一些常见的线程池
- 单线程线程池,通过newSingleThreadExecutor()创建
- 固定大小的线程池,通过newFixedThreadPool()创建
- 可缓存的线程池,通过newCachedThreadPool()创建
- 可周期性执行任务的线程池,通过newScheduleThreadPool()创建
看下Excutors的使用
Excutors的使用
几个线程的区别?
newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
这种类型的线程池特点是:
- 工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
- 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
- 在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
newFixedThreadPool
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
newSingleThreadExecutor
创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
newScheduleThreadPool
创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。
同步和异步有何异同?
问题回答
- 同步发了指令,会等待返回,然后再发送下一个。
- 异步发了指令,不会等待返回,随时可以再发送下一个请求
- 同步可以避免出现死锁,读脏数据的发生
- 异步则是可以提高效率
- 实现同步的机制主要有临界区、互斥、信号量和事件
哪些集合是线程安全?
问题回答
- Vector:就比Arraylist多了个同步化机制(线程安全)。
- Hashtable:就比Hashmap多了个线程安全。
- ConcurrentHashMap:是一种高效但是线程安全的集合。
如何异步获取多线程返回的数据?
问题包含
说一下Callable这个接口的理解?
说一下Future接口的理解?
说一下FutureTask类的理解?
说一下CompletionService接口的理解?
问题回答
- 通过Callable+Future,Callable负责执行返回,Future负责接收。Callable接口对象可以交给ExecutorService的submit方法去执行。
- 通过Callable+FutureTask,Callable负责执行返回,FutureTask负责接收。FutureTask同时实现了Runnable和Callable接口,可以给到ExecutorService的submit方法和Thread去执行。
- 通过CompletionService,jdk1.8之后提供了完成服务CompletionService,可以实现这样的需求。
- 注意,实现Runnable接口任务执行结束后无法获取执行结果。
Callable有返回值,Runnable没有返回值
Callable有返回值,Runnable没有返回值
看看Future接口
看看所有方法
Future接口方法
重点看看get方法
get方法
总结下:
V get() :获取异步执行的结果,如果没有结果可用,此方法会阻塞直到异步计算完成。
V get(Long timeout , TimeUnit unit) :获取异步执行结果,如果没有结果可用,此方法会阻塞,但是会有时间限制,如果阻塞时间超过设定的timeout时间,该方法将抛出异常。
boolean isDone() :判断任务是否完成如果任务执行结束,无论是正常结束或是中途取消还是发生异常,都返回true。future.isDone()
boolean isCanceller() :如果任务完成前被取消,则返回true。 future.isCanceller()
boolean cancel(boolean mayInterruptRunning) :如果任务还没开始,执行cancel(...)方法将返回false;如果任务已经启动,执行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务,如果停止成功,返回true;当任务已经启动,执行cancel(false)方法将不会对正在执行的任务线程产生影响(让线程正常执行到完成),此时返回false;当任务已经完成,执行cancel(...)方法将返回false。mayInterruptRunning参数表示是否中断执行中的线程。
通过方法分析我们也知道实际上Future提供了3种功能:
(1)能够中断执行中的任务
(2)判断任务是否执行完成
(3)获取任务执行完成后的结果。
看看ExecutorService的submit方法
submit方法
注意:
Callable接口的call方法有返回值,Runnable接口的run方法没有返回值
submit(Runnable):Future<?> ,返回的Future无法获取返回值。submit(Runnable, T):Future,返回的Future无法获取返回值。
看看FutureTask
看FutureTask定义
FutureTask定义
看RunnableFuture定义,同时实现了Runnable、Future
RunnableFuture定义
所以,FutureTask
ExecutorService的submit方法接收Runnable和Callable,所以接收FutureTask。
Thread只支持Runnable,所以也支持FutureTask。FutureTask让Thread也可以有返回值的效果。
看代码
Callable接口配合ExecutorService的submit方法
AsynTask1.java
结果
结果
Callable接口配合FutureTask,ExecutorService的submit方法去执行
AsynTask2.java
结果
结果
Callable接口配合FutureTask,给Thread类去执行
AsynTask3.java
结果
CompletionService配合Callable
AsynTask4
结果
结果
CompletionService和Future的区别呢?
- Future获取结果,一个一个地取,一个取完了,再取另外一个,就会等待
- CompletionService,任意一个线程有返回,就立马取出
public class AsynTask5 {
public static void main(String[] args) {
asynCompletionService();
System.out.println("----------------分割线-------------------");
asynFuture();
System.out.println("主线程执行完毕");
}
private static void asynCompletionService() {
try {
//使用ExecutorService
ExecutorService executorService = Executors.newCachedThreadPool();
// 构建完成服务
CompletionService<Integer> completionService = new ExecutorCompletionService<Integer>(executorService);
for (int i = 1; i <= 5; i++) {
//提交任务
completionService.submit(new HandleFuture<>(i));
}
//获取结果,一个一个阻塞的取出。这中间肯定会浪费一定的时间在等待上
for (int i = 1; i <= 5; i++) {
Integer result = completionService.take().get();
System.out.println("结果:" + result);
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
private static void asynFuture() {
try {
//使用ExecutorService
ExecutorService executorService = Executors.newCachedThreadPool();
//Future列表
List<Future<Integer>> result = new ArrayList<Future<Integer>>();
for (int i = 1; i <= 5; i++) {
//提交任务
Future<Integer> submit = executorService.submit(new HandleFuture<>(i));
result.add(submit);
}
//获取结果,输出和线程的放入顺序无关系。每一个线程执行成功后,立刻就输出
for (Future<Integer> integerFuture : result) {
Integer integer = integerFuture.get();
System.out.println("结果:" + integer);
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class HandleFuture<Integer> implements Callable<Integer> {
private Integer num;
public HandleFuture(Integer num) {
this.num = num;
}
@Override
public Integer call() throws Exception {
Thread.sleep(2 * 1000l);
System.out.println(Thread.currentThread().getName());
return num;
}
}
结果
如何自定义线程池?
corePoolSize:核心池的大小
- 默认情况下,在创建了线程池之后,线程池中的线程数为0
- 当有任务到来后,如果线程池中存活的线程数小于corePoolSize,则创建一个线程。
maximumPoolSize:线程池最大线程数
- 线程池中允许的最大线程数,这个参数表示了线程池中最多能创建的线程数量。
- 当任务数量比corePoolSize大时,任务添加到workQueue
- 当workQueue满了,将继续创建线程以处理任务。
- maximumPoolSize表示当wordQueue满了,线程池中最多可以创建的线程数量。
keepAliveTime:
- 当线程池处于空闲状态时,超过keepAliveTime时间之后,空闲的线程会被终止。
- 只有当线程池中的线程数大于corePoolSize时,这个参数才会起作用,但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
- 当线程数大于corePoolSize时,如果一个线程的空闲时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。
unit:参数keepAliveTime的时间单位,有7种取值
- TimeUnit.DAYS; //天
- TimeUnit.HOURS; //小时
- TimeUnit.MINUTES; //分钟
- TimeUnit.SECONDS; //秒
- TimeUnit.MILLISECONDS; //毫秒
- TimeUnit.MICROSECONDS; //微妙
- TimeUnit.NANOSECONDS; //纳秒
workQueue : 任务队列,阻塞队列,存储提交的等待任务。常见子类有:
- ArrayBlockingQueue;
- LinkedBlockingQueue;
- SynchronousQueue;
threadFactory : 线程工厂,指定创建线程的工厂
handler : 任务队列添加异常的捕捉器,当任务超出线程池范围和队列容量时,采取何种拒绝策略。参考 RejectedExecutionHandler,常见实现类。
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
看一个自定义线程的例子
简单演示
执行结果
执行结果
工作中哪些地方使用了多线程?
问题回答
- 一般业务,web层--> service层 -->dao --> sql基本用不到多线程
- 数据量很大(1000w级别、TB级别)的I/O操作,可以考虑多线程
举一些例子
- 自己做并发测试的时候,假如想写想模拟3000个并发请求。
- 多线程下单抢单,假如支持5000人的并发下单。
- 多线程写入mysql,假如有1000w条数据要入库。
- 多线程写入redis,假如有1000w的数据要存入redis。
- 多线程导入ES索引,假如有1000w的数据要添加到ES索引。
- poi多线程导出,假如xls里面有10w的数据需要导出。
- poi多线程导入,假如有10w条数据需要导入到xls。
- 多线程发送邮件,假如有10w用户需要发送邮件。
- 多线程发送短信,假如有10w用户需要发送邮件。
10.多线程备份日志,假如10tb日志文件要备份。
11.多线程验证数据,比如验证url是否存在,假如有100w个url
数据并发操作可能的问题?
问题回答
- 丢失的修改
- 不可重复读,读第二次,数据就不对了
- 读脏数据
- 幻影读
脏读:指事务读到了其它事务做了修改而未提交的数据
不可重复读:不能重复读两次,读两次就不同了
幻读:同一个事务T1在两个不同的时间段t执行同一条查询语句得到的记录数量不同
消息等待通知wait/notify具体的应用
问题回答
- 一个线程修改了一个对象的值,另外一个线程需要感知到这个变化
- Java中我们使用的对象锁以及wait/notify方法进行线程通信
- 等待方遵循的原则:
获取对象的锁不满足条件 就调用wait()方法条件满足继续执行 - 通知方原则:
获取对象的锁改变条件, 然后notify
线程池中 submit() 和 execute() 方法有什么区别?
问题回答
- execute() 参数 Runnable ;
- submit() 参数 (Runnable) 或 (Runnable 和 结果 T) 或 (Callable);
- execute(Runnable x) 没有返回值。可以执行任务,但无法判断任务是否成功完成。
- submit(Callable x)有返回值,返回一个Future类的对象。
- Future对象
- 通过get方法,获取线程返回结果
- 通过get方法,接收任务执行时候抛出的异常
- 通过isDone方法,可以判断线程是否执行完成。
看execute和submit方法的定义
看submit方法的定义
看看Future的get方法
可以接收结果,可以接收任务执行时候抛出的异常
看看Future的方法
线程的创建方式有哪些?
问题回答
- 继承Thread类实现
- 实现Runnable接口方式
- 实现Callable接口方式
- 其中前两种比较常用。但是,需要有返回值需要实现Callable接口。
继承Thread类实现
/**
* 继承Thread类,并重写run方法
*/
public class MyThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("MyThread...");
}
}
实现Runnable接口方式
/**
* 实现Runnable接口,并重写run方法
*/
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("MyRunnable...");
}
}
MyRunnable runnable=new MyRunnable();
Thread thread=new Thread(runnable);
thread.start();
调用
MyRunnable runnable=new MyRunnable();
Thread thread=new Thread(runnable);
thread.start();
实现Callable接口方式
/**
* 实现Callable接口,并重写call方法
*/
public class MyCallable implements Callable<String>{
@Override
public String call() throws Exception {
return "MyCallable...";
}
}
调用
//创建和调用
MyCallable callable=new MyCallable();
ExecutorService eService=Executors.newSingleThreadExecutor();
Future<String> future=eService.submit(callable);
//获取返回结果
try {
String result=future.get();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
注意
- callable需要配合线程池使用
- callable比runnable功能复杂一些
Callable的call方法有返回值并且可以抛异常,而Runnable的run方法就没有返回值也没有抛异常,也就是可以知道执行线程的时候除了什么错误。 - Callable运行后可以拿到一个Future对象,这个对象表示异步计算结果,可以从通过Future的get方法获取到call方法返回的结果。但要注意调用Future的get方法时,当前线程会阻塞,直到call方法返回结果。
说一下CAS锁机制?
问题回答
- CAS(Compare and Swap 比较并交换),是一种无锁算法,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
- CAS算法涉及到三个操作数
- 需要读写的内存位置(V)
- 进行比较的预期原值(A)
- 拟写入的新值(B)
3.如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,否则处理器不做任何操作。
说一下ConcurrentHashMap?
问题回答
- ConcurrentHashMap是Java中的一个线程安全且高效的HashMap实现
- JDK1.7 ConcurrentHashMap 由Segment 数组+HashEntry 组成,也就是数组+链表。
- JDK1.7 ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。支持 N 个 Segment 这么多次数的并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。
- JDK1.8抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性
- JDK1.8将HashEntry改为了Node,和 1.8 HashMap 结构类似,当链表节点数超过指定阈值的话,会转换成红黑树的。
看看图片
jdk 1.7 ConcurrentHashMap
jdk 1.7 ConcurrentHashMap
jdk1.8 ConcurrentHashMap
jdk1.8 ConcurrentHashMap
说一下Threadlocal关键字?
问题回答
- 线程本地变量,可以为变量在每个线程中都创建一个副本,使每个线程都可以访问自己内部的副本变量
说一下乐观锁和悲观锁的区别?
问题回答
- 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。
synchronized、Lock属于悲观锁。Lock有三种实现类:ReentrantLock、ReadLock(读锁)和WriteLock(写锁)。 - 乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁。
- CAS属于乐观锁。
- 悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
- 悲观锁对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
- 乐观锁不会上锁,在更新时会判断数据有没有被修改,一般会使用“数据版本机制”或“CAS操作”来实现。
数据版本机制
实现数据版本一般有两种,第一种是使用版本号,第二种是使用时间戳。以版本号方式为例。
版本号方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。
当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。核心SQL代码:
update table set xxx=#{xxx}, version=version+1 where id=#{id} and version=#{version};
CAS操作
CAS(Compare and Swap 比较并交换),当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS操作中包含三个操作数——需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,否则处理器不做任何操作。
说一下事务特性?
问题回答
- 事务特性指的就是ACID。
- 分别是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。
- 分别解释下:
- 原子性:原子性是指事务包含的操作要么全部成功,要么全部失败。因此事务的操作成功就必须要完全应用到数据库。
- 一致性:一致性强调的是数据是一致性的。假设用户A和用户B两者的钱加起来一共是5000,那么不管A还是B如何转账,转几次账,事务结束后两个用户的钱加起来应该还是5000,这就是事务的一致性。
- 隔离性:当多个用户并发访问数据库时,多个并发事务是相互隔离的。事务之间不能相互干扰。
- 持久性:一个事务一旦被提交了,那么对数据库中的数据改变是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作
简要理解
- 也就是acid。
- 分别是原子性、一致性、隔离性、持久性。
- 原子性,要么同时成功要么同时失败。
- 一致性,数据应该是一致的。
- 隔离性,多个并发事务是相互隔离的。
- 持久性,事务提交,对数据的改变是永久的。
关于几个特性,补充理解
- 原子性,算是事务最基本的特性了。
- 一致性,感觉像事务的目标,其他的三个特性都是为了保证数据一致性存在的。
- 隔离性,为了保证并发情况下的一致性而引入,并发状态下单靠原子性不能完全解决一致性的问题,在多个事务并发进行的情况下,即使保证了每个事务的原子性,仍然可能导致数据不一致。比如,事务1需要将100元转入帐号A:先读取帐号A的值,然后在这个值上加上100。但是,在这两个操作之间,另一个事务2将100元转入帐号A,为它增加了100元。那么最后的结果应该是A增加了200元。但事实上,事务1最终完成后,帐号A只增加了100元,因为事务1覆盖了事务2的修改结果。
- 持久性,好理解,事务一旦提交,对数据库的影响是永久的,保证所有操作都是有效。
看图
事务特性
说一下互斥锁/读写锁?
问题回答
- 上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁,就是具体的实现。
- 一次只能一个线程拥有互斥锁,其他线程只有等待
- 互斥锁在Java中的具体实现就是ReentrantLock。
- 读写锁在Java中的具体实现就是ReadWriteLock。
说一下偏向锁/轻量级锁/重量级锁?
问题回答
- 这三种锁是指锁状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
- 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
- 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
- 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。
说一下公平锁/非公平锁?
问题回答
- 公平锁是指多个线程按照申请锁顺序来获取锁。
- 非公平锁是指多个线程获取锁的顺序并不是按照申请锁顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
- 对于Java ReetrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
- 对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
说一下分段锁?
问题回答
- 分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
- 我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7和JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
- 当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对分段加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行插入。
- 但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
- 分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
说一下可重入锁?
问题回答
- 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
- 对于Java ReetrantLock而言,从名字就可以看出是一个重入锁,其名字是Re entrant Lock 重新进入锁。
- 对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。
看代码理解可重入锁
可重入锁
上面的代码就是一个可重入锁的一个特点。如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。
说一下对象锁和类锁?
问题回答
- java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,实际区别大
- 对象锁是用于对象实例方法,或者一个对象实例上的
- 类锁是用于类的静态方法或者一个类的class对象上的。
- 我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的
说一下死锁?
问题回答
- Java发生死锁的根本原因是:在申请锁时发生了交叉闭环申请。即线程在获得了锁A并且没有释放的情况下去申请锁B,这时,另一个线程已经获得了锁B,在释放锁B之前又要先获得锁A,因此闭环发生,陷入死锁循环。
说一下独享锁/共享锁?
问题回答
- 独享锁是指该锁一次只能被一个线程所持有。
- 共享锁是指该锁可被多个线程所持有。
- 对于Java ReentrantLock(重入锁)而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
- 读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。
- 独享锁与共享锁也是通过AQS(AbstractQuenedSynchronizer抽象的队列式同步器)来实现的,通过实现不同的方法,来实现独享或者共享。
- 对于Synchronized而言,当然是独享锁。
说一下自旋锁?
问题回答
- 在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁
- 优点是减少线程上下文切换的消耗
- 缺点是循环会消耗CPU。
需要更全面的Word文档的请点主页私信【学习】领取免费的哦