并发编程面试题
一.volatile关键字有什么用途,和Synchronize有什么区别
volatile是一个轻量级的Synchronize,保证了共享变量的可见性,能够防止脏读,被volatile关键字修饰的变量,如果值发生了改变,其他线程立刻可见
volatile能保证数据可见性,但是无法保证数据的原子性
Synchronize既能保证数据可见,也能保证数据原子性
场景:
1.volatile关键字不能修饰写入操作依赖于当前的值count++; count+=1; , 不是原子操作,JVM字节码层面不是一部操作
2.volatile可以禁止指令重排,JVM相关的优化没了,效率变低
二.什么是指令重排序
指令重排的分类:编译期重排序和运行时重排序
在JVM编译时期或者CPU执行JVM字节码时期,对现有的指令进行重排序,主要目的为了优化运行速度(在不程序运行结果的前提下)
int a = 3 //1
int b = 5; //2
int c = a + b; //3
虽然说指令重排可以调高程序的执行效率,但是在多线程环境下运行可能会影响到结果
解决办法:内存屏障
内存屏障是一个屏障指令,使CPU对屏障指令之前和之后的内存操作结果都是一样的,相当于一种约束
三.先行发生原则Happens-before
int k = 1; //男人
int j = k; //女人
先行发生原则是Java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,其实就是说在发生操作B之前,操作A所产生的影响能够被B观察到
简单来说:执行顺序的控制,后续代码可以监控到之前代码的所有操作
八大原则:
1.程序次序规则:
在一个线程内,按照代码的顺序,书写在前面的代码优先于发生书写在后面的代码
2.管程锁定原则:
一个unlock操作先行发生于后面对同一个锁的lock操作,注意是同一个锁
3.volatile原则:
对一个volatile变量的写操作先行发生于后面对该变量的读操作
4.线程启动原则:
Thread对象的start()方法优先于此线程的每一个动作
5.线程终止原则:
线程中所有的操作都优先发生于对此线程的终止
6.线程中断原则
对线程的interrupt()方法的调用先行发生于被中断线程的代码检测中断事件的发生 先中断再检测
7.对象终结原则:
一个对象的初始化(构造函数执行完毕)完成优先发生于它的finalize()方法的开始
8.传递性:
int a; //1
int b; //2
int c; //3
如果操作A先行发生于操作B,操作B先行发生于操作C,那么就可以得出来操作A优先发生于操作C
四.线程安全的三要素
1.原子性:原子性说明一个或者多个操作要么都成功,要么都失败,中间不能中断,也不存在上下文切换的问题
原子类可以避免操作数值的原子性问题,在java.util.concurrent.atomic下的原子类
如何保证线程运行的原子性:
解决办法:可以使用sync和Lock将多不操作变为原子操作,但是volatile不能保证原子性
可重入锁:对应前几章讲解案例
2.可见性:
保证多线程下,所有线程对数据操作其他线程是可见的,可以使用volatile和sync
3.有序性:
程序执行得顺序按照代码的先后顺序执行,JVM可能会对指令进行重排,不改变结果的前提下进行重排
并发编程面试题:
1.进程和线程还有协程之间的关系
进程:运行起来的程序。进程需要占用系统资源(内存,CPU)。进程是最小的系统资源分配单位,只是给线程提供执行环境。 由于一个进程产生一个进程地址空间,且进程地址空间相互独立,一个进程死亡,其他进程不会受到影响。
线程:LWP(light weight process)轻量级进程。线程是最小的执行单位。CPU分配时间轮片的对象。
协程:coroutine,也叫轻量级线程。 与传统的系统级线程和进程相比,携程最大的优势在于“轻量级”,可以轻松创建上万个而不会导致系统资源衰歇。
2.并发和并行之间的区别
并发:指统一时间内,宏观上处理多个任务
并行:指统一时间内,真正上处理多个任务
3.Java中多线程实现的方式
3.1 继承Thread类,重写run方法;
3.2 实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target
3.3 通过Callable和FutureTask创建线程
3.4 通过线程池创建线程
4.Callable和Future模式
4.1 Callable
在Java中,创建线程一般有两种方式,一种是继承Thread类,一种是实现Runnable接口。然而,这两种方式的缺点是在线程任务执行结束后,无法获取执行结果。我们一般只能采用共享变量或共享存储区以及线程通信的方式实现获得任务结果的目的。 不过,Java中,也提供了使用Callable和Future来实现获取任务结果的操作。Callable用来执行任务,产生结果,而Future用来获得结果。
不过,在java中,也提供了使用Callable和Future来实现获取任务结果的操作。Callable用来执行任务,产生结果,而Future用来获得结果;
@FunctionalInterface public interface Callable<V> { /** * Computes a result, or throws an exception if unable to do so. * * @return computed result * @throws Exception if unable to compute a result */ V call() throws Exception; }
Callable和Runnable的区别:
1.Callable能接受一个泛型,然后在call方法中返回一个这个类型的值,而Runnable的run方法没有返回值;
2.Callable的call方法可以抛出异常,而Runnable的run方法不会抛出异常;
4.2 Future
Future模式的核心在于:去除了主函数的等待时间,并使得原本需要等待的时间段可以用于处理其他业务逻辑
Futrure模式:对于多线程,如果线程A要等待线程B的结果,那么线程A没必要等待B,直到B有结果,可以先拿到一个未来的Future,等B有结果是再取真实的结果。
import java.util.concurrent.*; public class MyCallable implements Callable { @Override public Object call() throws Exception { System.out.println("callable接口中重写的call方法,可以有返回值并且抛出异常!!!"); return "callable"; } //方案二:submit(Callable task) public static void main(String[] args) throws ExecutionException, InterruptedException { MyCallable myCallable = new MyCallable(); //创建一个线程 ExecutorService executorService = Executors.newFixedThreadPool(3); //创建线程执行任务,接受任务结果 Future submit = executorService.submit(myCallable); //接受返回值,get方法会阻塞当前线程 System.out.println(submit.get()); System.out.println("利用线程池执行mycallable,完毕!!!"); //停止 executorService.shutdown(); } }
4.3 Future常用方法
V get():获取异步执行的结果,如果没有结果可用,此方法会阻塞知道异步计算完成;
V get(Long timeout,TimeUnit unit):获取异步执行结果,如果没哟结果可用,此方法会阻塞,但是会有时间限制,如果阻塞时间超过设定的timeout时间,该方法将抛出异常;
boolean isDone():如果任务执行结束,无论是正常结束或是中途取消还是发生异常,都返回true;
boolean isCanceller():如果任务完成前被取消,则返回true;
boolean cancel(boolean mayInterrupRunning):如果任务还没有开始,执行cancel方法将返回false;如果任务已经启动,执行cancel方法将以中断执行此任务线程的方式来试图停止任务,如果停止成功,返回true;
当任务已经启动,执行cancel(false)方法将不会对正在执行的任务线程产生影响(让线程正常执行到完成),此时返回false;
当任务已经启动,执行cancel方法将返回false,MayInterruptRunning参数表示是否中断执行中的线程;
实际上Future提供了三种功能:
1.能够中断执行中的任务;
2.判断任务是否执行完成;
3.获取任务执行完成后的结果;
5.线程池创建的方式
5.1 newCachedThreadPool
newCachedThreadPool:可缓存线程池,先查看池中有没有以前建立的线程,如果有,就直接使用;如果没有,就创建一个新的线程加入池中,缓存型池通常用于执行一些生存期很短的异步型任务;
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class NewCachedThreadPoolTest { //无限大侠线程池,JVM自动回收 public static void main( String[] args ){ ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); for (int i=1;i<=10;i++){ newCachedThreadPool.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }); } } }
5.2 newFixedThreadPool
newFixedThreadPool:创建一个可重用固定个数的线程池,以共享的无界队列方式来运行这些线程;
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class NewFixedThreadPoolTest { public static void main( String[] args ){ ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3); for (int i=0;i<10;i++){ newFixedThreadPool.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }); } } }
5.3 newScheduledThreadPool
newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行
import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class NewScheduledThreadPoolTest { public static void main( String[] args ) { ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3); for (int i=0;i<10;i++){ newScheduledThreadPool.schedule(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } },3, TimeUnit.SECONDS); } } }
5.4 newSingleThreadExecutor
newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行;
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class NewSingleThreadExecutorTest { public static void main( String[] args ){ ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor(); for (int i=0;i<10;i++){ final int index=i; newSingleThreadExecutor.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"正在被执行,index:"+index); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } }); } } }
6.Java当中线程状态有哪些
6.1 初始状态(New):
线程对象创建出来后,没有调用start方法,线程处于初始状态;
6.2 运行状态:
1.就绪状态(Ready):调用了Start方法,等待CPU分配资源;
2.运行状态(RUNNING):CPU分配资源给该线程,该线程处于运行状态;
6.3 阻塞状态 (BLOCKED):
线程获取资源,如果资源获取成功则正常运行,如果资源获取失败,就处于阻塞状态,等待什么时候获取到资源再变为运行状态;
6.4 等待状态 (WAITING):
线程手动调用了wait()方法,或者join()方法,这些方法都是主动进入等待状态,等待状态会将CPU资源让渡,需要其他线程手动唤醒,notify(),notifyAll()唤起所有的等待线程;
6.5 超时等待状态 (TIMED_WAITING):
与等待状态相同,都是主动进入等待,也是需要其他线程唤醒,但是区别在与超时等待,如果超过了等待时间,则自动唤醒;
6.6 终止状态(DIED):
线程结束之后的状态;
7.多线程中的常用方法
7.1 start()方法
用于启动一个线程,使相应的线程进入排队等待状态。一旦轮到它使用CPU的资源的时候,它就可以脱离它的主线程而独立开始自己的生命周期了。注意即使相应的线程调用了start方法,但相关的线程也不一定会立刻执行,调用start方法的主要目的是使当前线程进入排队等待。不一定就立刻得到cpu的使用权限...
7.2 run()方法
Thread类和Runnable接口中的run方法的作用相同,都是系统自动调用而用户不得调用的。
7.3 sleep()方法
是Java中Thread类中的方法,会使当前线程暂停执行让出cpu的使用权限。但是监控状态依然存在,即如果当前线程进入了同步锁的话,sleep方法并不会释放锁,即使当前线程让出了cpu的使用权限,但其它被同步锁挡在外面的线程也无法获得执行。待到sleep方法中指定的时间后,sleep方法将会继续获得cpu的使用权限而后继续执行之前sleep的线程。
7.4 wait()方法
是Object类的方法,wait方法指的是一个已经进入同步锁的线程内,让自己暂时让出同步锁,以便其它正在等待此同步锁的线程能够获得机会执行。,只有其它方法调用了notify或者notifyAll(需要注意的是调用notify或者notifyAll方法并不释放锁,只是告诉调用wait方法的其它 线程可以参与锁的竞争了..)方法后,才能够唤醒相关的线程。此外注意wait方法必须在同步关键字修饰的方法中才能调用。
7.5 notify()方法
唤醒在此对象监视器上等待的单个线程,使其进入“就绪状态”。
7.6 notifyAll()方法
唤醒在此对象监视器上等待的所有线程,使其进入“就绪状态”。
8.线程状态流程图
新建(new):新创建了一个线程对象。
可运行(runnable):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
运行(running):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
阻塞(block):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
等待阻塞:
运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
同步阻塞:
运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
其他阻塞:
运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
死亡(dead):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
9.volatile关键字有什么用途,和Synchronize有什么区别
volatile用途:
volatile是一个轻量级的Synchronize,保证了共享变量的可见性,能够防止脏读,被volatile关键字修饰的变量,如果值发生了改变,其他线程立刻可见
区别:
(1)volatile只能作用于变量,使用范围较小。synchronized可以用在变量、方法、类、同步代码块等,使用范围比较广。
(2)volatile只能保证可见性和有序性,不能保证原子性。而可见性、有序性、原子性synchronized都可以包证。
(3)volatile不会造成线程阻塞。synchronized可能会造成线程阻塞。
什么是脏读,为什么发生脏读
脏读是指当一个事务正在访问数据,并且对数据进行了修改。而这种修改还没有提交到数据库中,这时,另外一个事务也访问了这个数据,然后使用了这个数据。
10.先行发生原则
10.1 程序次序原则:
在一个线程内,按照代码的顺序,书写在前面的代码优先于书写后面的代码;
10.2 管程锁定规则:
一个unlock操作先行发生于后面对同一个锁的lock操作,注意是同一个锁;
10.3 volatile原则:
对于一个volatile变量的写操作先行发生于后面对变量的读操作;
10.4 线程启动原则:
Thread对象的start()方法优先于此线程的每一个动作;
10.5 线程终止原则:
线程中所有的操作都优先发生于此线程的每一个动作;
10.6 对象中断原则:
对象的interrupt()方法的调用优先发生于被中断线程的代码监测中断事件的发生;先中断再检测;
10.7 对象终结原则:
一个对象的初始化(构造函数执行完毕)完成优先发生于它的finalize()方法的开始;
10.8 传递性
如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
11.并发编程线程安全三要素
11.1 原子性(Synchronized, Lock)
即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
在Java中,基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
11.2 有序性(Volatile,Synchronized, Lock)
即程序执行的顺序按照代码的先后顺序执行。
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
11.3 可见性(Volatile,Synchronized,Lock)
指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取共享变量时,它会去内存中读取新值。
普通的共享变量不能保证可见性,因为普通共享变量被修改后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
12.进程和线程间调度算法
12.1 先来先服务(队列)
先来先服务(FCFS)调度算法是一种最简单的调度算法,该算法既可用于作业调度,也可用于进程调度。当在作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。在进程调度中采用FCFS算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理机,使之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃处理机。
缺点:比较有利于长作业,而不利于短作业。 有利于CPU繁忙的作业,而不利于I/O繁忙的作业。
12.2 最短优先(优先队列)
最短优先调度算法是指对短作业或短进程优先调度的算法。它们可以分别用于作业调度和进程调度。短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。而短进程优先(SPF)调度算法则是从就绪队列中选出一个估计运行时间最短的进程,将处理机分配给它,使它立即执行并一直执行到完成,或发生某事件而被阻塞放弃处理机时再重新调度。
缺点:长作业的运行得不到保证。
12.3 优先权调度算法
12.3.1 优先权调度算法的类型
为了照顾紧迫型作业,使之在进入系统后便获得优先处理,引入了最高优先权优先(FPF)调度算法。此算法常被用于批处理系统中,作为作业调度算法,也作为多种操作系统中的进程调度算法,还可用于实时系统中。当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程,这时,又可进一步把该算法分成如下两种。
1) 非抢占式优先权算法
在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时,系统方可再将处理机重新分配给另一优先权最高的进程。这种调度算法主要用于批处理系统中;也可用于某些对实时性要求不严的实时系统中。
2) 抢占式优先权调度算法
在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程。因此,在采用这种调度算法时,是每当系统中出现一个新的就绪进程i 时,就将其优先权Pi与正在执行的进程j 的优先权Pj进行比较。如果Pi≤Pj,原进程Pj便继续执行;但如果是Pi>Pj,则立即停止Pj的执行,做进程切换,使i 进程投入执行。显然,这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于要求比较严格的实时系统中,以及对性能要求较高的批处理和分时系统中。
12.3.2 高响应比优先调度算法
在批处理系统中,短作业优先算法是一种比较好的算法,其主要的不足之处是长作业的运行得不到保证。如果我们能为每个作业引入前面所述的动态优先权,并使作业的优先级随着等待时间的增加而以速率a 提高,则长作业在等待一定的时间后,必然有机会分配到处理机。该优先权的变化规律可描述为:
由于等待时间与服务时间之和就是系统对该作业的响应时间,故该优先权又相当于响应比RP。据此,又可表示为:
由上式可以看出:
(1) 如果作业的等待时间相同,则要求服务的时间愈短,其优先权愈高,因而该算法有利于短作业。
(2) 当要求服务的时间相同时,作业的优先权决定于其等待时间,等待时间愈长,其优先权愈高,因而它实现的是先来先服务。
(3) 对于长作业,作业的优先级可以随等待时间的增加而提高,当其等待时间足够长时,其优先级便可升到很高,从而也可获得处理机。
简言之,该算法既照顾了短作业,又考虑了作业到达的先后次序,不会使长作业长期得不到服务。因此,该算法实现了一种较好的折衷。当然,在利用该算法时,每要进行调度之前,都须先做响应比的计算,这会增加系统开销。
12.4 基于时间片的轮转调度算法
12.4.1 时间片轮转发
在早期的时间片轮转法中,系统将所有的就绪进程按先来先服务的原则排成一个队列,每次调度时,把CPU 分配给队首进程,并令其执行一个时间片。时间片的大小从几ms 到几百ms。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。这样就可以保证就绪队列中的所有进程在一给定的时间内均能获得一时间片的处理机执行时间。换言之,系统能在给定的时间内响应所有用户的请求。
12.4.2 多级反馈队列调度算法
前面介绍的各种用作进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程,而且如果并未指明进程的长度,则短进程优先和基于进程长度的抢占式调度算法都将无法使用。而多级反馈队列调度算法则不必事先知道各种进程所需的执行时间,而且还可以满足各种类型进程的需要,因而它是目前被公认的一种较好的进程调度算法。在采用多级反馈队列调度算法的系统中,调度算法的实施过程如下所述。
(1) 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。例如,第二个队列的时间片要比第一个队列的时间片长一倍,……,第i+1个队列的时间片要比第i个队列的时间片长一倍。
(2) 当一个新进程进入内存后,首先将它放入第一队列的末尾,按FCFS原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按FCFS原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第n队列后,在第n 队列便采取按时间片轮转的方式运行。
(3) 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第1~(i-1)队列均空时,才会调度第i队列中的进程运行。如果处理机正在第i队列中为某进程服务时,又有新进程进入优先权较高的队列(第1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第i队列的末尾,把处理机分配给新到的高优先权进程。
12.5 电梯调度算法
高层建筑中电梯请求不断地到来,控制电梯的计算机能够很容易地跟踪顾客按下请求的顺序。如果使用先来先服务算法调度,同时如果电梯负载很重,那么大部分时间电梯将停留在电梯的中部区域,而电梯两端区域的请求将不得不等待,直到负载中的统计波动使得中部区域没有请求位置,这样导致远离中部区域的请求得到的服务很差。因此获得最小响应时间的目标和公平性之间存在着冲突。
大多数电梯使用电梯算法来协调效率和公平性这两个相互冲突的目标。电梯算法电梯保持按一个方向移动,直到在那个方向上没有请求位置,然后改变方向。
电梯算法(elevation algorithm)需要软件维护一个二进制位,即当前方向位:向上(up)或向下(down)。当一个请求处理完成之后,电梯的驱动程序检查该位,如果是up,电梯移至下一个更高的未完成的请求。如果更高的位置没有未完成的请求,则方向位取反。当方向位设置为down时,同时存在一个低位置的请求,则移向该位置。
现在我们明白了,电梯的上下箭头按钮是为了告诉电梯你想向上还是向下去),而不是让电梯向上还是向下。
举例:电梯在上行,5楼有上召和下召。电梯会停5楼,但它是为上召服务的,所以下召灯还会保持点亮。然后启动向上,直到服务完上行的所有请求。转下行,到五楼时还是会停。这时是服务5楼下召的。
电梯处理请求规则:
电梯有移动方向,各楼层的请求有请求方向,这里维护一个请求表(记录请求ID,请求方向,该请求的停靠楼层)。因为电梯会按照移动方向移动,直到该方向没有请求(请求包括请求ID和停靠楼层的请求),所以不会根据请求方向突然改变电梯的移动方向。因此,电梯在移动过程中只处理与“电梯移动方向”相同的“请求方向”的请求。如电梯向下移动,只处理向下的请求,且该请求的方向也向下(停靠楼层请求无方向)。
13.Java开发中用过哪些锁
13.2 乐观锁
乐观锁顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS(Compare and Swap 比较并交换)实现的
乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升;
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
13.2 悲观锁
悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。比如Java里面的同步原语synchronized关键字的实现就是悲观锁。
悲观锁适合写操作非常多的场景;
悲观锁在Java中的使用,就是利用各种锁;
13.3 独享锁
独享锁是指该锁一次只能被一个线程所持有。
独享锁通过AQS来实现的,通过实现不同的方法,来实现独享锁。
对于Synchronized而言,当然是独享锁。
13.4 共享锁
共享锁是指该锁可被多个线程所持有。
读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。
共享锁也是通过AQS来实现的,通过实现不同的方法,来实现共享锁。
13.5 互斥锁
互斥锁在Java中的具体实现就是ReentrantLock。
13.6 读写锁
读写锁在Java中的具体实现就是ReadWriteLock。
13.7 可重入锁
重入锁也叫作递归锁,指的是同一个线程外层函数获取到一把锁后,内层函数同样具有这把锁的控制权限;
synchronized和ReentrantLock就是重入锁对应的实现;
synchronized重量级的锁 ;
ReentrantLock轻量级的锁;
13.8 公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。
对于Java ReetrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
13.9 非公平锁
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
13.10 分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7和JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
13.11 偏向锁
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
13.12 轻量级锁
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
13.13 重量级锁
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。
13.14 自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
14.synchronized关键字理解
使用了synchronized关键字可以轻松地解决多线程共享数据同步问题。
synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。
synchronized取得的锁都是对象;每个对象只有一个锁(lock)与之相关联;实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
1. 方法声明时使用,线程获得的是成员锁;
2. 对某一代码块使用,synchronized后跟括号,括号里是变量,线程获得的是成员锁;
3. synchronized后面括号里是一对象,此时,线程获得的是对象锁;
4. synchronized后面括号里是类,此时,线程获得的是对象锁;
15.CAS无锁机制
CAS:Compare and Swap,即比较交换;
jdk1.5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronized同步锁的一种乐观锁。jdk1.5之前java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是悲观锁;
本身无锁,采用乐观锁的思想,在数据操作时对比数据是否一致,如果一致代表之前没有线程操作该数据,那么就会更新数据,如果不一致代表有县城更新则重试;
CAS当中包含三个参数CAS(V,E,N),V标识要更新的变量,E标识预期值,N标识新值
运行过程:
1.线程访问时,先会将主内存中的数据同步到线程的工作内存当中;
2.假设线程A和线程B都有对数据进行更改,那么假如线程A先获取到执行权限;
3.线程A先会对比工作内存当中的数据和主内存当中的数据是否一致,如果一致(V==E)则进行更新,不一致则刷新数据,重新循环判断;
4.这时更新完毕后,线程B也要进行数据更新,主内存数据和工作内存数据做对比,如果一致则进行更新,不一致则将主内存数据重新更新到工作内存,然后循环再次对比两个内存中的数据,直到一致为止;
CAS无锁机制存在一个问题
ABA问题,如果将原来A的值改为了B,然后又改回了A,虽然最终结果没有发生改变,但是在过程中是对该数据进行了修改操作
解决该问题:在Java中并发包下有一个原子类:AtomicStampedReference,在该类当中通过版本控制判断值到底是否被修改
解释:如果对值进行了更改则版本号+1,那么在CAS当中不仅仅对比变量的值,还要对比版本号,如果值和版本号都相等则代表没有被修改,如果有一方不相等代表进行过更改
那么就从主内存中重新刷新数据到工作内存然后循环对比,直到成功为止~
16.AQS
AQS:全称AbstractQueueSynchronizer,抽象队列同步器,这个类在java.util.concurrent.locks包下
它是一个底层同步工具类,比如CountDownLatch,Sammphore,ReentrantLock,ReentrantReadWriteLock等等都是基于AQS
底层三个内容:
1.state(用于计数器)
2.线程标记(哪一个线程加的锁)
3.阻塞队列(用于存放阻塞线程)
AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架,如下图所示。AQS为一系列同步器依赖于一个单独的原子变量(state)的同步器提供了一个非常有用的基础。子类们必须定义改变state变量的protected方法,这些方法定义了state是如何被获取或释放的。
J.U.C是基于AQS实现的,AQS是一个同步器,设计模式是模板模式。
核心数据结构:双向链表 + state(锁状态)
底层操作:CAS
17.ReentrantLock底层实现
ReentrantLock是基于AQS的,AQS是Java并发包中众多同步组件的构建基础,它通过一个int类型的状态变量state和一个FIFO队列来完成共享资源的获取,线程的排队等待等。AQS是个底层框架,采用模板方法模式,它定义了通用的较为复杂的逻辑骨架,比如线程的排队,阻塞,唤醒等,将这些复杂但实质通用的部分抽取出来,这些都是需要构建同步组件的使用者无需关心的,使用者仅需重写一些简单的指定的方法即可(其实就是对于共享变量state的一些简单的获取释放的操作)。
无参构造器(默认为非公平锁)
public ReentrantLock() { sync = new NonfairSync();//默认是非公平的 }
sync是ReentrantLock内部实现的一个同步组件,它是Reentrantlock的一个静态内部类,继承于AQS;
带布尔值的构造器(是否公平)
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();//fair为true,公平锁;反之,非公平锁 }
看到了吧,此处可以指定是否采用公平锁,FailSync和NonFailSync亦为Reentrantlock的静态内部类,都继承于synchronized;
18.ReentrantLock和synchronized之间的区别
- synchronized 竞争锁时会一直等待;ReentrantLock 可以尝试获取锁,并得到获取结果
- synchronized 获取锁无法设置超时;ReentrantLock 可以设置获取锁的超时时间
- synchronized 无法实现公平锁;ReentrantLock 可以满足公平锁,即先等待先获取到锁
- synchronized 控制等待和唤醒需要结合加锁对象的 wait() 和 notify()、notifyAll();ReentrantLock 控制等待和唤醒需要结合 Condition 的 await() 和 signal()、signalAll() 方法
- synchronized 是 JVM 层面实现的;ReentrantLock 是 JDK 代码层面实现
- synchronized 在加锁代码块执行完或者出现异常,自动释放锁;ReentrantLock 不会自动释放锁,需要在 finally{} 代码块显示释放
19.ReentrantReadWriteLock(读写锁)
相比java中的锁(Lock in java)里Lock实现,读写锁更复杂一些;
假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时第一个资源没有任务问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其他线程对该资源进行读或写(也就是说:读和读能共享,读和写不能共享,写和写不能共享)。这就需要一个读写锁来解决这个问题。在java5中java.util.concurrent包中已经包含读写锁;
package com.wn.lock; import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; /* * 相比java中的锁Locks in java 里lock实现,读写锁更负载一些; * 假设你的程序中涉及到一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该都允许多个线程能在同时读取共享数据, * 但是如果一个线程想去写这些共享资源,就不应该再有其他线程对该资源进行读或写; * */ public class CacheTest { //创建map集合 static Map<String,Object> map=new HashMap<String,Object>(); //创建读写锁 static ReentrantReadWriteLock rwl=new ReentrantReadWriteLock(); //获取读操作 static Lock r=rwl.readLock(); //获取写操作 static Lock w=rwl.writeLock(); //获取一个key对应的value public static final Object get(String key){ r.lock(); try { System.out.println("正在做读的操作,key:"+key+"开始"); Thread.sleep(100); Object object = map.get(key); System.out.println("正在做读的操作,key:"+key+"结束"); System.out.println(); return object; } catch (InterruptedException e) { e.printStackTrace(); }finally { r.unlock(); } return key; } //设置key对应的value,并返回旧的value public static final Object put(String key,Object value){ w.lock(); try { System.out.println("正在做写的操作,key:"+key+",value:"+value+"开始"); Thread.sleep(100); Object o = map.put(key, value); System.out.println("正在做写的操作,key:"+key+",value:"+value+"结束"); System.out.println(); return o; } catch (InterruptedException e) { e.printStackTrace(); }finally { w.unlock(); } return value; } public static void main(String[] args){ new Thread(new Runnable() { @Override public void run() { for (int i=0;i<3;i++){ CacheTest.put(i+"",i+""); } } }).start(); new Thread(new Runnable() { @Override public void run() { for (int i=0;i<3;i++){ CacheTest.get(i+""); } } }).start(); } }
20.BlockingQueue阻塞队列的实现方式
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列,这两个附加的操作是:
在队列为空时,获取元素的线程会等待队列变为非空;
当队列满时,存储元素的线程会等待队列可用;
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器拿元素;
在java中,BlockingQueue的接口位于java.util.concurrent包中,阻塞队列是线程安全的;
在新增呢的concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题,通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利;
常用的队列主要由以下两种:
1.先进先出(FIFO):先插入的队列的元素也最先出队列,类似于排队的功能,从某种程度上来说这种队列也体现了一种公平性;
2.后进后出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件;
20.1 ArrayBlockingQueue
ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组,有边界意思就是它的容量是有限的,我们必须在其初始化的时候执行它的容量大小,容量大小一旦执行就不可改变;
ArrayBlockingQueue是以先进先出的方式存储数据,最新插入的对象是尾部,最新移除的对象是头部;
public class ArrayBlockingQueueTest { public static void main(String[] args) throws InterruptedException { ArrayBlockingQueue<String> arrays=new ArrayBlockingQueue<String>(3); arrays.add("张三"); arrays.add("李四"); arrays.add("王五"); //添加阻塞队列 arrays.offer("赵六",1, TimeUnit.SECONDS); //poll方法相当于消费了队列中的数据,队列的数据就会删除 System.out.println(arrays.poll()); System.out.println(arrays.poll()); System.out.println(arrays.poll()); System.out.println(arrays.poll()); } }
如果先出队一条数据,此时被阻塞的数据就可以添加进来
public class ArrayBlockingQueueTest { public static void main(String[] args) throws InterruptedException { ArrayBlockingQueue<String> arrays=new ArrayBlockingQueue<String>(3); arrays.add("张三"); arrays.add("李四"); arrays.add("王五"); System.out.println(arrays.poll()); //添加阻塞队列 arrays.offer("赵六",1, TimeUnit.SECONDS); //poll方法相当于消费了队列中的数据,队列的数据就会删除 System.out.println(arrays.poll()); System.out.println(arrays.poll()); System.out.println(arrays.poll()); } }
20.2 LinkedBlockingQueue
LinkedBlockingQueue阻塞队列大小的配置时可选的,如果我们初始化时指定大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE容量,它的内部是一个链表;
和ArrayBlockingQueue一样,LinkedBlockingQueue也是以先进先出的方式存储数据,最新插入的对象是尾部,最新移除的对象是头部;
public class LinkedBlockingQueueTest { public static void main(String[] args) throws InterruptedException { LinkedBlockingQueue linkedBlockingQueue=new LinkedBlockingQueue(3); linkedBlockingQueue.add("A"); linkedBlockingQueue.add("B"); linkedBlockingQueue.add("C"); System.out.println(linkedBlockingQueue.poll()); System.out.println(linkedBlockingQueue.size()); } }
20.3 PriorityBlockingQueue
PriorityBlockingQueue是一个没有边界的队列,它的排序规则和java.util.PriorityQueue一样。需要注意,PriorityBlockingQueue中国允许插入null对象;
所有插入PriorityBlockingQueue的对象必须实现java.lang.Comparable接口,队列优先级的排序规则就是按照我们对这个接口的实现来定义的;
另外,我们可以从PriorityBlockingQueue获得一个迭代器Iterator,但这个迭代器并不保证按照优先级顺序进行迭代;
public class PriorityBlockingQueueTest { public static void main(String[] args) throws InterruptedException { PriorityBlockingQueue<String> priorityBlockingQueue=new PriorityBlockingQueue<String>(3); priorityBlockingQueue.add("AA"); priorityBlockingQueue.add("BB"); priorityBlockingQueue.add("CC"); System.out.println(priorityBlockingQueue.poll()); System.out.println(priorityBlockingQueue.size()); } }
20.3 SynchronousQueue
SynchronousQueue队列内部仅容纳一个元素,当一个线程插入一个元素后会被阻塞,除非这个元素被另一个线程消费;
21.ConcurrentLinkedQueue
ConcurrentLinkedQueue:是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能好于BlockingQueue,它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。头是最先加入的,尾是最近加入的,该队列不允许null元素;
ConcurrentLinkedQueue重要方法:
add()和offer()都是加入元素的方法(在ConcurrentLinkedQueue中这两个方法没有任务区别);
poll()和peek()都是取头元素节点,区别在于前者会删除元素,后者不会;
package com.wn.Queue; import java.util.concurrent.ConcurrentLinkedDeque; public class ConcurrentLinkedDequeTest { public static void main(String[] args) throws InterruptedException { ConcurrentLinkedDeque q=new ConcurrentLinkedDeque(); q.offer("1"); q.offer("2"); q.offer("3"); //从头获取元素,删除该元素 System.out.println(q.poll()); //从头获取元素,不删除该元素 System.out.println(q.peek()); //获取总长度 System.out.println(q.size()); } }