Java 并发编程

Java 创建线程的方式

Java 创建线程主要有四种方式:继承 Thread 类实现 Runnable 接口使用Callable和Future,以及使用线程池

  1. 继承Thread类
    创建一个类继承 java.lang.Thread,重写 run() 方法定义线程执行任务,然后创建实例调用 start()
java 复制代码
public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程执行:" + Thread.currentThread().getName());
    }
    
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start();  // 启动线程
    }
}
  1. 实现 Runnable 接口
    实现 java.lang.Runnable 接口,实现 run() 方法,将该实例作为参数传入 Thread 对象,再调用 start()
java 复制代码
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程执行:" + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());
        t1.start();
    }
}
  1. 实现 Callable 接口 + FutureTask
    使用Callable和Future接口 :创建一个实现了Callable接口的类,并实现其call方法,该方法可以返回结果并抛出异常。使用ExecutorService来管理线程池,并提交Callable任务获取Future对象,以便在未来某个时刻获取Callable任务的计算结果
java 复制代码
public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        // 模拟耗时操作
        Thread.sleep(2000);
        return "任务结果";
    }

    public static void main(String[] args) throws Exception {
        // 方式1:使用 FutureTask
        FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
        Thread t1 = new Thread(futureTask);
        t1.start();
        String result = futureTask.get();  // 阻塞获取结果
        System.out.println(result);

        // 方式2:配合线程池(更常用)
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<String> future = executor.submit(new MyCallable());
        String result2 = future.get();
        executor.shutdown();
    }
}
  1. 通过线程池创建
    通过使用Executors类创建线程池,并通过线程池来管理线程的创建和复用。
java 复制代码
public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 常用静态工厂方法(不推荐在生产环境使用,易导致 OOM)
        ExecutorService fixedPool = Executors.newFixedThreadPool(5);
        ExecutorService cachedPool = Executors.newCachedThreadPool();
        ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3);
        ExecutorService singlePool = Executors.newSingleThreadExecutor();

        // 推荐:自定义 ThreadPoolExecutor,更可控
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2,                      // corePoolSize
                5,                      // maximumPoolSize
                60, TimeUnit.SECONDS,   // 空闲线程存活时间
                new LinkedBlockingQueue<>(100), // 工作队列
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );

        // 提交 Runnable 任务
        executor.execute(() -> System.out.println("Runnable 任务"));

        // 提交 Callable 任务
        Future<String> future = executor.submit(() -> {
            Thread.sleep(1000);
            return "Callable 结果";
        });

        executor.shutdown(); // 优雅关闭
    }
}

线程中 run() 和 start() 区别

run 方法是线程的执行体,包含线程要执行的代码 ,当直接调用 run 方法时,它会在当前线程的上下文中执行,而不会创建新的线程。

start 方法用于启动一个新的线程,并在新线程中执行 run 方法的代码

调用 start 方法会为线程分配系统资源,并将线程置于就绪状态,当调度器选择该线程时,会执行 run 方法中的代码。

关键点start() 只能被调用一次,重复调用会抛出 IllegalThreadStateException

一句话总结:start() 是让操作系统介入创建真正的线程,run() 只是一个普通方法调用。


Java 线程的六个状态:

  • NEW (新建):new Thread () 但没 start ()
  • RUNNABLE (就绪 + 运行,Java 合在一起):正在跑,或等着 CPU 调度。调用 start()
  • BLOCKED(阻塞,等锁):等着进 synchronized 同步块 / 方法
  • WAITING (无限等待):调用了 wait()join()LockSupport.park()
  • TIMED_WAITING (限时等待):sleep(xx)wait(xx)join(xx) 等带超时等待
  • TERMINATED(终止 / 结束):执行完 / 异常退出

线程执行时的一般状态:

  1. 就绪状态
    线程已经准备好,就等 CPU 分配时间片。
  2. 运行状态
    获得 CPU,正在执行代码。
  3. 阻塞状态
    主动 / 被动暂停,不参与 CPU 竞争,等条件满足。
  4. 终止状态
    执行完毕或异常退出,生命周期结束。

Java 中 有哪些锁

  1. 乐观锁 vs. 悲观锁:
    悲观锁认为多线程同时修改资源的概率比较高,所以访问资源时候要上锁,确保数据安全。
    乐观锁认为多线程同时修改资源的概率比较低,因此不加锁直接操作,乐观的认为,不加锁的并发操作是没有事情的。悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加 锁会带来大量的性能提升。
    典型代表悲观锁是 synchronizedReentrantLockCAS 是一种无锁编程技术,体现了乐观锁
  2. 公平锁 vs. 非公平锁:
    公平锁指多个线程按照申请锁的顺序来获取锁,
    非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优 先获取锁。有可能,会造成优先级反转或者饥饿现象。
    对于 Java ReentrantLock 而言,默认是非公平锁 , 也可以指定为公平锁,对于 Synchronized 而言,只支持非公平锁
  3. 可重入锁(递归锁):在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。(解决了线程在持有锁的情况下再次请求同一锁时的死锁问题
    对于 Java ReentrantLock 而言,是可重入锁,对于 Synchronized 而言,也是一个可重入锁。
  4. 排他锁 vs. 共享锁:排他锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。
    对于 Java ReentrantLockSynchronized 而言,其是独享锁。但是对于Lock的另一个实现类 ReadWriteLock,其读锁 是共享锁,其写锁是独享锁。
  5. 分段锁:这是一种设计思想(锁粒度的优化),将数据分段,每段独立加锁,用来提升并发度 。典型应用是 JDK 1.7 的 ConcurrentHashMap,它由 16 个 Segment 组成,每个 Segment 独立加锁。
  6. 自旋锁:让等待锁的线程通过循环重试来获取锁,而不是立即挂起,减少线程上下文切换的开销。在 JVM 内部,轻量级锁竞争时就会用到自旋。
  7. 偏向锁/轻量级锁/重量级锁:
    在 Java 1.6 之前,synchronized 是一个纯粹的 重量级锁 ,每次加锁、解锁都需要通过操作系统的互斥量(mutex)实现,涉及 用户态与内核态的切换,开销非常大。但在实际应用中,绝大多数锁的竞争并不激烈,甚至很多同步代码块只被一个线程反复访问。

因此,JVM 引入了 锁升级机制 ,根据竞争程度动态调整锁的状态,在低竞争或无竞争场景下用更轻量的方式处理,减少不必要的开销;在高竞争场景下才退化为重量级锁,保证线程安全

具体来说:

  • 偏向锁 :解决 单线程重复获取锁 的问题。当锁一直被同一个线程获取时,直接记录线程 ID,后续无需任何同步操作,几乎零开销。

  • 轻量级锁 :解决 两个线程交替或短暂竞争 的问题。此时通过 自旋(CAS) 尝试获取锁,避免线程阻塞和唤醒带来的上下文切换,适用于锁持有时间短、竞争不激烈的场景。

  • 重量级锁 :解决 高并发激烈竞争 的问题。当自旋次数过多或竞争线程数增加时,升级为重量级锁,让未获取锁的线程阻塞,由操作系统调度,避免 CPU 空转,保证系统整体吞吐量。

简单来说,这套机制的设计目标是:让锁在大部分场景下尽可能"轻",只在必要时才变"重",从而在吞吐量、响应时间和资源消耗之间达到最佳平衡。

CAS(Compare And Swap,比较并交换)是一种无锁并发操作 ,它通过一条原子性的 CPU 指令完成"比较并更新"的动作:如果内存中的当前值与预期值相等,则更新为新值;否则不做任何操作

在 Java 中,CAS 是 java.util.concurrent 包的基础,被广泛用于原子类、AQS 等组件中。它体现了乐观锁的思想------不加锁,而是通过尝试和重试来保证线程安全,避免了线程阻塞和上下文切换带来的开销。

简单来说:CAS = 原子化的"判断 + 赋值",失败则重试,让线程自己处理冲突,而非被动等待

synchronized 的理解

synchronized 是 Java 中解决并发问题最基础、最常用的手段 ,它可以保证原子性可见性有序性

当一个方法或代码块被 synchronized 修饰时,它将成为一个临界区,同一时刻只能由一个线程访问。其他线程必须等待当前线程退出临界区才能进入。确保多个线程在访问共享资源时不会产生冲突

synchronized 可以应用于方法或代码块。当它应用于方法时,整个方法被锁定;当它应用于代码块时,只有该代码块被锁定,这样做的好处是,可以选择性地锁定对象的一部分,而不是整个方法,锁的粒度更细。

在 Java 1.6 之前,synchronized 是一个纯粹的 重量级锁 ,每次加锁、解锁都需要通过操作系统的互斥量(mutex)实现,涉及 用户态与内核态的切换 ,开销非常大。

但在实际应用中,绝大多数锁的竞争并不激烈,甚至很多同步代码块只被一个线程反复访问。

因此,在Java 1.6, JVM 引入了 锁升级机制 ,根据竞争程度动态调整锁的状态,在低竞争或无竞争场景下用更轻量的方式处理,减少不必要的开销;在高竞争场景下才退化为重量级锁,保证线程安全

具体来说:

  • 偏向锁 :解决 单线程重复获取锁 的问题。当锁一直被同一个线程获取时,JVM 直接记录线程 ID,后续无需任何同步操作(加锁),几乎零开销。

  • 轻量级锁 :解决 两个线程交替或短暂竞争 的问题。此时通过 自旋(CAS) 尝试获取锁,避免线程阻塞和唤醒带来的上下文切换,适用于锁持有时间短、竞争不激烈的场景。

  • 重量级锁 :解决 高并发激烈竞争 的问题。当自旋次数过多或竞争线程数增加时,升级为重量级锁,让未获取锁的线程阻塞,由操作系统调度,避免 CPU 空转,保证系统整体吞吐量。

当线程通过 synchronized 等待锁时是不能被 Thread.interrupt() 中断的,因此程序设计时必须检查确保合理,否则可能会造成线程死锁的尴尬境地。

最后,尽管 Java 实现的锁机制有很多种,并且有些锁机制性能也比 synchronized 高,但还是强烈推荐在 多线程应用程序中使用该关键字,因为实现方便,后续工作由 JVM 来完成,可靠性高。只有在确定锁机 制是当前多线程程序的性能瓶颈时,才考虑使用其他机制,如 ReentrantLock 等。

原子性 :被 synchronized 修饰的代码块或方法,同一时刻只能有一个线程执行,相当于一个不可分割的操作。防止了中间状态被其他线程看到 ,从而避免数据不一致。

可见性 :线程解锁前,必须把共享变量的最新值刷新到主内存;线程加锁时,会清空工作内存中该变量的值,从而强制从主内存重新读取。这一机制保证了前一个线程的写结果对后一个线程立即可见。保证了跨线程的写结果能及时传播 ,让程序行为符合预期。

有序性 :虽然编译器和处理器可能会对指令进行重排序,但 synchronized 修饰的代码块内部,在同一时刻只允许一个线程执行,所以不会出现指令重排导致的其他线程看到中间状态的问题。避免因重排序导致的意想不到的结果

synchronized 和 lock 的区别是什么?

从功能来看:

lock和synchronized都是java中解决线程安全问题的一个工具

从使用来看:

  • synchronized 是 Java 内置的关键字,基于 JVM 层面的内置锁实现。锁的获取与释放由 JVM 自动管理;,无需开发者手动干预。

  • Lock 是 juc (java.util.concurrent.locks) 包下的接口 ,典型的实现如 ReentrantLock。Lock比Synchronized的灵活性更高,Lock可以自主决定什么时候加锁,什么时候释放锁,锁的获取和释放需要开发者在代码中显式调用 lock()unlock() 方法。

从锁粒度来看:

synchronized可以通过两种方式控制锁的粒度:

1.把synchronized关键字修饰在方法层面

2.修饰在同步代码块上

并且我们可以通过synchronized加锁对象的生命周期,来控制锁的作用范围,比如锁对象是静态对象,或者类对象,那么这个锁就是属于全局锁,如果锁对象是实例对象,那么这个锁的范围取决于这个对象的生命周期。

lock 中锁的粒度是通过它里面提供的 lock() 方法 和 unlock() 方法来决定的,包裹在这两个方法之间的代码能够保证线程安全性。而锁的作用域取决于Lock实例的生命周期。

功能方面

竞争锁:

Lock还提供了非阻塞的竞争锁方法 tryLock()方法,这个方法通过返回true/false来告诉当前线程是否已经有其他线程正在使用锁。

Synchronized由于是关键字,所以它无法实现非阻塞竞争锁的方法,synchronized拿不到锁就一直阻塞等待,另外Synchronized锁的释放是被动的,就是当Synchronized同步代码块执行完以后或者代码出现异常时才会释放。

公平锁和非公平锁:

Lock提供了公平锁和非公平锁的机制,公平锁是指线程竞争锁资源时,如果已经有其他线程正在排队等待锁释放,那么当前竞争锁资源的线程无法插队。而非公平锁,就是不管是否有线程在排队等待锁,它都会尝试去竞争一次锁。 Synchronized只提供了一种非公平锁的实现。

性能方面

synchronized在性能方面和lock相差不大,在实现上会有一个区别synchronized引入了偏向锁,轻量级锁,重量级锁,以及锁升级的机制去实现锁的优化,而lock则用到了自旋锁的方式实现性能优化。

使用场景

总结来说,synchronized使用简单,适合锁的粒度较小、竞争不激烈、实现简单的场景。而Lock提供了更多的灵活性和控制能力,适用于需要更复杂同步控制的场景。

synchronized 和 ReentranLock的区别是什么?

核心区别:

  • synchronized 是Java 内置的关键字,锁的获取与释放由 JVM 自动管理;
  • ReentrantLock 是 juc 包(java.util.concurrent.locks)包中Lock 接口的一个实现类,需要显式创建,ReentrantLocksynchronized的灵活性更高,并通过调用lock()unlock()方法来管理锁的获取和释放。(因为是一个类,所以提供的功能更多)

底层实现区别:

  • 实现原理是不一样,ReentrantLock 基于 AQS 实现的,synchronized 是基于 ObjectMonitor

效率区别:

  • synchronized 在 JVM 层面引入了锁升级(偏向锁 → 轻量级锁 → 重量级锁),一旦升级为重量级锁后不会降级。低竞争或无竞争场景下,偏向锁和轻量级锁的开销很小,性能往往优于 ReentrantLockReentrantLock 没有锁升级机制, 在高竞争场景下,ReentrantLock 的调度策略更加灵活(功能更多),性能可能略优于 synchronized 的重量级锁

功能向的区别:

  • ReentrantLock 的功能比 synchronized 更全面。
    • ReentrantLock 支持公平锁和非公平锁(默认非公平锁),synchronized 只支持非公平锁
    • ReentrantLock 提供了非阻塞的竞争锁方法 tryLock() 方法,尝试获取锁并立即返回 true/false,不会阻塞线程,也可以指定时间内尝试拿锁;而 synchronized 若获取锁失败则只能阻塞等待。

使用场景:

总结来说,synchronized适合简单的同步需求,而ReentrantLock提供了更丰富的控制能力和灵活性,适用于需要复杂同步控制的场景。

ReentrantLock 提供了更灵活的锁获取方式:

tryLock() :非阻塞获取锁,尝试后立即返回 true(获取成功)或 false(获取失败),线程不会阻塞等待。

tryLock(long time, TimeUnit unit) :支持超时等待,在指定时间内尝试获取锁,若超时仍未获取则返回 false,同时该方法支持响应中断(需处理 InterruptedException)。

synchronized 在获取锁失败时,线程会一直阻塞,直到锁被释放或被中断(但 synchronized 等待锁时无法响应中断)。
synchronized :基于 ObjectMonitor ,是 JVM 内部机制。锁状态存储在对象头,通过 monitorenter/monitorexit 字节码实现,自动释放锁,支持锁升级(偏向→轻量→重量)。

ReentrantLock :基于 AbstractQueuedSynchronizer (AQS) 抽象类 ,是 JDK 提供的 Java 层框架。通过 volatile int state + CLH 队列 管理锁竞争,显式 lock()/unlock(),支持非阻塞 tryLock()、可中断、公平/非公平等灵活策略。

本质 :一个是 JVM 内置 (关键字),一个是 API 实现(类)。

volatile 关键字的作用有哪些?

volatile 和 synchronized 对比

线程池

BIO、NIO、AIO

BIO、AIO和NIO是Java中不同的I/O模型,它们在处理输入输出操作时有不同的特点。

  • BIO:阻塞式的I/O模型。当一个线程执行I/O操作时,如果数据还没准备好,这个线程会被阻塞,直到数据到达。适合连接数较少且固定的场景,但扩展性较差。

  • NIO:非阻塞的I/O模型。NIO使用缓冲区和通道来处理数据,提高了I/O操作的效率。支持面向缓冲区的读写操作,可以处理大量并发的连接。

  • AIO:异步I/O模型,从Java 7开始引入。在AIO中,I/O操作被发起后,线程可以继续执行其他任务,一旦I/O操作完成,操作系统会通知线程。适合需要处理大量并发I/O操作,且希望避免I/O操作阻塞线程的场景。

    • 异步:IO操作会在后台线程完成,主线程不需要等待。
    • 非阻塞:主线程不会被IO操作阻塞,可以自由执行其他任务。
    • 回调机制:AIO通过回调函数来处理IO操作完成后的事件。
  • 使用场景

    • BIO 适合低并发、连接数较少的应用。
    • NIO 适合高并发、需要处理大量连接的应用。
    • AIO 适合需要高性能、异步处理I/O操作的场景。
相关推荐
long3162 小时前
Java 新手入门与实战开发指南
java·开发语言
执笔画流年呀2 小时前
Linux搭建Java项目部署环境
java·linux·运维
程序员天天困3 小时前
Arthas ognl 表达式从入门到实战:掌握在线调试最强的表达式引擎
java·jvm·后端
IT_Octopus3 小时前
Spring Boot 中 ThreadLocal 请求上下文的完整生命周期
java·spring boot·spring
用户40966601317514 小时前
Lombok 你用对了吗?@Data 之外的 6 个隐藏神器
java·后端·代码规范
梅头脑4 小时前
交易跨10个库、日均千万订单——选错一次分布式事务方案,加班三个月重写
java·分布式
久久学姐4 小时前
基础转码学 AI:Java+Python 双语言入门,3 个月可落地实战项目
java·python·ai·转码·实战项目
花生了什么事o4 小时前
synchronized 与 ReentrantLock:Java 锁机制原理与实现对比
java·开发语言
长不胖的路人甲5 小时前
Serial 串行、Parallel 并行、CMS 并发收集器
java·开发语言·jvm