Java面试题之多线程

1.进程间的通信方式

无名管道通信:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用,进程间的亲缘关系通常是指父子进程

高级管道通信:将另一个程序当作一个新的进程在当前程序进程中启动,那么它算是当前进程的子进程,这种方式我们称为高级管道方式

有名管道通信:有名管道通信也是半双工的通信方式,但是它允许在无亲缘关系的进程间使用

消息队列通信:消息队列是消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信号少、管道只能承载无格式字节流以及缓冲区大小受限等缺点

信号量通信:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程间的同步手段

信号通信:信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生

共享内存通信:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的,它往往与其他通信机制,比如信号量通信配合使用,来实现进程间的通信和同步

套接字通信:套接字也是进程间的一种通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信

2.创建线程的四种方式

         Java中创建线程的四种方法

3.什么情况下线程阻塞

线程执行了Thread.sleep(int n)方法,线程放弃CPU,睡眠n毫秒,然后恢复运行

线程要执行一段同步代码,由于无法获得相关的同步锁,只好进入阻塞状态

线程执行了一个对象的wait()方法,进入阻塞状态

线程执行I/O操作或者进行远程通信时,会因为等待相关的资源而进入阻塞状态

请求与服务器建立连接时,即当线程执行Socket的带参数的构造方法,或者执行Socket的connect()方法时,会进入阻塞状态,直至连接成功

线程从Socket的输入流读取数据时,如果没有足够的数据,就会进入阻塞状态,直到读取了足够的数据,或者到达输入流的末尾,或者出现了异常,才从输入流的read()方法返回或异常中断

线程向Socket的输出流写一批数据时,可能会进入阻塞状态,等到输出了所有的数据,或者出现异常,才从输出流的write()方法返回或异常中断

调用Socket的setSoLinger()方法设置了关闭Socket的延迟时间,那么当线程执行Socket的close方法时,会进入阻塞状态,直到底层Socket发送完所有剩余数据,或者超过了setSoLinger方法设置的延迟时间,才从close方法返回

4.Java线程的状态

new:刚刚创建的线程,还没有开始执行

runnable:线程正在执行,表示线程的一切资源都已经准备好了

blocked:遇到synchronized同步块,就会进入blocked阻塞状态,线程会暂停执行,直到获得所求的锁

waiting:正在等待一些特殊事件的发生,比如通过wait()方法等待的线程在等待notify()方法,通过join()方法等待的线程会等待目标线程的终止,当期望的事件发生,线程会再次执行,进入runnable状态

timed_waiting:同waiting一样,等待特殊事件的发生,只是它等待的时间是有限的

terminated:线程执行完毕后,进入此状态

5.线程池的实现原理和处理流程

线程池通过核心池大小、最大池大小、存活时间共同管理着线程的创建与销毁

创建线程:当提交一个新任务到线程池时

1).如果线程池的实际线程数小于corePoolSize,就会优先创建新的线程,若大于corePoolSize,则会把新任务加入等待队列

2).若等待队列已满,无法加入,就会在总线程不大于maximumPoolSize的前提下,创建新的进程执行任务,若大于maximumPoolSize,则执行拒绝策略

销毁线程:如果一个线程闲置的时间超过了存活时间,并且当前池的大小超过了核心池的大小,线程池会终止它

6.当线程池超负载了有哪些拒绝策略

AbortPolicy策略:会直接抛出异常,组织系统正常工作

CallerRunsPolicy策略:只要线程池是关闭的,该策略直接在调用者线程中,运行当前被丢弃的任务

DiscardOledstPolicy策略:丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务

DiscardPolicy策略:默默丢弃无法处理的任务,不予任何处理

7.什么是守护线程

Java里面的线程分为两种,用户线程和守护线程,用户线程就是应用程序中的自定义线程

守护线程具有最低的优先级,用于为系统中的其他对象和线程提供服务。将一个用户线程设置为守护线程的方法是在线程对象创建之前调用线程对象的setDaemon(bool on)方法,true则把该线程设置为守护线程。典型的守护线程就是JVM中的垃圾回收线程,它使终在低级别的状态下运行,用于实时监控和管理系统中的可回收资源

8.什么是死锁,死锁产生的原因

死锁就是两个或多个线程,相互占用对方需要的资源,而不进行释放。导致彼此之间都相互等待对方释放资源,产生了无限制等待的现象。死锁一旦发生,如果没有外力介入,这种等待将永远存在

 系统资源的竞争,对不可抢占资源和可消耗资源进行争夺时都可能引起死锁

进程推进顺序不当,请求和释放资源的顺序不当,会引起死锁

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

    互斥条件:在一段时间内,某资源只能被一个进程占用。如果此时还有其它进程请求该资源,该请求进程只能等待,直至占有该资源的进程用完释放

    请求和保持条件:进程已经保持了至少一个资源,但是又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己以获得的资源保持不放

    不可抢占条件:进程已获得的资源在未使用完之前不能被抢占,只能在进程使用完之后由自己释放

    循环等待条件:在发生死锁时,必然存在一个进程—资源的循环链

9.线程池过大或者过小带来的问题

线程池过大,那么线程对稀缺的CPU和内存资源的竞争,会导致内存的高使用量,还可能耗尽资源,线程池过小,由于存在很多可用的处理器资源,却还没有工作,会对吞吐量造成损失

10.处理死锁的方法

            如何处理死锁

11.悲观锁和乐观锁,CAS,AQS,TLAB

悲观锁:总是假设最坏的情况,每次拿数据的时候都认为别人会修改,所以在每次拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库就使用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在做操作之前先上锁。Java中的同步原语synchronized关键字的实现就是悲观锁,也是独占锁

乐观锁:每次拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断在此期间别人有没有去更新这个数据,可以用版本号等机制。乐观锁适用于多读的场景,可以提高吞吐量,比如数据库提供的write_condition机制,提供的就是乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是用了乐观锁的一种实现方式CAS

悲观锁机制存在的问题:

1).在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题

2).一个线程持有锁会导致其他所有需要此锁的线程挂起

3).假如一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险

乐观锁是一种思想,乐观锁假定数据一般情况下不会发生并发冲突,所以在数据进行更新的时候才会对数据能否产生并发冲突进行检测,假如发生并发冲突了,则返回给用户信息,让用户决定如何去做

乐观锁的具体实现主要是两个步骤:冲突检测和数据升级

CAS(Compare and Swap,比较与交换)

当多个线程尝试用CAS同时更新同一个变量时,只有其中一个线程可以更新变量的值,而其他线程都失败,失败的线程并不会被挂起,而是被告知在这次竞争中失败,并且可以再次尝试

CAS操作中包含三个操作数,需要读写的内存位置V,进行比较的预期原值A,拟写入的新值B,假如内存位置V的值和预期原值A相匹配,那么解决器会自动将该位置的值更新为新值B,否则不做任何操作。CAS有效的说明了:我认为位置V应该包含值A,假如包含该值,则将B放到这个位置,否则不要更改该位置,只告诉我这个位置现在的值即可。这和乐观锁的冲突检测+数据更新原理是一样的

CAS缺点:

1).ABA问题,因为CAS在操作值的时候会检查值有没有发生变化,如果一个值原来是A,变成了B,又变成了A,CAS检查时会发现值没有发生改变,但是实际上改变了。ABA问题的解决思路是使用版本号,在变量前面加上版本号,每次变量更新的时候把版本号加一,那么A—B—A就会变成1A—2B—3A

2).循环时间长开销大,自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销

3).只能保持一个共享变量的原子操作,当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性。这时候可以使用锁,或者把多个共享变量合并成一个来操作

从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference,可以解决ABA问题,也可以保证引用多个对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作

CAS与Synchronized的使用场景:

1).对于竞争资源较少(线程冲突较轻)的情况,用Synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费CPU资源,而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能

2).对于资源竞争严重的情况,CAS自旋的概率比较大,从而浪费更多的CPU资源,效率低于Synchronized

AQS(抽象的队列式的同步器):

AQS维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)

AQS定义了两种资源共享方式:Exclusive(独占,只有一个线程执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)

以ReentrantLock为例,state初始化为0,表示未锁定状态,A线程lock()时,会调用tryAcquire()独占该锁并将state+1。然后其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其他线程才有机会获取该锁。释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念,但是要注意,获取多少次就要释放多少次,这样才能保证state是能回到零态的

TLAB:

每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区(TLAB),线程内部需要分配内存时直接在TLAB上分配就行,避免了线程冲突,只有当缓冲区的内存用完了需要重新分配内存时才会进行CAS操作分配更大的内存空间

JVM使用TLAB通过参数设置:-XX:+/-UseTLAB,jdk5及以后的版本默认是开启TLAB的

12.Volatile、Synchronized、Lock

Volatile是一个变量修饰符,用来修饰变量,它可以保证多线程三大特性中的可见性和有序性,不能保证原子性

一个变量被声明成Volatile,Java内存模型确保所有线程看到这个变量的值是一致的

可见性:在Java内存模型中,每个线程都有一个工作内存和主内存,所有变量都存储在主内存中,线程每次读取和写入的都是工作内存中的变量副本,然后在某个时间点将工作内存和主内存中的变量进行同步。当用Volatile修饰了变量后,强制把对变量的修改同步到主内存,而其他线程在读取自己的工作内存中的值的时候,发现是Volatile修饰的并且已经被修改过了,会把自己工作内存中的值置为无效,然后从主内存中读取

有序性:在执行程序时,为了提高性能,处理器和编译器常常会对指令进行重排序,这种重排序一般只能保证单线程下执行结果不被改变。当变量被Volatile修饰后,将会禁止重排序

Volatile的实现原理:

代码层面:通过内存屏障来实现,所谓的内存屏障,是在某些指令中插入屏障指令,虚拟机读取到这些屏障指令时主动将本地内存中的变量刷新到主内存,或者直接从主内存中读取变量的值,通过屏障指令会禁止屏障前的操作命令和屏障后的命令进行重排序

系统层面实现:在多处理器下,保证各个处理器的缓存是一致的,每个处理器通过在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态

Synchronized用来修饰一个方法或者一个代码块,能够保证在同一时刻最多只有一个线程执行该段代码

Synchronized的实现原理:

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

  1).如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者

  2).如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1

  3).如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权

13.锁竞争激烈的方法

  1).减少锁持有的时间,有助于降低锁冲突的可能性,进而提高系统的并发能力

  2).减小锁的粒度,也就是缩小锁定对象的范围,比如ConcurrentHashMap

  3).读写分离锁来替换独占锁,读操作加读锁,可以并发读,写操作使用写锁,只能单线程写,在读锁写少的场合,性能会得到很大提升

  4).锁分离,比如LinkedBlockingQueue的做法,使用takeLock和putLock分别来控制取数据和写数据

14.JDK内部锁的优化策略

锁偏向:如果一个线程获得了锁就进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作,节省了大量有关锁申请的操作。适用于几乎没有锁竞争的场合

轻量级锁:将对象头部作为指针,指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁。如果线程获得轻量级锁成功,则可顺利进入临界区。如果轻量级锁失败,则表示其他线程先争夺了锁,那么当前线程的锁请求就会膨胀为重量级锁

自旋锁:锁膨胀后,虚拟机会让当前线程做几个空循环,在经过若干次循环后,如果可以得到锁,那么就顺利进入临界区,不能得到锁,会将线程在操作系统层面挂起

锁消除:Java虚拟机在JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。通过锁消除,节省无意义的请求锁时间

15.JDK1.6为什么要引入偏向锁和轻量级锁

synchronized是重量级别的锁,它使用monitorenter与monitorexit命令控制多线程同步,这两个命令是JVM依赖操作系统互斥(mutex)来实现的。互斥会导致线程挂起,并在较短的时间内又需要重新调度原线程,这样频繁出现线程的挂起和唤醒,非常消耗资源,程序运行效率低下。为了提高效率,引入了偏向锁和轻量级锁,尽量让多线程访问公共资源时,不进行程序运行状态的切换

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到锁竞争的线程,使用自旋会消耗CPU 追求响应时间,同步块执行速度非常快
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量,同步块执行速度较长

 

16.偏向锁的获取和撤销流程

偏向锁的获取:

 

偏向锁的释放:

 

17.轻量级锁的获取和撤销流程

轻量级锁加锁:

 

轻量级锁解锁:

18.阻塞队列,非阻塞队列

阻塞队列:

是一个支持两个附加操作的队列,这两个附加的操作支持阻塞的插入和移除方法。阻塞的插入是指当队列满时,队列会阻塞插入元素的线程,直至队列不满。阻塞的移除是指当队列为空时,获取元素的线程会等待队列变为非空。阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里获取元素的线程,阻塞队列就是生产者用来存放元素,消费者用来获取元素的容器

非阻塞队列:

使用CAS的方式来实现无锁的并发访问,比如ConcurrentLinkedQueue,是一个基于链接结点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当添加一个元素的时候,它会添加到队列的尾部,当获取一个元素的时候,它会返回队列头部的元素,入队和出队操作都基于CAS原子指令实现,CAS是一个CPU直接支持的硬件指令

19.线程和进程的区别

             进程与线程

 

 

 

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读