文章目录
- 1.什么是进程?什么是线程?
- 2.JVM、JRE、JDK的关系?
- 3.JVM中可以运行多种语言吗?
- 4.JVM有哪些内存区?
- 5.堆空间大小怎么配置?各区域怎么划分?
- 6.JVM中那些内存区域会发生内存溢出(OOM)
- 7.JVM在创建对象时采取了哪些并发安全机制
- 8.什么是对象头?里面有哪些东西
- 9.为什么不要使用Finalize方法
- 10.怎么判断对象的存活?
- 11.复制算法
- 12.标记清除算法
- 13.标记整理算法
- 14.扩容新生代为什么能提高GC效率
- 15.CMS垃圾回收期
- 16.守护线程和用户线程的区别
- 17.多线程中的上下文切换
- 18.死锁及造成的危害
- 19.Executor和Executors的区别
- 20.CAS
- 21.Lock接口是什么?对比synchronized有什么优势?
- 22.阻塞队列
- 23.Callable和Future
- 24.FutureTask
- 25.并发容器的实现
- 26.为什么调用start()方法会执行run()方法,而不是直接调用run()?
- 27.什么是不可变对象?和对并发应用的作用
- 28.乐观锁与悲观锁
- 29.wait和sleep方法的不同
- 30.为什么wait、notify、notifyAll不在thread中?
- 31.Java内存模型
- 32.什么是线程安全
- 33.线程运行异常会怎样
- 34.线程之间如何共享数据
- 35.volatile
- 36.ThreadLocal有什么用
- 37.生产者-消费者模型的作用是什么
- 38.为什么要使用线程池
- 40.如何唤醒阻塞的线程
- 41.线程调度算法
- 42.线程组
- 42.Java中实现线程的方法
- 43.SyschronizedMap和ConcurrentHashMap的区别
- 44.ConcurrentHashMap的并发度
- 45.CopyOnWriteArrayList
- 46.Thread.sleep(0)的作用
- 47.Thread中的yield
- 48.什么是线程调度器和CPU时间片
- 49.如何确保main()方法所在的线程是Java程序最后结束的线程
- 50.Java的Timer类
- 51.Samaphore
- 52.为什么代码会重排序
- 53.如何保证线程的顺序执行
- 54.线程池满了会发生什么?
- 55.ReadWriteLock
- 56.volatile变量和atomic变量有什么不同?
- 57.DCL(单例模式的双检锁)
- 58.CyclicBarrier和CountDownLatch
- 59.获取线程dump文件
- 60.如何检查一个线程是否持有对象监视器?
- 61.Linux环境下如何查找哪个线程使用CPU最长?
- 62.AQS
- 63.线程类的构造方法、静态快是被哪个线程调用的?
- 64.同步方法和同步块的选择
- 65.多线程的同步和互斥
- 66.竞争条件和如何发现、解决竞争
- 67.Java如何实现多线程间的通讯和协作
- 68.为什么wait和notify要在同步块中调用
- 69.JVM中控制线程栈堆大小的参数
- 70.阻塞式方法
- 71.线程优先级
- 72.对象创建的过程
- 73.SafePoint是什么?安全区域是什么?
- 74.Minor GC和Full GC
- 75.类加载器
- 77.如何开启JVM日志
- 78.常见JVM参数
- 79.G1垃圾回收器
- 80.JVM调优工具
1.什么是进程?什么是线程?
进程是操作系统分配资源的最小单元。
线程是操作系统调度的最小单元。
一个进程至少有一个线程,一个线程至少有一个进程。

2.JVM、JRE、JDK的关系?
JVM(Java Virtual Machine):Java虚拟机。能够识别并解析.class文件,以调用相应的操作系统上的函数,完成操作。
JRE(Java Runtime Environment):Java运行环境。由JVM加上一大堆基础类库组成。
JDK(Java Development Kit):Java开发工具。JRE加上一些工具组成,如Javac、java、jar等。
3.JVM中可以运行多种语言吗?
JVM只识别字节码,所以JVM其实是与语言解耦的,也就是没有直接关联。
如Scale、Groovy、Kotlin等等语言都可以在JVM上运行
4.JVM有哪些内存区?
- 线程私有区(每个线程私有)
- 虚拟机栈:在JVM运行过程中存储当前线程运行方法所需要的数据、指令、返回地址。
- 本地方法栈:与虚拟机栈类似,他的服务对象的是native方法。
- 程序计数器:用于记录各个线程执行的字节码的地址(行号)。确保多线程的正常运行。
- 线程共享区(由JDK定义)
- 方法区:JDK1.7及之前“永久代”,JDK1.8及以后“元空间”,存放类的信息、常量池、方法数据、方法代码等
- 堆:堆是JVM上最大的内存区域,我们申请的几乎所有的对象,都是在堆中存储。
5.堆空间大小怎么配置?各区域怎么划分?
例如:活跃数据为300M
总堆:300M*4=1.2G
新生代=450M
老年代=750M
永久代/元空间=300M(基于full gc后占用)
6.JVM中那些内存区域会发生内存溢出(OOM)
栈溢出(StackOverflowError)
每个线程默认分配1M大小的栈空间,如死循环、死递归等会导致压入的栈针过多,而爆栈
# 栈默认分配1M空间
static int count;
public void test(){
count++;
test();
}
堆溢出(OutOfMemoryError:Java heap space)
分配堆空间时,若空间不足,则会触发GC回收,以腾出更多的空间以供分配,当GC后仍然空间不足,则会爆内存。如初始化一个空间极大的数组。
# JVM参数:-Xms30 -Xmx30m -XX:+PrintGCDetails
String[] strs = new Strings[35*1024*1024]; //请求35M的数组空间,但只有30M的堆
方法区溢出(OutOfMemoryError:Metaspace)
静态数据过多的时候,会导致方法区溢出,如加载了大量的类。
# JVM参数:-XX:MaxMetaspaceSize=10M -XX:+PrintGC
堆外内存(OutOfMemoryError:Direct buffer memory)
JVM之外的内存,不受JVM管辖,因此需要手动GC。
# JVM参数:--XX:MaxDirectMemorySize=100m
ByteBuffer bb = ByteBuffer.allocationDirect(128*1024*1024)
程序计数器是唯一不会发生OOM的区域
7.JVM在创建对象时采取了哪些并发安全机制
默认:本地线程分配缓冲(TLAB,Thread Local Allocation Buffer)
每个线程在Java堆中先预先分配一小块私有内存,这样每个线程都拥有单独的一个Buffer,如果需要分配内存,就只需要在自己的buffer上分配,不需要竞争,可以大大提高分配效率
可选:CAS+失败重试(乐观锁)
8.什么是对象头?里面有哪些东西
9.为什么不要使用Finalize方法
一个对象要被回收,需要两次标过程,一次是没有找到与GCRoots的引用链,一次是进行筛选(如果对象覆盖了finalize,我们可以在finalize中拯救,并将其变为存活对象)
-
finalize方法执行线程优先级很低,需要等待
如果不等待,那么可能出现对象被调用时还没来得及拯救的情况
-
finalize方法只能执行一次
10.怎么判断对象的存活?
引用计数法
对象被引用的数量进行计数,大于0的则为存活,为0的则为垃圾
但是当两个对象相互引用时,将无法判断
可达性分析算法
通过GCRoot的对象为起始节点,向下搜索,走过的路径称为“引用链”。若某个对象在引用链中不可达,则为垃圾对象。
在Java语言中,可作为GC Roots的对象包含以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。(可以理解为:引用栈帧中的本地变量表的所有对象)
- 方法区中静态属性引用的对象(可以理解为:引用方法区该静态属性的所有对象)
- 方法区中常量引用的对象(可以理解为:引用方法区中常量的所有对象)
- 本地方法栈中(Native方法)引用的对象(可以理解为:引用Native方法的所有对象)
11.复制算法
优点:效率高
缺点:空间利用率只有50%
12.标记清除算法
先根据可达性算法标记存活对象,再清除没有标记的对象
优点:简单
缺点:会产生内存碎片(回收后的内存空间不连续)
比较适用于老年代,因为其垃圾比较少
13.标记整理算法
先标记存活对象,再将存活对象移动到连续的内存空间,最后清除掉端边界以外的所有内存
优点:没有内存碎片,空间利用率是100%
缺点:需要大量移动对象,效率偏低
14.扩容新生代为什么能提高GC效率
如:
对象存活时间为7s
100m空间的新生代A,每隔2.5s GC一次
200m空间的新生代B,每隔5s GC一次
400m空间的新生代C,每隔10s GC一次
那么在10s内,A需要扫描4次,复制2次;B需要扫描2次,复制1次;C只需要扫描1次,不需要复制;
扫描时间消耗相等,但减少了复制次数。
因此,扩大新生代空间,可以提高GC的时间间隔,以减少对象的复制次数
15.CMS垃圾回收期
Concurrent Mark Sweep 并发标记清除法
- 步骤
- 初始标记:标记与GC Roots直连的对象。因为直连对象较少,因此暂停用户线程的时间很短
- 并发标记:标记其他与GC Roots有关联的对象。用户线程和GC同时运行,时间较长。
- 重新标记:修正在并发标记阶段产生变动的对象。需要短时间暂停用户线程。
- 并发清除:并发处理,不影响用户线程
- 问题
- CPU敏感:GC过程中需要占用线程。因此建议至少4线程的CPU
- 浮动垃圾:并发清除过程中,产生的垃圾对象无法清理,只能等待下一次GC。因此建议预留空间,不要等到内存满了之后才进行GC。如占用92%就进行GC,预留8%的浮动垃圾空间。
- 内存碎片:同标记算法的缺点。当GC后,仍然没有足够的连续空间时,会退化为 SerialOld单线程垃圾回收(标记整理算法)
16.守护线程和用户线程的区别
一般程序使用的是用户线程(User)
守护线程通过调用Thread.setDaemon(ture)
设置,如GC就是守护线程
Thread.setDaemon(ture)
必须在Thread.start()
之前调用,否则会抛出异常- 守护线程是为其他线程提供服务。如果全部用户线程已经结束,守护线程没有可服务的用户线程,JVM会关闭。
17.多线程中的上下文切换
CPU时间片:CPU分配给每个线程的时间段,一般为几十毫秒
上下文切换:当一个线程的时间片用完了,或者因自身原因暂停运行,那么另一个线程就会被操作系统选中,来占用CPU。这种一个线程被暂停剥夺使用权,另一个线程被选中开始或继续运行的过程就叫做上下文切换。
上下文:在这种切入切出的过程中,操作系统需要保存和恢复相应的进度信息,这个进度信息就是上下文
18.死锁及造成的危害
死锁:指两个或两个以上的进程(线程)在执行过程中,因抢夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
危害:
- 进程得不到正确的结果
- 使资源的利用率降低
- 产生新的死锁
19.Executor和Executors的区别

# 使用excutor创建线程池
ThreadPoolExecutor tpe = new ThreadPoolExecutor(10,20,0L,TimeUnit.SECONDS,new LinkedBlockingQueue<>(10));
# 使用excutors创建线程池,其本质是用于快捷操作线程池的工具类
ExcutorService e = Excutors.newFixedThreadPool(10);
20.CAS
全程Compare and Sweep,即比较和交换
线程提供旧值与资源对象进行比较,如果相等,那么将资源对象的值替换为新值
如果不相等,则判定为CAS失败,获取资源对象的实时值,再次进行CAS操作
本质上是一个乐观锁
CAS的问题:
- 开销大
- 只能确保一个共享变量
- ABA问题。如果资源对象的实时值被从A修改到B再修改回A,那么在执行CAS操作时,将判定为该资源对象未被修改
21.Lock接口是什么?对比synchronized有什么优势?
lock接口是synchronized的扩展版,提供了无条件的、可轮询的(tryLock)、定时的(tryLock带参数)、可中断的(lockInterruptibly)、可多条件队列(newCondition)的锁操作。
此外lock的实现类基本都支持非公平锁(默认)和公平锁,synchronized只支持非公平锁。当然大部分情况下非公平锁更加高效。
因此,lock接口比synchronize提供了更具扩展性的锁操作。
22.阻塞队列
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列
- 在队列为空时,获取元素的线程会等待队列变为非空
- 在队列满时,存储元素的线程会等待队列可用。
主要利用Condition和Lock的等待通知模式。
23.Callable和Future
Callable接口类似于Runnable,但Runnable不会返回结果,并且无法抛出返回结果的异常
而Callable被线程执行后,可用返回值,这个返回值可用被Future拿到。
也就是说,Future可用拿到异步执行任务的返回值。
Callable可用认为是带有回调的Runnable。
Future接口表示异步任务,是还没有完成的任务给出的未来结果。
所以Callable用于产生结果,Future用于获取结果。
24.FutureTask
FutureTask表示一个可以取消的异步运算。
拥有启动和取消运算、查询运算是否完成和取回运算结果等方法,只有当运算完成的时候结果才能被取回,否则get方法将会阻塞。
本质上FutureTask也是调用的Runnable接口,所以也可以提交给Executor来执行。
25.并发容器的实现
并发容器可以简单的理解为通过synchronized来实现同步的容器。
如果有多个线程调用同步容器的方法,他们将会串行执行,如Vector、Hashtable、Collections.synchronizedSet、synchronizedList等方法返回的容器。
但如ConcurrentHashMap,并没有在方法层面加锁,而是通过CAS机制和方法内部节点加锁来降低锁的力度,提高执行效率。
26.为什么调用start()方法会执行run()方法,而不是直接调用run()?
调用start()方法会创建一个新的线程,并执行run()方法。
但如果直接调用run()方法,不会创建新的线程,而是当做普通方法直接在主线程调用。
27.什么是不可变对象?和对并发应用的作用
不可变对象(Immutable Objects)即对象一旦被创建,他的状态就不能再被改变,反之为可变对象(Mutable Objects)。
如Java类库自带的String、基本类型的包装类、BigInteger、BigDecimal等。
不可变对象天生是线程安全的。他们的常量(域)是构造函数中创建的,既然他们的状态永远都不会变,那么这些常量永远不会变。
28.乐观锁与悲观锁
-
悲观锁:总是假设最坏的情况,即每次获取数据都认为别人会修改,所以每次拿数据的时候都会上锁。这样别人想拿这个数据就会阻塞直到它获取到数据。
Java中的synchronized关键字的实现是悲观锁。
-
乐观锁:每次获取数据都认为别人不会修改,所以不会上锁,但在更新数据的时候会判断在此期间是否有别人也更新了这个数据,可以使用版本号等机制进行判断。
Java中的原子变量类就是使用的乐观锁CAS实现。
-
乐观锁的实现:
- 使用版本表示来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致的时候可以采取丢弃和再次尝试的策略。
- Java中的CAS机制。当多个线程尝试使用CAS更新同一变量时,只有其中一个线程能更新,其余线程都会失败。失败的线程并不会被挂起,而是被告知竞争失败,重新尝试。
29.wait和sleep方法的不同
等待的时候,wait方法会释放锁,而sleep会一直持有锁。
因此wait通常被用于线程间的交互,而sleep通常被用于暂停执行。
30.为什么wait、notify、notifyAll不在thread中?
Java提供的锁是对象级的而不是线程级的。
加锁的对象,通过线程获得。如果线程需要等待某些锁,那么调用对象中的wait方法就有意义了。
简而言之,wait、notify、notifyAll都是锁级别的操作,而锁本质上属于对象。
31.Java内存模型
-
可见性问题:多个线程处理同一个变量的时候,由于各自的工作内存独立,因此对于变量的改动互不可见。
解决方案为对变量添加volatile关键词,作用为禁止缓存
- 线程修改该变量时,将会强制同步到内存,并使其他线程中的缓存失效
- 线程读取该变量的时候,若变量被修改过,则缓存失效,直接去内存中读取
volatile不保证原子性,因此不可替代synchronized关键词,但性能在某些情况下优于synchronized。
-
线程安全性问题:加锁或者CAS
-
指令重排序问题
32.什么是线程安全
当多个线程访问某个类的时候,不论采用何种调度方式,或者如果交替执行,并且在调用中不需要任何额外的同步或者协同,这个类都能表现出正确的行为。那么称这个类为线程安全的。
-
不可变
像String、Interger、Long这些,都是final类型的类
-
加锁和CAS
33.线程运行异常会怎样
- 如果线程的异常没有被捕获,将会停止运行
- 如果异常被捕获或者抛出,则程序会继续执行
如果线程持有某个对象的监视器,那么这个对象的监视器会被立刻释放
34.线程之间如何共享数据
使用静态变量共享数据即可,但如果对数据进行修改,则会出现线程安全问题
35.volatile
作用:
-
确保多线程可见性
对变量进行写操作,会触发总线嗅探机制,将数据同步至主内存,并使其他线程工作内存中的flag副本缓存失效
-
禁止指令重排
CPU会对线程的指令进行重排序,会保证单线程中所有指令的顺序执行,但在多线程中可能会出现线程安全性问题。
36.ThreadLocal有什么用
ThreadLocal是Java里一种特殊的变量。每个线程都有一个ThreadLocal,就是每个线程都有自己独立的一个变量,竞争条件就被彻底消除了。
但请注意,若ThreadLocal使用不当,有可能会出现内存泄漏的问题。
37.生产者-消费者模型的作用是什么
- 通过平衡生产能力和消费能力,来提升整个系统的运行效率
- 解耦。降低生产者和消费者之间的联系。
38.为什么要使用线程池
- 节约资源:避免频繁的创建和销毁线程,达到线程对象的重用
- 灵活:根据项目灵活的控制并发的数目
40.如何唤醒阻塞的线程
- wait和notify
- park和unpark:阻塞和唤醒指定线程,基于LockSupport。
41.线程调度算法
抢占式。
一个线程时间片使用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据,计算出一个总的优先级,并分配下一个时间片给某个线程执行。
42.线程组
线程组与线程池是不同的概念,作用完全不同
- 线程组是为了方便线程的管理
- 线程池是为了管理线程的生命周期,复用线程,以减少创建和销毁线程的开销
不推荐使用的原因:
- 线程组ThreadGroup对象中的stop、resume、suspend会导致安全问题,主要是死锁问题,已被官方废弃。
- 线程组不是线程安全的,在使用过程中不能及时获取安全的信息。
42.Java中实现线程的方法
-
继承Thread类
new Thread(){ public void run(){ } }.start();
-
继承Thread类,并实现Runnable接口
new Thrad(new Runnable(){ public void run(){ } }).start();
-
Callback接口和FutureTask类,需要实现call()方法
public static void main(String[] args) { // 定义线程任务 FutureTask<Integer> futureTask = new FutureTask<>(new Callable<>() { @Override public Integer call() throws Exception { return new Random(100).nextInt(); } }); try { // 执行线程并获取结果 new Thread(futureTask).start(); System.out.println(futureTask.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } }
-
线程池
ExecutorService executorService= Executors.newFixedThreadPool(3); executorService.execute(() -> { // do something });
43.SyschronizedMap和ConcurrentHashMap的区别
SyschronizedMap是一次性锁住整张表,来确保线程安全,所以每次只能有一个线程来访问map。
CoucurrentHasMap是使用分段锁来保证多线程下的性能。
44.ConcurrentHashMap的并发度
-
JDK1.7及之前:ConcurrentHashMap使用Segment(锁段)实际map划分为若干部分,来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,她是ConCurrentHashMap类构造函数的一个可选参数,默认值为16.
-
在JDK1.8及之后,利用CAS算法,同时加入更多的辅助变量来提高并发度。
如在hash不冲突的情况下,可能不使用锁,hash冲突则对某个节点加锁,以提高并发效率。并发度并不确定,但整体效率高于使用锁段
45.CopyOnWriteArrayList
CopyOnWriteArrayList是java.util.concurrent包提供的方法,读操作无锁,写操作通过操作底层数组的新副本来实现,是一种读写分离的并发策略。
即读取数据时候无锁,写入数据的时候,将原先的ArrayList拷贝一份,在副本中修改数据,此过程中加锁,完成写入之后,将原容器引用指向新的副本。
使用场景:读入很频繁,但写入数据并不频繁。如黑名单、秒杀等场景。
46.Thread.sleep(0)的作用
由于大部分操作系统采用抢占式的线程调度算法,因此可能出现某线程经常获取到CPU控制权的情况。
为了让某些低优先级的线程也能获取到CPU控制权,因此可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作。
这也是平衡CPU控制权的一种操作。
在大循环里面可以写入一句Thread.sleep(0),这样就给了其他线程获取CPU控制权的可能,避免让低优先级的线程出现假死。
如RocketMQ源码中的Thread.sleep(0)。会每隔1000次操作让出执行一次该操作。
47.Thread中的yield
线程的状态主要有五种:
- 新建:NEW
- 就绪:RUNNABLE
- 运行:RUNNING
- 阻塞:BLOCKED
- 死亡:DEAD
yield即线程让步,可以使当前线程从运行状态变为就绪状态。
如有多个线程,其中线程A正在运行,A若执行yield则会变成就绪状态,与其他线程重新竞争执行权,有可能仍然是自己竞争成功,也有可能是其他线程竞争成功。
48.什么是线程调度器和CPU时间片
线程调度器是一个操作系统服务,负责为Runnable状态的线程分配CPU时间。
每次分配的时间通常只有几十毫秒,这个时间段则称为CPU时间片。
分配的规则基于线程优先级和饥饿程度。
49.如何确保main()方法所在的线程是Java程序最后结束的线程
使用Thread类中的join方法,来确保所有线程在main()方法退出之前结束。
join()方法必须在start()方法之后调用,作用为等待该线程先结束。
50.Java的Timer类
-
java.util.Timer是一个工具类,用于安排一个线程在未来的某个特定时间执行。
Timer类可以安排一次性任务或者周期任务
-
java.util.TimerTask是一个实现了Runnable接口的抽象类。我们需要去继承这个类来创建我们的定时任务,并用Timer去安排他的狮子那个
源码仅有几百行,很容易理解。
51.Samaphore
Samaphore就是一个信号量,用于限制某块代码的并发数。
拥有一个构造函数,可以传入一个int型参数n,表明某段代码最多可以有n个线程访问,如果超过n则会堵塞。
当n等于1时,就相当于变成了synchronized了。
52.为什么代码会重排序
在执行程序时,为保证性能,在不影响单线程的结果的前提下,处理器和编译器通常会对指令进行重排序
53.如何保证线程的顺序执行
如对于三个需要顺序执行的线程T1,T2,T3
- 使用join()。
- T3中启动T2,T2中启动T1。
54.线程池满了会发生什么?
- 核心线程充足时,会调用核心线程
- 核心线程不足时,会创建非核心线程,但总线程数量不超过MaxNumPool
- 核心线程和非核心线程都不足时,任务会进入阻塞队列
- 如果阻塞队列满了的话,任务会被拒绝
将堵塞队列设置为无限大小,则可以避免任务被拒绝
55.ReadWriteLock
ReadWriteLock接口的实现ReentrandReadWriteLock
ReadWriteLock等大部分锁都是排它锁,即只允许同一时刻一个线程访问,而读写锁允许同一时刻多个读线程访问,但在写线程访问时,所有其他读或写线程勋被堵塞。
读写锁维护一对锁(一个读锁,一个写锁),通过分离读锁和写锁,来提高并发性。
一般情况下,读写锁的性能优于其他排它锁,拥有更好的并发性和吞吐量,因为大多数场景下读多于写。
56.volatile变量和atomic变量有什么不同?
volatile可以确保先行关系,即写操作会发生在后续的读操作之前,但它并不能保证原子性。例如自增操作就不是原子性的。
而AtomicInteger提供的atomic方法,可以让这种操作具有原子性,如getAndIncrement()方法会原子性的进行自增,其他数据类型和引用变量也可以进行相似操作。
57.DCL(单例模式的双检锁)
需要使用volatile来避免出现半初始化状态的情况。
58.CyclicBarrier和CountDownLatch
都是同步工具,用于协调多个线程之间的同步。
通常用于控制线程等待,直到倒计时结束,再继续执行。
CyclicBarrier可以重复使用,而CountDownLatch不能。
59.获取线程dump文件
- 使用Java工具VisualVm工具
- 命令工具
jmap -dump:format=b,file=F:/heamdump.out 1654
60.如何检查一个线程是否持有对象监视器?
使用Thread.holdLock(Object obj)方法,当obj的监视器被某线程持有的时候会返回true。
注意这是一个static方法,意味着“某线程”指的是当前线程。
61.Linux环境下如何查找哪个线程使用CPU最长?
- 指令
top
查看所有进程,以查看哪个线程的占用过高 - 指令
top -p 2876
只查看进程2876,按下H
即可查看其拥有的线程
62.AQS
全称AbstractQueuedSynchronizer,用于构建锁或其他同步组件的基础框架,比如ReentrantLock、ReentrantReadWriteLock和CountDownLatch.
他使用一个int成员变量表示同步状态,通过内置的FIFO队列完成资源获取线程的排队工作。
他是CLH队列锁的一种变体实现。
可以实现两种同步方式:独占式、共享式。
AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,同步器的设计基于模板方法模式,所以如果要实现我们自己的同步工具,需要覆盖几个可重写的方法,容易tryAcquire、tryReleaseShared等等。
63.线程类的构造方法、静态快是被哪个线程调用的?
线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身调用的。
64.同步方法和同步块的选择
class Test{
// 同步方法
synchronized method{
// todo something
}
}
method2(){
// 同步代码块
synchronized(lock){
// todo something
}
}
取决于业务逻辑
- 同步方法相当于锁了this,范围是整个方法
- 同步块只锁了对象
65.多线程的同步和互斥
- 临界区(Critical Section):适用于一个进程内的多线程访问公共区域或代码段
- 互斥量(Mutes):适用于不同进程内多线程访问公共区域和代码段
- 事件(Event):通过线程间触发事件来实现同步互斥
- 信号量(Semaphore):可以实现多个线程同时访问公共区域数据,原理与操作系统中的PV操作类似:先设置一个访问公共区域的线程最大连接数,没有一个线程访问共享区资源数就减一,直到资源数小于等于零。
66.竞争条件和如何发现、解决竞争
多线程中线程不确定的执行时序导致不确定的结果,就是竞争条件,即线程不安全。
通常使用加锁的方式使线程串行访问临界区。
67.Java如何实现多线程间的通讯和协作
典型例子就是生产者-消费者模型
常用两种方式:
- synchronized+wait+notify模式
- lock+condition模式
68.为什么wait和notify要在同步块中调用
-
Java API强制要求,不这样做会抛出异常
-
wait/notify是线程之间的通信,存在竞争条件,我们必须保证在满足条件的情况下进行wait。
即,如果不加锁的话,那么wait被调用的时候可能wait的条件已经不满足了
69.JVM中控制线程栈堆大小的参数
-Xss 每个线程的栈的大小 默认1m
70.阻塞式方法
阻塞式方法是指程序会一直等待该方法,期间不做其他事情。如ServerSocket的accept()方法就是一直等待客户端连接。
这里的阻塞式是指调用结果返回之前,当前线程会被挂起,直到得到返回结果之后才会返回。
71.线程优先级
每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。
我们可以定义线程的优先级,但并不能保证高优先级的线程比低优先级的线程先执行。
线程优先级是一个int变量,从1-10优先级递增。
java的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,若无特别需要,一般不进行手动设置。
72.对象创建的过程
1.检查加载
检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查类是否已经被加载、解析和初始化过
2.分配内存
虚拟机为新生的对象分配内存。为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
3.内存空间初始化
内存分配完成后,虚拟机需要将分配到的空间都初始化为零值(如int为0,boolean为false等)。
以保证对象的实例字段在java代码中不需要赋初始值就可以直接使用。
4.设置
虚拟机对对象进行必要的设置,例如对象是哪个类的实例、如何才能找到类的元数据信息、对象的hash码、对象的GC分代年龄等信息。
这些信息存放在对象的对象头之中。
5.对象的初始化
把对象按照开发者的意愿进行初始化(构造方法)
73.SafePoint是什么?安全区域是什么?
安全点:
用户线程暂停,GC线程要开始工作,但必须确保用户线程暂停的这些字节码指令不会导致引用关系的变化。
所以JVM会在字节码指令中,选一些指令,作为安全点,比如方法调用、循环跳转、异常跳转等,一般是这些指令才会产生安全点。
GC要暂停业务线程,并不是抢占式中断,而是主动中断,即设置一个中断标志,各业务线程在运行过程中会不断主动轮询这个标志,一旦发现中断标志位true,就会在自己最近的安全点上主动挂起。
安全区域
如果业务线程都不执行,那么程序就没法进入安全点,这种情况就必须引入安全区域。
安全区域是指能够确保在某一段代码片段中,引用关系不会发生变化,因此这个区域任意位置开始垃圾回收都是安全的。
我们也可以把安全区域看做是延伸后的安全点。
当用户线程执行到安全区域时,会标识自己进入了安全区域,这段时间里JVM发起GC就会忽视这个线程。
当线程离开安全区域时,会检查JVM是否已经完成根节点枚举,或者其他GC中需要暂停用户线程的阶段
- 如果已经完成,那么继续执行
- 否则一直等待,直到收到可以离开安全区域的信号
74.Minor GC和Full GC
- 当新生代空间不足时,会发起一次Minor GC(Young GC)
- 当老年代空间不足时,会发起一次Full GC
- 当方法区空间不足时,也会发起Full GC
75.类加载器
类从被加载到被卸载,经历了7个阶段,其中验证、准备、解析三个部分统称为连接(Linking)
类加载器
负责完成加载、验证、准备、解析和初始化
Bootstrap ClassLoader
任何类的加载行为都必须经过,用于加载核心类库,如tr.jar、resource.jar、charsets.jar等。
当然这些jar包的路径可以是指定的,-Xbootclasspath
参数可以用于设置.
这个加载器是C++编写的,随着JVM启动。
Extention ClassLoader
扩展类加载器,主要用于加载lib/ext目录下的jar包和.class文件。可以通过系统变量java/ext/dirs
指定这个目录.
这个加载器是个Java类,继承自URLClassLoader。
Application ClassLoader
java类的默认加载器,也成为System ClassLoader,一般用于加载classpath下所有其他jar包和.class文件。
我们写的代码会首先尝试这个加载类进行加载。
Custom ClassLoader
自定义加载器,支持一些个性化的扩展功能。如加密解密等。
76.类加载器的双亲委派模型
77.如何开启JVM日志
一般只要开启gc日志打印,都会默认开启简单日志模式。
生产环境强烈建议开启详细gc日志模式。
-XX:+PrintGC
:简单日志-XX:+PrintGCDetails
:详细日志
78.常见JVM参数
1.标准:- 开头,所有HotSpot都支持
保证JVM的所有实现都支持标准选项。
常用语执行常见操作,如检查JRE版本、设置类路径、启用详细输出等
-d32
在32位环境中运行。默认为32位。
-d63
在64位环境中运行。
2.标准:-X 开头,特定版本HotSpot支持
-Xms
设置堆空间的最小值和初始大小。此值必须是1024的倍数且大于1MB,附加字母k或K表示千字节,m或M表示兆字节
若不设置则会自动设置为老年代和年轻代的大小之和。
年轻代堆的大小可以使用-Xmn选项或者-XX:NewSize来设置
3.高级选项:以开头-XX
可以使用命令行java -XX:PrintFlagsFinal -version
查询所有高级选项。
其中参数类型为product需要重启项目生效,为manageable的可以即时生效
79.G1垃圾回收器

会将堆空间划分为若干个大小固定的区域,如将2G空间划分为1000个2M的区域。
- 并发与并行
- 分代收集
- 空间整合。基于标记-整理算法,解决内存碎片的问题。
- 可以建立可预测的停顿模型。
- 将整个Java堆内存模型划分为多个大小相等的region,使得老年代和年轻代不再物理隔开。
80.JVM调优工具
命令行工具
-
JPS:列出当前机器上正在运行的虚拟机进程,JPS从操作系统的临时目录上去找(因此部分信息可能显示不全)
-
jstat:监控虚拟机中各种运行状态信息。可以显示本地或者远程虚拟机进程中的类装在。内存、垃圾收集、JIT编译等运行数据。
没有GUI图形界面,在只提供纯文本控制台环境的服务器上将会是首选。
-
jinfo:
- jinfo-sysprops:查看由System:getProperties()取得的参数
- jinfo-flag:通过进程id修改对应参数
- jinfo-flags:显示虚拟机参数
-
jmap:用于生成堆转储快照(headdump或者dump),也可以查询finalize执行队列,java堆和永久代的详细信息,如空间使用率、使用的收集器等
但部分功能在windows下受限,只能在linux/Solaris下使用
-
jstack:用于生成虚拟机当前时刻的线程快照,即当前虚拟机内每一条线程正在执行的方法堆栈的集合。
主要用于定位线程出现长时间停顿的原因,如线程间的死锁、死循环、请求外部资源过久等。
可视化工具
- Jconsole:jdk自带
- Arthas:由Alibaba开发的java诊断工具,支持JDK 6+,支持Linux/MAC/Windows,此阿勇命令行交互,且提供丰富的tab自动补全功能。
- MAT:基于eclipse平台开发,是一款很好的内存分析工具。