Java面试题解析
1. volatile
- 内存可见性:主内存中变量在多线程环境中被使用时会被复制到线程的私有内存中,
volatile
关键字修饰的变量可以保证主内存和线程私有内存的同一变量的一致性。 - 不保证原子性:
volatile
关键字修饰的变量可以保证单次运算的原子性(禁止指令重排),但是无法保证多次运算(例如++或–)的原子性。 - 禁止指令重排:JVM编译后的字节码指令并不一定按照编码顺序执行,但是使用
volatile
关键字修饰的变量在执行运算时会禁止字节码指令重排。
2. CAS(CompareAndSet)
CompareAndSet
是Unsafe
类的一个native
方法,在rt.jar
中,native
修饰的方法可以像C语言中的指针一样直接操作特定内存。
CAS是一条CPU的并发原语(原语的执行必须是连续的,且执行过程不能被中断)public class AtomicInteger extends Number implements java.io.Serializable { private volatile int value; public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
CompareAndSet的作用为以线程私有内存的身份获取当前主内存中的变量值
value
与预期值expect
进行比较,如果相等则更新value
值为update
并返回true,否则不更新并返回false。
CompareAndSet的缺点:
- 在于如果比较结果不相等则会一直进行尝试,可能会给CPU带来很大的开销。
- 只能保证一个共享变量的原子操作。
3. ABA
ABA问题是指两个或多个执行频率相差较大的线程中,执行频率高的线程在其他线程未知晓的情况下多次修改主内存的值并最终修改为原值,导致其他线程认为主内存中的值没有改变。
ABA问题会被发现于注重执行过程的程序当中,对于只注重调用结果的程序,可以忽略该问题。// ABA问题演示及解决方案 AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(100, 1); new Thread(() -> { boolean res = reference.compareAndSet(100, 101, reference.getStamp(), reference.getStamp() + 1); System.out.println(Thread.currentThread().getName() + "\t" + res + "\t" + reference.getStamp() + "\t" + reference.getReference()); res = reference.compareAndSet(101, 100, reference.getStamp(), reference.getStamp() + 1); System.out.println(Thread.currentThread().getName() + "\t" + res + "\t" + reference.getStamp() + "\t" + reference.getReference()); }, "t1").start(); new Thread(() -> { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } boolean res = reference.compareAndSet(100, 101, 1, reference.getStamp() + 1); System.out.println(Thread.currentThread().getName() + "\t" + res + "\t" + reference.getStamp() + "\t" + reference.getReference()); }, "t2").start();
解决方案:原子引用
原子引用AtomicReference<V>
可以将一个java类封装为原子对象,利用该特性将时间戳与原子引用结合使用就可以解决ABA问题了,java中已经提供了封装好的时间戳类AtomicStampedReference<V>
。
4. 集合类的线程安全
ArrayList并发异常举例
ArrayList<String> list = new ArrayList<>(); for (int i = 0; i < 30; i++) { new Thread(()-> { list.add(UUID.randomUUID().toString().substring(0, 8)); System.out.println(list); }, "t" + i).start(); }
异常名称:java.util.ConcurrentModificationException
解决方案:
- 使用
Vector<E>
:Vector<E>
的操作会加锁,可以保证数据一致性。 Collections.synchronizedList(new ArrayList<>())
,将线程不安全的集合封装为线程安全的集合。- 使用
CopyOnWriteArrayList<E>
实现读写分离。
CopyOnWriteArray源码
public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }
在对集合进行写操作时会先加锁,对当前集合进行复制,在复制的集合上进行写操作,最后再将写完的集合覆盖到原集合上再解锁。
5. 指针引用问题
class Person() {
public int age;
public String name;
public void setAge(int age) {
age = 30;
}
public void setName(Person person) {
person.name = "xxx";
}
public void setName(String name) {
name = "str";
}
}
演示案例1=3
// 案例1: setAge方法中改变的是形参的值,形参是实参变量的副本,因此改变副本的值并不影响变量本身的值
Person person = new Person("test", 10);
int age = 20;
person.setAge(age);
System.out.println("age=" + age);
// 结果: age=20
// 案例2: 目前有两个Person指针分别指向(test,10)和(abc,10),setName方法中将person1指针指向的Person对象的name值改变为"xxx",因此值被真的改变了
Person person1 = new Person("abc", 10);
person.setName(person1)
System.out.println("name=" + person1.name);
// 结果: name=xxx
// 案例3: 目前有两个String指针同时指向xxx(实参和形参),setName方法中将形参指向了str,实参并没有改变,因此str=xxx
String str = "xxx";
person.setName(str);
System.out.println("name=" + str);
// 结果: name=xxx
6. 线程锁
6.1. 公平锁/非公平锁
// ReentrantLock默认为非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁是指多个线程按照申请锁的先后顺序来获取锁,遵守先来后到原则;而非公平锁有可能会出现先申请的后获取到锁的现象。非公平锁的优点是吞吐量比较大。
注:synchronized
属于非公平锁。
6.2. 可重入锁(递归锁)
当代码存在嵌套锁时,同一线程在外部代码获取锁以后进入内部代码时会自动获取锁。
synchronized
和ReentrantLock
都是典型的可重入锁
6.3. 自旋锁
获取锁的线程获取失败时不会立即阻塞,而是会采用循环的方式去尝试获取锁
AtomicReference<Thread> reference = new AtomicReference<>(); // 使用自旋锁加锁 public void lock () { Thread thread = Thread.currentThread(); System.out.println(Thread.currentThread().getName() + "\t come in (+_+)?"); while (!reference.compareAndSet(null, thread)) { //TODO: 获取锁之后的操作 } } // 解锁自旋锁 public void unlock () { Thread thread = Thread.currentThread(); reference.compareAndSet(thread, null); }
6.4. 独占锁(写锁)/共享锁(读锁)/互斥锁
ReentrantLock和synchronized属于独占锁
读写锁的原理在于读写分离,写操作要保证原子性,操作过程不可被打断;读操作可以多个线程共享锁。ReentrantReadWriteLock lock = new ReentrantReadWriteLock() // 写锁-加锁 lock.writeLock().lock() // 写锁-解锁 lock.writeLock().unlock() // 读锁-加锁 lock.readLock().lock() // 读锁-解锁 lock.readLock().unlock()
6.4. Lock和Condition
多线程中判断条件时应该使用while而不是if,因为while可以在线程被唤醒以后再次判断条件是否满足,而if会直接往下执行
// 通过ReentrantLock创建Condition Condition condition = lock.newCondition();
- 一个
ReentrantLock
可以创建多个Condition
。- 当在线程A调用
condition.await()
函数时可以让当前线程处于阻塞状态;当线程B调用condition.signal()
函数时可以把线程A唤醒。- 一个
Condition
可以绑定在多个线程中,使用condition.signalAll()
可以唤醒所有处于阻塞状态的线程,如果调用condition.signal()
则会随机唤醒其中一个。
7. CountDownLatch/CyclicBarrier/Semaphore
7.1. CountDownLatch(倒计时)
倒计时锁,在CountDownLatch初始化时会指定从几开始倒计时,
countDown()
函数每执行一次会将计数器的值减一,当倒计时为0时await()
的阻塞状态才会结束,否则会一直等待countDown()
函数执行,直到计时到0。int count = 5; CountDownLatch latch = new CountDownLatch(count); // 倒计时线程 for (int i = 0; i < count; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + "执行完毕..."); latch.countDown(); }, "t" + i).start(); } latch.await(); System.out.println("主线程执行...");
7.2. CyclicBarrier(收集器)
收集器的初始化参数中包含两项,第一个时满足条件的收集个数,第二个时满足条件以后执行的线程。
当在线程中调用await()
函数时收集个数会加一,然后收集器会判断当前收集个数是否满足条件,当满足个数以后将会执行初始化参数中的线程。int count = 5; CyclicBarrier barrier = new CyclicBarrier(count, () -> { System.out.println("收集器执行..."); }); // 收集线程 for (int i = 0; i < count; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + "收集"); try { barrier.await(); } catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } }).start(); }
7.3. Semaphore(信号灯)
当多个线程抢占多个资源时,在资源不足以分配给所有线程的情况下为了保证所有所有线程能够分配到资源可以使用类似于信号灯的控制器来限流。
Semaphore
的构造函数需要传入有限的资源数量个数。该类会维持这个资源数量,当线程调用acquire()
函数时资源数减一;当线程调用release()
函数时资源数加一;当线程请求资源时资源数为0则会被阻塞。Semaphore semaphore = new Semaphore(3); // 抢占线程 for (int i = 0; i < 6; i++) { new Thread(() -> { try { semaphore.acquire(); System.out.println(Thread.currentThread().getName() + "\t抢到"); TimeUnit.SECONDS.sleep(3); System.out.println(Thread.currentThread().getName() + "\t离开"); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); } }).start(); }
8. 阻塞队列
- ArrayBlockingQueue:数组构成的有限阻塞队列
- LinkedBlockingQueue:链表构成的有限阻塞队列(默认大小为Integer.MAX_VALUE)
- PriorityBlockingQueue:支持优先级的无界阻塞队列
- DelayQueue:支持优先级的延迟无界阻塞队列
- SynchronousQueue:不存储元素的阻塞队列,单个元素的队列
- LinkedTransferQueue:链表组成的无界阻塞队列
- LinkedBlockingDeque:链表组成的双向阻塞队列
异常组函数
BlockingQueue<String> queue = new ArrayBlockingQueue<>(2); // 添加 System.out.println(queue.add("one")); System.out.println(queue.add("two")); // 当队列溢出时抛出异常: java.lang.IllegalStateException // System.out.println(queue.add("three")); // 获取下一个将被取出的元素 System.out.println(queue.element()); // 删除 System.out.println(queue.remove()); System.out.println(queue.remove()); // 删除空队列时抛出异常: java.util.NoSuchElementException System.out.println(queue.remove());
返回bool值组
BlockingQueue<String> queue = new ArrayBlockingQueue<>(2); // 添加 // offer(e, time, unit): 可指定阻塞时间 System.out.println(queue.offer("one")); System.out.println(queue.offer("two")); // 当队列溢出时返回: false System.out.println(queue.offer("three")); // 获取下一个将被取出的元素 System.out.println(queue.peek()); // 删除 // poll(time, unit): 可指定阻塞时间 System.out.println(queue.poll()); System.out.println(queue.poll()); // 删除空队列时返回: null System.out.println(queue.poll());
阻塞组
BlockingQueue<String> queue = new ArrayBlockingQueue<>(2); // 添加 queue.put("one"); queue.put("two"); // 当队列溢出时阻塞 queue.put("three"); queue.forEach(System.out::println); // 删除 System.out.println(queue.take()); System.out.println(queue.take()); // 删除空队列时阻塞 System.out.println(queue.take());
SynchronousQueue
BlockingQueue<String> queue = new SynchronousQueue<>(); // 添加 queue.add("one"); // 队列溢出抛出异常: java.lang.IllegalStateException queue.add("two"); // 取出 System.out.println(queue.take());
9. 线程池
9.1. 线程池7大参数
- corePoolSize: 线程池中的常驻核心线程数
- maximumPoolSize: 线程池能够同时容纳的最大线程数量
- keepAliveTime: 多余的空闲线程的存活时间
- unit:
keepAliveTime
的时间单位- workQueue: 任务队列,在所有核心线程处于忙碌状态时,新加入的线程会被放入任务队列;当任务队列溢出时,线程池会开启新的线程直到线程数达到最大(任务队列溢出时新加入的线程会抢先任务队列里的线程执行)
- threadFactory: 生成线程的工厂
- handler: 当线程池溢出时的拒绝策略
9.2. 拒绝策略
- AbortPolicy(默认):直接抛出RejectedExecutionException异常
- CallerRunsPolicy:将任务回退到调用者,不会抛出异常,也不会丢弃任务
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入任务队列尝试再次提交任务
- DiscardPolicy:直接丢弃任务,也不抛出异常
9.3. 常用方法
execute(Runnable thread)
: 执行任务submit(Callable<T> callable)
: 提交任务,返回Future<T>
suutdown()
: 等待未完成的任务执行完毕后停止线程池shutdownNow()
: 立即停止线程池(使用interrupt方法终止未完成的任务)awaitTermination(timeout, unit)
: 阻塞等待所有任务执行完毕后停止线程池invokeAll(Collection<? extends Callable<T>> tasks)
: 批量提交任务并等待所有任务执行完毕返回结果列表List<Future<T>>
invokeAny(Collection<? extends Callable<T>> tasks)
: 批量提交任务并返回最先执行完毕的任务的结果9.4. 线程池的创建
阿里巴巴Java开发手册中命令禁止使用
Executors
返回的线程池对象创建线程池:// 手动创建线程池 int CPU_COUNT = Runtime.getRuntime().availableProcessors(); ExecutorService executorService = new ThreadPoolExecutor( CPU_COUNT / 2, CPU_COUNT * 2, 1L, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(CPU_COUNT), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() );
如何确定合理的线程池个数?
- CPU密集型业务(计算量比较大,IO操作比较少):CPU核数 + 1
- IO密集型(阻塞系数:0.8~0.9)
- CPU核数 / (1 - 阻塞系数)
- CPU核数 * 2
10. 死锁问题
Java死锁演示死锁问题解决方案
calong > jps -l
10540
11436 jdk.jcmd/sun.tools.jps.Jps
12508 club.calong.jvm.demo.DeadLock
5788 org.jetbrains.jps.cmdline.Launcher
calong > jstack 12508
Java stack information for the threads listed above:
===================================================
"t3":
at club.calong.jvm.demo.SyncThread.run(DeadLock.java:22)
- waiting to lock <0x000000076b577cf8> (a java.lang.Object)
- locked <0x000000076b577d18> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
"t1":
at club.calong.jvm.demo.SyncThread.run(DeadLock.java:22)
- waiting to lock <0x000000076b577d08> (a java.lang.Object)
- locked <0x000000076b577cf8> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
"t2":
at club.calong.jvm.demo.SyncThread.run(DeadLock.java:22)
- waiting to lock <0x000000076b577d18> (a java.lang.Object)
- locked <0x000000076b577d08> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
11. JVM参数
java -XX:+PrintFlagsInitial -version
: 查看JVM参数默认初始值java -XX:+PrintFlagsFinal -version
: 查看被修改过的JVM参数以及修改后的值java -XX:+PrintFlagsFinal -XX:参数=值 T
: 修改JVM参数jinfo -flags PID
: 查看Java进程的JVM参数jinfo -flag 参数 PID
: 查看Java进程的指定JVM参数java -XX:+PrintCommandLineFlags
: 查看Java运行时命令行指定JVM参数
常用参数:
- -Xss(-XX:ThreadStackSize): 单个线程栈大小,默认为512~1024k
- -Xms(-XX:InitialHeapSize): 初始大小内存,默认为物理内存的1/64
- -Xmx(-XX:MaxHeapSize): 最大分配内存,默认为物理内存的1/4
- -XX: MetaspaceSize: 元空间大小,不在虚拟内存中,大小仅受本地内存限制,默认大小为20.8M
- -XX: PrintGCDetails: 查看Java线程的CG运行日志和内存占用情况
- -XX: SurvivorRatio: 设置新生代伊甸园中S0/S1的空间比例
- -XX: NewRatio: 设置新生代和老年代堆内存结构占比
- -XX: MaxTenuringThreshold: 设置垃圾最大年龄
12. 引用问题
- 强引用:
Object obj = new Object();
, 强引用无论发生任何情况都不会被回收。- 软引用:
SoftReference<Object> obj = new SoftReference<>(new Object())
, 当系统内存充足时不会被回收,不足时会被回收。- 弱引用:
WeakReference<Object> obj = new WeakReference<>(new Object())
, 只要运行GC该对象就会被回收。在WeakHashMap<T>
中如果将Key
指向null
后运行GC,则Key
和Value
都会被回收。- 虚引用:
PhantomReference<Object> obj = new PhantomRefence<>(new Object())
, 形同虚设得引用对象。该对象必须配合引用队列ReferenceQueue<T>
使用。
弱引用和虚引用对象在被GC回收之前会被放入引用队列当中,使用引用队列的方法poll()
可以获得被放入的对象。
本作品采用《CC 协议》,转载必须注明作者和本文链接