java 多线程T(重写) --- 3---2021-09-15
《面试题对标大纲》
题目列表集
1、什么是线程同步和线程互斥,有哪几种实现方式?
2、在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步?
3、如果你提交任务时,线程池队列已满,这时会发生什么
4、什么叫线程安全?servlet 是线程安全吗?
5、在 Java 程序中怎么保证多线程的运行安全?
6、你对线程优先级的理解是什么?
7、线程类的构造方法、静态块是被哪个线程调用的(重点)
8、 一个线程运行时发生异常会怎样?
9、Java 线程数过多会造成什么异常?
10、多线程的常用方法
11、Java中垃圾回收有什么目的?什么时候进行垃圾回收?
12、线程之间如何通信及线程之间如何同步
13、Java内存模型
14、如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存?
15、finalize()方法什么时候被调用?finalization()方法 的目的是什么?
16、什么是重排序
17、重排序实际执行的指令步骤
18、重排序遵守的规则
19、as-if-serial 规则和 happens-before 规则的区别
20、并发关键字 synchronized
1、什么是线程同步和线程互斥,有哪几种实现方式?
1、当一个线程对共享的数据进行操作时,应使之成为一个”原子操作“,即在没有完成相关操作之前,
不允许其他线程打断它,否则,就会破坏数据的完整性,必然会得到错误的处理结果,这就是线程
的同步。
2、在多线程应用中,考虑不同线程之间的数据同步和防止死锁。当两个或多个线程之间同时等待对方
释放资源的时候就会形成线程之间的死锁。为了防止死锁的发生,需要通过同步来实现线程安全。
3、线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。
4、线程间的同步方法大体可分为两类:用户模式和内核模式。
顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。
用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。
5、实现线程同步的方法
(1)同步代码方法:sychronized 关键字修饰的方法
(2)同步代码块:sychronized 关键字修饰的代码块
(3)使用特殊变量域volatile实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制
(4)使用重入锁实现线程同步:reentrantlock类是可冲入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义
2、在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步?
1、在 java 虚拟机中,监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。
2、一旦方法或者代码块被 synchronized 修饰,那么这个部分就放入了监视器的监视区域,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码 ,另外 java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案
3、如果你提交任务时,线程池队列已满,这时会发生什么
有俩种可能:
1、如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到
阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务
2、如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy
4、什么叫线程安全?servlet 是线程安全吗?
1、线程安全是编程中的术语,指某个方法在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
2、Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。
3、Struts2 的 action 是多实例多线程的,是线程安全的,每个请求过来都会 new 一个新的 action 分配给这个请求,请求完成后销毁。
★ SpringMVC 的 Controller 是线程安全的吗?
(1)不是的,和 Servlet 类似的处理流程。
(2)Struts2 好处是不用考虑线程安全问题;Servlet 和 SpringMVC 需要考虑线程安全问题,但是性能可以提升不用处理太多的 gc,可以使用 ThreadLocal 来处理多线程的问题。
5、在 Java 程序中怎么保证多线程的运行安全?
方法一:使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
方法二:使用自动锁 synchronized。
方法三:使用手动锁 Lock。
Lock lock = new ReentrantLock();
lock. lock();
try {
System. out. println("获得锁");
}
catch (Exception e) {
// TODO: handle exception
} finally {
System. out. println("释放锁");
lock. unlock();
}
6、你对线程优先级的理解是什么?
1、每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个 int 变量(从 1-10),1 代表最低优先级,10 代表最高优先级
2、Java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级。当然,如果你真的想设置优先级可以通过setPriority()方法设置,但是设置了不一定会改变,这个是不准确的。
7、线程类的构造方法、静态块是被哪个线程调用的(重点)
-
这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被 new这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用的。
-
如果说上面的说法让你感到困惑,那么我举个例子,假设 Thread2 中 new 了Thread1,main 函数中 new 了 Thread2,那么:
- Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run()方法是Thread2 自己调用的
- Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run()方法是Thread1 自己调用的
8、 一个线程运行时发生异常会怎样?
如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。
当一个未捕获异常将造成线程中断的时候,JVM 会使用 Thread.getUncaughtExceptionHandler()来查询线程的 UncaughtExceptionHandler 并将线程和异常作为参数传递给 handler 的 uncaughtException()方法进行处理。
9、Java 线程数过多会造成什么异常?
1、线程的生命周期开销非常高
2、消耗过多的 CPU
资源如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占
用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU资源时还将产生其他性能的开
销。
3、降低稳定性JVM
在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素
制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制
等。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常。
10、多线程的常用方法
image.png并发理论
11、Java中垃圾回收有什么目的?什么时候进行垃圾回收?
1、垃圾回收是在内存中存在没有引用的对象或超过作用域的对象时进行的。
2、垃圾回收的目的是识别并且丢弃应用不再使用的对象来释放和重用资源。
12、线程之间如何通信及线程之间如何同步
1、在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步。通信是指线
程之间以如何来交换信息。一般线程之间的通信机制有两种:共享内存和消息传递。
2、Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完
全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会
遇到各种奇怪的内存可见性问题。
13、Java内存模型
共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一
个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变
量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本
地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存
在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
image.png
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
- 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
- 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
下面通过示意图来说明线程之间的通信
image.png
总结:什么是Java内存模型:java内存模型简称jmm,定义了一个线程对另一个线程可见。共享变
量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能本
地内存没有及时刷新到主内存,所以就会发生线程安全问题。
14、如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存?
1、不会,在下一个垃圾回调周期中,这个对象将是被可回收的。
2、也就是说并不会立即被垃圾收集器立刻回收,而是在下一次垃圾回收时才会释放其占用的内存。
15、finalize()方法什么时候被调用?finalization()方法 的目的是什么?
-
垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法;
finalize是Object类的一个方法,该方法在Object类中的声明protected void finalize() throws Throwable { } 在垃圾回收器执行时会调用被回收对象的finalize()方法,可以覆盖此方法来实现对其资源的回收。
注意:一旦垃圾回收器准备释放对象占用的内存,将首先调用该对象的finalize()方法,并且下一次垃圾回收动作发生时,才真正回收对象占用的内存空间 -
GC本来就是内存回收了,应用还需要在finalization做什么呢? 答案是大部分时候,什么都不用做(也就是不需要重载)。只有在某些很特殊的情况下,比如你调用了一些native的方法(一般是C写的),可以要在finaliztion里去调用C的释放函数。
Finalizetion主要用来释放被对象占用的资源(不是指内存,而是指其他资源,比如文件(FileHandle)、端口(ports)、数据库连接(DB Connection)等)。然而,它不能真正有效地工作。
16、什么是重排序
- 程序执行的顺序按照代码的先后顺序执行。
- 一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,进行重新排序(重排序),它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
int a = 5; //语句1
int r = 3; //语句2
a = a + 2; //语句3
r = a * a; //语句4
(1)则因为重排序,他还可能执行顺序为(这里标注的是语句的执行顺序) 2-1-3-4,1-3-2-4 但绝不可能 2-1-4-3,因为这打破了依赖关系。
(2)显然重排序对单线程运行是不会有任何问题,但是多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。
17、重排序实际执行的指令步骤
image.png-
编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
-
指令级并行的重排序。现代处理器采用了指令级并行技术(ILP)来将多条指令重叠执行。如果不
存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 -
内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在
乱序执行。 -
这些重排序对于单线程没问题,但是多线程都可能会导致多线程程序出现内存可见性问题。
18、重排序遵守的规则
as-if-serial:
- 不管怎么排序,结果不能改变
- 不存在数据依赖的可以被编译器和处理器重排序
- 一个操作依赖两个操作,这两个操作如果不存在依赖可以重排序
- 单线程根据此规则不会有问题,但是重排序后多线程会有问题
19、as-if-serial 规则和 happens-before 规则的区别
- as-if-serial 语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多
线程程序的执行结果不被改变。 - as-if-serial 语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行
的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多
线程程序是按happens-before指定的顺序来执行的。 - as-if-serial 语义和 happens-before 这么做的目的,都是为了在不改变程序执行结果的前提下,尽
可能地提高程序执行的并行度。
20、并发关键字 synchronized
-
在 Java 中,synchronized 关键字是用来控制线程同步的,在多线程的环境下,控制
synchronized 代码段不被多个线程同时执行。synchronized 可以修饰类、方法、变量。 -
在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)
是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上
的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时
需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这
也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面
对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的
实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来
减少锁操作的开销。