多线程面试题 一

一:线程间是怎么通信的,通过调用几个方法来交互的?
        

线程是通过wait , notify等方法相互作用进行协作通信;

wait()方法使得当前线程必须要等待,直到到另外一个线程调用notify()或者notifyAll()方法唤醒
wait()和notify()方法要求在调用时线程已经获得了对象锁,因此对这两个方法的调用需要在  synchronized修饰的方法或代码块中。
Wait,notify,notifyAll都必须在synchronized修饰的方法或代码块中使用,都属于Object的方法,可以被所有类继承,都是final修饰的方法,不能通过子类重写去改变他们的行为

二、线程的生命周期及五种基本状态?

1. 新建(new):新创建了一个线程对象。

2. 可运行(runnable):线程对象创建后,当调用线程对象的 start()方

法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。

3. 运行(running):可运行状态(runnable)的线程获得了cpu时间片

(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入

口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

4. 阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU

的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有

机会再次被 CPU 调用以进入到运行状态。

阻塞的情况分三种

(一). 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待

队列(waitting queue)中,使本线程进入到等待阻塞状态;

(二). 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占

用),则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;

(三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会

进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 

处理完毕时,线程重新转入就绪状态。

5. 死亡(dead):线程run()、main()方法执行结束,或者因异常退出了

run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

三、请说出与线程同步以及线程调度相关的方法。

(1) wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的

锁;condition.await();

(2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此

方法要处理 InterruptedException 异常

(3)notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不

能确切的唤醒某一个等待状态的线程,而是 JVM 确定唤醒哪个线程,而且与

优先级无关;condition.signal();condition.signalAll();

https://blog.csdn.net/sophia__yu/article/details/84502578

(4)notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给

所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

四、并发编程有什么缺点

并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发

编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如

内存泄漏、上下文切换、线程安全、死锁等问题。

五、并发编程三要素是什么?在 Java 程序中怎么保证多线程的运行安全?

并发编程三要素(线程的安全性问题体现在):

原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。

可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)

有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序

出现线程安全问题的原因

1、线程切换带来的原子性问题

2、缓存导致的可见性问题

3、编译优化带来的有序性问题

解决办法:

JDK Atomic开头的原子类synchronizedLOCK可以解决原子性问题

synchronized、volatile、LOCK,可以解决可见性问题

Happens-Before 规则可以解决有序性问题

六、synchronized 的作用及优化介绍

在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环

境下,控制 synchronized 代码段不被多个线程同时执行。

另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视

器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 

线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需

要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内

核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是

为什么早期的 synchronized 效率低的原因。在 Java 6 之后 Java 官方

对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率

也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性

自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

通过JDK 反汇编指令 javap -c -v SynchronizedDemo

可以看出在执行同步代码块之前之后都有一个monitor字样,其中前面的是

monitorenter,后面的是离开monitorexit,不难想象一个线程也执行同步代码

块,首先要获取锁,而获取锁的过程就是monitorenter ,在执行完代码块之

后,要释放锁,释放锁就是执行monitorexit指令

七、Java内存模型

线程之间主要通过读-写共享变量(堆内存中的实例域,静态域和数组元素)来完成隐式通信JMM 控制 Java 线程之间的通信,决定一个线程对共享变量的写入何时对另一个线程可见,

Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量(线程共享的变量)存储到内存和从内存中取出变量这样底层细节。

Java 内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。

主内存、工作内存详解:

计算机在高速的 CPU 和相对低速的存储设备之间使用高速缓存,作为内存和处理器之间的缓冲。将运算需要使用到的数据复制到缓存中,让运算能快速运行,当运算结束后再从缓存同步回内存之中。

在多处理器的系统中(或者单处理器多核的系统),每个处理器内核都有自己的高速缓存,它们有共享同一主内存(Main Memory)。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致。

为此,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,来维护缓存的一致性

指令重排:

执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:

1.编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

2.指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

3.内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

指令重排需要满足以下两个条件

在单线程环境下不能改变程序运行的结果;

存在数据依赖关系的不允许重排序

happens-before具体的一共有八项规则:

  1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  3. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  5. start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  6. join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
  7. 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
  8. 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始

可参考:【并发重要原则】happens-before理解和应用 - 简书

八、多线程中 synchronized 锁升级的原理是什么?

synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在

第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置

为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果

一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁通过自旋

循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对

象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁

的升级。

锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 

synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的

方式,从而减低了锁带来的性能消耗。

补充对象头结构:

对象的几个部分的作用:

1.对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode;

2.Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例;

3.数组长度也是占用64位(8字节)的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分;

4.对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型;

5.对齐字是为了减少堆内存的碎片空间。

Mark Word介绍:

biased_lock对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。lock和biased_lock共同表示对象处于什么锁状态。

age4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。

identity_hashcode:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到管程Monitor中。

thread:持有偏向锁的线程ID。

epoch:偏向锁的时间戳。

ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。

ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。

九、synchronized、volatile、CAS 比较

(1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。

(2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。

volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可

见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主

存,当有其他线程需要读取时,它会去内存中读取新值。

从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性

(3)CAS 是基于冲突检测的乐观锁(非阻塞)

十、final,什么是不可变对象,它对写并发应用有什么帮助?

不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。

不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不

可变类,如 String、基本类型的包装类、BigInteger 和 BigDecimal 等。

只有满足如下状态,一个对象才是不可变的;

1、它的状态不能在创建后再被修改;

2、所有域都是 final 类型;

3、它被正确创建(创建期间没有发生 this 引用的逸出)。

不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

十一、AQS(Abstract Queued Synchronizer)

1、AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。

2、AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。

3、AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

状态信息通过protected类型的getState,setState,compareAndSetState进行操作。

4、AQS底层使用了模板方法模式

同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样

(模板方法模式很经典的一个应用):

1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)

2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。

AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()

会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就

会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会

获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累

加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能

保证state是能回到零态的。

再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为

N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行

完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子

线程都执行完后(即state=0),unpark()主调用线程然后主调用线程就会从

await()函数返回,继续后余动作

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现

tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS

也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

十二、ThreadLocal造成内存泄漏的原因?

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所

以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 

被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key

为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个

时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在

调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 

ThreadLocal方法后 最好手动调用remove()方法

十三、并发待补充

1、常用并发工具类

Semaphore,CountDownLatch,CyclicBarrier;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值