当前位置: 首页 > news >正文

Java多线程和锁_八股场景题

Java多线程_八股&场景题

Java多线程是面试和实际开发中非常重要的内容。以下是一些常见的Java多线程八股文问题和场景题,以及详细答案和示例代码。


1. Java中创建线程的几种方式?

答案:
主要有以下几种方式:

  1. 继承Thread:重写run()方法,通过start()启动线程。
  2. 实现Runnable接口:实现run()方法,通过Thread类启动线程。
  3. 实现Callable接口:通过FutureTask包装,支持返回值和异常处理。
  4. 使用线程池:通过ExecutorService管理线程,避免频繁创建和销毁线程。
  5. 使用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. 线程安全和线程不安全的区别?

答案:

  • 线程安全:多个线程访问共享资源时,不会出现数据不一致的情况。例如,VectorHashtableConcurrentHashMap等。
  • 线程不安全:多个线程访问共享资源时,可能出现数据不一致的情况。例如,ArrayListHashMap等。
  • 解决线程不安全问题
    • 使用同步机制(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. synchronizedReentrantLock的区别?

答案:

  • 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  // 拒绝策略
);
关键参数说明:
  1. corePoolSize(核心线程数)

    • 线程池中始终保持的线程数量,即使这些线程处于空闲状态也不会被销毁。
    • 通常根据 CPU 核心数和任务类型(CPU 密集型或 I/O 密集型)来设置。例如,对于 CPU 密集型任务,corePoolSize 可以设置为 CPU 核心数 + 1
  2. maximumPoolSize(最大线程数)

    • 线程池允许的最大线程数量。
    • 当任务队列满了且当前线程数小于 maximumPoolSize 时,线程池会继续创建新线程。
  3. keepAliveTimeunit(空闲线程存活时间)

    • 非核心线程在空闲时的存活时间。
    • 如果线程池中的线程数超过 corePoolSize,空闲线程会在指定的时间后被销毁。
  4. workQueue(任务队列)

    • 存储待执行任务的队列。常见的队列类型包括:
      • ArrayBlockingQueue:有界队列,适合固定大小的线程池。
      • LinkedBlockingQueue:无界队列,适合缓存线程池。
      • SynchronousQueue:直接提交队列,适合工作窃取线程池。
      • PriorityBlockingQueue:优先级队列,适合有优先级的任务。
  5. threadFactory(线程工厂)

    • 用于创建线程的工厂类。默认情况下,ThreadPoolExecutor 使用默认的线程工厂,但可以通过自定义线程工厂来设置线程的名称、优先级等属性。
  6. handler(拒绝策略)

    • 当任务过多且线程池已满时,如何处理新任务。常见的拒绝策略包括:
      • AbortPolicy:抛出 RejectedExecutionException
      • CallerRunsPolicy:由提交任务的线程执行任务。
      • DiscardPolicy:丢弃任务。
      • DiscardOldestPolicy:丢弃队列中最老的任务。

2. 选择合适的任务队列

任务队列的选择对线程池的性能和行为有重要影响:

  • ArrayBlockingQueue

    • 有界队列,适合固定大小的线程池。
    • 优点:可以限制任务队列的最大长度,防止内存溢出。
    • 缺点:如果队列满了,新任务会被拒绝。
  • LinkedBlockingQueue

    • 无界队列,适合缓存线程池。
    • 优点:任务队列几乎不会满,适合任务数量不确定的场景。
    • 缺点:可能会占用过多内存。
  • SynchronousQueue

    • 直接提交队列,适合工作窃取线程池。
    • 特点:任务直接提交给线程,不存储任务。如果线程池满了,任务会被拒绝。

3. 线程池的关闭

线程池使用完毕后,需要正确关闭,以释放资源。可以通过以下方法关闭线程池:

shutdown()
  • 尝试关闭线程池,但不会立即中断正在执行的任务。
  • 线程池会等待所有任务完成后再关闭。
shutdownNow()
  • 立即关闭线程池,并尝试中断正在执行的任务。
  • 返回尚未执行的任务列表。

4. 避免资源耗尽

  • 合理配置线程池大小:根据任务类型和系统资源合理设置 corePoolSizemaximumPoolSize
  • 限制任务队列大小:使用有界队列(如 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 时,需要注意以下细节:

  1. 合理配置线程池参数,根据任务类型和系统资源选择合适的线程池大小和任务队列。
  2. 选择合适的任务队列,避免资源耗尽。
  3. 正确关闭线程池,释放资源。
  4. 避免资源竞争和线程安全问题。
  5. 监控线程池状态,便于排查问题。

通过合理配置和使用 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()。它提供了更灵活的线程通信机制。

工作原理:
  • ConditionLock 配合使用。
  • 提供了 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() 更灵活。
  • 可以绑定多个条件变量,实现更复杂的线程通信。
缺点:
  • 使用 LockCondition 比内置锁更复杂。
  • 需要手动管理锁的释放。

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. 如何避免线程死锁?

答案:
死锁是指两个或多个线程互相等待对方释放资源,导致无法继续执行。避免死锁的方法:

  1. 避免嵌套锁:尽量减少嵌套锁的使用。
  2. 按固定顺序加锁:确保所有线程按相同顺序获取锁。
  3. 使用定时锁:通过tryLock()尝试加锁,超时则放弃。
  4. 减少锁的粒度:使用更细粒度的锁,减少锁

7. 锁的作用范围?如存在一个A类,A类中有一个synchronized修饰的叫update的方法,那么用A类创建了A1和A2两个对象,这两个对象都去调用update方法,会竞争锁资源吗?

答案:
在 Java 中,synchronized 关键字用于同步方法或代码块,以确保在多线程环境下对共享资源的互斥访问。synchronized 的作用范围取决于它修饰的对象:

  1. 修饰实例方法:锁的是当前对象实例。
  2. 修饰静态方法:锁的是整个类的类对象。
  3. 修饰代码块:锁的是指定的对象。

在你的问题中,A 类中的 update 方法被 synchronized 修饰,且没有明确指出是静态方法,因此可以推断它是实例方法。

分析:

  • 如果 update 是实例方法,synchronized 锁的是当前对象实例(this)。
  • 当你创建了两个对象 A1A2,它们分别调用 update 方法时:
    • A1 调用 update 时,锁的是 A1 对象。
    • A2 调用 update 时,锁的是 A2 对象。

因为 A1A2 是两个不同的对象实例,它们的锁是独立的,互不干扰。因此,A1A2 调用 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

从输出可以看出,A1A2update 方法可以同时运行,互不干扰。


如果 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 是实例方法,A1A2 调用时不会竞争锁资源。
  • 如果 update 是静态方法,A1A2 调用时会竞争同一个锁(类锁)。



synchronized 修饰的是代码块时,锁的作用范围取决于代码块中指定的对象。synchronized 代码块可以锁定以下两种对象:

  1. 实例对象(this:锁定当前对象实例。
  2. 任意对象:锁定代码块中指定的任意对象。

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

分析

  • A1A2 是不同的实例,它们各自锁定自己的 this 对象。
  • 因此,Thread-1Thread-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

分析

  • A1A2update 方法都锁定的是同一个共享对象 lock
  • 因此,Thread-1Thread-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

分析

  • A1A2update 方法都锁定的是同一个类对象 A.class
  • 因此,Thread-1Thread-2 会竞争同一个锁,导致互斥。

总结

  • 如果 synchronized 代码块锁定的是 当前实例对象(this,不同实例的锁是独立的,不会竞争锁资源。
  • 如果 synchronized 代码块锁定的是 同一个共享对象类对象(A.class,所有线程会竞争同一个锁,导致互斥。

通过合理选择锁对象,可以灵活控制线程之间的同步行为。

相关文章:

  • 2025年人工智能的发展前景将呈现多维度、深层次的变革,涵盖技术突破、行业应用、算力基础设施、政策法规等多个领域.结合工作情况,个人看法参考。
  • Cocos Creator Shader入门实战(三):CCEffect参数配置讲解
  • 捌拾贰- 贝尔不等式 (2)
  • 大白话JavaScript闭包实现原理与在实际开发中的应用场景
  • AF3 correct_msa_restypes函数解读
  • mac本地代理nginx,解决跨域问题
  • 【Java代码审计 | 第六篇】XSS防范
  • 【React】React + Tailwind CSS 快速入门指南
  • VBA高级应用30例Excel中ListObject对象:提取表内单元格的格式元素
  • WPF 之SizeToContent
  • 8.1linux竞争与并发知识讲解(尽可能详细)_csdn
  • pta L1-003 个位数统计
  • LeetCode 738. 单调递增的数字 java题解
  • 2.装饰器模式
  • 计算机安全 第四节:访问控制(上)
  • Qt常用控件之分组框QGroupBox
  • Express Router 全面教程与最佳实践
  • k8s下部署ansible进行node-export二安装
  • 使用PHP实现异步编程:挑战与解决方案
  • 【数据结构】一文解析跳表
  • 多个链接的网站怎么做的/可以免费发广告的网站有哪些
  • 用html做的零食网站/网站优化排名易下拉霸屏
  • 合肥外贸网站建设/推广赚钱的平台有哪些
  • 金融网站策划方案/查询网入口
  • 山西本土网站建设/app推广地推接单网
  • 企业英文网站制作/网络营销专业可以干什么工作