面试题一:在 Android 里,Array
和 ArrayList
区别?
ArrayList
动态,无需提前定大小。ArrayList
尾部添加快,其他位置操作慢。ArrayList
只能存对象,存基本类型需用包装类。Arrays
类;ArrayList
自带众多实用方法。扩展追问:扩容因子
数组无自动扩容机制。创建时长度固定,若要扩容,需手动新建更大数组,再将原数组元素复制过去。
ArrayList
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
包下的一个类,用于实现线程同步,它是一种可重入的互斥锁,在多线程编程中发挥着重要作用,下面从多个方面详细介绍它:
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
更简洁方便。感谢观看!!!