并发编程-线程

1. 什么是进程、什么是线程?

  • 线程是CPU调度的最小单位,进程是线程的集合,一个进程包含多个线程以及共享变量空间
  • 举个例子:当我们执行Main启动一个Java程序也就是启动一个进程,而至少会有两个线程启动起来,一个是main方法的主线程,还有就是GC线程,堆空间、元空间都是进程在管理

2. CPU和线程是什么关系

  • CPU是计算机的核心计算单元,负责执行指令并进行数据处理
  • 线程是CPU执行的基本单位,可以被调度到CPU内核上运行
  • 同一时刻CPU只会执行一个线程的任务,并发情况下一个线程会通过上下文切换在多个CPU分段调度

3. 什么是上下文切换?

  • 上下文切换是指CPU在并发执行多个线程时,需要保存和恢复他们的运行状态的过程,如保存当前任务上下文信息、更新调度器信息、加载新的任务信息,而在切换过程中,会造成时间、缓存、调度的开销,所以我们需要尽量避免上下文的频繁切换
  • 比如在设置线程池的核心线程数时,如果是CPU密集型任务,可以设置与CPU核心数相同的线程数

4. 什么是并发,什么是并行,他们有什么区别?

  • 并行:同一时刻,多个任务同时执行
  • 并发:一个CPU通过交替调度执行多个任务
  • 主要区别:同一时刻,并发只有一个任务在执行,并行有多个任务在执行
  • 举例:
    • 饭堂打饭有3个窗口,并行就是有3个阿姨给大家打饭,并发就是1个阿姨给大家打饭
    • 阿姨是CPU、3个窗口就是3个线程

5. 并发3要素

  • 可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到
    • 原因:CPU缓存引起
    • 现象:
      • A线程定义了变量i=0,并修改i的值,i=10
      • B线程访问变量i
  • 原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
    • 原因:分时复用引起
  • 有序性:即程序执行的顺序按照代码的先后顺序执行
    • 原因:重排序引起

代码参考资料:Java 并发 - 理论基础

6. 线程安全的实现方式有哪些

  1. 阻塞同步(synchronized 和 ReentrantLock)
  2. 非阻塞同步(CAS)
  3. 无同步的方案 (栈封闭:方法局部变量、线程隔离:ThreadLocal)

7. 为什么要使用多线程

首先CPU、内存、I/O 设备的速度是有极大差异的,而我们日常的作业都是要操作到内存和IO的,如果没有多线程这个时候CPU就是处于空闲状态,使用多线程就是合理的利用CPU资源,从而提升作业效率

8. (重要)线程有哪些状态,他们之间是怎么相互转换的?

  1. 线程的6个状态
    1. 正常运转的线程: New --> Runnable --> Terminated
    2. 在Runnable和Terminated之间:
      1. 获取锁阻塞:Bloked
      2. 等待唤醒:WaitingTimeWaiting
    3. 阻塞和等待的区别:
      1. 阻塞是被动的,它是在等待获取一个排它锁。
      2. 等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入
  2. 状态详细描述
  1. New 新建:创建后未启动 Thread thread = new MyThread()
  2. Runnable 可运行thread.strat() 可能在运行,也可能等待CPU调度
  3. Terminated 死亡:线程结束或异常退出
  4. Blocking 阻塞: 等待获取一个排它锁,如果拿到锁就会结束该状态
  5. Waiting 无限期等待:等待其他线程显示唤醒,可以在一定时间后自动唤醒

进入方法

退出方法

Object.wait()

Object.notify() / Object.notifyAll()

Thread.join()

被调用的线程执行完毕

LockSupport.park()

 -

      6. TimeWaiting 限期等待:无序其他线程显式唤醒

进入方法

退出方法

Thread.sleep()

时间结束

设置了超时时间的Object.wait()

时间结束 / Object.notify() / Object.notifyAll()

设置了超时时间的 Thread.join() 方法

时间结束 / 被调用的线程执行完毕

LockSupport.parkNanos()

LockSupport.parkUntil()

9. 如何开启多线程

  • 继承Threa类,实现run方法,调用start方法
  • 实现Runnable接口,实现run方法
  • 实现Callable接口,实现call方法(有返回值),返回值通过FutureTask包装

在实际应用上,使用较多的是通过new Thread通过lambda表达式实现任务

然后通过线程池的方式进行调度管理

10. 什么是Deamon线程,守护线程

  • 守护线程就是在程序运行时后台提供服务的线程,比如GC线程
  • 当所有非守护线程结束,程序也会终止,同时所有守护线程也会结束
  • 使用Thread的实例方法thread.setDaemon(true); 可以设置线程为守护线程

11. 线程中断  interrupt()

  • 实例方法:
    • interrupt()
      • 方法作用:尝试中断该线程
      • 影响:
        • 如果线程处于Waiting状态:wait()、join()、sleep()方法阻塞,会抛出InterruptedException异常
        • 其他情况下线程正常运行,可以通过检查isInterrupted()检查中断标志进行处理
    • isInterrupted()
      • 方法作用:检查线程是否被中断,中断返回true,不清除中断状态
  • 静态方法:
    • interrupted()检查线程是否被中断,清除中断标识

12. 线程之间的协作

当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调

join():等待目标线程结束

  • 在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束

wait() notify() notifyAll()

  • 调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起
  • 当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程

Java
public static class WaitNotifyExample {
    public synchronized void before() {
        System.out.println("before");
        this.notify();
    }
    public synchronized void after() {
        try {
            this.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("after");
    }
}
public static void main(String[] args) throws InterruptedException {
    ExecutorService executorService = Executors.newCachedThreadPool();
    WaitNotifyExample example = new WaitNotifyExample();
    executorService.execute(() -> example.after());
    Thread.sleep(1000);
    executorService.execute(() -> example.before());
}

Thread.sleep()

wait()和sleep()都是进入线程等待状态,他们的区别

  1. wait()是Object的实例方法,而sleep()是Thread的静态方法
  2. wait()会释放锁,sleep()不会释放锁
  3. wait()需要在同步代码块中使用

await() signal() signalAll()

  • java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调
  • 可以在 Condition 上调用 await() 方法使线程等待
  • 其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程
  • 相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活

Java
public  static class AwaitSignalExample {
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    public void before() {
        lock.lock();
        try {
            System.out.println("before");
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
    public void after() {
        lock.lock();
        try {
            condition.await();
            System.out.println("after");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public static void main(String[] args) throws InterruptedException {
    ExecutorService executorService = Executors.newCachedThreadPool();
    AwaitSignalExample example = new AwaitSignalExample();
    executorService.execute(() -> example.after());
    Thread.sleep(1000);
    executorService.execute(() -> example.before());
}

13. 为什么要使用线程池

  • 如果每次任务执行都要创建线程销毁线程,会造成cpu资源浪费和性能的开销
  • 使用线程池可以实现线程的服用,提高执行效率
  • 通过线程池可以更好管理线程的执行,避免无限制的创建线程

14. Executors提供的线程池以及弊端

  • FixedThreadPool:
    • 使用固定数量的线程。线程数量固定后不会改变,超出的任务将会在队列中等待执行
  • CachedThreadPool:
    • 根据需要创建新线程的线程池。对于许多短期异步任务,这种线程池通常能够提高程序性能。如果线程闲置时间超过60秒,将被终止并从池中移除
  • SingleThreadExecutor:
    • 创建一个单线程执行的线程池。确保所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
  • ScheduledThreadPool:
    • 线程池可以调度命令在给定延迟后运行或定期执行任务
  • WorkStealingPool:
    • 基于工作窃取算法的线程池,能够有效处理不均匀负载。
    • 它使用守护线程池,适用于大多数计算密集型任务。

弊端:没有限制阻塞队列容量,大量任务提交时会导致内存溢出

15. (重要)自定义线程池的参数

ThreadPoolExecutor customThreadPool = new ThreadPoolExecutor(
    int corePoolSize, // 核心线程数
    int maximumPoolSize, // 最大线程数
    long keepAliveTime, // 线程空闲时间
    TimeUnit unit, // 线程空闲时间单位
    BlockingQueue<Runnable> workQueue, // 任务队列,存放待执行任务
    ThreadFactory threadFactory, // 线程工厂,可以自定义线程名称、优先级、是否守护线程等
    RejectedExecutionHandler handler // 拒绝策略
);

keepAliveTime TimeUnit

线程空闲时间和单位,当线程数超过核心线程后创建了非核心线程,会在存活多久后被销毁

RejectedExecutionHandler 拒绝策略

  • AbortPolicy(默认策略)
    • 这是默认的拒绝策略。当线程池的任务队列已满且无法接受新任务时,会抛出一个 RejectedExecutionException 异常。
  • CallerRunsPolicy
    • 当任务被拒绝时,会在调用者的线程中执行该任务。这种策略不会丢弃任务,但可能会导致调用线程阻塞。
  • DiscardPolicy
    • 当任务被拒绝时,会直接丢弃该任务,不做任何处理。
  • DiscardOldestPolicy
    • 当任务被拒绝时,会丢弃队列中最老的一个任务,然后尝试重新提交被拒绝的任务。

16.  当一个任务被提交到线程池运行时,是如何运作的

17. 线程池中线程异常的处理方案

  1. 手动try-catc,将异常任务记录下来,或者输出日志监控
  2. submit执行,Future.get() 接收异常
  3. 重写ThreadPoolExecutor的afterExecute方法,处理传递的异常引用
  4. 为工作线程设置UncaughtExceptionHandler处理异常

18. 线程池状态,怎么关闭

如果没有正确关闭线程池,可能导致任务未执行完成、资源泄露等情况

  • ExecutorService 提供的方法
  • shutdown() 停止接受新任务
  • shutdownNow() :等待线程池中所有任务完成,或超时等待
  • awaitTermination():强制关闭线程池

Java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ThreadPoolShutdownDemo {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池
        ExecutorService executor = Executors.newFixedThreadPool(5);

        // 提交一些任务给线程池
        for (int i = 0; i < 10; i++) {
            executor.submit(() -> {
                try {
                    Thread.sleep(1000); // 模拟任务执行
                    System.out.println("Task completed by thread: " + Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    System.err.println("Task interrupted: " + Thread.currentThread().getName());
                }
            });
        }

        // 关闭线程池
        executor.shutdown();

        try {
            // 等待所有任务完成,最多等待5秒钟
            if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
                // 如果超时,强制关闭线程池
                System.out.println("Some tasks were not terminated within 5 seconds. Forcing shutdown...");
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            // 处理中断异常
            System.err.println("Thread interrupted while waiting for tasks to complete.");
            executor.shutdownNow(); // 强制关闭线程池
            Thread.currentThread().interrupt();
        }
    }
}

19. 什么是ThreadLocal

  • ThreadLocal简单来说就是线程的全局变量,每个Thread都有自己的ThreadLocal,并且线程之间是隔离的
  • ThreadLocal适用于每个线程需要自己独立的全局变量,存储需要在方法间透传的常用信息

20. ThreadLocal源码解读

  • 每个线程都有自己的ThreadLocalMap,其中key是ThreadLocal实例,value是我们定义的值

Java
public class Thread implements Runnable {
    ……
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ……
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
    
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
    ……
}

  • set方法
  1. 获取当前线程
  2. 获取ThradLocalMap对象
  3. 将值存入Entry中,key为ThreadLocal对象,value为存入的值

Java
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

  • get方法
  1. 获取当前线程
  2. 获取ThradLocalMap对象
  3. 获取ThradLocalMap中的Entry

Java
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

21. ThreadLocal应用场景

  • 用户信息存在ThreadLocal,随处可用
  • 对于多数据源的项目,可以通过ThreadLocal实现数据源的动态切换
  • 应用日志打印可以获取到当前线程的TraceId,方便日志追溯,traceId一般由前台传入,用于追溯用户体验监控,也可用于APM应用性能监控
  • simpleDateFormatter线程不安全,如果要复用可以放在ThreadLocal

22. ThreadLocal设计与分析

Java中的4种引用类型

Java的四种引用的级别: 强引用 > 软引用 > 弱引用 > 虚引用

经验:平常开发中一些大的任务,大对象在用完之后可以通过赋值null弱化引用,帮助GC进行垃圾回收

  • 强引用(StrongReference)
    • 强引用是最常见的引用,比如我们创建一个对象就是强引用
    • 强引用对象不会被GC回收,当内存空间不足虚拟机会抛出OOM
  • 软引用
    • 如果对象只有软引用,空间足够时是不会回收它的,但如果空间不足就会回收
    • 只要对象没有被回收就可以被程序使用,软引用适用于实现内存敏感的高速缓存
  • 弱引用
  • 虚引用

4种引用的区别:

级别

回收时机

用途

生存时间

强引用

从不回收

对象的一般状态

JVM停止

软引用

内存不足

联合ReferenceQueue构造有效期短、占内存大、生命周期长的对象的二级高速缓冲器

内存不足

弱引用

垃圾回收时

联合ReferenceQueue构造有效期短、占内存大、生命周期长的对象的一级高速缓冲器(系统发生gc则清空)

gc后终止

虚引用

垃圾回收时

联合ReferenceQueue来跟踪对象倍垃圾回收器回收的活动

gc后终止

  • 20
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值