Day21
1. 说一下volatile关键字
volatile 关键字主要用来保证变量的可见性和防止指令重排序。当一个变量被声明为 volatile,它的修改会立刻被刷新到主内存,保证其他线程能够及时看到最新值。不过,volatile 并不能保证操作的原子性,比如自增操作还是不安全的。如果需要保证原子性,通常需要结合锁或原子类来实现。
volatile boolean flag = false;Thread A:flag = true; // 修改后对其他线程立即可见Thread B:if (flag) { // 立即读取到最新的 flag 值// 执行相关操作}
2. volatile可以保证线程安全吗?
volatile 关键字能保证变量的 可见性,也就是说,当一个线程修改了 volatile 变量的值,其他线程能立即看到最新值,避免数据不一致的问题。但 volatile 不能保证操作的原子性,比如自增 i++ 是多个步骤组成的复合操作(读取-加一-写回),volatile 不能保证它们整体的线程安全。
所以,volatile 适用于状态标记这类简单读写场景,但对于需要多个操作一起完成的场景,比如计数器累加,就需要使用 synchronized 或者 Lock 来保证线程安全。
3. 假如现在有15个任务 5个核心线程 最大线程是10 工作队列是5 请问执行顺序是怎样的?
- 先使用核心线程执行任务:新提交任务时,如果当前核心线程数未满(<5),直接创建或使用空闲核心线程执行任务。
- 核心线程满后任务入队:核心线程数达到5时,新任务会放入容量为5的工作队列等待。
- 工作队列满后扩展线程:工作队列也满了(5个任务等待),且当前线程数少于最大线程数(<10),线程池会创建非核心线程来处理任务。
- 达到最大线程数后拒绝任务:当线程数达到最大值10且工作队列已满,再有任务提交时,线程池会根据拒绝策略处理(默认抛出 RejectedExecutionException)。
4. 说一下ThreadLocal
ThreadLocal 可以理解为本地变量,它会在每个线程中创建一个副本,实现线程之间的隔离,避免并发冲突,相当于是用空间换时间的方式来解决线程安全问题。
ThreadLocal 底层有个静态内部类 ThreadLocalMap,它里面有个 Entry 数组,每个 Entry 是一个弱引用的 key(指向 ThreadLocal 对象)和值 value,用来保存线程自己的数据副本。
用弱引用 的好处是:如果 ThreadLocal 对象没有外部强引用时可以被 GC 回收,避免内存泄漏。但要注意,即使 key 被回收,value 还可能残留在内存里(key=null,value 还在),所以最好在用完后调用 remove() 删除,防止内存泄漏。
相比 synchronized 或 Lock 是通过加锁来共享同一个变量,ThreadLocal 则是让每个线程有自己独立的变量副本,不需要加锁,是空间换时间的思路。总结来说:
- Lock 是多线程共享同一个变量,需要加锁,是时间换空间;
- ThreadLocal 是每个线程有自己的变量,不需要加锁,是空间换时间。
5. 说一下Java创建对象的过程
- 类加载检查:执行 new 指令时,JVM 会先检查这个类是否已被加载、解析和初始化。如果没有,会先触发类加载过程,保证类的元数据信息可用。
- 分配内存:根据对象大小,在 Java 堆中为对象划分一块内存空间,内存大小在类加载完成后就已经确定。
- 初始化零值:分配好内存后,将对象实例字段初始化为对应类型的默认零值(如数值型为0,引用型为null),不包括对象头部分。
- 设置对象头:JVM 设置对象头信息:指向类的元数据指针、对象的哈希码、GC分代年龄等。如果启用了偏向锁等,还会设置相应的锁标记。
- 执行构造方法init:最后调用构造方法,对对象进行初始化,把字段设为实际需要的值,执行初始化逻辑。
6. 说一下Java对象的生命周期
- 创建:对象通过关键字new在堆内存中被实例化,构造函数被调用,对象的内存空间被分配。
- 使用:对象被引用并执行相应的操作,可以通过引用访问对象的属性和方法,在程序运行过程中被不断使用。
- 销毁:当对象不再被引用时,通过垃圾回收机制自动回收对象所占用的内存空间。垃圾回收器会在适当的时候检测并回收不再被引用的对象,释放对象占用的内存空间,完成对象的销毁过程。