首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >JavaEE初阶——从入门到掌握线程安全

JavaEE初阶——从入门到掌握线程安全

作者头像
想不明白的过度思考者
发布2025-10-29 16:13:53
发布2025-10-29 16:13:53
6900
举报
文章被收录于专栏:JavaEEJavaEE
运行总次数:0

Java多线程编程初阶:从入门到掌握线程安全

1. 认识线程(Thread)

线程是什么

线程是程序中的执行流,多个线程可以并发执行多个任务。例如,一家公司办理银行业务,多个员工分别处理转账、发福利、缴社保,这就是多线程的典型场景。

进程和线程的区别
  • 进程是包含线程的.每个进程⾄少有⼀个线程存在,即主线程。
  • 进程和进程之间不共享内存空间.同⼀个进程的线程之间共享同⼀个内存空间.
  • 进程是系统分配资源的最⼩单位,线程是系统调度的最小单位。
  • ⼀个进程挂了⼀般不会影响到其他进程.但是⼀个线程挂了,可能把同进程内的其他线程⼀起带走(整个进程崩溃).
创建线程的几种方式

方式

示例代码

继承Thread类

class MyThread extends Thread { public void run() { ... } }

实现Runnable接口

class MyRunnable implements Runnable { public void run() { ... } }

匿名内部类

new Thread(() -> { ... }).start();

Lambda表达式

new Thread(() -> System.out.println("Hello")).start();

使用重写Thread类创建一个多线程程序
代码语言:javascript
代码运行次数:0
运行
复制
import java.util.Random;

public class ThreadDemo {
    private static class MyThread extends Thread {
        @Override
        public void run() {
            Random random = new Random();
            while (true) {
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(random.nextInt(10));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start();

        Random random = new Random();
        while (true) {
            System.out.println(Thread.currentThread().getName());
            try {
                Thread.sleep(random.nextInt(10));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
使用lambda表达式创建一个多线程程序
代码语言:javascript
代码运行次数:0
运行
复制
package Thread;

public class Demo5 {
    public static void main(String[] args) {
        // 使用lambda表达式
        Thread thread1 = new Thread(() -> {
            while (true) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }, "thread-1");
        thread1.start();
    }
}
使用 jconsole 命令观察线程
在这里插入图片描述
在这里插入图片描述
多线程的优势

多线程能充分利用多核CPU,提高程序运行效率。以下是一个对比串行与并发执行的示例:

代码语言:javascript
代码运行次数:0
运行
复制
public class ThreadAdvantage {
    private static final long count = 10_0000_0000;

    public static void main(String[] args) throws InterruptedException {
        concurrency();
        serial();
    }

    private static void concurrency() throws InterruptedException {
        long begin = System.nanoTime();
        Thread thread = new Thread(() -> {
            int a = 0;
            for (long i = 0; i < count; i++) a--;
        });
        thread.start();
        int b = 0;
        for (long i = 0; i < count; i++) b--;
        thread.join();
        long end = System.nanoTime();
        double ms = (end - begin) * 1.0 / 1000 / 1000;
        System.out.printf("并发: %f 毫秒%n", ms);
    }

    private static void serial() {
        long begin = System.nanoTime();
        int a = 0;
        for (long i = 0; i < count; i++) a--;
        int b = 0;
        for (long i = 0; i < count; i++) b--;
        long end = System.nanoTime();
        double ms = (end - begin) * 1.0 / 1000 / 1000;
        System.out.printf("串行: %f 毫秒%n", ms);
    }
}

2. Thread类及常见方法

构造方法

方法

说明

Thread()

创建线程对象

Thread(Runnable target)

使用Runnable对象创建线程

Thread(String name)

创建命名线程

Thread(Runnable target, String name)

使用Runnable创建命名线程

常见属性

属性

获取方法

ID

getId()

名称

getName()

状态

getState()

优先级

getPriority()

是否后台线程

isDaemon()

是否存活

isAlive()

是否被中断

isInterrupted()

启动线程:start()
代码语言:javascript
代码运行次数:0
运行
复制
Thread t = new Thread(() -> System.out.println("线程运行"));
t.start(); // 真正启动线程
中断线程
方式一:自定义标志位
代码语言:javascript
代码运行次数:0
运行
复制
public class ThreadDemo {
    private static class MyRunnable implements Runnable {
        public volatile boolean isQuit = false;

        @Override
        public void run() {
            while (!isQuit) {
                System.out.println("转账中...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("停止转账");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread t = new Thread(target, "李四");
        t.start();
        Thread.sleep(5000);
        target.isQuit = true;
    }
}
方式二:使用interrupt()
代码语言:javascript
代码运行次数:0
运行
复制
package Thread;

//中断线程
//通过调用线程的interrupt()方法,来中断线程
public class Demo8 {
    public static void main(String[] args) throws InterruptedException {
        // 注意,此处针对lambda的定义其实是在new Thread之前的!
        // 所以如果在lambda中使用thread,是会报错的!
        Thread thread = new Thread(() -> {
            // currentThread()是Thread提供的一个静态方法
            // 它的功能是哪个线程调用这个方法,就返回哪个线程对象的引用
            while (!Thread.currentThread().isInterrupted()) {
                // 由于这个 currentThread()方法,是在后续thread.start 之后,才执行的.
                // 并且是在thread线程中执行的。返回的结果就是指向thread线程对象的引用了
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // 如果线程在sleep状态时,被中断了,会触发InterruptedException异常
                    // 当线程thread正在sleep时,主线程(main)调用thread的interrupt方法,可以提前唤醒sleep状态的线程,此时会触发InterruptedException异常。通过这个异常,能区分线程是正常休眠结束还是被提前中断唤醒。并且,当sleep因提前唤醒触发异常后,会将线程的中断标志位(isInterrupted)重置为false。因此会无限循环!

                    // 这个printStackTrace()方法,只是用来打印异常的栈轨迹的。
                    e.printStackTrace();

                    // 当线程在sleep状态时被中断,会抛出InterruptedException异常。我们可以在catch块中处理这个异常,例如记录日志、清理资源等。同时,我们可以根据业务需求,选择是否继续循环或退出线程或稍等一会儿在退出循环。这里我们选择退出线程
                    // 添加break之后,触发异常时就会结束循环,让线程结束
                    // 在break之前也可以添加一些其他善后逻辑,相当于稍后再结束
                    // doSomething();
                    break;

                    // 使用IDEA自动生成catch语句,此时默认给的代码就是再次抛出一个其他异常,如RuntimeException,但是这个做法太粗暴了,它不只是让thread线程结束,也会使整个进程结束,因为没有人catch这个RuntimeException异常。不推荐
                    // throw new RuntimeException(e);

                }
            }
        });
        thread.start();
        // 在main线程中尝试终止thread线程
        Thread.sleep(3000);
        thread.interrupt();
    }
}
等待线程:join()
代码语言:javascript
代码运行次数:0
运行
复制
package Thread;

//join方法(主线程等thread线程结束)

public class Demo9 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    // e.printStackTrace();
                    break;
                }
            }
        });
        thread.start();

        // 在主线程中就可以对thread线程进行等待
        System.out.println("主线程等待之前");
        // join也有可能触发阻塞,可能会抛出InterruptedException异常
        // 哪个线程调用了join方法,哪个线程就是等的一方,此处是main
        // join前面那个引用,对应的就是线程就是“被等的一方”
        // 此处是main线程等待thread线程结束

        thread.join();
        System.out.println("主线程等待之后");
    }
}

![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/372ba31abc2e404bbd6939f143122b3c.p
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/372ba31abc2e404bbd6939f143122b3c.p
获取当前线程引用
代码语言:javascript
代码运行次数:0
运行
复制
Thread currentThread = Thread.currentThread();
System.out.println(currentThread.getName());
休眠线程:sleep()
代码语言:javascript
代码运行次数:0
运行
复制
Thread.sleep(1000); // 休眠1秒

3. 线程的状态和转移

在这里插入图片描述
在这里插入图片描述
线程状态枚举
代码语言:javascript
代码运行次数:0
运行
复制
public class ThreadState {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
        }
    }
}

状态包括:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED

在这里插入图片描述
在这里插入图片描述
状态转移图
代码语言:javascript
代码运行次数:0
运行
复制
[NEW]
   |
   | start()
   v
[RUNNABLE] <----------------+
   |                        |
   | 获取CPU                 | yield()/时间片用完
   v                        |
[RUNNING] ------------------+
   |
   | sleep()/wait()/join()
   v
[TIMED_WAITING/WAITING]
   |
   | 时间到/notify()/interrupt()
   v
[RUNNABLE] <----------------+
   ^                        |
   |                        | 获取锁
   | 等待锁                 |
   +------------------------+
   |                        |
   v                        |
[BLOCKED] ------------------+
                           
[RUNNING]
   |
   | 执行完毕
   v
[TERMINATED]

4. 线程安全与同步机制

线程不安全示例
代码语言:javascript
代码运行次数:0
运行
复制
package Thread;

//线程安全

public class Demo14 {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {

        // 如果把上面的count移到这里,改成局部变量,那么就会出现lambda变量捕获报错。因为他不是final属性
        // int count = 0;

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(count);
    }
}

实际打印结果基本都是小于100000 如果出现多次这种请款情况,那么实际打印结果甚至很可能会低于50000

在这里插入图片描述
在这里插入图片描述
线程不安全的原因

原因

说明

线程调度是随机的

随机调度使⼀个程序在多线程环境下,执⾏顺序存在很多的变数

修改共享数据

如上面代码涉及到多个线程针对count 变量进⾏修改

原子性

操作被中断导致数据不一致

可见性

线程间数据更新不可见

指令重排序

编译器/CPU优化导致执行顺序变化

synchronized关键字

synchronized的特性 synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同⼀个对象synchronized就会阻塞等待.

  • 进⼊synchronized修饰的代码块,相当于加锁
  • 退出synchronized修饰的代码块,相当于解锁
修饰代码块加锁
代码语言:javascript
代码运行次数:0
运行
复制
package Thread;

//加锁和同步

public class Demo15 {
    private static int count = 0;
    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                // 进去synchronized代码块加锁,出去synchronized就是解锁
                synchronized (lock) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                // 加锁就是把若干个操作“打包为一个原子”,不是说把count++三个指令变成成一个指令,也不是说,这三个指令要在cpu上一口气执行完不会触发调度
                // 加锁会影响到其他加锁线程,而且是加同一个锁的线程
                // 进去synchronized代码块加锁,出去synchronized就是解锁,()括号里面需要填一个对象,这个对象就是锁对象
                // 只有当两个线程竞争同一把锁,才会产生“阻塞”,竞争不同的锁或者只有一个加锁另一个没加锁,则都不会产生“锁冲突、锁竞争”
                synchronized (lock) {
                    count++;
                }
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(count);
    }
}
在这里插入图片描述
在这里插入图片描述
synchronized 加锁范围对多线程执行效率的影响
在这里插入图片描述
在这里插入图片描述

这张图核心讲解的是 synchronized 加锁范围对多线程执行效率的影响,需要结合“锁的粒度”和“并发场景”来分析:

一、左图:synchronized 加在 for 循环外

代码语言:javascript
代码运行次数:0
运行
复制
synchronized (locker) {
    for (int i = 0; i < 50000; i++) {
        count++;
    }
}

执行逻辑

  • 线程 t1 获取锁后,会持续持有锁,执行完整个 for 循环(5 万次 count++)后才释放锁。
  • 线程 t2 必须等待 t1 完全释放锁,才能获取锁并执行自己的循环。

效率特点

  • 当前简单任务下更快:因为加锁 / 解锁的 “overhead(开销)” 小,且任务本身简单(只有 count++),“串行执行整个循环” 的总耗时比 “频繁加锁解锁” 更短。
  • 但并发度极低t2 完全被阻塞,直到 t1 执行完 5 万次循环,多线程几乎退化为 “单线程串行”。

二、右图:synchronized 加在 for 循环内

代码语言:javascript
代码运行次数:0
运行
复制
for (int i = 0; i < 50000; i++) {
    synchronized (locker) {
        count++;
    }
}

执行逻辑

  • 每次循环时,线程会短暂获取锁,执行 count++ 后立即释放锁。
  • 线程 t1t2 可以交替获取锁t1 释放后,t2 能立即获取),实现一定程度的 “并发执行”。

效率特点

  • 当前简单任务下更慢:因为每次循环都要执行 “加锁 → 操作 → 解锁”,频繁的锁操作带来额外开销,总耗时比 “一次加锁执行全部循环” 更长。
  • 复杂任务下更优:如果 synchronized 内部是复杂逻辑(如图中 “一系列很复杂的逻辑”),“缩小锁的范围” 能让非临界区代码并发执行,整体并发度更高,总效率会超过 “大锁范围” 的方式。

三、核心结论:锁的粒度要 “恰到好处”

  • 锁的粒度:指加锁代码的范围大小(大粒度 = 锁范围大,小粒度 = 锁范围小)。
  • 选择原则
    • 任务简单时:大粒度锁(如左图)开销更小,效率可能更高。
    • 任务复杂时:小粒度锁(如右图)能让更多非临界区并发执行,整体效率更优。
  • 日常最佳实践:尽量缩小锁的范围(只对 “必须线程安全的临界区” 加锁),这样能最大化并发度,应对复杂业务场景时更灵活。

简单总结:锁的范围不是越大或越小越好,要根据任务的 “复杂度” 和 “并发需求” 来权衡,日常开发优先选择 “小粒度锁” 以保留更高的并发潜力。

修饰方法
代码语言:javascript
代码运行次数:0
运行
复制
public synchronized void increment() {
    count++;
}
修饰静态方法
代码语言:javascript
代码运行次数:0
运行
复制
public static synchronized void increment() {
    count++;
}
锁的可重入性(避免死锁!)

由于 Java 中 synchronized 锁是可重入锁,这种 “同一线程对同一把锁的嵌套加锁” 是安全的:

  • 当线程第一次获取 counter 锁时,锁的 “持有计数” 会从 0 变为 1;
  • 线程再次获取同一把锁时,计数会增至 2(不会阻塞或死锁);
  • 只有当所有嵌套的锁都执行完(即外层和内层的 synchronized 代码块都执行结束),锁的持有计数才会减为 0,真正释放锁,允许其他线程获取。

展示死锁

代码语言:javascript
代码运行次数:0
运行
复制
package Thread;

// 死锁示例
public class Demo17 {
    // 创建两个锁对象
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) {
        // 线程1:先获取lockA,再尝试获取lockB
        Thread thread1 = new Thread(() -> {
            synchronized (lockA) {
                System.out.println("线程1已获取lockA,尝试获取lockB...");
                // 休眠100ms,让线程2有机会获取lockB,增加死锁概率
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockB) {
                    System.out.println("线程1已获取lockB");
                }
            }
        });

        // 线程2:先获取lockB,再尝试获取lockA(与线程1顺序相反)
        Thread thread2 = new Thread(() -> {
            synchronized (lockB) {
                System.out.println("线程2已获取lockB,尝试获取lockA...");
                // 休眠100ms,让线程1有机会获取lockA,增加死锁概率
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockA) {
                    System.out.println("线程2已获取lockA");
                }
            }
        });

        // 启动两个线程
        thread1.start();
        thread2.start();
        System.out.println("线程1和线程2已启动");
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程1和线程2已执行完毕");
    }
}

展示可重入性

代码语言:javascript
代码运行次数:0
运行
复制
        // 针对一把锁连续加锁两次(锁的可重入性,避免死锁)
        Thread thread3 = new Thread(() -> {
            synchronized (counter) {
                synchronized (counter) {
                    for (int i = 0; i < 50000; i++) {
                        counter.add();
                    }
                }
            }
        });
volatile关键字

内存可见性问题解析 内存可见性问题指在多线程程序里,一个线程对共享变量的修改,其他线程无法及时察觉的现象。 原因(结合 CPU 寄存器、内存和 JMM)

  • CPU 与内存的架构层面 现代计算机中,CPU 运算速度远超内存读写速度。为提升性能,CPU 配备了高速缓存(寄存器等属于高速缓存的一部分)。当线程操作变量时,会先将变量从内存加载到 CPU 的高速缓存里。若多个线程在不同 CPU 核心上运行,每个核心都有专属高速缓存。当一个线程在自身高速缓存中修改了共享变量,其他线程所在 CPU 核心的高速缓存里,该变量仍为原值,这就导致其他线程看不到最新修改,此为内存可见性问题的硬件层面根源。
  • Java 内存模型(JMM)层面 JMM 定义了线程与主内存的抽象关系。线程拥有自己的工作内存(可类比 CPU 高速缓存),主内存用于存储共享变量。线程对变量的操作(如读取、修改等)都在工作内存中进行,之后再同步到主内存。若线程间工作内存与主内存的同步不及时,就会出现一个线程修改了主内存变量,另一个线程的工作内存中仍是旧值的情况,进而引发内存可见性问题。比如在缺乏适当同步机制(如 volatile、锁等)时,JMM 不保证线程对共享变量的修改能被其他线程立即看到。

volatile修饰的变量,能够保证"内存可见性".

在这里插入图片描述
在这里插入图片描述
代码语言:javascript
代码运行次数:0
运行
复制
public class VolatileDemo {
    static class Counter {
        public volatile int flag = 0;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            while (counter.flag == 0) {
                // 空循环
            }
            System.out.println("循环结束");
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入整数:");
            counter.flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

潜在问题(结合编译器优化) 由于多线程环境下,线程间共享变量的可见性可能存在问题(若不加volatile关键字):

  • 对于 thread1 中的 while (flag == 0) 循环,编译器可能会进行优化(比如为了提高效率,将 flag 从主内存读取到线程的工作内存后,后续多次循环不再重新从主内存读取,直接使用工作内存中的缓存值)。
  • 当 thread2 修改了 flag 的值后,thread1 可能因为缓存一致性问题,无法及时感知到 flag 的变化,导致 thread1 一直卡在 while 循环中,程序无法正常结束。

5. 线程间协作:wait与notify

在 Java 中,wait()notify() 是 Object 类的两个核心方法,用于多线程间的协作,通过控制线程的等待与唤醒,实现线程间的同步和通信。它们必须配合同步锁(synchronized) 使用,核心作用是让线程暂时释放锁并等待特定条件,直到其他线程满足条件后唤醒它。 1. wait() 方法

  • 作用:让当前线程释放锁并进入WAITING 状态(无限等待),直到被其他线程通过 notify() 或 notifyAll() 唤醒,或被中断。
  • 使用条件:必须在 synchronized 代码块或方法中调用(即当前线程必须持有该对象的锁),否则会抛出 IllegalMonitorStateException。
  • 执行逻辑: 当前线程释放持有的锁,允许其他线程获取该锁; 线程进入等待队列,暂停执行; 当被唤醒或超时(若用 wait(long timeout)),线程会重新尝试获取锁,获取成功后继续执行 wait() 之后的代码。

2. notify() 方法

  • 作用:唤醒一个正在等待当前对象锁的线程(具体唤醒哪个线程由 JVM 随机决定)。
  • 使用条件:同样必须在 synchronized 代码块或方法中调用(当前线程必须持有该对象的锁)。
  • 执行逻辑:从等待队列中随机选择一个线程唤醒,使其从 WAITING 状态变为就绪状态;唤醒后,被唤醒的线程不会立即执行,而是需要等待当前线程释放锁后,重新竞争获取锁;若要唤醒所有等待的线程,需使用 notifyAll()。
代码语言:javascript
代码运行次数:0
运行
复制
package Thread;

import java.util.Scanner;

//wait和notify的使用

public class Demo21 {
    private static Object locker = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println("t1 wait之前");
            synchronized (locker) {
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1 wait之后");
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入任意内容,唤醒t1线程:");
            scanner.next();
            synchronized (locker) {
                locker.notify();
            }
        });
        t1.start();
        t2.start();
    }
}

6. 多线程案例实战

单例模式
饿汉式(线程天然安全)
代码语言:javascript
代码运行次数:0
运行
复制
package Thread;

//单例模式————>饿汉模式

//Singleton就是单例的意思
class Singleton {
    // 把这个唯一的实例先创建出来
    // 由于此处的instance是一个static的,所以它会在类加载的时候就被创建出来(程序一启动时)
    private static Singleton instance = new Singleton();

    // 提供一个静态的方法,返回这个唯一的实例
    public static Singleton getInstance() {
        return instance;
    }

    // 核心操作:把构造方法私有化,防止外部直接new
    private Singleton() {

    }
}

public class Eager_Initialization {
    public static void main(String[] args) {
        // 做了一个君子协定,你必须通过getInstance()方法来获取实例,而不能直接new
        // 如果有人不会遵守上述决定,那就打破单例了
        // 所以要做一些措施
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        // Singleton s3 = new Singleton();
        System.out.println(s1 == s2);
    }
}
懒汉式(本身线程不安全,需要加一些操作)
代码语言:javascript
代码运行次数:0
运行
复制
package Thread;

//单例模式————>懒汉模式

class SingletonLazy {
    private static SingletonLazy instance = null;
    // 添加volatile为了不使编译器进行优化,防止触发指令重排序
    private static volatile Object locker = new Object();

    private SingletonLazy() {
        // 防止通过反射破坏单例
        if (instance != null) {
            throw new RuntimeException("不能通过反射创建实例!");
        }
        System.out.println("SingletonLazy构造方法被调用");
    }

    // 提供一个静态的方法,返回这个唯一的实例
    public static SingletonLazy getISingletonLazy() {
        // 先判断是否已经创建了实例
        if (instance == null) {
            synchronized (locker) {
                if (instance == null) {
                    // 如果没有创建实例,就创建一个
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
}

public class Lazy_Initialization {
    public static void main(String[] args) {

        SingletonLazy s1 = SingletonLazy.getISingletonLazy();
        SingletonLazy s2 = SingletonLazy.getISingletonLazy();
        // Singleton s3 = new Singleton();
        System.out.println(s1 == s2);
    }
}
阻塞队列
代码语言:javascript
代码运行次数:0
运行
复制
public class BlockingQueue {
    private final int[] items;
    private int head = 0;
    private int tail = 0;
    private int size = 0;

    public BlockingQueue(int capacity) {
        items = new int[capacity];
    }

    public synchronized void put(int value) throws InterruptedException {
        while (size == items.length) {
            wait();
        }
        items[tail] = value;
        tail = (tail + 1) % items.length;
        size++;
        notifyAll();
    }

    public synchronized int take() throws InterruptedException {
        while (size == 0) {
            wait();
        }
        int value = items[head];
        head = (head + 1) % items.length;
        size--;
        notifyAll();
        return value;
    }
}
定时器

定时器是什么 定时器也是软件开发中的⼀个重要组件.类似于⼀个"闹钟".达到⼀个设定的时间之后,就执行某个指定好的代码

在这里插入图片描述
在这里插入图片描述
代码语言:javascript
代码运行次数:0
运行
复制
package Thread;

import java.util.PriorityQueue;

// 自己实现的定时器
// 定时器要同时管理多个任务
// 有一定的数据结构来管理多个任务
// 使用优先级队列(堆)

// 由于我们把这个类要放在优先级队列中,就需要指定优先级队列的“比较规则“
class MyTimerTask implements Comparable<MyTimerTask> {
    private Runnable task;
    // 这个地方为了和当前时间进行对比,确认这个任务是否要执行,保存时间戳更方便
    private long time;

    public MyTimerTask(Runnable task, long delay) {
        this.task = task;
        // 把delay转换成要执行任务的绝对时间戳记录下来
        this.time = System.currentTimeMillis() + delay;
    }

    public Runnable getTask() {
        return task;
    }

    public long getTime() {
        return time;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        // this - o就会实现一个小堆的效果
        // o - this就会实现一个大堆的效果
        // 这里我们要实现的是谁时间小,就先执行谁
        return (int) (this.time - o.time);
    }
}

class MyTimer {

    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    private Object locker = new Object();

    // 创建一个构造方法
    public MyTimer() {
        // 我们在这里要创建一个线程,让这个线程去检测是否到时间了,以及去执行这个任务
        Thread thread = new Thread(() -> {
            // 循环从队列中取出元素,然后来判定这个任务的时间是否到达
            // 如果到达就执行任务,并且出队列
            // 如果没到达时间,就暂时不执行
            try {
                while (true) {
                    synchronized (locker) {
                        // 判断队列是否为空
                        if (queue.isEmpty()) {
                            // 如果不处理会触发”忙等“问题,因为线程会一直占用CPU资源
                            // 所以我们要让线程进入等待状态,等下一次被唤醒
                            locker.wait();
                        }
                        // 取出队首元素
                        MyTimerTask task = queue.peek();
                        // 记录当前时刻的时间戳
                        long currentTime = System.currentTimeMillis();
                        // 判断是否到达时间
                        if (task.getTime() > currentTime) {
                            // 时间还没到,也会触发忙等,等待到时间再执行
                            // 这里的wake要处理两种情况
                            // 1. 随时有新任务加入,导致队首元素发生变化
                            // 2. 如果没有新任务,时间到了,线程被唤醒

                            // 如:任务是 14:30,就 wait 30 分钟
                            // 在这 30 分钟过程中,如果没有任何新的任务到来,30 分钟时间到,就应该被唤醒,进而去执行任务~~
                            // 如果在 30 分钟过程中,有新任务来了.新任务是 14:15 分的任务~~此时必然要把 wait 30 分钟唤醒, 根据最新的任务的时间,重新设定
                            // wait 时间.
                            // 如果在 30 分钟过程中,有新任务来了,新任务是 15:00 的任务虽然 wait 暂时唤醒了一下,还是依据 14:30 的任务,重新设置等待时间~~
                            locker.wait(task.getTime() - currentTime);
                        } else {
                            // 执行任务
                            task.getTask().run();
                            // 出队列
                            queue.poll();
                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread.start();
    }

    // 创建schedule方法,此处的任务用Runnable表示
    public void schedule(Runnable runnable, long delay) {
        synchronized (locker) {
            queue.offer(new MyTimerTask(runnable, delay));
            locker.notify();
        }
    }

}

public class MyTimerDemo {
    public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("定时器执行任务 3000");
            }
        }, 3000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("定时器执行任务 2000");
            }
        }, 2000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("定时器执行任务 1000");
            }
        }, 1000);
    }
}
线程池
一、线程池的核心作用

线程池是一种线程管理机制,核心目的是减少线程创建与销毁的开销,提高系统资源利用率。

可通过一个生活场景类比理解:

  • 无线程池模式:快递店无固定员工,每次有业务就临时找同学送快递,送完后解雇。对应“来一个任务,新建一个线程处理,任务结束后销毁线程”——频繁创建/销毁线程会消耗大量系统资源(如CPU调度、内存分配)。
  • 线程池模式:快递店固定雇3名员工,业务来了优先让现有员工处理;若员工都忙碌,就把业务记在本子上(任务队列)等待。对应“线程池维护固定数量的线程,任务提交后复用现有线程,避免频繁创建/销毁”。
二、标准库中线程池的基础使用

1. 快速创建线程池(Executors工具类) Executors是Java提供的线程池工具类,封装了ThreadPoolExecutor的复杂配置,可快速创建不同类型的线程池,返回值类型均为ExecutorService(线程池核心接口)。

通过ExecutorService.submit(Runnable task)可向线程池提交任务。

示例代码(固定线程数线程池

代码语言:javascript
代码运行次数:0
运行
复制
// 创建包含10个固定线程的线程池
ExecutorService pool = Executors.newFixedThreadPool(10);
// 向线程池提交任务
pool.submit(new Runnable() {
    @Override
    public void run() {
        System.out.println("任务执行:hello");
    }
});

2. Executors创建线程池的4种方式

方法

功能描述

newFixedThreadPool(n)

创建固定线程数的线程池,线程数始终为n,空闲线程不会被销毁。

newCachedThreadPool()

创建动态增长的线程池,线程空闲60秒后会被销毁,适合短期高频任务。

newSingleThreadExecutor()

创建单线程线程池,所有任务按顺序执行,避免多线程并发问题。

newScheduledThreadPool(n)

创建定时/延迟执行的线程池,可替代Timer,支持周期性任务。

三、线程池的底层实现:ThreadPoolExecutor

Executors本质是对ThreadPoolExecutor的封装。ThreadPoolExecutor提供7个核心参数,可精细化控制线程池行为,对应“快递店运营规则”的具象化。

7个核心参数(类比快递店场景)

参数名

作用(类比快递店)

核心逻辑

corePoolSize

正式员工数量

线程池维护的“核心线程数”,即使空闲也不会被销毁(除非开启allowCoreThreadTimeOut)。

maximumPoolSize

正式员工 + 临时工总数

线程池允许的最大线程数,任务高峰期可临时创建线程(临时工),任务减少后临时工被辞退。

keepAliveTime

临时工的空闲时间上限

当线程数超过corePoolSize时,多余线程(临时工)空闲超过该时间会被销毁。

unit

keepAliveTime的时间单位

可选TimeUnit.SECONDS(秒)、MILLISECONDS(毫秒)等。

workQueue

任务队列(记录待处理业务的“本子”)

核心线程都忙碌时,新任务会放入阻塞队列(如LinkedBlockingQueue)等待。

threadFactory

创建线程的工厂

定制线程属性(如线程名称、优先级、是否为守护线程),默认使用Executors.defaultThreadFactory()。

RejectedExecutionHandler

拒绝策略(业务超负载时的处理规则)

任务队列满且线程数达maximumPoolSize时,触发拒绝策略。

4种默认拒绝策略

拒绝策略类

处理逻辑

AbortPolicy()

默认策略,直接抛出RejectedExecutionException异常,中断任务提交。

CallerRunsPolicy()

让“提交任务的线程”自己执行任务(如快递店老板亲自送快递),减轻线程池压力。

DiscardOldestPolicy()

丢弃队列中“最老的任务”(最早进入队列的任务),再尝试提交新任务。

DiscardPolicy()

直接丢弃“新提交的任务”,不抛出异常,适合非核心任务。

自行实现一个简单的固定线程池

代码语言:javascript
代码运行次数:0
运行
复制
package Thread;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.BlockingQueue;

// 自行实现一个简单的固定线程池
class FixedThreadPool {

    // 创建一个阻塞队列
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

    // 创建一个构造方法
    public FixedThreadPool(int n) {
        for (int i = 0; i < n; i++) {
            Thread thread = new Thread(() -> {
                // 每个线程要完成的工作, 就是从队列中取出任务并执行任务
                try {
                    while (true) {
                        Runnable task = queue.take();
                        task.run();
                        // 如果里面没有任务, 就会阻塞在take()方法上
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            thread.start();
        }
    }

    public void sumbit(Runnable task) throws InterruptedException {
        queue.put(task);
    }

}

public class MyFixedThreadPool {
    // 测试创建一个固定线程池
    public static void main(String[] args) throws InterruptedException {
        FixedThreadPool threadPool = new FixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            int id = i;
            threadPool.sumbit(() -> {
                System.out.println("Thread:" + id + " 执行完毕");
            });
        }
    }
}

7. 总结与对比

进程 vs 线程

对比项

进程

线程

资源分配

独立内存空间

共享进程资源

创建开销

切换开销

通信方式

复杂(IPC)

简单(共享内存)

线程安全保证思路
  1. 无共享资源:避免共享数据
  2. 只读共享:使用不可变对象
  3. 同步机制
    • 原子性:synchronized
    • 可见性:volatile
    • 有序性:避免指令重排序

本文详细介绍了Java多线程编程的基础知识、常见问题及解决方案,并提供了丰富的代码示例。掌握多线程编程是Java工程师的必备技能,希望本文能帮助你更好地理解和应用多线程技术。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-09-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Java多线程编程初阶:从入门到掌握线程安全
    • 1. 认识线程(Thread)
      • 线程是什么
      • 进程和线程的区别
      • 创建线程的几种方式
      • 使用重写Thread类创建一个多线程程序
      • 使用lambda表达式创建一个多线程程序
      • 使用 jconsole 命令观察线程
      • 多线程的优势
    • 2. Thread类及常见方法
      • 构造方法
      • 常见属性
      • 启动线程:start()
      • 中断线程
      • 等待线程:join()
      • 获取当前线程引用
      • 休眠线程:sleep()
    • 3. 线程的状态和转移
      • 线程状态枚举
      • 状态转移图
    • 4. 线程安全与同步机制
      • 线程不安全示例
      • 线程不安全的原因
      • synchronized关键字
      • 修饰静态方法
      • volatile关键字
    • 5. 线程间协作:wait与notify
    • 6. 多线程案例实战
      • 单例模式
      • 阻塞队列
      • 定时器
      • 线程池
    • 7. 总结与对比
      • 进程 vs 线程
      • 线程安全保证思路
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档