时常在面试中被问到的多线程问题:下篇

文章目录

  • 线程和线程池有什么区别?
      • 线程池 (ThreadPool)
      • 区别
  • 如何创建线程池?
      • 1. 固定大小线程池 (Fixed Thread Pool)
      • 2. 可缓存线程池 (Cached Thread Pool)
      • 3. 单线程线程池 (Single Thread Pool)
      • 4. 定时线程池 (Scheduled Thread Pool)
  • 推荐使用哪种方式来创建线程池?
      • 使用 ThreadPoolExecutor 创建线程池
  • 说一下 ThreadPoolExecutor 的参数含义?
      • 参数解释
      • 示例代码
      • 常见的线程池类型及其配置
        • 1. 固定大小线程池
        • 2. 可缓存线程池
        • 3. 单线程线程池
        • 4. 定时线程池
  • 说一下线程池的执行流程
      • 1. 任务提交
      • 2. 判断线程数
      • 3. 任务队列
      • 4. 任务执行
      • 5. 线程复用
      • 6. 空闲线程处理
      • 7. 线程池关闭
      • 线程池执行流程示意图
      • 示例代码
  • 线程池的拒绝策略都有哪些
      • 1. `AbortPolicy`(默认策略)
      • 2. `CallerRunsPolicy`
      • 3. `DiscardPolicy`
      • 4. `DiscardOldestPolicy`
      • 5. 自定义拒绝策略
  • 如何实现自定义拒绝策略?
      • 示例代码
  • 线程池中 shutdownNow() 和 shutdown() 有什么区别?
      • `shutdown()`
      • `shutdownNow()`
      • 示例代码
      • 运行结果说明
  • 多线程存在什么问题?
      • 1. 线程安全问题
        • 竞态条件(Race Condition)
        • 示例
      • 2. 死锁(Deadlock)
        • 示例
      • 3. 活锁(Livelock)
        • 示例
      • 4. 资源饥饿(Resource Starvation)
        • 示例
      • 5. 线程泄漏(Thread Leakage)
      • 6. 竞态条件引发的难以复现的错误
      • 7. 内存可见性问题
        • 示例
      • 8. 过度同步(Over-synchronization)
      • 9. 中断和恢复问题
      • 解决多线程问题的建议
  • 为什么会有线程安全问题?
  • 如何解决线程安全问题?
  • synchronized 有几种用法?
  • synchronized 修饰静态方法和普通方法有什么区别吗?

线程和线程池有什么区别?

线程适用于少量、短期的任务执行,而线程池适用于大量、长期的任务执行。通过线程池管理线程,可以更高效地利用系统资源,提升系统性能和稳定性。

线程池 (ThreadPool)

  • 定义: 线程池是一个管理线程的集合,通过复用固定数量的线程来执行多个任务,避免频繁创建和销毁线程的开销。
  • 创建: 在 Java 中,可以使用 Executors 工具类来创建线程池,例如 FixedThreadPoolCachedThreadPoolScheduledThreadPool 等。
  • 管理: 线程池管理线程的创建、调度和生命周期,并通过队列来调度任务。
  • 开销: 通过复用线程,减少了频繁创建和销毁线程的开销,提高了系统性能和资源利用率。
  • 示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Main {public static void main(String[] args) {ExecutorService executor = Executors.newFixedThreadPool(5);for (int i = 0; i < 10; i++) {Runnable task = new Runnable() {public void run() {// 线程任务}};executor.execute(task);}executor.shutdown();}
}

区别

  1. 资源管理:

    • 线程: 每个任务创建一个新的线程,线程的创建和销毁由程序员控制。
    • 线程池: 通过复用线程来执行多个任务,线程的创建和销毁由线程池管理。
  2. 性能:

    • 线程: 频繁的线程创建和销毁会带来性能开销。
    • 线程池: 通过复用线程减少性能开销,提高系统性能。
  3. 任务调度:

    • 线程: 任务由线程独立执行,每个线程有自己的执行路径。
    • 线程池: 任务由线程池调度和管理,通过队列来分配任务。
  4. 生命周期:

    • 线程: 线程的生命周期独立管理,包括创建、运行、等待和终止。
    • 线程池: 线程的生命周期由线程池管理,线程池会自动管理线程的复用和销毁。

如何创建线程池?

在 Java 中,创建线程池通常使用 java.util.concurrent.Executors 工具类。Executors 提供了一些工厂方法来创建不同类型的线程池。以下是一些常见的线程池类型及其创建方法:

1. 固定大小线程池 (Fixed Thread Pool)

固定大小线程池使用固定数量的线程来执行任务。适用于任务数量已知且不会动态增加的情况。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class FixedThreadPoolExample {public static void main(String[] args) {// 创建一个包含5个线程的固定大小线程池ExecutorService executor = Executors.newFixedThreadPool(5);for (int i = 0; i < 10; i++) {Runnable task = new Runnable() {public void run() {System.out.println(Thread.currentThread().getName() + " is executing task");}};// 提交任务给线程池执行executor.execute(task);}// 关闭线程池executor.shutdown();}
}

2. 可缓存线程池 (Cached Thread Pool)

可缓存线程池根据需要创建新线程,并在先前创建的线程空闲时重用它们。适用于大量短期任务。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class CachedThreadPoolExample {public static void main(String[] args) {// 创建一个可缓存的线程池ExecutorService executor = Executors.newCachedThreadPool();for (int i = 0; i < 10; i++) {Runnable task = new Runnable() {public void run() {System.out.println(Thread.currentThread().getName() + " is executing task");}};// 提交任务给线程池执行executor.execute(task);}// 关闭线程池executor.shutdown();}
}

3. 单线程线程池 (Single Thread Pool)

单线程线程池只有一个线程来执行任务。适用于需要保证任务按顺序执行的情况。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class SingleThreadPoolExample {public static void main(String[] args) {// 创建一个单线程的线程池ExecutorService executor = Executors.newSingleThreadExecutor();for (int i = 0; i < 10; i++) {Runnable task = new Runnable() {public void run() {System.out.println(Thread.currentThread().getName() + " is executing task");}};// 提交任务给线程池执行executor.execute(task);}// 关闭线程池executor.shutdown();}
}

4. 定时线程池 (Scheduled Thread Pool)

定时线程池可以在指定的延迟后或定期执行任务。适用于需要定时或周期性执行任务的情况。

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;public class ScheduledThreadPoolExample {public static void main(String[] args) {// 创建一个包含5个线程的定时线程池ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);Runnable task = new Runnable() {public void run() {System.out.println(Thread.currentThread().getName() + " is executing task");}};// 在3秒后执行一次性任务executor.schedule(task, 3, TimeUnit.SECONDS);// 每2秒执行一次任务,首次延迟1秒executor.scheduleAtFixedRate(task, 1, 2, TimeUnit.SECONDS);// 每2秒执行一次任务,上次任务结束后等待2秒再执行executor.scheduleWithFixedDelay(task, 1, 2, TimeUnit.SECONDS);// 添加延迟以便观察任务执行效果,然后关闭线程池try {Thread.sleep(10000); // 延迟10秒以便观察任务执行} catch (InterruptedException e) {e.printStackTrace();}executor.shutdown();}
}

推荐使用哪种方式来创建线程池?

推荐使用 java.util.concurrent.ThreadPoolExecutor 类来创建线程池,因为它提供了高度的灵活性和配置选项,可以满足各种复杂的并发需求。虽然 Executors 工具类提供了便捷的方法来创建不同类型的线程池,但直接使用 ThreadPoolExecutor 可以更好地控制线程池的行为和参数。

使用 ThreadPoolExecutor 创建线程池

ThreadPoolExecutor 构造方法允许你指定线程池的各种参数:

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler
)

说一下 ThreadPoolExecutor 的参数含义?

参数解释

  • corePoolSize: 线程池维护的最小线程数,即使这些线程处于空闲状态。
  • maximumPoolSize: 线程池允许的最大线程数。
  • keepAliveTime: 当线程数超过 corePoolSize 时,多余的空闲线程的存活时间。
  • unit: keepAliveTime 的时间单位。
  • workQueue: 用来存储等待执行任务的队列。
  • threadFactory: 用来创建新线程的工厂。
  • handler: 处理无法执行的任务的策略。

示例代码

import java.util.concurrent.*;public class CustomThreadPoolExample {public static void main(String[] args) {int corePoolSize = 5;int maximumPoolSize = 10;long keepAliveTime = 60;TimeUnit unit = TimeUnit.SECONDS;BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100);ThreadFactory threadFactory = Executors.defaultThreadFactory();RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory,handler);for (int i = 0; i < 20; i++) {Runnable task = new Runnable() {public void run() {System.out.println(Thread.currentThread().getName() + " is executing task");}};executor.execute(task);}executor.shutdown();}
}

常见的线程池类型及其配置

如果你希望使用类似于 Executors 提供的便捷线程池配置,可以参考以下配置:

1. 固定大小线程池
ThreadPoolExecutor fixedThreadPool = new ThreadPoolExecutor(5,  // corePoolSize5,  // maximumPoolSize (same as corePoolSize)0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()
);
2. 可缓存线程池
ThreadPoolExecutor cachedThreadPool = new ThreadPoolExecutor(0,  // corePoolSizeInteger.MAX_VALUE,  // maximumPoolSize60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>()
);
3. 单线程线程池
ThreadPoolExecutor singleThreadPool = new ThreadPoolExecutor(1,  // corePoolSize1,  // maximumPoolSize0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()
);
4. 定时线程池
ScheduledThreadPoolExecutor scheduledThreadPool = new ScheduledThreadPoolExecutor(5);

说一下线程池的执行流程

线程池的执行流程可以分为以下几个步骤,从任务提交到任务执行完毕,每一步都有明确的操作过程。以下是一个典型的线程池执行流程:

1. 任务提交

当一个任务(RunnableCallable)被提交给线程池时,通过 execute()submit() 方法,任务会被包装成一个 FutureTask 对象(如果使用 submit() 方法)。

2. 判断线程数

线程池判断当前运行的线程数是否小于核心线程数(corePoolSize)。

  • 如果是,创建一个新的线程来执行任务。
  • 如果不是,将任务放入任务队列(workQueue)。

3. 任务队列

  • 如果任务队列未满,任务被放入队列等待执行。
  • 如果任务队列已满,线程池会判断当前运行的线程数是否小于最大线程数(maximumPoolSize)。
    • 如果是,创建一个新的线程来执行任务。
    • 如果不是,执行拒绝策略(RejectedExecutionHandler),如抛出异常、调用任务的 run() 方法直接在提交线程中执行等。

4. 任务执行

任务由线程池中的工作线程从任务队列中取出并执行。每个工作线程不断循环从任务队列中获取任务并执行,直到线程池被关闭。

5. 线程复用

线程池中的线程执行完任务后不会立即销毁,而是继续从任务队列中获取新的任务执行。这种线程复用机制大大减少了线程创建和销毁的开销。

6. 空闲线程处理

当线程池中的线程数超过核心线程数且这些超出部分的线程处于空闲状态超过指定的存活时间(keepAliveTime)时,这些线程将被销毁,直到线程池中的线程数不超过核心线程数。

7. 线程池关闭

当线程池被调用 shutdown() 方法时,不再接受新的任务,但会继续执行已提交的任务,直到任务队列中的所有任务执行完毕。当调用 shutdownNow() 方法时,线程池会试图停止所有正在执行的任务,并清空任务队列。

线程池执行流程示意图

            任务提交|v判断线程数 < corePoolSize ?/ \是    否/       \创建新线程    任务放入队列|v任务队列已满?/ \是    否/       \判断线程数 < maximumPoolSize ?/ \是    否/       \创建新线程    执行拒绝策略|v任务执行|v线程复用|v空闲线程处理|v线程池关闭

示例代码

以下是一个示例代码,展示了如何创建和使用线程池以及解释线程池的执行流程:

import java.util.concurrent.*;public class ThreadPoolExample {public static void main(String[] args) {// 创建一个包含5个核心线程,10个最大线程,和60秒存活时间的线程池ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100),Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());// 提交20个任务给线程池for (int i = 0; i < 20; i++) {Runnable task = new Runnable() {public void run() {System.out.println(Thread.currentThread().getName() + " is executing task");try {Thread.sleep(2000); // 模拟任务执行时间} catch (InterruptedException e) {e.printStackTrace();}}};executor.execute(task);}// 关闭线程池executor.shutdown();try {// 等待线程池中的所有任务执行完毕if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {executor.shutdownNow(); // 强制关闭}} catch (InterruptedException e) {executor.shutdownNow(); // 强制关闭}}
}

线程池的拒绝策略都有哪些

线程池的拒绝策略定义了当任务不能被执行时应采取的措施。当线程池已达到其最大容量,且任务队列也已满时,线程池会调用拒绝策略。Java 的 java.util.concurrent 包提供了几种内置的拒绝策略,具体如下:

1. AbortPolicy(默认策略)

AbortPolicy 直接抛出 RejectedExecutionException 异常,阻止系统正常工作。

RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();

2. CallerRunsPolicy

CallerRunsPolicy 会让提交任务的线程自己来运行这个任务。这种策略提供了一种退化机制,可以降低新任务的提交速度,从而减缓新任务的生成。

RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();

3. DiscardPolicy

DiscardPolicy 直接丢弃无法执行的任务,不予处理,也不抛出异常。

RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardPolicy();

4. DiscardOldestPolicy

DiscardOldestPolicy 会丢弃队列中最老的任务(即最早加入队列的任务),然后尝试重新提交被拒绝的任务。

RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardOldestPolicy();

5. 自定义拒绝策略

你也可以实现自己的拒绝策略,通过实现 RejectedExecutionHandler 接口并覆盖 rejectedExecution 方法。

如何实现自定义拒绝策略?

import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;public class CustomRejectedExecutionHandler implements RejectedExecutionHandler {@Overridepublic void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {// 自定义处理逻辑System.out.println("Task " + r.toString() + " rejected from " + executor.toString());// 可以记录日志,或者执行其他处理措施}
}

然后在创建 ThreadPoolExecutor 时使用自定义的拒绝策略:

RejectedExecutionHandler handler = new CustomRejectedExecutionHandler();
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);

示例代码

以下是一个示例,展示了如何创建线程池并使用不同的拒绝策略:

import java.util.concurrent.*;public class RejectedExecutionExample {public static void main(String[] args) {int corePoolSize = 2;int maximumPoolSize = 4;long keepAliveTime = 10;TimeUnit unit = TimeUnit.SECONDS;BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);// 使用不同的拒绝策略RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); // 改为不同的策略进行测试ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);for (int i = 0; i < 10; i++) {Runnable task = new Runnable() {public void run() {System.out.println(Thread.currentThread().getName() + " is executing task");try {Thread.sleep(2000); // 模拟任务执行时间} catch (InterruptedException e) {e.printStackTrace();}}};try {executor.execute(task);} catch (RejectedExecutionException e) {System.out.println("Task " + i + " rejected");}}executor.shutdown();try {if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {executor.shutdownNow();}} catch (InterruptedException e) {executor.shutdownNow();}}
}

线程池中 shutdownNow() 和 shutdown() 有什么区别?

在 Java 的线程池中,shutdown()shutdownNow() 方法都用于关闭线程池,但它们的行为有显著不同。

  • shutdown(): 有序关闭,不接受新任务,但会完成已提交的任务。
  • shutdownNow(): 立即关闭,尽力中断正在执行的任务,并返回未执行的任务列表。

选择使用哪种关闭方法取决于应用场景和具体需求。如果需要确保所有任务都执行完毕,应使用 shutdown();如果需要尽快停止线程池,则使用 shutdownNow()

shutdown()

  • 行为: 启动有序关闭,线程池不再接受新任务,但会继续处理已提交的任务,包括那些已提交但尚未开始执行的任务和正在执行的任务。
  • 工作线程: 线程池中的工作线程在执行完当前任务后将继续从队列中取出并执行任务,直到所有任务完成。
  • 调用效果: 调用此方法后,线程池将继续运行直到所有任务完成,然后才终止。

shutdownNow()

  • 行为: 尝试立即停止线程池的所有活动任务,并返回等待执行的任务列表。它会尽力中断正在执行的任务,并不保证能成功中断所有任务。
  • 工作线程: 线程池中的工作线程会被尝试中断。已提交但尚未开始执行的任务将不会被执行,并返回这些任务的列表。
  • 调用效果: 调用此方法后,线程池会立即停止接受新任务,尽力中断所有正在执行的任务,清空任务队列,并返回队列中未执行的任务。

示例代码

以下代码展示了如何使用 shutdown()shutdownNow() 方法,以及它们的不同效果:

import java.util.concurrent.*;public class ThreadPoolShutdownExample {public static void main(String[] args) throws InterruptedException {ExecutorService executor = Executors.newFixedThreadPool(3);for (int i = 0; i < 5; i++) {executor.execute(new RunnableTask(i));}// 使用 shutdown() 方法System.out.println("Calling shutdown()");executor.shutdown();// 等待线程池终止if (executor.awaitTermination(5, TimeUnit.SECONDS)) {System.out.println("All tasks completed");} else {System.out.println("Timeout reached before termination");}// 重新创建线程池executor = Executors.newFixedThreadPool(3);for (int i = 0; i < 5; i++) {executor.execute(new RunnableTask(i));}// 使用 shutdownNow() 方法System.out.println("Calling shutdownNow()");List<Runnable> notExecutedTasks = executor.shutdownNow();System.out.println("Tasks not executed: " + notExecutedTasks.size());// 等待线程池终止if (executor.awaitTermination(5, TimeUnit.SECONDS)) {System.out.println("All tasks terminated");} else {System.out.println("Timeout reached before termination");}}
}class RunnableTask implements Runnable {private int taskId;public RunnableTask(int taskId) {this.taskId = taskId;}@Overridepublic void run() {System.out.println("Executing task " + taskId + " by " + Thread.currentThread().getName());try {Thread.sleep(2000); // 模拟任务执行时间} catch (InterruptedException e) {System.out.println("Task " + taskId + " was interrupted");}}@Overridepublic String toString() {return "RunnableTask{" + "taskId=" + taskId + '}';}
}

运行结果说明

  • 调用 shutdown():

    • 线程池将处理所有已提交的任务,但不再接受新任务。
    • 输出表明所有任务执行完成。
  • 调用 shutdownNow():

    • 尝试立即停止所有正在执行的任务,并返回未执行的任务列表。
    • 输出表明一些任务被中断,一些任务未被执行。

多线程存在什么问题?

多线程编程可以显著提高程序的性能和响应能力,但它也带来了许多复杂性和潜在的问题。以下是一些常见的多线程问题:

1. 线程安全问题

当多个线程同时访问和修改共享数据时,可能会发生线程安全问题。

竞态条件(Race Condition)

多个线程同时读取和修改共享数据,导致不可预测的结果。

示例
class Counter {private int count = 0;public void increment() {count++;}public int getCount() {return count;}
}public class RaceConditionExample {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Runnable task = () -> {for (int i = 0; i < 1000; i++) {counter.increment();}};Thread thread1 = new Thread(task);Thread thread2 = new Thread(task);thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("Final count: " + counter.getCount());}
}

在这个示例中,Counter 类没有同步机制,可能导致竞态条件,最终的计数结果可能不是预期的 2000。

2. 死锁(Deadlock)

两个或多个线程相互等待对方持有的资源,从而进入无限等待状态。

示例
class Resource {private final String name;public Resource(String name) {this.name = name;}public synchronized void use(Resource other) {System.out.println(Thread.currentThread().getName() + " using " + name);try {Thread.sleep(50); // 模拟工作} catch (InterruptedException e) {e.printStackTrace();}other.doSomething();}public synchronized void doSomething() {System.out.println(Thread.currentThread().getName() + " doing something with " + name);}
}public class DeadlockExample {public static void main(String[] args) {final Resource resourceA = new Resource("ResourceA");final Resource resourceB = new Resource("ResourceB");Thread thread1 = new Thread(() -> resourceA.use(resourceB), "Thread1");Thread thread2 = new Thread(() -> resourceB.use(resourceA), "Thread2");thread1.start();thread2.start();}
}

在这个示例中,Thread1 持有 resourceA 并等待 resourceB,而 Thread2 持有 resourceB 并等待 resourceA,导致死锁。

3. 活锁(Livelock)

线程频繁地让出资源,但由于某种条件无法满足,导致线程不能继续执行。

示例

活锁的示例较为复杂,通常涉及多个线程不断改变状态以响应对方的行为,但始终无法取得进展。

4. 资源饥饿(Resource Starvation)

某些线程长时间得不到所需资源,导致无法继续执行。

示例

一个优先级较低的线程可能会因为优先级高的线程一直占用资源而无法获得运行机会。

5. 线程泄漏(Thread Leakage)

线程启动后没有正常结束,且没有被正确管理和回收,导致资源浪费。

6. 竞态条件引发的难以复现的错误

多线程程序中的竞态条件往往导致难以复现的间歇性错误,增加调试难度。

7. 内存可见性问题

在多核处理器上,线程可能在各自的缓存中看到不同步的内存视图,导致读取到过期的数据。

示例
class VisibilityProblem {private volatile boolean running = true;public void run() {while (running) {// ...}System.out.println("Stopped.");}public void stop() {running = false;}public static void main(String[] args) throws InterruptedException {VisibilityProblem visibilityProblem = new VisibilityProblem();Thread thread = new Thread(visibilityProblem::run);thread.start();Thread.sleep(1000);visibilityProblem.stop();}
}

在这个示例中,通过使用 volatile 关键字确保 running 变量的可见性,保证 stop() 方法调用后,run() 方法可以及时感知到变化。

8. 过度同步(Over-synchronization)

过度使用同步机制会导致性能下降和线程争用(Contention),增加系统开销。

9. 中断和恢复问题

线程在执行过程中被中断,需要正确处理中断信号,确保程序的健壮性。

解决多线程问题的建议

  • 使用高层次的并发工具(如 java.util.concurrent 包中的类)。
  • 避免手动管理线程,使用线程池来管理线程的创建和销毁。
  • 使用正确的同步机制,如 synchronizedLock,以及 volatile
  • 尽量减少共享数据,使用线程安全的数据结构(如 ConcurrentHashMap)。
  • 小心处理线程中断和恢复,确保在中断时进行必要的清理工作。
  • 避免死锁,尽量使用锁的顺序,或者使用尝试锁定(如 tryLock)来避免死锁。
  • 进行充分的测试,尤其是并发测试,模拟实际的多线程环境来发现潜在的问题。

多线程编程复杂且容易出错,但通过合理的设计和工具,可以有效地利用多核处理器的能力,提高程序性能。

为什么会有线程安全问题?

线程安全问题主要出现在多线程编程中,当多个线程同时访问共享资源时,如果不加以控制和协调,就可能导致数据的不一致性和不可预测的行为。以下是一些导致线程安全问题的常见原因:

  1. 共享可变数据
    当多个线程同时读写同一个共享变量时,如果没有适当的同步机制,就可能导致数据竞争(Race Condition)。例如,两个线程同时对一个计数器进行递增操作,可能会导致最终的计数结果不正确。

  2. 原子性
    某些操作在多线程环境下需要确保其原子性,即操作要么全部执行完毕,要么完全不执行。缺乏原子性会导致部分执行的操作留下不一致的状态。

  3. 可见性
    在多线程环境中,一个线程对共享变量的修改可能对其他线程不可见。现代处理器和编译器可能会对代码进行优化,导致一个线程对变量的更新不会立即被其他线程看到。这种情况下,程序可能会基于过时的数据做出错误的决策。

  4. 有序性
    多线程环境下,程序的执行顺序可能与代码的书写顺序不同。编译器、处理器和内存模型可能会重新排序指令,导致实际执行顺序与预期不一致,从而引发线程安全问题。

如何解决线程安全问题?

为了解决线程安全问题,可以使用以下方法:

  • 锁(Lock):使用互斥锁(如Java中的ReentrantLock)或同步块(如Java中的synchronized关键字)来确保同一时间只有一个线程可以访问共享资源。
  • 原子变量:使用原子类(如Java中的AtomicInteger)来保证对单个变量的原子性操作。
  • 线程本地存储:使用线程本地变量(如Java中的ThreadLocal)来确保每个线程都有自己的独立副本,避免共享数据。
  • 不可变对象:使用不可变对象(如Java中的StringInteger)来避免修改共享数据。

synchronized 有几种用法?

在Java中,synchronized关键字用于实现线程同步,确保在同一时间只有一个线程可以访问被同步的代码块或方法。synchronized有以下几种用法:

  1. 同步实例方法
    使用synchronized关键字修饰实例方法,表示整个方法是同步的,只有一个线程可以访问这个方法的实例。

    public class Example {public synchronized void syncMethod() {// 同步方法的代码}
    }
    
  2. 同步静态方法
    使用synchronized关键字修饰静态方法,表示整个方法是同步的,只有一个线程可以访问这个方法的类。

    public class Example {public static synchronized void syncStaticMethod() {// 同步静态方法的代码}
    }
    
  3. 同步代码块
    使用synchronized关键字同步代码块,可以指定具体的对象作为锁。这种方式更灵活,可以选择只同步某些关键部分的代码。

    public class Example {private final Object lock = new Object();public void syncBlock() {synchronized (lock) {// 同步代码块的代码}}
    }
    

    也可以使用this作为锁,来同步当前实例的代码块:

    public class Example {public void syncBlock() {synchronized (this) {// 同步代码块的代码}}
    }
    
  4. 类对象锁
    使用Class对象作为锁,来同步静态方法或代码块,确保同一时间只有一个线程可以访问这个类的静态资源。

    public class Example {public void syncClassBlock() {synchronized (Example.class) {// 同步类对象锁的代码块}}
    }
    

synchronized 修饰静态方法和普通方法有什么区别吗?

synchronized修饰静态方法和普通方法的主要区别在于锁的对象不同,从而影响了线程同步的范围和粒度:

  1. 锁的对象

    • 静态方法:当synchronized修饰静态方法时,锁是当前类的Class对象。例如,如果有一个类Example,那么锁是Example.class。这意味着同一时间只有一个线程可以执行该类的任意一个静态同步方法。
    • 普通方法:当synchronized修饰普通实例方法时,锁是当前实例对象(this)。这意味着同一时间只有一个线程可以执行该实例对象的任意一个同步实例方法,但多个线程可以同时执行同一个类的不同实例的同步方法。
  2. 同步范围

    • 静态方法:由于锁是类对象(Class),所以同步范围是整个类的所有静态方法。即使多个线程操作不同的实例,静态同步方法仍然会被同步。
    • 普通方法:由于锁是实例对象(this),所以同步范围是该实例对象的所有同步实例方法。不同实例的同步方法不会相互影响。

具体示例如下:

public class Example {// 静态同步方法public static synchronized void staticSyncMethod() {// 静态同步方法的代码}// 实例同步方法public synchronized void instanceSyncMethod() {// 实例同步方法的代码}
}public class Test {public static void main(String[] args) {Example obj1 = new Example();Example obj2 = new Example();// 线程1调用obj1的静态同步方法new Thread(() -> Example.staticSyncMethod()).start();// 线程2调用obj2的静态同步方法new Thread(() -> Example.staticSyncMethod()).start();// 线程3调用obj1的实例同步方法new Thread(() -> obj1.instanceSyncMethod()).start();// 线程4调用obj2的实例同步方法new Thread(() -> obj2.instanceSyncMethod()).start();}
}

在这个示例中:

  • 线程1和线程2因为调用的是同一个类的静态同步方法,所以它们会互相阻塞,只有一个线程可以执行staticSyncMethod
  • 线程3和线程4因为调用的是不同实例的同步方法,所以它们不会互相阻塞,可以同时执行instanceSyncMethod

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/37297.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【贪心】个人练习-Leetcode-2271. Maximum White Tiles Covered by a Carpet

题目链接&#xff1a;https://leetcode.cn/problems/maximum-white-tiles-covered-by-a-carpet/description/ 题目大意&#xff1a;给定一个左右区间序列tiles[][]&#xff0c;每个元素代表这个区间被瓷砖覆盖&#xff08;左右都是闭合的&#xff09;。给定一块毯子&#xff0…

使用 Ubuntu x86_64 平台交叉编译适用于 Linux aarch64(arm64) 平台的 QT5(包含OpenGL/WebEngine支持) 库

使用 Ubuntu AMD64 平台交叉编译适用于 Linux ARM64 平台的 QT5(包含 OpenGL/WebEngine 支持) 库 目录 使用 Ubuntu AMD64 平台交叉编译适用于 Linux ARM64 平台的 QT5(包含 OpenGL/WebEngine 支持) 库写在前面前期准备编译全流程1. 环境搭建2. 复制源码包并解压&#xff0c;创…

PrestaShop的目录结构详解

admin-dev&#xff1a;这个目录通常包含开发和测试PrestaShop后台时所需的脚本和配置文件。例如&#xff0c;它可能包含用于测试API的脚本或用于在开发过程中快速访问某些后台功能的快捷方式。 app&#xff1a;这个目录是PrestaShop的核心&#xff0c;包含了许多关键的组件。例…

解决HTTP 400 Bad Request错误的方法

解决HTTP 400 Bad Request错误的方法 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01; 在进行网络通信时&#xff0c;HTTP 400 Bad Request错误是相对常见的问题…

在Mac上恢复丢失或未保存的Word文档的5种有效方法

“救命&#xff01;我想在Mac上恢复丢失的Word文档。就在 1 小时前&#xff0c;我错误地删除了它们&#xff0c;并清空了垃圾桶。这些Word文档对我来说非常重要。我不知道如何恢复它们&#xff0c;谁能帮我&#xff1f;提前致谢&#xff01; 没有什么比忘记保存 Word 文档或在…

3d模型里地毯的材质怎么赋予?---模大狮模型网

在进行3D建模时&#xff0c;赋予地毯逼真的材质是营造现实感和增强场景氛围的重要步骤。模大狮将介绍在常见的3D建模软件中&#xff0c;如何有效地为地毯赋予各种材质&#xff0c;以及一些实用的技巧和注意事项。 一、选择合适的地毯材质 在3D建模中&#xff0c;地毯的材质选择…

Spring Boot与WebFlux的实战案例

Spring Boot与WebFlux的实战案例 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;今天&#xff0c;我们将探讨如何利用Spring Boot和WebFlux构建响应式应用的实战…

MySQL之可扩展性(七)

可扩展性 通过集群扩展 理想的扩展方案时单一逻辑数据库能够存储尽可能多的数据&#xff0c;处理尽可能多的查询&#xff0c;并如期望的那样增长。许多人的第一想法就是建立一个"集群"或者"网格"来无缝处理这些事情&#xff0c;这样应用就无须去做太多工…

LangServe APIHandler:定制化LangChain REST API

LangServe&#xff0c;这一创新性的工具&#xff0c;专为简化LangChain对象及链的部署而生&#xff0c;将它们转化为REST API&#xff0c;从而赋能开发人员构建更为敏捷高效的应用。与FastAPI的深度融合以及Pydantic的精准数据验证&#xff0c;使得LangServe成为构建微服务的理…

K8S中的某个容器突然出现内存和CPU占用过高的情况解决办法

当K8S中的某个容器突然出现内存和CPU占用过高的情况时&#xff0c;可以采取以下步骤进行处理&#xff1a; 观察和分析&#xff1a; 使用kubectl top pods命令查看集群中各个Pod的CPU和内存占用情况&#xff0c;找出占用资源高的Pod。使用kubectl describe pod <pod-name>…

双向长短期记忆神经网络BiLSTM

先说一下LSTM LSTM 是一种特殊的 RNN&#xff0c;它通过引入门控机制来解决传统 RNN 的长期依赖问题。 LSTM 的结构包含以下几个关键组件&#xff1a; 输入门&#xff08;input gate&#xff09;&#xff1a;决定当前时间步的输入信息对细胞状态的影响程度。遗忘门&#xff…

C盘满了怎么办?用这方法彻底拯救你的C盘

C盘满了怎么办&#xff1f;用这方法彻底拯救你的C盘。我们的C盘是整个电脑运行的核心部分&#xff0c;里面装载了很重要的系统框架和数据&#xff0c;由于使用的时间越来越长&#xff0c;C盘也会积累很多的垃圾&#xff0c;这样就经常容易出现爆满的情况。 对于C盘爆满&#x…

扫扫地,搞搞卫生 ≠ 车间5S管理

在制造业的日常运营中&#xff0c;车间管理是一项至关重要的工作&#xff0c;它直接关系到生产效率、产品质量以及员工的工作环境。然而&#xff0c;许多人常常将简单的“扫扫地&#xff0c;搞搞卫生”等同于车间5S管理&#xff0c;这种误解不仅可能导致管理效果不佳&#xff0…

Halcon 如何让图像自适应窗口

一 如何让图像自适应窗口 read_image(Image,1)get_image_size(Image,Width, Height)dev_close_window()dev_open_window(0,0,Width/2,Height/2,black,WindowHandle)dev_set_part(0,0,Height-800,Width-800)dev_display(Image)二 如何实现彩色图像转化为灰色图像 read_image(I…

浅谈逻辑控制器之Switch控制器

浅谈逻辑控制器之Switch控制器 Switch Controller是Apache JMeter中一个强大的逻辑控制器&#xff0c;它允许用户基于特定的变量值或参数来控制哪些子采样器被执行。与简单地按照配置顺序执行的控制器不同&#xff0c;Switch Controller根据提供的“switch value”来决定执行哪…

鸿蒙HCIP应用开发学什么?

HCIP-HarmonyOS Application Developer 课程大纲 一&#xff1a;HarmonyOS 系统介绍 -&#xff08;3 课时&#xff09; - 系统及应用场景介绍&#xff1b;HarmonyOS 系统介绍&#xff1b;HarmonyOS 定义 HarmonyOS 特征&#xff1b; - 统- OS&#xff0c;弹性部署&#xff1b…

深度相机识别物体——实现数据集准备与数据集分割

一、数据集准备——Labelimg进行标定 1.安装labelimg——pip install labelimg -i https://pypi.tuna.tsinghua.edu.cn/simple 2.建立相应的数据集存放文件夹 3.打开labelimg&#xff0c;直接在命令行输入labelimg即可&#xff0c;并初始化 4.开始标注&#xff0c;设置标注好…

【高考志愿】自动化

目录 一、专业概述 二、课程设计 三、就业前景与方向 四、志愿填报 五、自动化专业排名 一、专业概述 高考志愿自动化专业选择&#xff0c;无疑是迈向现代化工业与科技发展的一把金钥匙。自动化专业&#xff0c;作为现代工程领域的重要支柱&#xff0c;融合了计算机、电子…

Streams.js:简化 JavaScript 数据流处理

在现代的 Web 开发中&#xff0c;数据流处理是一个常见的需求。从处理用户输入到处理大规模数据集&#xff0c;JavaScript 开发者需要一种有效的方式来处理和转换数据流。Streams.js 是一个优秀的 JavaScript 库&#xff0c;提供了强大的工具来简化数据流处理的复杂性。 什么是…

宝兰德开源多款仓颉产品组件,共筑新语言生态

2024年6月21日下午&#xff0c;华为终端BG软件部总裁龚体先生在华为开发者大会主题演讲《鸿蒙原生应用&#xff0c;全新出发&#xff01;》中向全球开发者介绍了华为自研仓颉编程语言&#xff0c;并发布了HarmonyOS NEXT仓颉语言开发者预览版。这是华为首次公开发布仓颉编程语言…