一、基础知识
1、什么是线程
线程是操作系统能进行运算调度的最小单位。它被包含在进程中,一个进程中可以并发多个线程,每天线程可以执行不同的任务。线程的出现是为了更加合理的利用CPU资源。
2、为什么使用并发编程
提升多核CPU的利用率:一般来说一台机器会有多个CPU核心,我们可以创建多个线程,理论上讲操作系统可以把多个线程分配给不同的CPU去执行,每个CPU执行一个线程,每个CPU执行一个线程,这样就可以提高CPU的使用率,如果使用单线程就只能有一个CPU核心被使用。
面对复杂业务模型,并行程序会比串行程序更适合业务需求,而并发编程更能吻合这种业务拆分。
简单说就是:
充分利用多核CPU的计算能力
方便进行业务拆分,提升应用性能
3、并发编程的缺点
并发编程的目的是为了能提高程序的执行效率,提高运行速度,但是并发编程并总是能提升运行速度的,而且并发编程可能遇到很多问题,比如内存泄漏,上下文切换,线程安全,死锁等问题。
4、并发编程的三个必要因素
原子性:原子,即一个不可再被分隔的颗粒。原子性指一个或多个操作要么全部执行成功,要么全部执行失败。
可见性:一个线程对一个共享变量的修改,另一个线程能够立刻看见(synchronized,volatile)
有序性:程序执行的顺序按照代码的先后顺序执行(处理器可能会对指令进行重排序)
5、Java中怎么保证多线程的运行安全
出现线程安全问题的原因一般有三点:
线程切换带来的原子性问题。解决方法:使用线程之间同步synchronized或者使用锁(Lock)
缓存导致的可见性问题。解决办法:synchronized,volatile,Lock等可以解决可见性问题
编译优化带来的有序性问题。解决办法:Happens-before规则可以拮据有序性问题
6、并行和并发有什么区别
并发:多个任务在同一个CPU核上,按细分的时间片轮流(交替)执行,从逻辑上看那些任务是同时执行。两个人用一台电脑,轮流使用
并行:单位时间内,多个处理器或者多核处理器同时处理多个任务,是真正意义上的“同时进行”。两个人用两台电脑,独自使用。
串行:有n个任务,由一个线程按照顺序执行。由于一个方法,任务都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。两人排队使用一台电脑。
7、什么是多线程,优点,缺点
多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程执行不同的任务。
优点:提高CPU的利用率,在多线程程序中,一个线程必须等待的时候,CPU可以运行其他的线程而不用等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程
来完成各自的任务。
缺点:线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;多线程需要协调和管理,所以需要CPU时间跟踪线程;线程对共享资源的访问会相互影响,必须解决竞用共享资源的问题。
8、线程和进程的区别
进程:一个在内存中运行的应用程序。每个正在系统上运行的程序都是进程。
线程;进程中的一个执行任务(控制单元),他负责在程序里独立执行。
一个进程至少一个线程,一个进程可以运行多个线程,多个线程可以共享数据。
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
资源开销:每个进程都有独立的代码运行和数据空间(程序上下文),程序之间的切换会有比较大的开销;同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器,线程之间的切换开销小。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线,而是多条(线程)共同完成的。线程是进程的一部分,线程也可以叫做轻权进程或轻量级进程。
内存分配:同一个进程的线程共享本进程的内存空间和资源,而进程和进程之间的地址空间和资源是独立的
影响关系:一个进程奔溃后在保护模式下不会对其他进程产生影响,但是一个线程奔溃可能导致整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程都有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中。
9、什么是上下文切换
一般多线程个数都会大于CPU核个数,而一个CPU核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU采用的策略是为每个线程分配时间片并交替的执行。当一个线程的时间片用完的时候就会重新处于就绪状态
让给其他线程使用,这个过程就是一次上下文切换。
概括来说就是:当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次再切回这个任务时,可以在加载这个任务的状态。任务从保存到加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的。在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以上下文切换需要耗费大量CPU时间。可能是操作系统中时间消耗最大的操作。
Linux其上下文切换和模式切换的时间消耗非常少。
10、守护线程和用户线程区别
用户线程:运行在前台,执行具体的任务,如程序的主程序、链接网络的子线程等都是用户线程。
守护线程:运行在后台,为其他前台线程服务。一旦所有用户线程结束,守护线程会随JVM一起结束。
11、如何在Windowshe Linux上查找哪个线程CPU利用率最高
直接使用JDK自带的工具“jconsole”,“visualVM”。JDK bin目录下。
12、什么是线程死锁
死锁是指两个或这两个以上的线程在执行过程中,由于竞争资源或者彼此通信而造成的一种阻塞现象,若无外力,它们都将无法推进下去。互相等待的线程(进程)称为死锁线程(进程)。
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期的阻塞,因此程序不可能正常终止。
线程A持有资源2,线程B持有资源1,它们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
13、形成死锁的四个必要条件
互斥条件:在一段时间内某资源只有一个进程占用。如果此时还有其他进程请求该资源,就只能等待,直至占有资源的进程用完释放。
占有且等待条件(请求和保持条件):进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已经被其他进程所占有,此时请求进程阻塞,但又对自己获的其他资源保持不放。
不可抢占条件(不可剥夺条件):某一进程已占有某项资源,你不能因为自己也需要该资源,就去抢占别人的资源,只能等待持有资源的进程自己释放。
循环等待条件:若干进程之间形成的一种头尾相接的循环等待资源的关系(A等B,B等C,C等A)。
15、如何避免线程死锁
避免一个线程同时获取多个锁。
避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
16、创建线程的四种方式
(1)继承Thread类
public class MyThread extends Thread{
@Override
public void run(){
System.out.println("run method")
}
}
(2)实现Runnable接口
public class MyRunnable implements Runnalble{
@Override
public voide run(){
System.out.println("runnable interface implemetns")
}
}
(3)实现Callable接口
public class MyCallable implements Callable{
@Override
public Integer call(){
System.out.println("callable interface implements")
}
}
(4)使用匿名内部类
Thread myThread=new Thread(new Runnalble(){
public void run(){
System.out.println("thread new runnable")
}
});
myThread.start();
17、Runnable和Callable区别
相同点:都是接口;都可以编写多线程(创建一个线程);都采用thread.start启动。
区别:Runnable接口run方法无返回值;Callable接口call有返回值,是个泛型,和Future和FutureTask配合可以用来获取异步执行结果。
Runnable接口run方法只能抛出运行时异常,且无法捕获处理;Callable接口call方法允许抛出异常,可以捕获异常信息。
注:Callable接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
18、线程的run()和start()区别
每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。
start()方法用于启动线程,run()方法用于执行线程的运行时代码。run()方法可以重复调用,而start()只能调用一次。
start()方法启动一个线程,真正实现了多线程运行。调用start()方法无需等待run()方法体代码执行完毕,可以继续执行其他代码;此时线程是处于就绪状态,并没有运行。然后通过此Thread类调用run()方法来完成其运行状态,
run()方法运行结束,此线程终止。然后CPU调度其他线程。
run()方法是本线程里面的,只是线程的一个函数,而不是多线程的。如果调用run()方法,其实就相当于调用了一个普通函数而已,直接调用run()方法,必须等待run()方法执行完毕后才能执行下面的代码,所以执行路径还是
只有一条,所以多线程执行时要用start方法,而不是run()方法。
19、为什么调用start()方法时会执行run()方法,为什么不能直接调用run()方法
new一个Thread,线程进入新建状态。调用 start()方法会启动一个线程并使线程进入就绪状态,当分配到时间片后就可以开始运行了。start()方法会执行线程的相应准备工作,然后自动执行run()方法的内容,这就是真正的多线程工作。
而直接执行run()方法,会把run()方法当做main线程下的一个普通函数去执行,并不会在某个线程中执行她,所以这并不是多线程工作。
总结:调用start可以启动线程并使线程进入就绪状态,而run方法只是thread的一个普通调用,还是在主线程里面。
20、什么是Callable和Future
Callable接口类似于Runnable,Runnable无法返回结果,并且无法抛出返回结果的异常。而Callable功能更强大一些,被线程执行后可以返回值,Future可以拿到异步执行任务的返回值。
Future接口表示异步任务,是一个可能还没有完成的异步任务结果。所以说Callable用于产生结果,Future用于返回结果。
21、什么是FutureTask
FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步执行任务的结果等待获取,判断是否已经完成,取消任务等操作。只有当运算完成的时候结果才能取回,如果任务尚未完成get
方法将会阻塞。一个FutureTask对象可以对调用Callable和Runnable的对象进行包装,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。
22、线程的状态
图见Process-高并发
新建(new):新创建一个线程对象。
就绪(runnable)可运行状态:线程对象创建后,当调用线程对象的start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。
运行(running):可运行状态(runnable)的线程获取了CPU的时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要进入运行状态,首先必须进入就绪状态中;
阻塞(block):处于运行状态的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调度进入运行状态。
阻塞的情况分为三种:
(1)等待阻塞:运行状态中的线程执行wait()方法,JVM会把该线程放入等待队列(waiting queue)中,线程进入到等待阻塞状态;
(2)同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),则JVM会把线程放入锁池(lock pool)中,线程进入同步阻塞状态;
(3)其他阻塞:通过调用sleep()方法或者join()或发出I/O请求时,线程会进入到阻塞状态。当sleep状态超时,join()等待线程终止或超时,或者IO处理完毕,线程重新进入就绪状态。
死亡(dead)结束:线程run(),main()方法执行结束,或者因为异常退出了run()方法,则该线程生命周期技术。死亡的线程不可再次发生。
23、Java中用到的线程调度算法是什么
分时调度模型和抢占调度模型
分时调度模型是所有线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU的时间片。
Java虚拟机采用抢占式调用模型,是指让可运行池中优先级高的线程占用CPU,如果可运行池中的优先级相同,那么久随机选择一个线程,使其占用CPU。处于运行状态的CPU会一直运行,直至不得不放弃CPU。
24、线程的调度策略
线程调度器会选择优先级最高的线程执行,但是如果发生以下情况,就会终止线程的运行。
(1)线程体中调用了yield(暂停当前线程,执行其他线程)方法让出来cpu占用权。
(2)线程体中调用了sleep方法,使线程进入睡眠状态。
(3)线程由于IO操作受阻
(4)另外一个更高优先级线程出现。
(5)在支持时间片的系统中,该时间片用完。
25、什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing)
线程调度器是一个操作系统服务,他负责为可运行状态(Runnable)线程分配CPU时间。一旦我们创建一个线程并启动它,它的执行变依赖线程调度器的实现。
时间分片是指将可用的CPU时间段分配给可用的(Runnable)线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。
线程调度并不受到java虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)
26、请说出与线程同步以及线程调度相关的方法
(1)wait():使一个线程处于等待(阻塞)状态,并且释放所有的对象的锁;
(2)sleep():使一个正在运行的线程进入睡眠状态,是一个静态方法,调用此方法要处理Interrupted异常;
(3)notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而且是由JVM确定唤醒哪个线程,而且与优先级无关;
(4)notifyAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给线程,而是让他们竞争,只有获得锁的线程才能进入就绪状态。
27、sleep和wait()区别
两者都可以暂停线程的执行。
类的不同:sleep是Thread的静态方法,而wait是Objeect类的方法。
是否释放锁:sleep不释放锁,wait释放锁。
用途不同:wait通常被用于线程交互/通信,而sleep通常被用于暂停线程。
用法不同:wait方法被调用后线程不会自动苏醒,需要别的线程调用一个对象上的notify或者notifyAll方法。sleep方法执行完成后,线程会自动苏醒。或者可以用wait(long timeout)超时后线程会自动苏醒。
28、你是如何调用wait方法的,使用if块还是循环块,为什么
wait方法是让运行这行代码的线程等待,而不是monitor这个线程等待。
处于等待状态的线程可能会收到错误报警和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。
唤醒后再次进入while条件判断,避免条件谓词发生改变而继续处理业务逻辑的错误.
如果采用if判断,当线程从wait中唤醒时,那么将执行处理其他业务逻辑代码,但这时候可能出现,条件谓词已经不满足业务逻辑的条件,从而出现错误结果,于是有必要在进行一次判断。
wait()方法应该在循环调用,因为线程获取到CPU开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。
synchronized (monitor) {
//判断条件谓词是否得到满足
while(!locked) {
//等待唤醒
monitor.wait();
}
//处理其他的业务逻辑
}
29、为什么线程通信的方法,wait(),notify(),notifyAll都被定义在Object类里
因为java所有的类都继承了Object,Java想让任何对象都可以作为锁,并且wait(),notify()等方法用于等待对象的锁或者唤醒线程,在Java的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。
30、为什么wait(),notify(),notifyAll()必须在同步方法或者同步块中调用
当一个对象调用wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他对象调用这个线程上的notify方法。同样的,当一个对象需要调用线程的notify时,会释放这个对象的锁,
以便其他在等待的线程可以获取到这个对象锁。由于所有的对象都需要线程持有对象锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。
31、Thread类中的yield方法有什么作用
使当前线程从执行状态(运行状态)变为可执行状态(就绪状态)。
当前线程进入了就绪状态,
32、为什么Thread类sleep()和yield()方法是静态的
Thread类的sleep()和yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线上调用这些方法是没有意义的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。
33、线程的sleep()方法和yield()方法有什么却别
(1)sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程已运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会。
(2)线程执行sleep()方法后转入如阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
(3)sleep()方法声明抛出InterruptedException,而yield()方法没有声明异常;
(4)sleep()方法比yield()方(跟操作星通CPU调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。
34、如何停止一个正在运行的线程
使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
使用stop方法强行终止,但是不推荐这个方法,因为stop和supend及resume一样都是过期作废的方法。
使用interrupt方法中断线程。
35、Java中interrupted和isInterrrupted方法的区别
interrupt:用于中断线程。调用该方法的线程的状态将被置位“中断”状态。
注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态位并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException的方法)就是监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。
interrupted:是静态方法,查看当前中断信号是 true还是false并且清除中断信号。如果一个线程被中断了,第一次调用interrupted则返回true,第二次和后面的就返回false了(线程复位)。
isInterupted:是可以返回当前中断信号是true还是false。
36、什么是阻塞式方法
阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket的accpet()方法就是一直等待客户端连接。这里的阻塞指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。
37、Java中怎么阻塞唤醒一个阻塞的线程
首先,wait(),notify()方法是针对对象的,调用任意对象的wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取该对象的锁,直到获取成功才能往下执行;
其次,wait,notify方法必须在synchronized块或方法中被调用,并且要保证同步块或方法的锁对象与调用wait,notify方法的对象是同一个,如此一来在调用wait之前当前线程就已经成功获取某对象的锁,执行wait阻塞后当前线程就将之前获取的对象锁释放。
38、notify()和nofityAll()有什么区别
如果线程调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
notifyALl()会唤醒所有的线程,notify()只会唤醒一个线程。
notifyAll()调用后,会将全部线程由等待池移到锁池,然后参与锁竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。
39、如何在两个线程间共享数据
在两个线程间共享变量即可实现共享
一般来说,共享变量要求变量本身是线程安全的,然后在线程内使用的时候,如果有对共享变量的复合操作,那么也得保证复合操作的线程安全性。
40、Java如何实现多线程之间的通讯和协作
可以通过中断和共享变量的方式实现线程间的通讯和协作
比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。
因此,一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。同样的当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。这种互相通信的过程就是线程间的协作。
Java中线程通信协作的最常见方式:
(1)synchronized加锁的线程的Object类的wait()/notify/notifyAll()
(2)ReentrantLock类加锁的线程的Condition类的await()/signal()/signalALL()
线程间直接的数据交换:
(3)通过管道进行线程间通信:字节流,字符流。
41、同步方法和同步快,哪个更好
同步块是更好的选择,因为他不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕整个类中有很多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得整个对象上的锁。
同步块更符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说可以避免死锁。
同步的范围越小越好。
42、什么是线程同步和线程互斥,有哪几种实现方式
当一个线程对共享数据进行操作时,应使之成为一个“原子操作”,即在没有完成相关操作事前,不允许其他线程打断它,否则,就会破坏数据的完整性,必然会得到错误的处理结果,这就是线程的同步。
在多线程应用中,考虑不同线程之间的数据同步和防止死锁。当两个或多个线程之间同时等待对方释放资源的时候就会形成线程之间的死锁。为了防止死锁的发生,需要通过同步来实现线程安全。
线程互斥是指对于共享的进程系统资源,在各个单线程访问它时的排他性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其他要使用该资源的线程必须要等待,直到占用资源者,释放该资源。线程同步可以看成一种特殊的线程同步。
线程间的同步方法大体可以分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态和用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。
用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。
实现线程同步的方法
同步代码方法:synchronized关键字修饰的方法。
同步代码块:synchronized修饰的代码块
使用特殊变量域volatile实现线程同步:volatile关键字为域变量的访问提供了一种勉锁机制。
使用重入锁实现线程同步:ReentrantLock类是可重入,互斥,实现了Lock接口的锁,它与synchronized方法具有相同的基本行为和语义。
43、在监视器(Monitor)内部,是如何做线程同步的,程序应该做到哪种级别的同步
在Java虚拟机中,监视器和锁在java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。
一旦方法或者代码被synchronized修饰,那么这部分就放入了监视器的监视区域,确保每一次都只有一个线程执行该部分代码,线程在获取锁之前不允许执行该部分代码。
另外java换提供了显示监视器(LOck)和隐式监视器(synchronized)两种锁方案。
44、如果提交任务,线程池队列已满,会发生什么
两种情况
(1)如果使用的无界队列LinkedBlockingQueue,可以继续添加到阻塞队列中等待执行。
(2)如果使用的有界队列ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue也满了,会根据maximumPoolSize的值增加数量,如果增加了数量还是处理不过来,ArrayBlockingQueue继续满,那么久随机选择一个线程,使其占用CPU。处于运行状态的CPU会一直运行,直至不得不放弃CPU。
则会使用拒绝策略RejectedExecutionHandler处理满的任务,默认AbortPolicy。
45、在java程序中怎么保证多线程的运行安全。
使用安全类,如java.util.concurrent下的类,使用原子类AtomicInteger
使用自动锁synchronized
使用手动锁Lock,Lock lock=new ReentrantLock();lock.lock();lock.unlock();
46、线程类的构造方法,静态块是被哪个线程调用的。
线程类的构造方法,静态块是被new这个线程所在的线程所调用的,而run()方法里面的代码块才是被线程自身锁调用的。
47、线程数过多会造成什么影响
线程的生命周期开销非常高
消耗过多的CPU,大量空闲的线程会占用许多内存,给垃圾回收带来压力,而且大量的线程在竞争CPU资源时还会产生其他性能开销。
降低稳定性JVM,OutOfMemoryError
二、并发理论
1、Java内存模型
见图process-高并发
共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中(main memory)中,每个CPU执行一个线程,这样就可以提高CPU的使用率,如果使用单线程就只能有一个CPU核心被使用。
线程都一个私有的本地内存(local memory),本地内存中存储了该线程用以读写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。
java内存模型简称jmm,定义了一个线程对另一个线程可见。共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据时候,可能本地线程没有及时刷新到主内存中,就会发生线程安全问题。
2、如果对象的应用被设置为null,垃圾收集器会立即释放内存?
不会,在下一个垃圾回收周期中,这个对象将被回收。
3、finalize()什么时候调用
垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的finallize方法;finallize是Object类的一个方法,protected void finalize() threow throwable{};大部分的时候不需要重载。只有在某些很特殊的情况下,比如调用了native方法(C语言写的),可以要在finalization里去调用C的释放函数。
4、什么是重排序
程序的执行顺序按照代码的先后顺序执行。
一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,进行重新排序(重排序),它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是会保证执行的结果和代码一致。
5、 重排序实际执行的指令步骤
见图process-高并发
(1)编译器优化重排,编译器再不改变单线程语义的前提下,可以重新安排语句的执行顺序。
(2)指令级并行重排,现代处理器采用了指令级并行技术(ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改语句对应机器指令的执行顺序。
(3)内存系统的重排,由于处理器使用缓存和读写缓存区,这使得加载存储操作看上去可能实在了乱序执行。
重排序对于单线程没问题,但是多线程可能会导致多线程程序内存可见性问题。
6、重排序遵守的规则
as-if-serial:
(1)不管怎么排序,结果不能改变
(2)不存在数据依赖的可以被编译器和处理器重排序
(3)一个操作依赖两个操作,这两个操作如果不存在依赖可以重排序
(4)单线程根据此规则不会有问题,但是重排序后多线程会有问题。
7、as-if-serial和happens-before规则区别
(1)as-if-serial保证单线程程序内执行结果不被改变,happens-before保证正确同步的的多线程程序的执行结果不被改变。
(2)as-if-serial语义给编写单线程程序的程序员一个幻境;单线程是按照程序的顺序来执行。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按照
happens-before指定的顺讯来执行。
(3)as-if-serial和happens-before都是为了再不改变程序执行结果的前提下,尽可能的提高程序执行的并行度。
8、并发关键字synchronized
在java中synchronized是来控制线程同步的,就是在多线程环境下,控制synchronized代码段同时不被多个线程同时执行。synchronized可以修饰类,方法,变量。
Java6只手java官方对从JVM层面底synchronized较大优化。JDK1.6对锁的实现引入了大量的优化,如自旋锁,适应性自旋锁,锁消除,锁粗化,偏向锁,轻量级锁等技术来减少锁操作的开销。
9、synchronized修饰最主要的是那种方式
修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁。
public synchronized void test(){ System.out.println("修饰实例方法"); }
修饰静态方法:也就是当前类加锁,会作用于类的所有对象实例,因为静态成员变量不属于任何一个实例对象,是类成员(static表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A
调用一个实例对象的非静态synchronized方法,而线程B调用这个实例对象所属类的静态Synchronized方法,是运行的,不会发生互斥现象,因为访问静态Synchronized方法占用的锁是当前类的锁,而非访问非静态
Synchronized方法占用的锁是当前是对象的锁。
public static synchronized void test2(){ System.out.println("修饰静态方法"); }
修饰代码块:指定加锁对象,给指定对象加锁,进入同步代码块前要获得给定对象的锁。
public void test3(){ synchronized (this){ System.out.println("修饰代码块"); } }
总结:synchronized加到static静态方法和synchronized(class)代码块上都是给Class类上加锁。synchronized加到实例方法上属于对象实例加锁,尽量不要使用synchronized(String a)因为JVM中,字符串常量池具有缓存功能。
10、说一下synchronized底层实现原理
Synchronized的语义底层是通过一个monitor(监视器锁)的对象来完成。monitorenter和monitorexit指令,monitorenter指向开始位置,monitorexit指向结束位置。
每个对象都有一个监视锁(monitor)每个synchronized 修饰过的代码当它的monitor被占用就会处于锁定状态并且尝试获取monitor的所有权,过程:
(1)如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
(2)如果线程已经占有该monitor,只是重新进入,则进入monitor数增加1。
(3)如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,在重新尝试获取monitor的所有权。
synchronized 是可以通过反汇编指令,javap命令,查看响应字节码文件。
11、synchronized可重入原理
重入锁是指一个线程获取到锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器其值为0时,表明该锁未被任何线程所持有,
其他线程可以竞争获取锁。
12、单例模式了解,解释一下双重校验锁方式实现单例
双重校验锁实例对象单列(线程安全)
双重锁机制的出现是为了解决前面同步问题和性能问题。
public class Singleton{
private volate static Singleton uniqueInstance;
private singleton(){}
public static Singleton getUniqueInstance(){
//先判断对象是否已经是实例过,如果没有实例过才进入加锁代码
if(uniqueInstance==null){
synchronized(Singleton.class){
if(uniqueInstance==null){
uniqueInstance=new Singleton();
}
}
}
return uniqueInstance;
}
}
第一次判断是否为null:
第一次判断是在Synchronized同步代码块外,理由是单例模式只会创建一个实例,并通过getInstance方法返回singleton对象,所以如果已经创建了singleton对象,
就不用进入同步代码块,不用竞争锁,直接返回前面创建的实例即可,这样大大提升效率。
第二次判断singleton是否为null:
第二次判断原因是为了保证同步,假若线程A通过了第一次判断,进入了同步代码块,但是还未执行,线程B就进来了(线程B获得CPU时间片),线程B也通过了第一次判断(线程A并未创建实例,所以B通过了第一次判断),准备进入同步代码块,假若这个时候不判断,
就会存在这种情况:线程B创建了实例,此时恰好A也获得执行时间片,如果不加以判断,那么线程A也会创建一个实例,就会造成多实例的情况。
所以,为了满足单例模式的要求,双重校验是必不可少的。
声明变量时为什么要用volatile关键字进行修饰?
volatile关键字可以防止jvm指令重排优化,使用了volatile关键字可用来保证其线程间的可见性和有序性;
需要注意uniqueInstance采用volatile关键字修饰很有必要,uniqueInstance=new singleton();这段代码其实分为三步执行。
(1)为uniqueInstance分配内存空间
(2)初始化uniqueInstance
(3)将uniqueInstance指向分配的内存地址。
由于JVM具有指令重排的特性,执行顺序有可能变成1->3->2。指令重排在单线程下不会出现问题,但是在多线程环境下会导致一个线程获取到还没有初始化的实例。例如T1执行了1和3,此时T2调用getUniqueInstance后
发现uniqueInstance不为空,因此返回uniqueInstance,但此时uniqueInstance还未被初始化。使用volatile可以禁止JVM的指令重排,保证多线程环境下也能正常运行。
13、什么是自旋
很多synchronized里面的代码都是一些很简单的代码,执行非常快。既然synchronized里面的代码执行快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次循环发现还没有获得锁,在阻塞,这样可能是一种更好的策略。
忙循环:就是用循环让一个等待线程,不像传统方法wait(),sleep(),或yield()它们都放弃了 CPU控制,而忙循环不会放弃CPU,它是在运行一个空循环。为了避免重建缓存和减少重建的时候就可以用它了。
14、多线程中Synchronized锁升级的原理
synchronized锁升级:在锁对象的对象头里有个threadid字段,在第一次访问的时候threadid为空,jvm让其持有偏向锁,并将threadid设置为其线程id,再次进入的时候会判断threadid是否与其线程id是否一致,如果一致则可以直接使用此对象,如果不一致则升级偏向锁为轻量级锁,
通过自旋循环一定次数获得锁,执行一定次数后,如果还没有正常获取到要使用的对象,此时就会把轻量级锁升级为重量级锁,此过程就构成了synchronized的锁升级。
锁升级的目的:锁升级是为了降低锁带来性能消耗。在java6之后优化synchronized的实现方式,使用了偏向锁升级为轻量级锁在升级为重量级锁的方式,从而降低了锁带来的性能消耗。
偏向锁:顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程竞争的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一下CAS操作),这种情况下,就会给线程加一个偏向锁。如果在运行过程中,遇到了
其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,轻量锁就会升级为重量锁;
重量锁就是synchronized,是java中jvm中最为基础的锁实现。这这种状态下,java虚拟机会阻塞加锁失败的线程,并且在目标所被释放的时候,唤醒这些线程。
15、线程B怎么知道线程A修改了变量
volatile修饰变量,synchronized修饰变量的方法,wait/notify,while轮询。
16、synchronized,volatile,CAS比较
synchronized是悲观锁,属于抢占式,会引起其他线程阻塞。
volatile提供多线程共享变量可见性和禁止指令重排序优化。
CAS是基于冲突检测的悲观锁(非阻塞)。
17、Synchronized和Lock区别
Synchronized是java内置关键字,在jvm层面,Lock是个java类。
Synchronized可以给类,方法,代码块加锁;而Lock只能给代码块加锁。
Synchronized不需要手动获取锁和释放锁,发生异常会自动释放锁,不会造成死锁;而Lock需要自己加锁和释放,如果使用不当没有unLock()去释放锁就会造成死锁。
通过Lock可以知道有没有成功获取锁,而Synchronized无法知道。
18、synchronized和ReentrantLock区别
synchronized是和if,else,for,一样的关键字,ReetrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比Synchronized更多更灵活的特性,可以被集成可以有方法,可以有各种类变量。
synchronized早期版本比ReetrantLock低效,但java6对Synchronized进行了非常多的改进。
相同点:两者都是可重入锁。
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当线程再次想要获取这个对象锁的时候还是可以获取的,如果不可锁重入的话就会造成死锁。
同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
主要区别如下:
ReentrantLock使用起来比较灵活,但是必须有释放锁的配合动作。
ReentrantLock必须手动获取和释放锁,而Synchronized不需要手动释放开启锁。
ReentrantLock只适合用于代码锁,而Synchronized可以修饰类,方法,变量等。
二者的锁机制其实也是不一样的。ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中的mark word。
Java中每个对象都可以作为锁,这是synchronized实现同步的基础:
普通同步方法,锁是当前实例对象
静态方法,锁是当前类的class对象
同步方法快,锁是括号里面的对象
19、volatile关键字的作用
对于可见性,java提供了volatile关键字保证可见性和禁止指令重排。volatile提供happens-before的保证,确保一个线程的修改能对其他线城是可见的。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主内存中,当有其他线程
需要读取时,它会去内存中读取新值。
从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,如AtomicInteger。
volatile常用于多线程环境下的单次操作(单次读或者单次写)
20、Java中能创建volatile数组
能,java中可以创建volatile类型数组,不过只是一个指向数组的应用,而不是整个数组。意思是,如果改变引用指向的数组,将会受到volatile保护,但是如果多个数组同时改变数组的元素,volatile标识符就不能起到保护作用。
21、volatile变量和atomic变量有什么不同
volatile可以确保先行关系,即写操作会发生在后续的读操作之前,但它并不能保证原子性。例如volatile修饰count变量,count++操作就不是原子性的。
而atomicInteger类提供的atomic方法可以让这种操作具有原子性如getAdnIncrement()方法会原子性的进行增量操作把当前值加一,其他数据类型和引用变量也可以进行相似操作。
22、volatile可以使一个非原子操作变为原子操作
volatile主要作用是使变量在多个线程间可见,但无法保证原子性,对于多个线程访问同一个变量需要加锁进行同步。
虽然volatile只能保证可见性不能保证原子性,但是用volatile修饰long和double可以保证其操作原子性。
23、synchronized和volatile区别
synchronized表示同一时间内只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。
volatile表示变量在CPU的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量可见性,禁止指令重排。
区别:
volatile是变量修饰符,synchronized可以修饰类,方法,变量。
volatile仅能实现变量的修改可见性,不能保证原子性,而synchronized则可以保证变量的修改可见性和原子性。
volatile不会造成线程的阻塞,synchronized会造成线程的阻塞。
volatile的标记不会被编译器优化,synchronized标记的变量可以被编译器优化。
volatile关键字是线程同步的轻量级实现,所以性能volatile肯定比synchronized要好,但是volatile只能用于变量而synchronized关键字可以修饰方法及代码块。
24、final不可变对象,对写并发有什么帮助
不变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,业即对象属性值)就不能改变,反之即为可变对象。
不可变对象的类即为不可变类。java平台包含许多不可边磊,如String,基本类型的包装类,BigInteger和bigDecimal等。
满足下面条件才会是不可变对象:它的对象不能再创建后改变,所有域都是final;
不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。
25、Lock接口和synchronized对比同步有什么优势
Lock接口比同步方法和同步块提供了根据扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的兴致,并且可以支持多个相关类的条件对象。
优势:(1)可以使锁公平new ReentrantLock(true);(2)可以使线程在等待锁的时候响应中断;(3)可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间。(4)可以在不同的范围,以不同的顺序获取和释放锁。
整体上说,Lock是Synchronized的扩展板,Lock提供了无条件的、可轮询的(tryLock())、定时的(tryLock带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition)锁操作。另外Lock的
实现类基本都支持非公平锁(默认)和公平锁,synchronized只支持非公平锁。
26、乐观锁和悲观锁的理解及如何实现,有哪些实现方式
悲观锁(写多读少):总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次获取数据的时候都会加上锁,这样别人要拿数据就会阻塞直到它拿到锁。关系型数据库就用了很多这种锁机制,比如行锁,表锁,读锁,写锁等,
都是在做操作之前先上锁。比如java里面的synchronized关键字实现也是悲观锁。
乐观锁(读多写少):每次拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间是否有人修改,可以使用版本号等机制。乐观锁适用于多读的应用类型,原子变量类使用了乐观锁。
27、什么是CAS
CAS是compare and swap的缩写,即我们所说的比较交换
CAS是一种基于锁的操作,而且是乐观锁。CAS操作包含三个操作数,主内存值(V),工作内存中的预期值为A,要修改的更新值为B,当且仅当A和V相同,将V修改为B。CAS操作是通过无限循环来获取数据的,如果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能有机会执行。
AtomicInteger,AtomicBoolean,AtomicLog,AtomicReference都是CAS操作实现的。
28、CAS会产生什么问题
(1)ABA问题:比如说一个线程X从内存位置V中取出A,这时候另一个贤线程Y也从内存中取出A,并且Y进行一些操作变成了B,然后Y又将V位置的数据变成A,这时候线程X进行CAS操作发现内存中仍然是A,然后X操作成功。尽管线程X的CAS操作成功,但可能存在潜藏问题。
从java1.5开始的JDK的atomic包里面提供了一个类AtomicStampendReference来解决ABA问题。
(2)循环时间长开销大:对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
(3)只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这时候用锁。
原理:在原子类中CAS操作都是通过Unsafe类完成的。
Unsafe:是rt.jar下的sun.misc包下的一个类,基于该类可以直接操作特定内存的数据。Java方法无法直接访问底层系统,需要native访问方法,Unsafe类的每个方法都是native方法,其中的方法可以像C的指针一样直接操作内存。
下面看下AtomicInteger中实现i++;
在getAndAddInt函数中,var1代表了AtomicInteger对象,var2代表了该对象在内存中的地址,var4代表了期望增加的数值。首先通过var1和var2获取当前的主内存中真实的int值,也就是var5。然后通过循环进行数据更改,
当比较到真实值和对象的当前值相等,则更新,退出循环;否则再次获取当前的真实值,继续尝试直到成功。
//unsafe.getAndAddInt
public final int getAndAddInt(Object var1,long var2,int var4){
int var5;
do{
//获取当前的值的地址
var5=this.getIntVolatile(var1,var2);
//compareAndSwapInt(obj, offset, expect, update)比较清楚,意思就是如果obj内的value和expect相等,就证明没有其他线程改变过这个变量,那么就更新它为update,如果这一步的CAS没有成功,那就采用自旋的方式继续进行CAS操作
//var1代表对象,var2(对象的值)和var5(工作内存期望值)分别代表当前对象的真实值和期望值,如果二者相等,更新为var5+var4
}while(!this.compareAndSwapInt(var1,var2,var5,var5+var4);
return var5;
}
valueOffset:改变量标识变量值在内存张的偏移地址,Unsafe就是根绝内存偏移地址获取数据的。
29、什么是原子类
java.util。concurrent.atomic包:是原子类的小工具包,支持在在单个变量上接触锁的线程安全编程,原子变量类相当于一种泛化的volatile变量,能够支持原子的读-改-写操作。
简单说就是原子类实现了CAS无锁模式算法,CAS底层调用的是Unsafe.CompareAndSwapInt方法实现的。
30、死锁与活锁,死锁与饥饿
死锁:两个或两个以上的线程在执行过程中,因互相争夺各自其持有的资源而造成的一种互相等待的现象,若无外力作用,他们都将无法推进下去。
活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败。
活锁和死锁的区别:处于活锁的实体是在不断的改变状态,而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
饥饿:一个或者多个线程因为种种原因无法获得所需要的的资源,导致一直无法执行的状态。
java中导致饥饿的原因:
(1)高优先级线程吞噬所有低优先级线程的CPU时间。
(2)线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总能在它之前持续的对该同步块访问。
(3)线程在等一个本身也处于永久等待完成的对象(比如调用这个对象的wait方法),因为其他线程总是再被持续的获得唤醒。
三、线程池
1、线程池作用
线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少创建和销毁线程所需要的的时间,从而提高效率。
2、线程池优点
降低资源消耗:重用存在的线程,减少线程创建销毁的开销。
提高资源响应速度:可有效的控制最大线程并发数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。任务到达时可以不许奥等待线程创建就能立即执行。
提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,监控he调优。
附加功能:提供定时执行,定期执行,单线程,并发数控制等功能。
3、什么是ThreadPoolExecutor
ThreadPoolExecutor线程池
corePoolSize:核心线程数量。maximumPoolSize:最大线程数。keepAliveTime:线程保持时间,N个时间单位。unit时间单位(比如秒,分)。workQueue:阻塞队列。threaFactory:线程工厂。handler:线程池拒绝策略。
4、Excutor和Excutors区别
Executors工具类的不同方法按照不同的需求创建了线程池。
Executor接口对象能执行我们的线程任务
ExectorService接口集成了Executor接口并进行了扩展,提供了更多的方法我们能获的任务执行的状态并且可以获取任务的返回值。
使用ThreadPoolExecutor创建自定义线程池。
5、四种构建线程池的区别特点
(1)newCachedThreadPool:
特点:创建一个可缓存的线程池,如果当前线程池的长度超过了处理的需要时,它可以灵活的回收空闲的线程,当需要新增时,不会对池的长度做任何限制。
缺点:它虽然可以无限的创建线程,但是很容易造成堆外内存溢出,因为它的最大值是在初始化的时候设置为Integer。MAX_VALUE,一般来说机器都没那么大内存来使用。
总结:线程池无限大,会复用线程,而不是每次新建线程。
(2)newFixedThreadPool
特点:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置。
缺点:线程数量是固定的,但是阻塞队列是无界队列。如果有很多请求积压,阻塞队列太大,会导致OOM(超出内存空间)
总结:请求的积压一定要和分配的线程池大小匹配,定线程池的大小最好根据系统资源进行设置。如Rutime.getRutime().availableProcessors(),差点电脑CPU核心数量。
(3)newScheduledThreadPool
特点:创建一个固定长度的线程池,而且支持定时的以及周期性的任务执行,类似于Timer。
缺点:由于所有的任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或者异常影响后面的任务。
(4)newSingleThreadExector
特点:创建一个单线程化的线城池,它只会用唯一的线程来执行任务,如果这个唯一的线程因为异常结束,那么会有一个新的线程来代替它,保证所有的任务按照指定顺序(FIFO,LIFO)执行。
缺点:单线程高并发不高
总结:保证所有任务按照指定顺序执行,如果这个唯一的线程因为异常结束,那么会有其他线程来代替它。
6、线程池有哪些状态
RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
STOP:不接受新的任务提交,不在处理等待队列中的任务,中断正在执行任务的线程。
TIDYING:所有任务都销毁,workCount为0,线程池的状态在转换为TIDYING状态时,会执行钩子方法terminated()。
TERMINATED:terminated()方法执行后,线程池的状态就会变成这个。
7、线程池中submit()和execute()方法有什么区别
相同点:都可以开启线程池中的任务。
不同点:(1)接受参数:execute()只能执行Runnable类型的任务。submit可以执行Ruannable和Callable类型的任务。(2)返回值:submit()方法可以返回持有计算结果的Future对象,而execute()没有。
(3)异常处理:submit()方便Exception处理。
8、ThreadPoolExecutor饱和策略
如果当前同时运行的线程数量达到最大线程数量并且队列也已被放满时,ThreadPoolExecutor定义了一些策略:
AbortPolicy:抛出RejectedExectionException来拒绝新任务的处理。
CallerRunsPolicy:调用执行自己的线程运行任务。这种策略会降低对于新任务的提交速度,影响程序的整体性能。
DiscardPolicy:不处理新任务,直接丢弃调。
DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求。
9、线程池的执行原理
见图process-高并发
提交一个任务到线程池中,处理流程如下。
(1)判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
(2)线程判断工作队列是否已满,如果工作队列没有满,则将形体较的任务存储在这个队列中。如果队列已满,进入下个流程。
(3)判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务,如果已满,则交给饱和策略来处理这个任务。
10、合理分配线程池大小
(1)CPU密集:该任务需要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论开多少线程,该任务都不会得到加速。
(2)IO密集:该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型任务会导致浪费大量的CPU运算能力,浪费等待。所以在IO密集型任务送使用多线程可以大大的加速程序运行,即时在单核CPU上,这种加速就是利用率被浪费掉的阻塞时间。
分配CPU和IO密集:
(1)CPU密集型时,任务可以少配置线程数,大概和机器的CPU和CPU核数相当,这样就可以使得每个线程都在执行。
(2)IO密集型时,大部分线程都阻塞,故多配制线程数,2*CPU核数。
精确来说的话从以下几个角度分析任务特性:
任务性质:CPU密集型,IO密集型,混合型
任务优先级:高中低
任务执行时间:长中短
任务依赖性:是否依赖系统资源,如数据库链接。
得出结论:
线程等待时间比CPU执行时间比例越高,需要越多线程。
线程CPU执行时间比等待时间比例越高,需要越少线程。
四、并发容器
1、经常使用的并发容器
Vector、ConcurrentHashMap、HashTable
Vector方法带上了synchronized关键字,是线程同步的。比ArrayList方法源码多了synchronized。
public synchronized boolean add(E e){
modCount++;
add(e,elementData,elementCount);
return true;
}
HashTable内部方法都被synchronized修饰了,是线程安全的。
public synchronized V put(K key,V value){
if(value==null){
throw new NullPointException();
}
}
ConcurrentHashMap是java5中支持高并发,高吞吐量的线程安全HashMap实现。ConcurrentHashMap底层采用分段的数组+链表实现,线程安全。ConcurrentHashMap通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认是16倍。
并且读操作不加锁,由于HashEntry的value变量是volatile的,也能保证读取到最新的值。ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map扩容)。
插入前检测需不需要扩容,有效避免无效扩容。
2、Collections.synchronized*是什么
完全可以吧List,Map,Set接口底下的集合变成线程安全的集合。原理代理模式。
3、ConcurrentHashMap的并发度
ConcurrentHashMap把实际Map划分成若干部分来实现它的可扩展性和线程安全。默认值是16.
在Jdk8后,它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。加入了更多辅助变量来提高并发度。
4、Synchronized Map和ConcurrentHashMap区别
Synchronized Map一次锁住整张表来保证线程安全,所以每次只能有一个线程访问Map。
ConcurrentHashMap使用分段锁来保证在多线程下的性能。
ConcurrentHashMap则是一次锁住一个桶。ConcurrentHashMap默认将hash表分为16个桶,现在能同时16个写线程执行,并发性能提高。
5、CopyOnWriteArrayList
CopyOnWriteArrayList 是一个并发容器。非复合场景下操作它是线程安全的。CopyOnWriteArrayList(勉锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表式=时,不会抛出ConrrentModificationException,在CopyOnWriteArrayList写入将导致创建整个底层数据组的副本,
而源数组将保留在原地,使得复制的数组再被修改时读取操作可以安全执行。
它比较适合读多写少的场景
缺点,由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组内容比较多的情况下,可能会导致young gc或者full gc;不能用于实时读的场景,像拷贝数组,新增元素都需要时间,所以一致性没法满足实时;无法保证到底要
放多少数据,在高性能的互联网应用中,可能引起故障。
设计思想,读写分离,读和写分开;最终一致性;使用另外开辟空间的思路,解决并发冲突。
五、并发队列
1、并发队列
并发队列是多个线程以有次序共享数据的重要组件。
2、并发队列和并发集合区别
队列遵循FILI(先进先出)的规则,队列一般用来解决大数据量采集处理和显示。
并发集合就是在多线程张共享数据的。
3、阻塞队列和费阻塞队列
在并发队列上JDK提供了Queue接口,一个是以Queue接口下的BlockingQueue接口为代表的的阻塞队列,另一个是高性能(无阻塞)队列。
区别:
当阻塞队列是空时,从队列中获取元素的操作将会被阻塞。
当阻塞队列是满时,往队列中添加元素的操作将会被阻塞。
或者试图从空的阻塞队列中获取原色线程将会被阻塞,直到其他的线程忘空的队列插入新的元素
试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程队列重新变的空闲起来。
4、常用并发队列
非阻塞队列:
ArrayDeque(数组双端队列)是JDK容器中的一个双端队列实现,内部使用数组进行元素出处,不允许存储null值,可以高效的进行元素查找和尾部插入取出,是用作队列,双端队列,栈的绝佳选择,性能比LinkedList好。
PrioriyQueue(优先级队列)一个基于优先级的无界优先级队列。通过无锁的方式,实现了高并发状态下的高性能。该队列元素遵循先进先出原则,不允许null元素。
阻塞队列:
DelayQueue(基于时间优先级的队列,延期阻塞队列)DelayQueue是一个没有边界BlockingQueue实现,加入其中的元素必须实现Delayed接口。当生产者调用put方法时会竹筏Delay接口中的compareTo排序,按到期时间排序,
排在队列头部的元素是最早到期的,越往后到期时间越晚。
ArrayBlockingQueue(基于数组的并发阻塞队列)ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。初始化的时候必须制定容量打下,一旦指定不可改变。
LinkedBlockingQueue(基于链表的FIFO阻塞队列)LinkedBlockingQueue阻塞队列大小的配置是可选,初始化指定值为有界,不指定为无界。默认采用Integer.Max_VALUE。内部实现是个链表
LinkedBlockingDeque(基于链表的FIFO双端阻塞队列)
PriorityBlockingQueue(带优先级的无界阻塞队列)
SynchronousQueue(并发同步阻塞队列)
六、并发工具类
1、CountDownLatch类位于java.until。concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务,它要等待其他三个任务执行完毕后才能执行,此时就可以利用。
2、CyclicBarrier(回环栅栏)它的作用就是会让所有线程都等待完成后才会继续下一步行动。CyclicBarrier初始化时规定一个数目,然后计算调用了CyclicBarrier.await()进入等待的线程数。
当线程数达到了这个数目时,所有进入等待状态的线程都被唤醒继续执行。CyclicBarrier初始化是还可以带一个Runnable参数,所有其他线程被唤醒前执行。
3、Semaphore(信号量)它是Synchronized的加强版,作用是控制线程的并发数量(允许自定义多少线程同时访问)。Semaphore是一种基于技术的信号量,它可以设置一个阈值,基于此,多个线程竞争获取许可型号,做自己的申请后归还,超过阈值后,线程申请
许可信号量将会被阻塞。可以用来构建一些对象池,资源池之类的,比如数据库链接池,可以创建计数为前端Semaphore,将其作为一种类似互斥锁的机制,也叫二元信号量。
4、ThreadLocal是一个线程内部的变量,只在本线程中使用,隔离其他线程,内部维护了一个ThreadLocalMap,一个ThreadLocal只能保存一个值,并且各个数据互不干扰,存储时的key永远为当前ThreadLocal,存储时的Key为弱引用。
七、原理
(四)、happens-before
1、先行发生原则(happens-before)
(1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二操作之前。
(2)两个操作之前存在happens-before关系,并不意味着java的具体实现必须按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-bfore关系来执行的结果一直,那么这种排序并不非法。
2、happens-before规则示例
private volatile boolean flag=false;
private int a=0;
public void writer(){
a=1;//1
flag=true;//2
}
public void read(){
if(flag){//3
int i=a;//4
}
}
(1)程序次序规则:在一个线程内,书写在前面的操作先行发生于后面的操作。准确的说,是控制流顺序而不是程序代码顺序,因为要考虑分支和循环等结构。1 happens-before 2,3 happens-before 4
(2)volatile变量规则:对于vaolatile修饰的变量的写操作,一定happen-before后续对于volatile变量的读操作,如上代码中flag加了volatile,所以2happens-before3。
(3)传递性规则:如上顺序规则示例中,根据顺序规则:1happens-before2;根据volatile变量规则:2happens-before3;故而根据传递性规则可导出:1happens-before3.
(4)线程启动规则:thread对象的start()方法,先行发生与此线程的每一个动作。
(5)线程终止规则:线程中所有操作happens-before于对此线程的终止检测,可以通过Thread.join()等手段检测到线程已终止。
......
4、as-if-serial语义
不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。
5、总结
(1)happens-before关系保证正确同步的多线程程序的执行结果不被改变。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
(2)as-if-serial语义保证单线程内程序的执行结果不被改变。as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。
目的都是为了在不改变执行结果的前提了,尽可能的提升程序执行的并行度。
(五)、ReentrantLock和AQS同步队列实现原理
ReentrantLock采用的是一种同步队列的数据结构存储阻塞等待的线程。
八、AQS:
AQS是一个同步队列,通过一个voliate int state标识同步状态(对于标识的更新是通过CAS完成的),通过FIFO的CLH队列完成资源线程的排队工作。
内置队列有一个头结点(head),尾节点(tail)都是虚拟节点,中间的其他都为Node节点。内置队列其实是一个虚拟队列,没有队列实力,只有节点之间的关联关系。
当一个线程竞争共享资源的时候,如果竞争成功,则锁定资源状态,如果竞争失败则将当前线程和等待信息组装放到Node节点中,同时会阻塞当前线程。当锁被释放是会
唤醒节点中的线程,使其再次尝试获取同步状态。
AQS有两种对资源的共享方式
独占:就只有一个线程可以访问资源,如ReentrantLock,有公平锁new ReentrantLock(true),非公平锁。
共享:多个线程可以共享访问资源,如Semaphore和CountDownLatch.
ReentrantReaWirteLock,可以看成是组合方式,因为ReentrantReadWriteLock也就是读写锁同时允许多个线程访问某一资源进行读。
AQS底层实现了模板方法,如果需要自定同步器,可以继承AbstractQueuqdSychronizer,实现指定方法就可以
tryAcquire:独占方式,尝试获取锁资源,如果成功返回true,失败返回fasle。
tryRelease:独占方式,尝试释放锁资源,成功返回true,失败返回false。
tryAcquireShared:共享方式,尝试获取锁资源,成功返回true,失败返回false。
tryReleaseShared:共享方式,尝试释放锁资源,成功返回true,失败返回false。
isHeldExclusively:该线程是否正在独占资源。
所有方法默认都跑出UnSppourtOPerationException异常。
1、同步队列AQS
同步队列,全称AbstractQueuedSychronizer,简称AQS。在Lock中这是一个非常核心的组件,java.util.concurrent工具包中很多地方都是用了AQS。
2、AQS的两种功能
从使用层面分为独占和共享。
独占锁:每次只有一个线程持有锁,如:ReentrantLock就是独占方式实现的互斥锁。
共享锁:允许多个线程同时获取锁,并发访问共享资源,如:ReentrantReadWriteLock。
3、AQS内部实现
AQS依赖内部的一个FIFO双向队列来完成同步状态的管理,当前线程获取锁失败时,AQS会将当前线程以及等待信息构造成为一个节点(Node对象)并将其加入到AQS中,同时会阻塞当前线程,当锁被释放时,会把节点中的线程唤醒,使其再次尝试获取同步状态。
AQS中有一个头(head,虚节点)和一个为(tail)节点,中间每个节点(Node)都有一个prev和next指针指向前一个节点和后一个节点。
AQS中每一个节点就是一个Node对象,并且通过节点中的状态等信息来控制队列,Node对象是AbstractQueuedSynchronizer对象中的一个静态内部类。
4、AQS设计介绍
AQS是一个用来构建锁和同步器的框架, 使用AQS能简单且高效的构造出应用广泛的大量同步器,比如:ReentrantLock,Semaphore,其他的注入ReettrantReadWriteLock,SynchronousQueue,FutureTask等等都是基于AQS。
5、AQS原理分析
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,
这个机制AQS使用CLH队列锁实现,即将暂时获取不到锁的线程加入到队列中。
CLH()队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列中的一个Node节点来实现分配锁的。
AQS使用一个int成员变量来标识同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
private volatile int state;//共享变量,使用volatile修饰保证线程可见性。
状态信息通过protected类型的getState,setState,compareAdnSetState进行操作
//返回同步状态
protected final int getState() {
return state;
}
// 设置同步状态
protected final void setState(int newState) {
state = newState;
}
//原子的(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
6、AQS对资源的共享方式
Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可以分为公平锁和非公平锁:
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁:当线程要获取锁时,无视队列顺序直接取抢锁,谁抢到就是谁的。
Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。
ReentrantReadWriteLock,可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。
7、AQS底层使用了模板方法模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样的
(1)使用者继承AbstractQuquedSynchronizer并重写指定的方法。(重写方法很简单,无非就是对于共享资源state的获取和释放)
(2)将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这模板方法会调用使用者重写的方法。
自定义同步器时需要重写下面几个AQS提供的模板方法
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功返回true,失败返回false。
tryRelease(int)//独占方式。尝试释放资源,成功返回true,失败返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。正数表示成功,且有剩余资源。
tyrRelaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
默认情况下,每个方法都抛出UnsupportedOperationException。这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final,所以无法被其他类使用。
8、示例
以ReentrantLock为例,state初始化为0,标识未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程在tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,
其他线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state累加),这就是可重入的概念。
以CountDownLatch为例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减一。等到所有子线程都执行完成后
(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,它们也只需要实现tryAcquire-tryRelease,tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,
如ReentrantReadWriteLock。