文章

java并发编程

线程

什么是线程和进程

  • 进程:是操作系统进行资源分配和调度的独立单位,有着独立的空间,每个进程有一个线程就是主线程
  • 线程:进程中的一个执行流,CPU调度的基本单位,线程共享进程资源

java线程和操作系统的线程有什么区别

Java线程和操作系统线程在JDK 1.2之前和之后有显著的区别:

  • JDK 1.2之前:Java线程是基于绿色线程(Green Threads)实现的,这是一种用户级线程。这意味着JVM自己模拟多线程运行,不直接依赖操作系统内核。绿色线程有限制,例如不能直接使用操作系统的异步I/O,只能在单个内核线程上运行,无法利用多核处理器的优势。
  • JDK 1.2及以后:Java线程是基于原生线程(Native Threads)实现的,即JVM直接使用操作系统的内核级线程。这样,Java线程由操作系统内核调度和管理,可以充分利用多核处理器的优势。

用户线程与内核线程的区别概括为:

  • 用户线程:在用户空间运行,由用户空间程序管理,创建和切换成本低,但不能直接利用多核处理器。
  • 内核线程:在内核空间运行,由操作系统内核管理,创建和切换成本高,可以利用多核处理器。

现代Java线程本质上是操作系统的线程。线程模型,即用户线程和内核线程的关联方式,主要有三种:

  1. 一对一模型:一个用户线程对应一个内核线程。
  2. 多对一模型:多个用户线程映射到一个内核线程。
  3. 多对多模型:多个用户线程映射到多个内核线程。

在Windows和Linux等主流操作系统中,Java线程通常采用一对一模型。而Solaris系统支持多对多模型。

速记技巧:绿线(绿色线程)用户轻,原生(原生线程)内核行。一对一直接,多对多灵活。

线程与进程的关系、区别以及各自的优缺点可以从以下几个方面进行简要描述:

关系:

  • 线程是进程的一部分:一个进程可以包含多个线程,线程是进程中的执行单元。
  • 资源共享:同一进程内的多个线程共享进程的资源,如内存空间(堆和方法区)。

区别:

  • 独立性:进程是系统资源分配的最小单位,具有较高的独立性;线程则共享进程资源,独立性较低。
  • 资源占用:进程间资源相互独立,线程间共享进程资源,因此线程的创建和切换开销小于进程。
  • 执行:进程为系统分配资源的单位,线程是程序执行的单位,线程依赖于进程存在。

优缺点:

  • 线程
    • 优点:创建和切换成本低,资源共享导致通信和数据共享容易实现。
    • 缺点:同一进程内线程间相互影响,一个线程的失败可能影响到整个进程的稳定性。
  • 进程
    • 优点:独立性强,一个进程的崩溃不会直接影响到其他进程。
    • 缺点:创建和切换成本高,进程间通信需要借助IPC(进程间通信)机制。

扩展内容

  • 程序计数器:私有的,用于记录线程执行的字节码指令位置,确保线程切换后能恢复正确位置。
  • 虚拟机栈和本地方法栈:私有的,用于存储方法调用的局部变量和状态,保证线程安全。
  • 堆和方法区:共享的,用于存放对象实例和类信息,支持线程间的数据共享。

速记技巧:进程独立资源多,线程共享成本低;线程私有计栈栈,进程共享堆方法。

谐音:进程独立资源多(jin cheng du li zi yuan duo),线程共享成本低(xian cheng gong xiang di cheng xiao);线程私有计栈栈(xian cheng si you ji zhan zhan),进程共享堆方法(jin cheng gong xiang dui fang fa)。

说说线程的生命周期和状态?

线程的生命周期和状态是Java并发编程中的重要概念,以下是对这些状态的简要描述:

  1. NEW(新建):线程对象创建后,但尚未调用 start()方法的初始状态。
  2. RUNNABLE(可运行):线程调用了 start()方法后,处于就绪状态,等待操作系统分配CPU时间片以便执行。
  3. BLOCKED(阻塞):线程因为请求一个被其他线程持有的锁而进入阻塞状态,直到锁被释放。
  4. WAITING(等待):线程执行 wait()方法后,进入等待状态,需要其他线程执行通知 notify()notifyAll()来唤醒。
  5. TIMED_WAITING(超时等待):线程调用了带有超时参数的 sleep(long millis)wait(long timeout)方法,进入超时等待状态,超时后自动返回到可运行状态。
  6. TERMINATED(终止):线程执行完 run()方法后,结束生命周期,进入终止状态。

速记技巧:新(NEW)可(RUNNABLE)等(BLOCKED)待(WAITING)睡(TIMED_WAITING)完终止(TERMINATED)。

谐音:新(xin)可(ke)等(deng)睡(shui)完终止(zhong zhi)。

重点:理解线程状态的转换,以及每个状态的含义和进入条件。JVM通常不区分操作系统层面的READY和RUNNING状态,而是统一为RUNNABLE状态,因为现代操作系统使用时间分片轮转调度,线程在这两个状态之间切换非常快,区分它们没有太大意义。

什么是线程上下文切换

线程上下文切换是操作系统中线程从一种状态转换到另一种状态的过程,包括保存当前线程状态和加载新线程状态两个步骤。核心要点是:线程状态保存与恢复,涉及资源占用。

速记技巧:"线程切换,状态保存,资源消耗",谐音:"线成切换,状态宝存,资援消耗"。

重要点:线程状态的保存与恢复,资源消耗。

精简答案:线程上下文切换是操作系统中线程状态的保存与恢复过程,涉及CPU和内存资源,频繁切换会降低效率。

Thread#sleep() 和 Object#wait()

共同点:Thread#sleep() 和 Object#wait() 都能暂停线程。

区别要点:

  • sleep() 保持锁,wait() 释放锁。
  • wait() 用于线程间通信,sleep() 用于暂停。
  • wait() 需通知唤醒,sleep() 可自动或超时唤醒。
  • sleep() 是静态方法,wait() 是实例方法。

速记技巧:"睡不释锁,等释锁通;睡静等动,通知唤醒。",谐音:"睡不释锁,等释锁通;睡静等动,通知换醒。"

精简答案:Thread#sleep() 和 Object#wait() 都暂停线程,但 sleep() 保持锁且自动或超时苏醒,而 wait() 释放锁且需通知唤醒,sleep() 是静态方法,wait() 是实例方法。

为什么 wait() 方法不定义在 Thread 中?

重点:wait() 方法是用于线程间协作的,它需要释放对象锁并让线程进入等待状态。而 sleep() 方法是让当前线程暂停,不涉及其他线程或对象。

速记技巧:"等(wait)别人,放(lock)手;睡(sleep)自己,不牵手。"

英文谐音

  • wait:外特
  • sleep:斯利普
  • Thread:斯利的
  • Object:欧布杰克特

可以直接调用 Thread 类的 run 方法吗?

重点:直接调用 run() 方法不会启动新线程,而 start() 方法会创建新线程并执行 run() 方法。

速记技巧:"启动(start)新线,普通(run)旧线。"

英文谐音

  • start:斯达特
  • run:润
  • Thread:斯利的

多线程

并发并行,同步异步

并发与并行的区别

  • 并发:同一时间段内多个任务交替执行,可能不是同时。
  • 并行:同一时刻多个任务同时执行。

速记技巧:"并发交替,时间共享;并行同框,时刻并行。"

同步与异步的区别

  • 同步:调用后必须等待结果,不返回。
  • 异步:调用后不用等结果,直接返回。

速记技巧:"同步等果,异步先行。"


为什么要使用多线程?

重点:多线程的使用是为了减少上下文切换开销,提高资源利用率,满足高并发需求,以及在多核CPU上实现任务的并行处理。

速记技巧:"轻量线程,多核并行,单核IO等,效率提升。"

英文谐音

  • Thread:斯利的(线程)
  • CPU:C-P-U(中央处理器)
  • IO:I-O(输入输出)

例子:假设有一个涉及大量计算和IO操作的程序,如果只用单线程,一旦遇到IO操作,整个程序就会暂停等待IO完成,CPU在此期间无法进行其他工作。使用多线程后,当一个线程等待IO时,其他线程可以继续执行计算任务,从而提高CPU的利用率。在多核CPU上,多线程可以同时在不同的核心上运行,实现真正的并行处理,大大加快程序的执行速度。

单核 CPU 支持 Java 多线程吗?

重点:单核CPU通过时间片轮转支持Java多线程,实现任务的并发执行。Java的线程调度是抢占式的,由操作系统负责。

速记技巧:"单核多线,时间片转;Java抢占,系统调度。"

英文谐音

  • CPU:C-P-U(中央处理器)
  • Java:扎瓦
  • Preemptive:普利姆普提夫(抢占式)
  • Scheduling:斯凯久灵(调度)
  • Cooperative:考欧普瑞提夫(协同式)
  • JVM:J-V-M(Java虚拟机)

例子:想象你在一个餐厅里,只有一个厨师(单核CPU),但有很多顾客(线程)点餐。厨师通过快速地在不同顾客之间切换来准备食物,虽然每次只能为一个顾客服务,但顾客感觉像是同时被服务的。Java的线程调度就像这样,操作系统就像厨师,通过时间片来决定哪个线程获得“服务”。

死锁是什么

重点:线程死锁是多个线程因互相等待对方持有的资源而无法继续执行的情况。死锁的产生需要满足四个条件:互斥、请求与保持、不剥夺、循环等待。

速记技巧:"互斥一资源,请求不放手,不夺已占有,循环等头尾。"

英文谐音

  • Deadlock:得得洛克(死锁)
  • Thread:斯利的(线程)
  • Resource:瑞索斯(资源)
  • Synchronized:顺克如奈特义贼得(同步)

例子:想象两组人分别在两个不同的门(资源)前,每组人都需要两个门的钥匙才能进入。如果一组人拿着钥匙1等待钥匙2,而另一组人拿着钥匙2等待钥匙1,那么两组人都无法进入,形成了死锁。Java中的线程如果按不同顺序获取同步锁,就可能发生类似的情况。

JMM

重点:Java内存模型(JMM)主要解决多线程中的原子性、可见性和有序性问题,定义了线程与主内存之间的交互规范,并通过happens-before原则来确保操作的内存可见性。

速记技巧:JMM是Java并发编程的规范,记住“三性”:原子性、可见性、有序性;用“三规则”:程序顺序、锁定解锁、volatile变量。

英文谐音

  • JMM:杰姆
  • Cache:卡池
  • volatile:我哦太布尔
  • synchronized:赛恩-克瑞-奈-泰-子德
  • Lock:洛克
  • happens-before:海-喷斯-比-佛
  • Thread:斯利的
  • Memory Barrier:美莫瑞-拜尔里尔
  • start:斯达特

代码示例

// 使用volatile关键字确保变量的可见性
volatile int sharedValue = 0;

void increment() {
    sharedValue++; // 写操作,对其他线程可见
}

int readValue() {
    return sharedValue; // 读操作,从主内存读取
}

精简答案

  • 原子性:确保操作不可分割,如 synchronizedLock
  • 可见性:确保一个线程对共享变量的修改对其他线程立即可见,如 volatile
  • 有序性:防止指令重排序影响多线程程序的正确性,volatile可禁止重排序。
  • happens-before:定义操作间内存可见性,遵循8条规则,如程序顺序、解锁后加锁等。
  • JMM:Java内存模型,简化并发编程,增强跨平台可移植性。

乐观和悲观锁

悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程

像 Java 中 synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。

  • 悲观锁通常多用于写比较多的情况(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。不过,如果乐观锁解决了频繁失败和重试这个问题的话(比如 LongAdder),也是可以考虑使用乐观锁的,要视实际情况而定。
  • 乐观锁通常多用于写比较少的情况(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量(参考 java.util.concurrent.atomic包下面的原子变量类)。

CAS 的全称是 Compare And Swap(比较与交换) ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。

举一个简单的例子:线程 A 要修改变量 i 的值为 6,i 原值为 1(V = 1,E=1,N=6,假设不存在 ABA 问题)。

  1. i 与 1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。
  2. i 与 1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。

ABA问题:如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。

ABA 问题的解决思路是在变量前面追加上版本号或者时间戳

synchronized 是什么?有什么用?

synchronized 是 Java 中的一个关键字,翻译成中文是同步的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

在 Java 早期版本中,synchronized 属于 重量级锁,效率低下。

不过,在 Java 6 之后, synchronized 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销

如何使用 synchronized?

synchronized 关键字的使用方式主要有下面 3 种:

  1. 修饰实例方法
  2. 修饰静态方法
  3. 修饰代码块

synchronized 底层原理了解吗?

重点synchronized 关键字在 Java 中用于实现线程同步,其底层原理基于 JVM 的监视器(Monitor)机制。

速记技巧"监视器锁,进出记牢"monitorenter 进,monitorexit 出。

英文谐音

  • synchronized:赛克森尼赛特
  • monitorenter:莫尼特 恩特
  • monitorexit:莫尼特 埃克斯它

底层原理

  1. Monitor 机制:基于进入和退出 Monitor 的操作来实现同步,Monitor 与对象关联。
  2. monitorenter 指令:每个线程在进入同步代码块时执行,尝试获取 Monitor。
  3. monitorexit 指令:每个线程在离开同步代码块时执行,释放 Monitor。
  4. 锁计数器:记录获取 Monitor 的次数,退出同步块时递减,为 0 时释放。
  5. ACC_SYNCHRONIZED 标识:标记方法是否同步,JVM 根据此标识进行同步调用。

没完呢

多线程

ThreadLocal有什么用

重点:ThreadLocal 用于线程隔离,每个线程拥有自己的变量副本,避免线程间的数据竞争。

速记技巧:一人一袋,各取所需,无争无抢。

英文谐音

  • ThreadLocal:斯利的咯嚷
  • get:给特
  • set:赛特

代码示例

ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
threadLocal.set(10); // 当前线程设置值
Integer value = threadLocal.get(); // 当前线程获取值

ThreadLocal原理

重点:ThreadLocal 原理基于每个线程的 Thread 对象中维护的 ThreadLocalMap,其中以 ThreadLocal 对象为键,线程局部变量为值。

速记技巧:一“线”牵,一“图”挂,各“线”各“图”,互不干扰。

英文谐音

  • ThreadLocal:斯利的咯嚷
  • ThreadLocalMap:斯利的咯嚷麦普
  • Thread:斯利的
  • currentThread:卡伦特斯利的
  • set:赛特
  • getMap:给特麦普

代码示例

public class ThreadLocalExample {
    public static void main(String[] args) {
        ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
      
        // 模拟线程操作
        Thread thread1 = new Thread(() -> {
            threadLocal.set(10); // 设置线程局部变量
            System.out.println("Thread1 value: " + threadLocal.get()); // 获取线程局部变量
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("Thread2 value: " + threadLocal.get()); // 获取线程局部变量,初始值
        });

        thread1.start();
        thread2.start();
    }
}

原理简述

  • 每个线程(Thread)对象内部有一个 threadLocals 字段,它是 ThreadLocalMap 类型的。
  • ThreadLocalMapThreadLocal 的静态内部类,它使用 ThreadLocal 实例作为键,线程局部变量作为值。
  • 当调用 ThreadLocalset() 方法时,实际上是在当前线程的 ThreadLocalMap 中设置键值对。
  • ThreadLocalMap 的键值对结构避免了多线程间的变量共享问题,实现了线程隔离。

ThreadLocal 内存泄露问题是怎么导致的?

重点:ThreadLocal 内存泄露问题是由于 ThreadLocalMap 中的 key 使用弱引用,而 value 使用强引用导致的。当 ThreadLocal 对象被垃圾回收后,如果没有及时清理 ThreadLocalMap 中的 entry,value 将无法被回收,从而造成内存泄露。

速记技巧:弱键强值,键失值存,及时清理,避免泄露。

英文谐音

  • ThreadLocal:斯利的咯嚷
  • ThreadLocalMap:斯利的咯嚷麦普
  • Entry:恩特瑞
  • WeakReference:威客瑞发论斯
  • value:瓦卢
  • remove:瑞莫夫

代码示例

public class ThreadLocalExample {
    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void setThreadLocalValue() {
        threadLocal.set(100);
    }

    public static void getThreadLocalValue() {
        System.out.println(threadLocal.get());
    }

    public static void clearThreadLocal() {
        threadLocal.remove();
    }

    public static void main(String[] args) {
        // 使用 ThreadLocal 后,应手动调用 remove 方法避免内存泄露
        setThreadLocalValue();
        getThreadLocalValue();
        clearThreadLocal();
    }
}

原理简述

  • ThreadLocalMapEntry 类使用 WeakReference 作为 key 的引用类型,这意味着 ThreadLocal 对象在没有外部强引用时可以被垃圾回收。
  • 由于 Entry 的 value 是强引用,如果 ThreadLocal 被回收,value 仍然存活,导致内存泄露。
  • ThreadLocalMapset()get()remove() 方法中会清除 key 为 null 的 entry,以避免内存泄露。
  • 使用 ThreadLocal 时,应确保在不再需要时调用 remove() 方法,手动清理 ThreadLocalMap 中的 entry。

线程池

为什么用管线程池

重点:使用线程池主要是为了减少资源消耗、提高响应速度和增强线程管理性。

速记技巧:池中养线,速应管好。

英文谐音

  • ThreadPool:特里普普尔
  • resource:瑞索斯
  • manageability:慢内及比里提
  • response:瑞斯朋斯

代码示例

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

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

        // 提交任务给线程池执行
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            executor.submit(() -> {
                System.out.println("Task " + finalI + " executed by " + Thread.currentThread().getName());
            });
        }

        // 关闭线程池,不再接受新任务
        executor.shutdown();
    }
}

好处简述

  • 降低资源消耗:线程池通过复用已创建的线程,减少了频繁创建和销毁线程的开销。
  • 提高响应速度:任务提交后可以立即在现有线程上执行,无需等待线程创建。
  • 提高线程的可管理性:线程池可以统一管理线程,进行调优和监控,避免线程过多导致的资源浪费和系统不稳定。

怎么创建

重点:创建线程池可以通过 ThreadPoolExecutor 的构造函数直接指定参数,或者使用 Executors 类提供的静态工厂方法。

速记技巧:手搭池(手搭特里普),定单快(定单快了),随心定(随心定了)。

英文谐音

  • ThreadPoolExecutor:特里普普尔 爱克西卡特
  • Executors:爱克西卡特斯
  • FixedThreadPool:费克斯特里普普尔
  • SingleThreadExecutor:辛格乐特里普普尔
  • CachedThreadPool:凯吃特里普普尔
  • ScheduledThreadPool:斯凯久乐德特里普普尔

代码示例

import java.util.concurrent.*;

// 方式一:通过 ThreadPoolExecutor 构造函数创建
ExecutorService threadPool1 = new ThreadPoolExecutor(
    5, // corePoolSize
    10, // maximumPoolSize
    60L, TimeUnit.SECONDS, // keepAliveTime
    new LinkedBlockingQueue<Runnable>(), // workQueue
    Executors.defaultThreadFactory(), // threadFactory
    new ThreadPoolExecutor.AbortPolicy() // handler
);

// 方式二:通过 Executors 工具类创建
ExecutorService threadPool2 = Executors.newFixedThreadPool(5); // 固定大小线程池
ExecutorService threadPool3 = Executors.newSingleThreadExecutor(); // 单线程池
ExecutorService threadPool4 = Executors.newCachedThreadPool(); // 可缓存线程池
ExecutorService threadPool5 = Executors.newScheduledThreadPool(5); // 定时及周期性任务线程池

创建方法简述

  • ThreadPoolExecutor:直接指定参数创建,包括核心线程数、最大线程数、线程存活时间、工作队列、线程工厂和拒绝策略。
  • Executors
    • newFixedThreadPool(int nThreads):创建固定大小的线程池。
    • newSingleThreadExecutor():创建单线程的线程池。
    • newCachedThreadPool():创建可根据需求动态调整的线程池。
    • newScheduledThreadPool(int corePoolSize):创建可执行定时任务的线程池。
License:  CC BY 4.0