Java多线程,ThreadLocal,锁,Synchronized的理解

目录

多线程

1.进程与线程

2.并发与并行

3.线程的使用创建

4.线程的生命周期

5.常用方法

ThreadLocal 

1.常用方法

2.使用步骤(以苍穹外卖为列)

3.ThreadLocal与Synchronized的区别

Synchronized

1.使用方式

1.死锁

(1)产生死锁的 4 个必要条件

(2)代码示例

2.乐观锁与悲观锁


多线程

1.进程与线程

想象一下,开了一家餐厅(服务器)

进程(Process)=整个餐厅,如果餐厅倒闭了=进程结束了,里面的东西都没了

线程(Thread)=厨师。单线程就是一个厨师干店里面活,多线程就是多个厨师干店里面的活

2.并发与并行

并发:同一时间内,A先发生,B后发生(多个任务在同一个内核上)

并行:同意时间内,AB同时发生(多个任务在多个内核上)

3.线程的使用创建

(1)继承 Thread 类 (不推荐,因为受限于 Java 的单继承特性(继承了 Thread 就不能继承其他业务类了)

// 1. 定义一个类继承 Thread
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("子线程运行中... 当前线程: " + Thread.currentThread().getName());
        // 写你的业务逻辑
    }
}
// 2. 创建对象并启动
public class Demo1 {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start(); //  必须调用 start(),不能直接调用 run()
        // 如果调用 t.run(),那就只是普通方法调用,还是在主线程执行!
        
        System.out.println("主线程继续运行...");
    }
}

(2)实现 Runnable 接口 (推荐,将“任务逻辑”和“线程控制”分离,避免了单继承的限制)

// 1. 定义任务类实现 Runnable
class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("任务执行中... 当前线程: " + Thread.currentThread().getName());
    }
}

public class Demo2 {
    public static void main(String[] args) {
        MyTask task = new MyTask();
        // 2. 把任务交给 Thread 对象
        Thread t = new Thread(task); 
        t.start();
        
        // Lambda 简化写法 (Java 8+)
        new Thread(() -> System.out.println("Lambda 方式运行")).start();
    }
}

(3)实现 Callable 接口 + FutureTask (需要返回值时用),如果你需要线程执行完后返回结果,或者需要抛出检查型异常,用这个

import java.util.concurrent.*;

public class Demo3 {
    public static void main(String[] args) throws Exception {
        // 1. 定义任务,可以有返回值 (Integer)
        Callable<Integer> task = () -> {
            System.out.println("计算中...");
            Thread.sleep(2000); // 模拟耗时
            return 100 + 200; // 返回结果
        };

        // 2. 包装成 FutureTask
        FutureTask<Integer> futureTask = new FutureTask<>(task);
        Thread t = new Thread(futureTask);
        t.start();

        System.out.println("主线程在做别的事...");

        // 3. 获取结果 (会阻塞,直到子线程算完)
        // 如果子线程没跑完,这里会一直等;跑完了直接拿结果
        Integer result = futureTask.get(); 
        System.out.println("子线程返回结果: " + result);
    }
}

(4)使用线程池 

4.线程的生命周期

NEW

新建
RUNNABLE可运行(正在运行或等待CPU)
BLOCKED阻塞,等待锁
WAITING等待(无超时,等待其他线程
TIMED_WAITING超时等待(如 sleep、wait(long))
TERMINATED终止

5.常用方法

方法名作用
start()启动线程。JVM 会分配资源并调用 run() 方法。
run()线程的具体执行逻辑(任务代码)。
join()等待线程终止。调用 t.join() 的线程会阻塞,直到 t 执行完。
join(long ms)等待线程终止,但最多等指定毫秒数。
interrupt()中断线程。给线程设置一个“中断标志位”。
isInterrupted()检查线程是否被中断(返回 boolean)。
Thread.interrupted()(静态方法) 检查当前线程是否中断,并清除标志位。
setDaemon(true)设置为守护线程

ThreadLocal 

定义:让每个线程拥有自己独立的变量副本,互不干扰,用于线程隔离

1.常用方法

方法签名作用返回值/行为
set(T value)void set(T value)存值。将值绑定到当前线程无返回值。后续该线程调用 get() 将返回此值。
get()T get()取值。获取当前线程绑定的值。如果从未 set 过,默认返回 null(除非重写了 initialValue)。
remove()void remove()删值。移除当前线程绑定的值。极其重要:防止内存泄漏,必须在 finally 块中调用。
initialValue()protected T initialValue()初始值。当第一次 get() 且未 set 时调用。通常通过匿名内部类或 Lambda 重写,提供默认值(如 new ArrayList())。

2.使用步骤(以苍穹外卖为列)

(1)定义 ThreadLocal 变量

(2)设置值 (Set)。在请求进入时(如拦截器、Filter、AOP)或任务开始时,将数据存入。

(3)获取值 (Get)。在业务逻辑的任何地方(Service, Controller, Mapper),只要是在同一个线程内,都可以直接取,不需要层层传递参数。

(4)清理值 (Remove) 这是最关键的一步! 必须在请求结束或任务完成后清理,否则会导致内存泄漏。通常在 finally 块中执行。

package sky.context;

public class BaseContext {
    /**
     * 基于ThreadLocal封装工具类,用于保存和获取当前登录用户ID
     */
    //定义 ThreadLocal 变量
    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
    //设置当前线程的ID
    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }
    //获取当前线程的ID
    public static Long getCurrentId() {
        return threadLocal.get();
    }//移除当前线程的ID
    public static void removeCurrentId() {
        threadLocal.remove();
    }

}

3.ThreadLocal与Synchronized的区别

特性synchronized (锁机制)ThreadLocal (线程封闭)
核心思想以时间换空间
通过牺牲时间(等待锁),让多个线程共享同一个变量。
以空间换时间
通过牺牲内存(每个线程存一份副本),让线程之间互不干扰。
数据共享性共享:所有线程操作的是同一个变量。隔离:每个线程操作的是自己独有的变量副本。
并发方式串行:同一时刻,只有一个线程能执行临界区代码(排队)。并行:所有线程同时执行,互不阻塞,无需等待。
适用场景需要线程间通信、数据必须一致的场景(如:计数器、库存扣减)。数据无需共享、仅需在当前线程内传递的场景(如:用户上下文、数据库连接、事务管理)。
性能开销:涉及线程挂起、唤醒、上下文切换,竞争激烈时性能下降明显。:无锁竞争,直接读写本地内存,速度极快(但消耗更多内存)。
主要风险死锁 (Deadlock)
线程 A 等 B 释放锁,B 等 A 释放锁,大家永远卡住。
内存泄漏 (Memory Leak)
线程池复用线程时,如果不手动 remove(),旧数据会一直占着内存,甚至导致下一个请求读到脏数据(串号)。
性能瓶颈锁竞争
线程多了,大家都在排队,CPU 大量时间花在切换线程上。
内存占用
线程越多,占用的内存越多。如果存的对象很大,容易 OOM。

Synchronized

1.使用方式

(1)修饰实例方法(锁的是 this

public synchronized void doSomething() {
    // 临界区代码
}

(2)修饰静态方法(锁的是 Class 对象)

public static synchronized void doStaticSomething() {
    // 临界区代码
}

(3) 修饰代码块(锁的是指定的对象)最推荐

public void doWork() {
    // 这里的 "lockObj" 可以是 this, 也可以是 new Object(), 或者一个常量
    synchronized (this) { 
        // 临界区代码
    }
}

在多线程编程中,锁(Lock) 是解决线程安全问题的核心机制。

它的本质是:同一时刻,只允许一个线程访问共享资源。如果没有锁,多个线程同时修改同一个变量(如 count++),就会导致数据错乱(线程不安全)。

1.死锁

死锁 (Deadlock) 是多线程编程中最棘手的问题之一。简单来说,就是两个或多个线程互相等待对方释放资源,结果谁也无法继续执行,大家永远卡在那里

(1)产生死锁的 4 个必要条件

名称解释
互斥条件 (Mutual Exclusion)资源是独占的。同一时刻,一个资源只能被一个线程占用。(如:筷子不能劈成两半用)
请求与保持 (Hold and Wait)线程已经持有了至少一个资源,但又去请求新的资源,而新资源被别的线程占用了,此时当前线程阻塞,但不释放已持有的资源。
不剥夺条件 (No Preemption)线程已获得的资源,在未使用完之前,不能被其他线程强行夺走,只能由自己主动释放。
循环等待 (Circular Wait)存在一种线程资源的环形链:T1 等 T2,T2 等 T3 ... Tn 等 T1。

(2)代码示例

public class DeadLockDemo {
    // 定义两个锁对象
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) {
        // 线程 1:先拿 A,再拿 B
        Thread t1 = new Thread(() -> {
            synchronized (lockA) {
                System.out.println("线程 1 获得了锁 A,准备获取锁 B...");
                try { Thread.sleep(100); } catch (InterruptedException e) {} // 模拟耗时,增加死锁概率
                
                synchronized (lockB) {
                    System.out.println("线程 1 获得了锁 B");
                }
            }
        });

        // 线程 2:先拿 B,再拿 A (顺序反了!)
        Thread t2 = new Thread(() -> {
            synchronized (lockB) {
                System.out.println("线程 2 获得了锁 B,准备获取锁 A...");
                try { Thread.sleep(100); } catch (InterruptedException e) {} 
                
                synchronized (lockA) {
                    System.out.println("线程 2 获得了锁 A");
                }
            }
        });

        t1.start();
        t2.start();
        
        // 程序将在这里卡住,没有任何输出后续...
    }
}

2.乐观锁与悲观锁

核心区别就一句话:你是“先锁门再办事”,还是“先办事再检查”?

维度悲观锁 (Pessimistic)乐观锁 (Optimistic) 通俗大白话解释
假设前提假设冲突一定会发生假设冲突很少发生悲观:“人心险恶,肯定有人抢!”
乐观:“大家都有素质,应该没人抢。”
加锁时机操作前就加锁更新提交时才检查悲观:进屋前先把门反锁。
乐观:进屋不锁门,出门前看一眼有没有人动过东西。
实现机制依赖独占锁
(如:synchronized)
依赖版本号或对比
(如:CAS)
悲观:拿一把唯一的钥匙,谁有钥匙谁进。
乐观:给文件贴个标签(版本号),改的时候看标签变没变。
线程行为获取不到?→ 阻塞 (睡觉)
乖乖排队等待
获取失败?→ 不阻塞
要么放弃,要么重试
悲观:门口排队,原地罚站,直到轮到你了才醒。
乐观:发现被人抢先了?不排队,要么回家,要么重新再试一次。
CPU 开销
(睡觉不费电)

(一直重试很费电)
悲观:排队的人都在睡觉,不消耗体力 (CPU)。
乐观:失败的人一直在原地转圈重试,累得半死 (浪费 CPU)。
吞吐量
(一个个来,慢)

(大家一起跑,快)
悲观:像过独木桥,一次只能过一个人,后面堵长队。
乐观:像宽阔马路,大家并排跑,只有撞车了才停下来处理。
典型场景写入频繁
(如:抢票、转账)
读取频繁
(如:看新闻、查余额)
悲观:大家都要改数据(如抢红包),必须排队防乱。
乐观:大部分人只是看数据,偶尔改一下,不用排队。
Java 代表synchronized
ReentrantLock
AtomicInteger
LongAdder
悲观:Java 里的“重锁”工具。
乐观:Java 里的“原子类”工具 (自带重试功能)。
数据库代表SELECT ... FOR UPDATEUPDATE ... WHERE version=旧值悲观:查出来直接锁住行,别人动不了。
乐观:更新时带个条件:“只有版本号没变我才改”,变了就不改。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值