一、简述 synchronized 和 ReentrantLock 的区别
- 使用方法不同
Synchronized 可以用来修饰普通方法,静态代码块和普通代码块
ReentrantLock 只能在普通代码块上使用
- 获取锁和释放锁的方式不同
Synchronized 会自动的加锁和释放锁,当进入synchronized 修饰的代码块之后会自动的加锁,当离开代码块的时候会自动的释放锁
ReentrantLock 需要手动的加锁和释放锁
lock() 方法加锁, unlock() 方法释放锁
unlock() 释放锁一定要放在 finally中,否则有可能会出现锁一直被占用,从而导致其他线程一直阻塞等待的
- Synchronized 是非公平锁,而 ReentrantLock 既可以是公平锁,也可以是非公平锁,默认是非公平锁,也可以手动指定为公平锁
- 中断响应不同
ReentrantLock 可以使用 lockInterruptibly 获取锁并响应中断指令,而 synchronized 不能响应中断,如果发生了死锁就会一直等待下去,而使用 ReentrantLock 可以响应中断并释放锁,从而解决死锁的问题。
- 底层实现不同
synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的
AQS,全称是 AbstractQueuedSynchronizer,中文译为抽象队列式同步器。这个抽象类对于JUC并发包非常重要,JUC包中的ReentrantLock,,Semaphore,ReentrantReadWriteLock,CountDownLatch 等等几乎所有的类都是基于AQS实现的。
二、volatile 关键字的作用
- volatile 是java 中的关键字,是一个变量修饰符,常常被用来修饰需要被不同线程访问和修改的变量,
- 被 volatile 修饰的变量具有可见性,当一个线程修改了该变量的值时,其他线程如果对该变量进行操作的时候会从内存中重新读取该变量的数据,可以保障了数据的有效性,避免内存可见性造成的线程安全问题(bug)
- volatile 只能保证单次读/写操作的原子性(针对一条指令),不能保证多步操作的原子性。例如:修改一个变量的值:第一步将数据从内存中读取到寄存器中,cpu 从寄存器中读取数据,对数据进行处理后重新写入到内存,我们可以把三步操作看作是修改变量的一个事件,使用 volatile 不能保证该事件的原子性,可能执行第一步的时候cpu 就执行了别的线程,如果别的线程也对同一变量进行修改,就会造成bug , 针对这个问题我们需要使用 synchrnized 对事件进行加锁,即可保证事件的原子性,此时其他线程如果也需要对同一变量进行操作,只能阻塞等待当前线程将事件处理完毕。
- 在 Java 内存模型中,允许编译器和处理器对指令进行重排序(最优的处理效率),重排序过程不会影响到单线程程序的执行结果,但是可能会影响到多线程并发执行的正确性。volatile 修饰的变量的读写指令不能和其前后的任何指令重排序,其前后的指令可能会被重排序, volatile 关键字可以禁止指令重排序,
三、wait() 和 sleep() 的区别
- wait是Object类中的一个方法,sleep是Thread类中的一个方法;
- wait必须在synchronized修饰的代码块或方法中使用,sleep方法可以在任何位置使用;
- wait被调用后当前线程进入BLOCK状态并释放当前对象锁,并可以通过notify和notifyAll方法进行唤醒;sleep被调用后当前线程进入TIMED_WAIT状态,不涉及锁相关的操作;
四、线程池的执行流程和拒绝策略
线程池的执行流程有 3 个重要的判断点:
判断当前工作的线程数是否小于核心线程数
判断当前任务队列是否已满.
判断当前线程数是否已达最大线程数.
如果在经过上述三个过程后, 得到的结果都是 true , 那么就会执行线程池的拒绝策略.
拒绝策略有四种,是封装在 ThreadPoolExecutor类中的静态方法
AbortPolicy:中止策略,线程池会抛出RejectedRxecutionException异常并中止执行此任务.
CallerRunsPolicy:把任务交给添加此任务的(main)线程来执行.
DiscardPolicy:忽略此任务,忽略最新的一个任务.
DiscardOldestPolicy:忽略最早的任务,最先加入队列的任务
五、Callable 带返回值的任务
创建一个匿名内部类,实现 Callable接口. Callable带有泛型参数.泛型参数表示返回值的类型.
重写 Callable的 call()方法,完成累加的过程.直接通过返回值返回计算结果.
把 callable实例使用 FutureTask包装一下.
创建线程,线程的构造方法传入 FutureTask .此时新线程就会执行 FutureTask内部的
Callable的call方法,完成计算.计算结果就放到了 FutureTask对象中.
在主线程中调用 futureTask.get()能够阻塞等待新线程计算完毕.并获取到 FutureTask中的结果.
可以看到,使用 Callable和 FutureTask之后,代码简化了很多,也不必手动写线程同步代码了.
Callable<Integer>callable=newCallable<Integer>() { @OverridepublicIntegercall() throwsException { intsum=0; for (inti=1; i<=1000; i++) { sum+=i; } returnsum; } }; FutureTask<Integer>futureTask=newFutureTask<>(callable); Threadt=newThread(futureTask); t.start(); intresult=futureTask.get(); System.out.println(result);
Callable和 Runnable相对,都是描述一个 “任务”. Callable描述的是带有返回值的任务,Runnable描述的是不带返回值的任务.Callable通常需要搭配 FutureTask来使用. FutureTask用来保存 Callable的返回结果.因为Callable往往是在另一个线程中执行的,啥时候执行完并不确定.FutureTask就可以负责这个等待结果出来的工作.
理解 FutureTask
想象去吃麻辣烫.当餐点好后,后厨就开始做了.同时前台会给你一张 “小票” .这个小票就是FutureTask.后面我们可以随时凭这张小票去查看自己的这份麻辣烫做出来了没.
相关面试题
介绍下 Callable是什么
Callable是一个 interface .相当于把线程封装了一个 “返回值”.方便程序猿借助多线程的方式计算结果.
Callable和 Runnable相对,都是描述一个 “任务”. Callable描述的是带有返回值的任务,Runnable描述的是不带返回值的任务.
Callable通常需要搭配 FutureTask来使用. FutureTask用来保存 Callable的返回结果.因为Callable往往是在另一个线程中执行的,啥时候执行完并不确定.