java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java多线程与并发

Java多线程与并发完整代码详解

作者:匿名侠士

Java并发编程和多线程是高级开发中的一个重要部分,涉及多线程的创建、线程协同、线程安全、性能优化、并发工具等内容,这篇文章主要介绍了Java多线程与并发的相关资料,需要的朋友可以参考下

一、多线程基础:优缺点与核心代价

1. 核心优点

2. 主要代价(易忽略细节)

二、线程创建与启动(关键细节补全)

1. 两种标准实现方式

方式1:继承Thread类

class MyThread extends Thread {
    @Override
    public void run() { 
        // 线程执行的业务逻辑(线程体)
    }
}
// 启动线程:调用start(),而非run()
new MyThread().start();

方式2:实现Runnable接口(推荐)

class MyTask implements Runnable {
    @Override
    public void run() {
        // 线程执行的业务逻辑(解耦:任务与线程分离)
    }
}
// 启动线程:将任务传入Thread实例
new Thread(new MyTask()).start();

2. 选型建议(笔记补充)

优先选择实现Runnable接口,原因如下:

3. 经典错误(必记)

调用run()方法而非start()

直接调用run()只是普通方法调用(运行在当前线程),不会启动新线程;start()才会触发JVM创建新线程,执行run()方法。

三、线程安全核心:竞态条件与临界区

1. 核心概念

2. 本质原因

共享资源的“读-改-写”复合操作非原子性(如count++,实际分为3步:读count值→自增→写回count,多线程交错执行会导致数据错乱)。

四、线程安全判定:线程控制逃逸规则(重点)

核心判定原则:若一个资源(对象、文件、数据库连接等)的创建、使用、销毁,全程在同一个线程内完成,且不会逃逸到线程外部(即其他线程无法访问该资源),则该资源的使用是线程安全的。

1. 天生线程安全的场景

2. 天生线程不安全的场景

对象成员变量(存储在堆内存,多线程可通过对象引用访问并修改,若未加同步,必存在线程安全问题)。

五、不可变性与线程安全(补全细节)

六、synchronized同步机制(核心,笔记细节补全)

synchronized是Java原生的悲观锁,通过“互斥”保证临界区原子性,同时保证可见性(解锁前的修改对后续加锁线程可见)和可重入性。

1. 四种作用范围(附实例与锁对象)

作用范围锁对象实例代码说明
实例方法同步当前对象(this)public synchronized void add(int value) { … }同一对象的多个同步实例方法,同一时刻仅一个线程可执行
静态方法同步当前类的Class对象(全局唯一)public static synchronized void add(int value) { … }同一类的所有静态同步方法,同一时刻仅一个线程可执行
实例方法中同步块自定义对象(常用this)synchronized(this) { this.count += value; }缩小同步范围,仅保护临界区,提升性能
静态方法中同步块当前类的Class对象synchronized(MyClass.class) { … }与静态同步方法锁对象一致,可灵活控制同步范围

2. 核心特性(必记)

七、线程间通信(重点:wait/notify/notifyAll)

线程间通信的核心是“协作”(如生产者-消费者模型),常用方式:共享对象通信、忙等待、wait/notify/notifyAll(推荐)。

1. 共享对象通信(基础)

通过共享对象的成员变量传递信号(需配合同步,避免竞态条件)。例如:线程A在同步块中设置hasDataToProcess = true,线程B在同步块中读取该变量。

2. 忙等待(不推荐)

线程B循环等待信号,浪费CPU资源(空闲时也占用CPU):

while(!sharedSignal.hasDataToProcess()){
    // 空循环,忙等待,浪费CPU
}

3. wait()、notify()、notifyAll()(核心,补全细节)

强制规则(违反抛IllegalMonitorStateException)

必须在synchronized同步块/方法中调用,且调用对象必须是“当前持有锁的对象”(即同步块的锁对象)。

核心机制(易混淆点)

关键疑问解答(笔记补充)

问:等待线程持有锁,会阻塞唤醒线程进入同步块吗?

答:不会。线程调用wait()后,会立即释放锁,允许其他线程(包括唤醒线程)获取锁并进入同步块;唤醒线程执行完同步块、释放锁后,被唤醒的线程才会竞争锁,成功后退出wait()。

4. 常见问题与解决方案(必记)

问题原因解决方案
丢失信号notify()先于wait()执行,信号未保存,等待线程错过唤醒用成员变量保存信号(如boolean wasSignalled),唤醒时置为true,等待时检查该变量
假唤醒线程被唤醒但未收到有效信号(JVM底层机制)用while循环检查信号(而非if),即“自旋锁”:while(!wasSignalled) { wait(); }
意外唤醒用常量字符串、全局对象作为锁,JVM会复用该对象,导致跨实例唤醒使用唯一锁对象(如new Object()),避免使用""、Class对象等全局共享对象

正确实现示例(避免所有问题)

public class MyWaitNotify {
    private final Object monitor = new Object(); // 唯一锁对象
    private boolean wasSignalled = false; // 保存信号,避免丢失

    // 等待信号
    public void doWait() throws InterruptedException {
        synchronized (monitor) {
            while (!wasSignalled) { // while循环,防止假唤醒
                monitor.wait();
            }
            wasSignalled = false; // 清除信号,准备下次等待
        }
    }

    // 发送信号
    public void doNotify() {
        synchronized (monitor) {
            wasSignalled = true; // 保存信号
            monitor.notify(); // 唤醒等待线程
        }
    }
}

八、死锁(核心:原因与避免)

1. 定义

两个或多个线程互相持有对方需要的锁,且永久阻塞,无法继续执行(线程“互相僵持”)。

2. 典型场景(必记)

3. 死锁产生的4个必要条件(缺一不可)

4. 避免死锁的3种核心策略(重点)

策略1:固定加锁顺序(最常用、最易实现)

所有线程按相同顺序获取锁(如先锁A、再锁B),打破“循环等待”条件。

策略2:加锁时限

尝试获取锁时设置超时时间(如用Lock.tryLock(long timeout, TimeUnit unit)),超时则释放已持有锁、回退,等待随机时间后重试,打破“持有并等待”条件。

策略3:死锁检测

适用于无法固定加锁顺序、超时不可行的场景:

九、饥饿与公平性

1. 核心概念

2. Java中导致饥饿的3个原因

3. 实现公平性的方案

用Lock锁替代synchronized(synchronized默认非公平),自定义公平锁或使用JUC的公平锁(如ReentrantLock(true)):

十、Java中的锁(进阶,补全笔记细节)

1. 简单锁的实现(理解原理)

核心是用“状态变量+同步”控制线程访问,示例(不可重入锁):

public class Lock {
    private boolean isLocked = false; // 锁状态

    // 获取锁(阻塞)
    public synchronized void lock() throws InterruptedException {
        while (isLocked) { // 自旋,防止假唤醒
            wait();
        }
        isLocked = true; // 标记为锁定状态
    }

    // 释放锁
    public synchronized void unlock() {
        isLocked = false;
        notify(); // 唤醒等待线程
    }
}

2. 锁的可重入性(重点)

定义

同一线程可重复获取已持有的锁(synchronized、ReentrantLock均支持可重入),示例:

public class ReentrantDemo {
    // 两个同步方法,锁对象都是this
    public synchronized void outer() { inner(); }
    public synchronized void inner() { /* 业务逻辑 */ }
}

可重入锁的实现要点

需记录“持有锁的线程”和“重入次数”,修改后的可重入锁示例:

public class ReentrantLock {
    private boolean isLocked = false;
    private Thread lockedBy = null; // 持有锁的线程
    private int lockedCount = 0; // 重入次数

    public synchronized void lock() throws InterruptedException {
        Thread currentThread = Thread.currentThread();
        // 若锁被占用,且不是当前线程持有,阻塞
        while (isLocked && lockedBy != currentThread) {
            wait();
        }
        isLocked = true;
        lockedCount++;
        lockedBy = currentThread;
    }

    public synchronized void unlock() {
        // 仅持有锁的线程可释放
        if (Thread.currentThread() != lockedBy) {
            throw new IllegalMonitorStateException("未持有锁,无法释放");
        }
        lockedCount--;
        // 重入次数为0时,才释放锁
        if (lockedCount == 0) {
            isLocked = false;
            lockedBy = null;
            notify();
        }
    }
}

3. 关键注意点:finally中调用unlock()

用Lock锁时,临界区可能抛出异常,需在finally中释放锁,避免锁泄露(锁永久被占用):

lock.lock();
try {
    // 临界区(可能抛出异常)
} finally {
    lock.unlock(); // 确保无论是否异常,都释放锁
}

十一、读写锁(ReentrantReadWriteLock)

1. 核心场景

适用于“读多写少”的场景(如缓存查询、配置读取),解决“读-读互斥”的性能问题,核心原则:

读-读共存、读-写互斥、写-写互斥。

2. 简单实现(理解原理)

public class ReadWriteLock {
    private int readers = 0; // 读线程数量
    private int writers = 0; // 写线程数量
    private int writeRequests = 0; // 写请求数量(优先写)

    // 获取读锁
    public synchronized void lockRead() throws InterruptedException {
        // 有写线程或写请求,阻塞(优先写)
        while (writers > 0 || writeRequests > 0) {
            wait();
        }
        readers++;
    }

    // 释放读锁
    public synchronized void unlockRead() {
        readers--;
        notifyAll(); // 唤醒等待的写线程
    }

    // 获取写锁
    public synchronized void lockWrite() throws InterruptedException {
        writeRequests++;
        // 有读线程或写线程,阻塞
        while (readers > 0 || writers > 0) {
            wait();
        }
        writeRequests--;
        writers++;
    }

    // 释放写锁
    public synchronized void unlockWrite() {
        writers--;
        notifyAll(); // 唤醒所有等待的读/写线程
    }
}

3. 读写锁的可重入性(补全笔记)

上述简单实现不可重入,会导致死锁(如持有写锁的线程再次请求写锁、持有读锁的线程再次请求读锁),需优化:

十二、信号量(Semaphore)

1. 核心作用

控制并发线程数量(如限流、连接池控制),可看作“可计数的锁”,JUC中已提供java.util.concurrent.Semaphore,无需自定义。

2. 核心特性

3. 常用示例(限流)

// 上限3个线程同时执行
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 10; i++) {
    new Thread(() -> {
        try {
            semaphore.acquire(); // 获取许可(阻塞,直到有空闲许可)
            // 临界区(如接口调用、资源操作)
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release(); // 释放许可
        }
    }).start();
}

十三、阻塞队列(BlockingQueue)

1. 核心区别(与普通队列)

当队列空时,获取元素(take())会阻塞;当队列满时,添加元素(put())会阻塞,无需手动处理线程同步(内部已实现)。

2. 核心作用

实现生产者-消费者模型(解耦生产者和消费者,平衡两者速度),线程池底层核心组件(缓存任务)。

3. 简单实现(理解原理)

public class BlockingQueue {
    private final List<Object> queue = new LinkedList<>();
    private final int limit; // 队列最大容量

    public BlockingQueue(int limit) {
        this.limit = limit;
    }

    // 入队(满则阻塞)
    public synchronized void enqueue(Object item) throws InterruptedException {
        while (queue.size() == limit) {
            wait(); // 队列满,阻塞生产者
        }
        if (queue.size() == 0) {
            notifyAll(); // 队列空,唤醒消费者
        }
        queue.add(item);
    }

    // 出队(空则阻塞)
    public synchronized Object dequeue() throws InterruptedException {
        while (queue.size() == 0) {
            wait(); // 队列空,阻塞消费者
        }
        if (queue.size() == limit) {
            notifyAll(); // 队列满,唤醒生产者
        }
        return queue.remove(0);
    }
}

4. JUC中的阻塞队列(常用)

如ArrayBlockingQueue(有界)、LinkedBlockingQueue(无界/有界)、SynchronousQueue(无缓冲),直接使用即可,无需自定义。

十四、线程池(核心,补全笔记细节)

1. 核心价值

2. 核心结构

3. 简单实现(理解原理)

// 线程池核心类
public class ThreadPool {
    private final BlockingQueue<Runnable> taskQueue; // 任务队列
    private final List<PoolThread> threads; // 工作线程集合
    private boolean isStopped = false; // 线程池状态

    // 构造方法:指定线程数、队列最大容量
    public ThreadPool(int threadCount, int maxTaskCount) {
        taskQueue = new BlockingQueue<>(maxTaskCount);
        threads = new ArrayList<>(threadCount);
        // 初始化工作线程
        for (int i = 0; i < threadCount; i++) {
            threads.add(new PoolThread(taskQueue));
        }
        // 启动所有工作线程
        for (PoolThread thread : threads) {
            thread.start();
        }
    }

    // 提交任务
    public synchronized void execute(Runnable task) {
        if (isStopped) {
            throw new IllegalStateException("线程池已停止");
        }
        taskQueue.enqueue(task); // 任务入队
    }

    // 停止线程池
    public synchronized void stop() {
        isStopped = true;
        // 停止所有工作线程
        for (PoolThread thread : threads) {
            thread.toStop();
        }
    }
}

// 工作线程类
class PoolThread extends Thread {
    private final BlockingQueue<Runnable> taskQueue;
    private boolean isStopped = false;

    public PoolThread(BlockingQueue<Runnable> queue) {
        this.taskQueue = queue;
    }

    @Override
    public void run() {
        // 循环获取任务,直到线程池停止
        while (!isStopped()) {
            try {
                // 从队列获取任务(空则阻塞)
                Runnable task = taskQueue.dequeue();
                task.run(); // 执行任务
            } catch (Exception e) {
                // 捕获异常,避免线程退出(线程池继续运行)
                e.printStackTrace();
            }
        }
    }

    // 停止当前工作线程
    public synchronized void toStop() {
        isStopped = true;
        this.interrupt(); // 打断阻塞在dequeue()的线程
    }

    public synchronized boolean isStopped() {
        return isStopped;
    }
}

4. JUC中的线程池(重点)

使用java.util.concurrent.ExecutorService,推荐手动创建ThreadPoolExecutor(避免Executors工具类的OOM风险),核心参数:核心线程数、最大线程数、空闲线程存活时间、任务队列、拒绝策略。

十五、CAS与原子类(无锁编程核心)

1. CAS核心概念

CAS(Compare and Swap,比较并替换):一种无锁原子操作,底层由CPU指令(如cmpxchg)保证原子性,核心逻辑:

2. 核心优点

无锁、无线程上下文切换开销、无死锁风险,性能优于synchronized(高并发、低冲突场景)。

3. 核心问题与解决方案

问题原因解决方案
ABA问题V的值从A→B→A,CAS认为未修改,导致错误替换用AtomicStampedReference(加版本号)
自旋消耗CPU高并发下,CAS多次失败,循环重试消耗CPU限制自旋次数,或搭配锁使用
只能保证单个变量原子性CAS仅能操作单个变量,无法保证多个变量的复合操作原子性用AtomicReference包装多个变量,或使用锁

4. JUC原子类(常用)

十六、同步器核心思想(查漏补缺)

锁、信号量、阻塞队列等同步器,底层设计逻辑一致,均包含4个核心部分:

同步器的两种核心方法

十七、阻塞算法与非阻塞算法(补全笔记)

1. 核心区别

类型核心逻辑优点缺点示例
阻塞算法获取不到资源时,线程挂起,直到资源可用实现简单,无CPU自旋消耗线程切换开销大,可能死锁synchronized、Lock
非阻塞算法获取不到资源时,不挂起,直接返回或重试无线程切换开销,无死锁实现复杂,高冲突下自旋消耗CPUCAS、原子类

2. 乐观锁(非阻塞算法核心)

核心思想:乐观假设“无并发冲突”,线程先拷贝共享资源、修改,再通过CAS将修改写回主内存,冲突则重试。

十八、高频面试重点(浓缩必记)

  1. 线程启动必须用start(),调用run()仅为普通方法调用,不启动新线程。

  2. 线程安全三要素:原子性(CAS、锁)、可见性(synchronized、volatile)、有序性(synchronized、volatile)。

  3. synchronized与Lock的区别:Lock可中断、可超时、可公平/非公平,synchronized自动释放锁、可重入、简单易用。

  4. 死锁产生的4个条件及避免方法(固定加锁顺序最常用)。

  5. CAS的原理、优点、ABA问题及解决方案。

  6. 线程池的核心价值、结构,手动创建ThreadPoolExecutor的原因(避免Executors的OOM)。

  7. 读写锁的核心原则(读-读共存、读-写/写-写互斥),适用场景(读多写少)。

  8. wait()与sleep()的区别:wait()释放锁、需在同步块中,sleep()不释放锁、可在任意位置。

总结 

到此这篇关于Java多线程与并发的文章就介绍到这了,更多相关Java多线程与并发内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文