Java多线程和锁_八股场景题
Java多线程_八股&场景题
Java多线程是面试和实际开发中非常重要的内容。以下是一些常见的Java多线程八股文问题和场景题,以及详细答案和示例代码。
1. Java中创建线程的几种方式?
答案:
主要有以下几种方式:
- 继承
Thread
类:重写run()
方法,通过start()
启动线程。 - 实现
Runnable
接口:实现run()
方法,通过Thread
类启动线程。 - 实现
Callable
接口:通过FutureTask
包装,支持返回值和异常处理。 - 使用线程池:通过
ExecutorService
管理线程,避免频繁创建和销毁线程。 - 使用
CompletableFuture
:支持异步编程,可以组合多个异步任务。
示例代码:
// 继承Thread
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread via Thread class: " + Thread.currentThread().getName());
}
}
// 实现Runnable
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread via Runnable: " + Thread.currentThread().getName());
}
}
// 实现Callable
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Thread via Callable: " + Thread.currentThread().getName();
}
}
public class ThreadCreation {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 继承Thread
new MyThread().start();
// 实现Runnable
new Thread(new MyRunnable()).start();
// 实现Callable
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
new Thread(futureTask).start();
System.out.println(futureTask.get());
}
}
2. 线程安全和线程不安全的区别?
答案:
- 线程安全:多个线程访问共享资源时,不会出现数据不一致的情况。例如,
Vector
、Hashtable
、ConcurrentHashMap
等。 - 线程不安全:多个线程访问共享资源时,可能出现数据不一致的情况。例如,
ArrayList
、HashMap
等。 - 解决线程不安全问题:
- 使用同步机制(
synchronized
)。 - 使用锁(
ReentrantLock
)。 - 使用线程安全的类(如
ConcurrentHashMap
)。 - 使用原子类(如
AtomicInteger
)。
- 使用同步机制(
示例代码:
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadSafety {
private static int count = 0; // 线程不安全
private static AtomicInteger atomicCount = new AtomicInteger(0); // 线程安全
public static void increment() {
count++;
atomicCount.incrementAndGet();
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new Thread(() -> increment());
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
System.out.println("Non-atomic count: " + count); // 可能不等于1000
System.out.println("Atomic count: " + atomicCount.get()); // 一定等于1000
}
}
3. synchronized
和ReentrantLock
的区别?
答案:
synchronized
:- 是Java内置的同步机制,使用简单。
- 只能用于方法或代码块。
- 不支持中断、超时和尝试锁定。
ReentrantLock
:- 是显式锁,功能更强大。
- 支持中断、超时和尝试锁定。
- 可以实现更复杂的锁机制(如读写锁)。
示例代码:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SynchronizedVsReentrantLock {
private static final Object lock = new Object();
private static final Lock reentrantLock = new ReentrantLock();
public static void synchronizedExample() {
synchronized (lock) {
System.out.println("Synchronized block: " + Thread.currentThread().getName());
}
}
public static void reentrantLockExample() {
reentrantLock.lock();
try {
System.out.println("ReentrantLock block: " + Thread.currentThread().getName());
} finally {
reentrantLock.unlock();
}
}
public static void main(String[] args) {
new Thread(SynchronizedVsReentrantLock::synchronizedExample).start();
new Thread(SynchronizedVsReentrantLock::reentrantLockExample).start();
}
}
ReentrantLock
是 Java 中提供的一种可重入锁,它是 java.util.concurrent.locks
包中的一个重要同步工具。与传统的 synchronized
相比,ReentrantLock
提供了更灵活的锁操作,包括中断、超时和尝试锁定等功能。这些特性使得 ReentrantLock
在复杂的并发场景中更加适用。
以下是对 ReentrantLock
的详细讲解,包括中断、超时和尝试锁定的使用方法。
1. ReentrantLock 的基本概念
ReentrantLock
是一种可重入的互斥锁,支持多个线程对共享资源的互斥访问。它与 synchronized
的主要区别在于:
ReentrantLock
是基于java.util.concurrent
包实现的,而synchronized
是基于 JVM 的内置锁。ReentrantLock
提供了更丰富的锁操作,如尝试锁定、超时锁定、中断等待锁等。ReentrantLock
支持公平锁和非公平锁(默认是非公平锁)。
2. ReentrantLock 的基本使用
ReentrantLock
的使用方式如下:
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 获取锁
try {
count++;
} finally {
lock.unlock(); // 释放锁
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
3. 中断等待锁
ReentrantLock
支持线程在等待锁时被中断。这可以通过 lockInterruptibly()
方法实现。
示例代码:
import java.util.concurrent.locks.ReentrantLock;
public class InterruptibleLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void doWork() throws InterruptedException {
lock.lockInterruptibly(); // 可中断的获取锁
try {
// 执行任务
System.out.println("任务正在执行,线程:" + Thread.currentThread().getName());
Thread.sleep(2000);
} finally {
lock.unlock(); // 释放锁
}
}
public static void main(String[] args) throws InterruptedException {
InterruptibleLockExample example = new InterruptibleLockExample();
Thread t1 = new Thread(() -> {
try {
example.doWork();
} catch (InterruptedException e) {
System.out.println("线程被中断:" + Thread.currentThread().getName());
}
});
t1.start();
Thread.sleep(1000); // 等待一段时间后中断线程
t1.interrupt();
}
}
输出:
任务正在执行,线程:Thread-0
线程被中断:Thread-0
说明:
lockInterruptibly()
方法在获取锁时可以响应中断。如果线程在等待锁时被中断,会抛出InterruptedException
。- 这种方式允许线程在等待锁时被外部中断,从而避免线程长时间阻塞。
4. 超时尝试锁定
ReentrantLock
提供了 tryLock(long timeout, TimeUnit unit)
方法,允许线程在尝试获取锁时设置超时时间。如果在指定时间内无法获取锁,线程可以放弃等待。
示例代码:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TimeoutLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void doWork() throws InterruptedException {
// 尝试在 1 秒内获取锁
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println("获取锁成功,线程:" + Thread.currentThread().getName());
// 执行任务
Thread.sleep(2000);
} finally {
lock.unlock(); // 释放锁
}
} else {
System.out.println("获取锁超时,线程:" + Thread.currentThread().getName());
}
}
public static void main(String[] args) throws InterruptedException {
TimeoutLockExample example = new TimeoutLockExample();
Thread t1 = new Thread(() -> {
try {
example.doWork();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(() -> {
try {
example.doWork();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(500); // 确保 t1 先获取锁
t2.start();
}
}
输出:
获取锁成功,线程:Thread-0
获取锁超时,线程:Thread-1
说明:
tryLock(long timeout, TimeUnit unit)
方法允许线程在指定时间内尝试获取锁。如果在超时时间内获取到锁,则继续执行;否则返回false
。- 这种方式可以避免线程无限期地等待锁,从而提高系统的响应性。
5. 尝试锁定
ReentrantLock
提供了 tryLock()
方法,允许线程尝试获取锁,但不会阻塞。如果锁已经被其他线程持有,tryLock()
方法会立即返回 false
。
示例代码:
import java.util.concurrent.locks.ReentrantLock;
public class TryLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void doWork() {
if (lock.tryLock()) { // 尝试获取锁
try {
System.out.println("获取锁成功,线程:" + Thread.currentThread().getName());
// 执行任务
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
}
} else {
System.out.println("获取锁失败,线程:" + Thread.currentThread().getName());
}
}
public static void main(String[] args) throws InterruptedException {
TryLockExample example = new TryLockExample();
Thread t1 = new Thread(() -> example.doWork());
Thread t2 = new Thread(() -> example.doWork());
t1.start();
Thread.sleep(500); // 确保 t1 先获取锁
t2.start();
}
}
输出:
获取锁成功,线程:Thread-0
获取锁失败,线程:Thread-1
说明:
tryLock()
方法尝试获取锁,但不会阻塞线程。如果锁可用,则立即获取锁并返回true
;否则返回false
。- 这种方式适用于需要快速尝试获取锁的场景,避免线程阻塞。
6. ReentrantLock 的其他特性
6.1 可重入性
ReentrantLock
是可重入的,即同一个线程可以多次获取同一把锁。每次获取锁时,锁的计数器会递增;每次释放锁时,计数器会递减。当计数器为 0 时,锁被完全释放。
6.2 公平锁与非公平锁
ReentrantLock
支持公平锁和非公平锁:
- 非公平锁(默认):线程获取锁时可能会插队,效率较高,但可能导致某些线程长时间等待。
- 公平锁:线程按照请求锁的顺序获取锁,保证公平性,但效率较低。
ReentrantLock lock = new ReentrantLock(true); // 公平锁
ReentrantLock lock = new ReentrantLock(false); // 非公平锁(默认)
7. 总结
ReentrantLock
是一种功能强大的锁机制,提供了以下特性:
- 中断等待锁:支持线程在等待锁时被中断。
- 超时尝试锁定:允许线程在尝试获取锁时设置超时时间。
- 尝试锁定:允许线程尝试获取锁,但不会阻塞。
- 可重入性:支持同一个线程多次获取同一把锁。
- 公平锁与非公平锁:可以根据需要选择公平锁或非公平锁。
ReentrantLock
的这些特性使得它在复杂的并发场景中更加灵活和强大,但同时也需要开发者谨慎使用,以避免死锁和性能问题。
4. 使用ThreadPoolExecutor创建多线程有哪些细节?
答案:
ThreadPoolExecutor
是 Java 中用于创建和管理线程池的核心类,提供了更灵活的线程池配置和管理功能。使用 ThreadPoolExecutor
创建多线程时,需要注意一些关键细节,以确保线程池的性能、资源利用和线程安全。
以下是使用 ThreadPoolExecutor
时需要注意的细节:
1. 合理配置线程池参数
ThreadPoolExecutor
的构造函数需要多个参数,这些参数的配置直接影响线程池的性能和资源占用。
构造函数参数:
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
);
关键参数说明:
-
corePoolSize
(核心线程数)- 线程池中始终保持的线程数量,即使这些线程处于空闲状态也不会被销毁。
- 通常根据 CPU 核心数和任务类型(CPU 密集型或 I/O 密集型)来设置。例如,对于 CPU 密集型任务,
corePoolSize
可以设置为CPU 核心数 + 1
。
-
maximumPoolSize
(最大线程数)- 线程池允许的最大线程数量。
- 当任务队列满了且当前线程数小于
maximumPoolSize
时,线程池会继续创建新线程。
-
keepAliveTime
和unit
(空闲线程存活时间)- 非核心线程在空闲时的存活时间。
- 如果线程池中的线程数超过
corePoolSize
,空闲线程会在指定的时间后被销毁。
-
workQueue
(任务队列)- 存储待执行任务的队列。常见的队列类型包括:
ArrayBlockingQueue
:有界队列,适合固定大小的线程池。LinkedBlockingQueue
:无界队列,适合缓存线程池。SynchronousQueue
:直接提交队列,适合工作窃取线程池。PriorityBlockingQueue
:优先级队列,适合有优先级的任务。
- 存储待执行任务的队列。常见的队列类型包括:
-
threadFactory
(线程工厂)- 用于创建线程的工厂类。默认情况下,
ThreadPoolExecutor
使用默认的线程工厂,但可以通过自定义线程工厂来设置线程的名称、优先级等属性。
- 用于创建线程的工厂类。默认情况下,
-
handler
(拒绝策略)- 当任务过多且线程池已满时,如何处理新任务。常见的拒绝策略包括:
AbortPolicy
:抛出RejectedExecutionException
。CallerRunsPolicy
:由提交任务的线程执行任务。DiscardPolicy
:丢弃任务。DiscardOldestPolicy
:丢弃队列中最老的任务。
- 当任务过多且线程池已满时,如何处理新任务。常见的拒绝策略包括:
2. 选择合适的任务队列
任务队列的选择对线程池的性能和行为有重要影响:
-
ArrayBlockingQueue
- 有界队列,适合固定大小的线程池。
- 优点:可以限制任务队列的最大长度,防止内存溢出。
- 缺点:如果队列满了,新任务会被拒绝。
-
LinkedBlockingQueue
- 无界队列,适合缓存线程池。
- 优点:任务队列几乎不会满,适合任务数量不确定的场景。
- 缺点:可能会占用过多内存。
-
SynchronousQueue
- 直接提交队列,适合工作窃取线程池。
- 特点:任务直接提交给线程,不存储任务。如果线程池满了,任务会被拒绝。
3. 线程池的关闭
线程池使用完毕后,需要正确关闭,以释放资源。可以通过以下方法关闭线程池:
shutdown()
- 尝试关闭线程池,但不会立即中断正在执行的任务。
- 线程池会等待所有任务完成后再关闭。
shutdownNow()
- 立即关闭线程池,并尝试中断正在执行的任务。
- 返回尚未执行的任务列表。
4. 避免资源耗尽
- 合理配置线程池大小:根据任务类型和系统资源合理设置
corePoolSize
和maximumPoolSize
。 - 限制任务队列大小:使用有界队列(如
ArrayBlockingQueue
),避免任务队列占用过多内存。 - 设置合理的拒绝策略:根据业务需求选择合适的拒绝策略,避免任务丢失或系统崩溃。
5. 线程池的监控
- 监控线程池状态:可以通过
ThreadPoolExecutor
提供的方法(如getActiveCount()
、getCompletedTaskCount()
等)监控线程池的运行状态。 - 日志记录:在任务执行前后记录日志,便于排查问题。
6. 线程池的线程安全
- 共享资源的同步:即使使用线程池,任务中对共享资源的操作仍需要同步。
- 避免线程局部变量泄漏:如果任务中使用了线程局部变量(
ThreadLocal
),需要确保在任务结束时清理这些变量。
示例代码:使用 ThreadPoolExecutor
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // 核心线程数
10, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>(100), // 任务队列
Executors.defaultThreadFactory(), // 线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
// 提交任务
for (int i = 0; i < 200; i++) {
int taskId = i;
executor.submit(() -> {
System.out.println("任务 " + taskId + " 正在执行,线程:" + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 模拟任务执行时间
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
}
总结
使用 ThreadPoolExecutor
时,需要注意以下细节:
- 合理配置线程池参数,根据任务类型和系统资源选择合适的线程池大小和任务队列。
- 选择合适的任务队列,避免资源耗尽。
- 正确关闭线程池,释放资源。
- 避免资源竞争和线程安全问题。
- 监控线程池状态,便于排查问题。
通过合理配置和使用 ThreadPoolExecutor
,可以显著提高多线程程序的性能和稳定性。
5. 如何解决线程间的通信问题?
答案:
线程间的通信是多线程编程中的一个重要问题,它涉及到线程之间如何传递信息、协调操作以及同步状态。在 Java 中,提供了多种机制来解决线程间的通信问题,包括内置的同步机制、显式的线程通信工具以及高级并发工具类。
以下是解决线程间通信问题的几种常见方法:
1. 使用 wait()
和 notify()
/ notifyAll()
wait()
和 notify()
是 Java 中最基本的线程通信机制,它们依赖于对象的内置锁(synchronized
)。
工作原理:
wait()
:当前线程释放对象锁,并进入等待状态,直到其他线程调用该对象的notify()
或notifyAll()
方法。notify()
:唤醒一个正在等待该对象锁的线程。notifyAll()
:唤醒所有正在等待该对象锁的线程。
示例:生产者-消费者问题
public class ProducerConsumer {
private final Object lock = new Object();
private boolean available = false;
public void produce() throws InterruptedException {
synchronized (lock) {
while (available) { // 确保不会重复生产
lock.wait(); // 等待消费
}
// 生产操作
System.out.println("生产者生产数据");
available = true;
lock.notifyAll(); // 唤醒等待的线程
}
}
public void consume() throws InterruptedException {
synchronized (lock) {
while (!available) { // 确保有数据可消费
lock.wait(); // 等待生产
}
// 消费操作
System.out.println("消费者消费数据");
available = false;
lock.notifyAll(); // 唤醒等待的线程
}
}
public static void main(String[] args) {
ProducerConsumer pc = new ProducerConsumer();
Thread producer = new Thread(() -> {
try {
pc.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread consumer = new Thread(() -> {
try {
pc.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producer.start();
consumer.start();
}
}
优点:
- 基于内置锁,使用简单。
- 可以实现线程间的精确通信。
缺点:
- 必须在
synchronized
块中使用。 - 容易出错,如忘记调用
notify()
或wait()
,可能导致线程死锁。
2. 使用 Condition
接口
Condition
是 Java java.util.concurrent.locks
包中的一个接口,用于替代传统的 wait()
和 notify()
。它提供了更灵活的线程通信机制。
工作原理:
Condition
与Lock
配合使用。- 提供了
await()
(类似wait()
)和signal()
/signalAll()
(类似notify()
/notifyAll()
)方法。
示例:生产者-消费者问题
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerConsumerWithCondition {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private boolean available = false;
public void produce() throws InterruptedException {
lock.lock();
try {
while (available) {
condition.await(); // 等待消费
}
// 生产操作
System.out.println("生产者生产数据");
available = true;
condition.signalAll(); // 唤醒等待的线程
} finally {
lock.unlock();
}
}
public void consume() throws InterruptedException {
lock.lock();
try {
while (!available) {
condition.await(); // 等待生产
}
// 消费操作
System.out.println("消费者消费数据");
available = false;
condition.signalAll(); // 唤醒等待的线程
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ProducerConsumerWithCondition pc = new ProducerConsumerWithCondition();
Thread producer = new Thread(() -> {
try {
pc.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread consumer = new Thread(() -> {
try {
pc.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producer.start();
consumer.start();
}
}
优点:
- 比
wait()
和notify()
更灵活。 - 可以绑定多个条件变量,实现更复杂的线程通信。
缺点:
- 使用
Lock
和Condition
比内置锁更复杂。 - 需要手动管理锁的释放。
3. 使用 BlockingQueue
BlockingQueue
是一个线程安全的队列接口,提供了阻塞操作,使得线程间通信更加简单。它是解决生产者-消费者问题的推荐方式。
工作原理:
- 生产者线程调用
put()
方法将数据放入队列,如果队列已满,线程会阻塞。 - 消费者线程调用
take()
方法从队列中取出数据,如果队列为空,线程会阻塞。
示例:生产者-消费者问题
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class ProducerConsumerWithBlockingQueue {
private final BlockingQueue<String> queue = new LinkedBlockingQueue<>(1);
public void produce() throws InterruptedException {
String data = "数据";
System.out.println("生产者生产数据:" + data);
queue.put(data); // 放入队列,队列满时阻塞
}
public void consume() throws InterruptedException {
String data = queue.take(); // 从队列中取出数据,队列空时阻塞
System.out.println("消费者消费数据:" + data);
}
public static void main(String[] args) {
ProducerConsumerWithBlockingQueue pc = new ProducerConsumerWithBlockingQueue();
Thread producer = new Thread(() -> {
try {
pc.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread consumer = new Thread(() -> {
try {
pc.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producer.start();
consumer.start();
}
}
优点:
- 简化了线程间的通信逻辑。
- 内置阻塞操作,无需手动管理锁。
缺点:
- 功能相对固定,无法实现复杂的线程通信。
4. 使用 CountDownLatch
CountDownLatch
是一个同步辅助工具,允许一个或多个线程等待其他线程完成操作。
工作原理:
- 初始化时设置一个计数值。
- 每次调用
countDown()
方法,计数值减 1。 - 调用
await()
方法的线程会阻塞,直到计数值为 0。
示例:线程同步
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3); // 初始化计数器为 3
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 开始执行");
try {
Thread.sleep(1000); // 模拟任务执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 执行完成");
latch.countDown(); // 计数器减 1
}).start();
}
System.out.println("主线程等待所有子线程完成");
latch.await(); // 主线程阻塞,直到计数器为 0
System.out.println("所有子线程已完成,主线程继续执行");
}
}
优点:
- 简单易用,适用于线程同步场景。
- 可以实现线程间的等待和通知。
缺点:
- 计数器只能使用一次,无法重置。
5. 使用 CyclicBarrier
CyclicBarrier
是一个同步辅助工具,允许一组线程在某个点上相互等待。
工作原理:
- 初始化时设置一个屏障点的线程数。
- 每个线程到达屏障点后调用
await()
方法。 - 当所有线程都到达屏障点时,所有线程同时继续执行。
示例:线程同步
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(3); // 设置屏障点的线程数为 3
6. 如何避免线程死锁?
答案:
死锁是指两个或多个线程互相等待对方释放资源,导致无法继续执行。避免死锁的方法:
- 避免嵌套锁:尽量减少嵌套锁的使用。
- 按固定顺序加锁:确保所有线程按相同顺序获取锁。
- 使用定时锁:通过
tryLock()
尝试加锁,超时则放弃。 - 减少锁的粒度:使用更细粒度的锁,减少锁
7. 锁的作用范围?如存在一个A类,A类中有一个synchronized修饰的叫update的方法,那么用A类创建了A1和A2两个对象,这两个对象都去调用update方法,会竞争锁资源吗?
答案:
在 Java 中,synchronized
关键字用于同步方法或代码块,以确保在多线程环境下对共享资源的互斥访问。synchronized
的作用范围取决于它修饰的对象:
- 修饰实例方法:锁的是当前对象实例。
- 修饰静态方法:锁的是整个类的类对象。
- 修饰代码块:锁的是指定的对象。
在你的问题中,A
类中的 update
方法被 synchronized
修饰,且没有明确指出是静态方法,因此可以推断它是实例方法。
分析:
- 如果
update
是实例方法,synchronized
锁的是当前对象实例(this
)。 - 当你创建了两个对象
A1
和A2
,它们分别调用update
方法时:A1
调用update
时,锁的是A1
对象。A2
调用update
时,锁的是A2
对象。
因为 A1
和 A2
是两个不同的对象实例,它们的锁是独立的,互不干扰。因此,A1
和 A2
调用 update
方法时不会竞争锁资源。
示例代码:
class A {
public synchronized void update() {
System.out.println("更新方法被调用,当前线程:" + Thread.currentThread().getName());
try {
Thread.sleep(2000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("更新方法结束,当前线程:" + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
A A1 = new A();
A A2 = new A();
// 创建两个线程分别调用 A1 和 A2 的 update 方法
Thread t1 = new Thread(() -> A1.update(), "Thread-1");
Thread t2 = new Thread(() -> A2.update(), "Thread-2");
t1.start();
t2.start();
}
}
输出示例:
更新方法被调用,当前线程:Thread-1
更新方法被调用,当前线程:Thread-2
更新方法结束,当前线程:Thread-1
更新方法结束,当前线程:Thread-2
从输出可以看出,A1
和 A2
的 update
方法可以同时运行,互不干扰。
如果 update
是静态方法:
如果 update
方法是静态的(static synchronized
),情况会有所不同。静态方法的锁是类对象(A.class
),而不是实例对象。因此,无论 A1
还是 A2
调用静态的 update
方法,它们都会竞争同一个锁(类锁),从而导致线程互斥。
示例代码(静态方法):
class A {
public static synchronized void update() {
System.out.println("更新方法被调用,当前线程:" + Thread.currentThread().getName());
try {
Thread.sleep(2000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("更新方法结束,当前线程:" + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
A A1 = new A();
A A2 = new A();
Thread t1 = new Thread(() -> A.update(), "Thread-1");
Thread t2 = new Thread(() -> A.update(), "Thread-2");
t1.start();
t2.start();
}
}
输出示例:
更新方法被调用,当前线程:Thread-1
更新方法结束,当前线程:Thread-1
更新方法被调用,当前线程:Thread-2
更新方法结束,当前线程:Thread-2
从输出可以看出,Thread-2
必须等待 Thread-1
完成后才能执行,说明它们竞争同一个锁。
总结:
- 如果
update
是实例方法,A1
和A2
调用时不会竞争锁资源。 - 如果
update
是静态方法,A1
和A2
调用时会竞争同一个锁(类锁)。
当 synchronized
修饰的是代码块时,锁的作用范围取决于代码块中指定的对象。synchronized
代码块可以锁定以下两种对象:
- 实例对象(
this
):锁定当前对象实例。 - 任意对象:锁定代码块中指定的任意对象。
1. 锁定当前实例对象(this
)
如果 synchronized
代码块锁定的是当前实例对象(this
),那么行为与同步实例方法类似。每个对象实例都有自己的锁,不同实例的锁是独立的。
示例代码:
class A {
public void update() {
synchronized (this) { // 锁定当前实例对象
System.out.println("更新方法被调用,当前线程:" + Thread.currentThread().getName());
try {
Thread.sleep(2000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("更新方法结束,当前线程:" + Thread.currentThread().getName());
}
}
}
public class Main {
public static void main(String[] args) {
A A1 = new A();
A A2 = new A();
Thread t1 = new Thread(() -> A1.update(), "Thread-1");
Thread t2 = new Thread(() -> A2.update(), "Thread-2");
t1.start();
t2.start();
}
}
输出示例:
更新方法被调用,当前线程:Thread-1
更新方法被调用,当前线程:Thread-2
更新方法结束,当前线程:Thread-1
更新方法结束,当前线程:Thread-2
分析:
A1
和A2
是不同的实例,它们各自锁定自己的this
对象。- 因此,
Thread-1
和Thread-2
不会竞争锁资源,可以同时运行。
2. 锁定同一个共享对象
如果 synchronized
代码块锁定的是同一个共享对象,那么所有线程都会竞争同一个锁。
示例代码:
class A {
private static final Object lock = new Object(); // 共享锁对象
public void update() {
synchronized (lock) { // 锁定同一个共享对象
System.out.println("更新方法被调用,当前线程:" + Thread.currentThread().getName());
try {
Thread.sleep(2000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("更新方法结束,当前线程:" + Thread.currentThread().getName());
}
}
}
public class Main {
public static void main(String[] args) {
A A1 = new A();
A A2 = new A();
Thread t1 = new Thread(() -> A1.update(), "Thread-1");
Thread t2 = new Thread(() -> A2.update(), "Thread-2");
t1.start();
t2.start();
}
}
输出示例:
更新方法被调用,当前线程:Thread-1
更新方法结束,当前线程:Thread-1
更新方法被调用,当前线程:Thread-2
更新方法结束,当前线程:Thread-2
分析:
A1
和A2
的update
方法都锁定的是同一个共享对象lock
。- 因此,
Thread-1
和Thread-2
会竞争同一个锁,导致互斥。
3. 锁定类对象(A.class
)
如果 synchronized
代码块锁定的是类对象(A.class
),那么行为与同步静态方法类似。所有线程都会竞争同一个类锁。
示例代码:
class A {
public void update() {
synchronized (A.class) { // 锁定类对象
System.out.println("更新方法被调用,当前线程:" + Thread.currentThread().getName());
try {
Thread.sleep(2000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("更新方法结束,当前线程:" + Thread.currentThread().getName());
}
}
}
public class Main {
public static void main(String[] args) {
A A1 = new A();
A A2 = new A();
Thread t1 = new Thread(() -> A1.update(), "Thread-1");
Thread t2 = new Thread(() -> A2.update(), "Thread-2");
t1.start();
t2.start();
}
}
输出示例:
更新方法被调用,当前线程:Thread-1
更新方法结束,当前线程:Thread-1
更新方法被调用,当前线程:Thread-2
更新方法结束,当前线程:Thread-2
分析:
A1
和A2
的update
方法都锁定的是同一个类对象A.class
。- 因此,
Thread-1
和Thread-2
会竞争同一个锁,导致互斥。
总结
- 如果
synchronized
代码块锁定的是 当前实例对象(this
),不同实例的锁是独立的,不会竞争锁资源。 - 如果
synchronized
代码块锁定的是 同一个共享对象 或 类对象(A.class
),所有线程会竞争同一个锁,导致互斥。
通过合理选择锁对象,可以灵活控制线程之间的同步行为。