面试题一:在 Android 里,Array
和 ArrayList
区别?
- 定义与大小:数组声明时要指定大小,之后固定;
ArrayList
动态,无需提前定大小。 - 性能:二者访问元素快,时间复杂度 O(1);数组插入删除繁琐,
ArrayList
尾部添加快,其他位置操作慢。 - 数据类型:数组能存基本类型和对象,
ArrayList
只能存对象,存基本类型需用包装类。 - 方法功能:数组自身方法少,靠
Arrays
类;ArrayList
自带众多实用方法。
扩展追问:扩容因子
数组无自动扩容机制。创建时长度固定,若要扩容,需手动新建更大数组,再将原数组元素复制过去。
ArrayList
- 初始容量:未指定时默认初始容量为 10。
- 扩容规则:添加元素使数量达容量上限时会自动扩容。新容量约为原容量 1.5 倍(
oldCapacity + (oldCapacity >> 1)
)。若新容量小于所需最小容量,就以最小容量为准;若超数组最大容量,会特殊处理。扩容通过Arrays.copyOf
复制元素到新数组。
private void grow(int minCapacity) {
// 获取旧容量
int oldCapacity = elementData.length;
// 计算新容量,大约是原容量的 1.5 倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果新容量小于所需的最小容量,则将新容量设置为最小容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量大于数组的最大容量,则调用 hugeCapacity 方法处理
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 调用 Arrays.copyOf 方法将原数组元素复制到新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
在 grow
方法中,首先计算新容量,大约是原容量的 1.5 倍(oldCapacity + (oldCapacity >> 1)
)。然后检查新容量是否满足最小容量要求,如果不满足,则将新容量设置为最小容量。最后,使用 Arrays.copyOf
方法将原数组元素复制到新数组中。
综上所述,数组没有自动扩容机制,需要手动处理;而 ArrayList
有自动扩容机制,扩容后的新容量大约是原容量的 1.5 倍。
面试二:synchronized锁对类,对象,代码块的作用
对类加锁(同步静态方法)
当使用 synchronized
修饰静态方法时,相当于对类加锁,锁对象是该类的 Class
对象。同一时间只有一个线程能够获取该类的 Class
对象的锁并执行该静态方法,其他线程必须等待锁释放。
public class ClassLockExample {
// 同步静态方法
public static synchronized void staticMethod() {
try {
System.out.println(Thread.currentThread().getName() + " 进入静态方法");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " 离开静态方法");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// 创建两个线程调用静态方法
Thread thread1 = new Thread(ClassLockExample::staticMethod, "线程1");
Thread thread2 = new Thread(ClassLockExample::staticMethod, "线程2");
thread1.start();
thread2.start();
}
}
作用分析
- 在上述代码中,
staticMethod
是一个同步静态方法。由于静态方法属于类,而不是类的实例,所以synchronized
修饰静态方法时,锁的是类的Class
对象。 - 当
线程1
进入staticMethod
方法时,它会获取ClassLockExample.class
对象的锁,此时线程2
必须等待线程1
释放锁后才能进入该方法,从而保证了同一时间只有一个线程能执行该静态方法。
对对象加锁(同步实例方法)
当使用 synchronized
修饰实例方法时,相当于对对象加锁,锁对象是调用该方法的实例对象。同一时间只有一个线程能够获取该实例对象的锁并执行该实例方法,其他线程必须等待锁释放。
public class ObjectLockExample {
// 同步实例方法
public synchronized void instanceMethod() {
try {
System.out.println(Thread.currentThread().getName() + " 进入实例方法");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " 离开实例方法");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ObjectLockExample example = new ObjectLockExample();
// 创建两个线程调用实例方法
Thread thread1 = new Thread(example::instanceMethod, "线程1");
Thread thread2 = new Thread(example::instanceMethod, "线程2");
thread1.start();
thread2.start();
}
}
作用分析
- 在上述代码中,
instanceMethod
是一个同步实例方法。synchronized
修饰实例方法时,锁的是调用该方法的实例对象,即example
对象。 - 当
线程1
进入instanceMethod
方法时,它会获取example
对象的锁,此时线程2
必须等待线程1
释放锁后才能进入该方法,从而保证了同一时间只有一个线程能执行该实例方法。
对代码块加锁
使用 synchronized
修饰代码块时,需要显式指定一个锁对象。同一时间只有一个线程能够获取该锁对象的锁并执行代码块中的代码,其他线程必须等待锁释放。
public class BlockLockExample {
private final Object lock = new Object();
public void blockMethod() {
// 同步代码块
synchronized (lock) {
try {
System.out.println(Thread.currentThread().getName() + " 进入同步代码块");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " 离开同步代码块");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
BlockLockExample example = new BlockLockExample();
// 创建两个线程调用包含同步代码块的方法
Thread thread1 = new Thread(example::blockMethod, "线程1");
Thread thread2 = new Thread(example::blockMethod, "线程2");
thread1.start();
thread2.start();
}
}
作用分析
- 在上述代码中,
synchronized
代码块指定了lock
对象作为锁。当线程1
进入同步代码块时,它会获取lock
对象的锁,此时线程2
必须等待线程1
释放锁后才能进入该代码块,从而保证了同一时间只有一个线程能执行该代码块中的代码。 - 使用同步代码块的好处是可以更细粒度地控制同步范围,只对需要同步的代码部分加锁,减少锁的持有时间,提高性能。
总结
- 对类加锁:通过同步静态方法实现,锁的是类的
Class
对象,确保同一时间只有一个线程能执行该类的静态同步方法,适用于控制对类级别的共享资源的访问。 - 对对象加锁:通过同步实例方法实现,锁的是调用该方法的实例对象,确保同一时间只有一个线程能执行该实例的同步实例方法,适用于控制对实例级别的共享资源的访问。
- 对代码块加锁:通过同步代码块实现,需要显式指定锁对象,确保同一时间只有一个线程能执行该代码块中的代码,适用于更细粒度的同步控制,减少锁的持有时间,提高性能。
扩展追问:
ReentrantLock 是什么?
ReentrantLock
是 Java 中 java.util.concurrent.locks
包下的一个类,用于实现线程同步,它是一种可重入的互斥锁,在多线程编程中发挥着重要作用,下面从多个方面详细介绍它:
基本概念
- 可重入性:“可重入” 意味着同一个线程可以多次获取同一把锁而不会造成死锁。当线程第一次获取锁时,锁的持有计数变为 1,若该线程再次获取这把锁,计数会相应增加,每次释放锁时计数减 1,只有当计数为 0 时,锁才真正被释放,其他线程才能获取该锁。
- 互斥性:同一时刻,
ReentrantLock
只能被一个线程持有,这保证了对共享资源的独占访问,避免多个线程同时修改共享资源导致的数据不一致问题。
基本使用
ReentrantLock
的使用通常包含获取锁、执行同步代码和释放锁这几个步骤,以下是一个简单示例:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
private int sharedResource = 0;
public void increment() {
// 获取锁
lock.lock();
try {
// 执行同步操作
sharedResource++;
System.out.println(Thread.currentThread().getName() + " 执行递增操作后,共享资源的值为: " + sharedResource);
} finally {
// 释放锁,确保在发生异常时也能释放锁
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
// 创建两个线程进行操作
Thread thread1 = new Thread(example::increment, "线程1");
Thread thread2 = new Thread(example::increment, "线程2");
thread1.start();
thread2.start();
}
}
在上述代码中,increment
方法使用 ReentrantLock
来保证线程安全。lock.lock()
用于获取锁,lock.unlock()
用于释放锁,为了防止在执行同步代码时发生异常导致锁无法释放,将 lock.unlock()
放在 finally
块中。
与 synchronized
的比较
灵活性
ReentrantLock
:提供了更多的灵活性。例如,它可以使用tryLock()
方法尝试获取锁,如果锁不可用,线程可以选择不阻塞,继续执行其他操作;还可以使用lockInterruptibly()
方法允许线程在等待锁的过程中被中断。synchronized
:是 Java 语言的内置特性,语法相对简单,但灵活性较差,一旦线程进入同步块,就会一直阻塞直到获取到锁。
公平性
ReentrantLock
:可以通过构造函数指定是否为公平锁。公平锁会按照线程请求锁的顺序依次获取锁,避免某些线程长时间得不到锁。例如ReentrantLock fairLock = new ReentrantLock(true);
就创建了一个公平锁。synchronized
:是非公平锁,线程获取锁的顺序是不确定的,可能会导致某些线程长时间等待。
锁的释放
ReentrantLock
:需要手动调用unlock()
方法释放锁,因此必须确保在finally
块中释放锁,以避免死锁。synchronized
:会在同步块或同步方法执行完毕后自动释放锁。
高级特性
tryLock()
方法
tryLock()
有两种重载形式:
- 无参的
tryLock()
:尝试获取锁,如果锁可用,则获取锁并返回true
;如果锁不可用,则立即返回false
,线程不会阻塞。 - 带超时时间的
tryLock(long timeout, TimeUnit unit)
:在指定的时间内尝试获取锁,如果在该时间内获取到锁则返回true
,否则返回false
。
lockInterruptibly()
方法
lockInterruptibly()
方法允许线程在等待锁的过程中被中断。如果线程在等待锁的过程中被其他线程中断,会抛出 InterruptedException
异常,线程可以根据需要进行相应的处理。
适用场景
- 当需要更灵活的锁控制,如尝试获取锁、可中断的锁等待时,使用
ReentrantLock
更合适。 - 对于公平性有要求的场景,
ReentrantLock
可以通过设置为公平锁来满足需求。 - 若代码逻辑较为简单,对锁的灵活性要求不高,使用
synchronized
更简洁方便。
感谢观看!!!