Java 多线程 — 并发工具与实战
📋 本文节选自《Java 多线程面试题》,返回 查看全部章节
46. Java 内存模型是什么,哪些区域是线程共享的,哪些是不共享的?
我们知道的 JVM 内存区域有:堆和栈,这是一种泛的分法,也是按运行时区域的一种分法,堆是所有线程共享的一块区域,而栈是线程隔离的,每个线程互不共享。线程不共享区域每个线程的数据区域包括程序计数器、虚拟机栈和本地方法栈,它们都是在新线程创建时才创建的。程序计数器(Program Counter Rerister)程序计数器区域一块内存较小的区域,它用于存储线程的每个执行指令,每个线程都有自己的程序计数器,此区域不会有内存溢出的情况。虚拟机栈(VM Stack)虚拟机栈描述的是 Java 方法执行的内存模型,每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。本地方法栈(Native Method Stack)本地方法栈用于支持本地方法(native 标识的方法,即非Java 语言实现的方法)。虚拟机栈和本地方法栈,当线程请求分配的栈容量超过JVM允许的最大容量时抛出StackOverflowError 异常。线程共享区域线程共享区域包含:堆和方法区。堆(Heap)堆是最常处理的区域,它存储在 JVM 启动时创建的数组和对象,JVM垃圾收集也主要是在堆上面工作。如果实 际 所 需 的 堆 超 过 了 自 动 内 存 管 理 系 统 能提供的最大容量时抛出OutOfMemoryError 异常。方法区(Method Area)方法区是可供各条线程共享的运行时内存区域。存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法。当创建类和接口时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大内存空间后就会抛出 OutOfMemoryError运行时常量池(Runtime Constant Pool)运行时常量池是方法区的一部分,每一个运行时常量池都分配在JVM的方法区中,在类和接口被加载到 JVM 后,对应的运行时常量池就被创建。运行时常量池是每一个类或接口的常量池(Constant_Pool)的运行时表现形式,它包括了若干种常量:编译器可知的数值字面量到必须运行期解析后才能获得的方法或字段的引用。如果方法区的内存空间不能满足内存分配请求,那 Java 虚拟机将抛出一个OutOfMemoryError 异常。栈包含 Frames,当调用方法时,Frame 被推送到堆栈。一个 Frame 包含局部变量数组、操作数栈、常量池引用。
47. 什么是乐观锁和悲观锁?
乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。
48. Hashtable 的 size()方法为什么要做同步?
同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访问。所以,这样就有问题了,可能线程A 在执行Hashtable 的put方法添加数据,线程 B 则可以正常调用 size()方法读取Hashtable 中当前元素的个数,那读取到的值可能不是最新的,可能线程 A 添加了完了数据,但是没有对size++,线程 B 就已经读取 size了,那么对于线程 B 来说读取到的size 一定是不准确的。而给 size()方法加了同步之后,意味着线程 B 调用size()方法只有在线程A调用 put 方法完毕之后才可以调用,这样就保证了线程安全性CPU 执行代码,执行的不是 Java 代码,这点很关键,一定得记住。Java 代码最终是被翻译成机器码执行的,机器码才是真正可以和硬件电路交互的代码。即使你看到Java 代码只有一行,甚至你看到 Java 代码编译之后生成的字节码也只有一行,也不意味着对于底层来说这句语句的操作只有一个。一句"return count"假设被翻译成了三句汇编语句执行,一句汇编语句和其机器码做对应,完全可能执行完第一句,线程就切换了。
49. 同步方法和同步块,哪种更好?
同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好。
50. 什么是自旋锁?
自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时才能进入临界区。
51. Runnable 和 Thread 用哪个好?
Java 不支持类的多重继承,但允许你实现多个接口。所以如果你要继承其他类,也为了减少类之间的耦合性,Runnable 会更好。
52. Java 中 notify 和 notifyAll 有什么区别?
notify()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。而notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。
53. 为什么 wait/notify/notifyAll 这些方法不在thread类里面?
这是个设计相关的问题,它考察的是面试者对现有系统和一些普遍存在但看起来不合理的事物的看法。回答这些问题的时候,你要说明为什么把这些方法放在Object类里是有意义的,还有不把它放在 Thread 类里的原因。一个很明显的原因是JAVA 提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在 Thread 类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify 和 notifyAll 都是锁级别的操作,所以把他们定义在Object 类中因为锁属于对象。
54. 为什么 wait 和 notify 方法要在同步块中调用?
主要是 因 为 Java API 强 制 要 求 这 样 做 , 如 果 你不这么做,你的代码会 抛 出IllegalMonitorStateException 异常。还有一个原因是为了避免wait 和notify之间产生竞态条件。
55. 为什么你应该在循环中检查等待条件?
处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。因此,当一个等待线程醒来时,不能认为它原来的等待状态仍然是有效的,在 notify()方法调用之后和等待线程醒来之前这段时间它可能会改变。这就是在循环中使用 wait()方法效果更好的原因,你可以在 Eclipse 中创建模板调用 wait和 notify 试一试。
56. Java 中堆和栈有什么不同?
每个线程都有自己的栈内存,用于存储本地变量,方法参数和栈调用,一个线程中存储的变量对其它线程是不可见的。而堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时 volatile 变量就可以发挥作用了,它要求线程从主存中读取变量的值。
57. 你如何在 Java 中获取线程堆栈?
对于不同的操作系统,有多种方法来获得 Java 进程的线程堆栈。当你获取线程堆栈时,JVM会把所有线程的状态存到日志文件或者输出到控制台。在Windows 你可以使用 Ctrl + Break 组合键来获取线程堆栈,Linux 下用kill -3 命令。你也可以用 jstack 这个工具来获取,它对线程 id 进行操作,你可以用jps 这个工具找到id。
58. 如何创建线程安全的单例模式?
单例模式即一个 JVM 内存中只存在一个类的对象实例分类1、懒汉式类加载的时候就创建实例
饿汉式;使用的时候才创建实例
59. 什么是阻塞式方法?
阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket 的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。
60. 提交任务时线程池队列已满会时发会生什么?
当线程数小于最大线程池数 maximumPoolSize 时就会创建新线程来处理,而线程数大于等于最大线程池数 maximumPoolSize 时就会执行拒绝策略。
为了帮助更多的粉丝朋友,找到个好工作,多拿几个offer。苏三最近建一些工作内推群,群里目前已经收集了很多内推岗位,大厂、中厂、小厂、外包都有。欢迎HR、开发、测试、运维和产品加入。

扫描下方微信,备注:网站+所在城市,即可拉你进工作内推群。
