16. 说一下synchronized的修饰范围以及锁的部门?
`synchronized`是Java中实现线程同步的关键字,它可以修饰方法或代码块。通过`synchronized`关键字,Java能够保证在同一时刻只有一个线程可以执行被保护的代码块。
- **修饰实例方法**:
当`synchronized`修饰实例方法时,它修饰的是对象的实例锁。每个对象都有一个唯一的实例锁,只有获取该锁的线程才能执行被`synchronized`修饰的方法。
```java
public synchronized void someMethod() {
// 同步代码
}
```
- **修饰静态方法**:
当`synchronized`修饰静态方法时,它修饰的是类级别的锁。类级别的锁是针对类的所有实例共享的,因此静态方法的同步是针对类的类锁。
```java
public static synchronized void someStaticMethod() {
// 同步代码
}
```
- **修饰代码块**:
`synchronized`还可以修饰代码块,确保只有一个线程能够执行特定的代码块。通常使用`synchronized(this)`或者`synchronized(ClassName.class)`来加锁。
```java
public void someMethod() {
synchronized (this) {
// 同步代码
}
}
```
- **锁的部门**:
锁的部门就是加锁的对象。例如:
-
对于实例方法,锁是当前对象(`this`)。
-
对于静态方法,锁是当前类的`Class`对象。
-
对于代码块,锁是提供的对象。
17. 了解volatile吗?都有哪些特性?分别举例子
`volatile`是Java中的一个轻量级同步机制,它确保了变量的可见性,即当一个线程修改了`volatile`变量的值,其他线程能立刻看到修改后的值。`volatile`变量不会被缓存到寄存器中,它直接从主内存读取。
**特性**:
- **保证可见性**:
`volatile`修饰的变量在多个线程之间是可见的。修改一个`volatile`变量的线程会立刻将修改结果刷新到主内存,其他线程能立即读取到修改后的值。
```java
private volatile boolean flag = false;
public void changeFlag() {
flag = true; // 设置为true
}
public boolean checkFlag() {
return flag; // 立即反映flag的最新值
}
```
- **禁止指令重排**:
`volatile`禁止编译器和JVM对该变量的读写操作进行重排序优化。它会确保对`volatile`变量的操作在执行时的顺序是按照代码的顺序进行的。
```java
private volatile int a = 0;
public void setA() {
a = 1; // 第一行
// 其他操作
}
public void printA() {
System.out.println(a); // 保证printA在setA之后才能看到a的最新值
}
```
- **不能保证原子性**:
`volatile`不能保证复合操作的原子性,如`i++`。如果需要复合操作的原子性,应该使用`synchronized`或`AtomicInteger`等机制。
```java
private volatile int count = 0;
public void increment() {
count++; // 不是原子操作
}
```
18. JMM内存模型
JMM(Java Memory Model)是Java虚拟机中用来定义多线程之间如何交互的内存模型。它规定了线程对共享变量的访问规则,保证了程序在多线程环境下的正确性。
-
**工作内存**:每个线程都有自己的工作内存(缓存),即该线程的局部变量和对共享变量的缓存副本。
-
**主内存**:所有线程共享的内存,存放所有的实例字段、静态字段和构成对象的字段。
-
**规则**:
-
**原子性**:基本的读写操作是原子的,但复合操作(如`i++`)不是原子的。
-
**可见性**:一个线程对共享变量的修改必须立刻对其他线程可见(通过`volatile`、`synchronized`等机制)。
-
**有序性**:Java编译器和JVM为了提高性能可能会进行指令重排,但JMM规定了一些发生顺序,以确保程序执行时的顺序符合预期(通过`synchronized`、`volatile`等控制顺序)。
19. JVM运行时区域
JVM(Java Virtual Machine)运行时区域主要包括以下几部分:
- **程序计数器**:
每个线程有一个程序计数器,它指示当前线程执行的字节码的行号。
- **Java虚拟机栈**:
每个线程有一个私有的虚拟机栈,用于存储方法调用的局部变量、操作栈、方法返回地址等。每个方法调用时都会创建一个栈帧,方法结束时栈帧被销毁。
- **本地方法栈**:
本地方法栈与Java虚拟机栈类似,但它用于支持JVM调用的本地方法。
- **堆**:
堆是存放Java对象的地方,所有的对象实例和数组都分配在堆中。堆是JVM中最大的一块内存区域。
- **方法区**:
方法区存储的是类的元数据、常量、静态变量和即时编译器编译后的代码。它是共享的,多个线程可以访问。
- **直接内存**:
直接内存并不属于JVM运行时区域的一部分,但通过`java.nio`包,程序可以直接操作系统的内存。
20. 堆、方法区和虚拟机栈分别是什么?存了什么?
- **堆**:
堆是JVM中存放对象实例和数组的地方,是Java内存中最大的一块区域。JVM会对堆进行垃圾回收。
- **方法区**:
方法区存储JVM加载的类信息、常量池、静态变量、即时编译器编译后的代码等。
- **虚拟机栈**:
虚拟机栈用于存储方法调用的栈帧,每个方法调用都会在栈中分配一个栈帧,栈帧中包含局部变量、操作数栈等信息。
21. 方法区的版本变化以及内部常量池的变化?
- **方法区的版本变化**:
在早期的JVM实现中,方法区是堆的一部分,但在Java 8以后,方法区被移除并被称为**元空间(Metaspace)**。元空间不再在JVM堆中分配内存,而是使用本地内存。
-
**常量池的变化**:
-
**JDK 7及以前**:常量池存储在方法区内。
-
**JDK 8及以后**:常量池被移到了堆中,变成了运行时常量池。原本存储在方法区的类元数据也被移到了元空间。
22. 堆为什么要分成新生代和老年代?
JVM的堆被分为新生代(Young Generation)和老年代(Old Generation),主要是为了优化垃圾回收(GC)过程,减少GC的开销。
- **新生代**:
新生代主要存放新创建的对象。新生代中的对象大部分生命周期短暂,因此GC频繁发生,但每次回收的代价较小。
- **老年代**:
老年代用于存放存活较久的对象。因为老年代对象的生命周期较长,GC较少发生,但回收代价较大。
通过分代回收,JVM能够优化GC过程,提高性能。年轻代使用**Minor GC**,老年代使用**Major GC**或**Full GC**。