Java 多线程核心基础与线程安全

一,认识synchronized关键字(监视器锁 monitor lock)

1.1synchronized的特性

a)互斥

当某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象synchronized就会阻塞等待 (进入synchronized修饰的代码块就是加锁 ,离开代码块就是解锁

阻塞等待:

针对每一把锁,操作系统内部都维护了一个等待队列.当这个锁被某个线程占有的时候,其他线程尝试进行加锁,就加不上了,就会阻塞等待,一直等到之前的线程解锁之后,由操作系统唤醒一个新的线程,再来获取到这个锁.但需注意上一线程解锁后下一线程不是立刻就能获取锁,线程间不遵守先来后到,而是要靠操作系统来调度。

b)可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;

在可重入锁的内部,包含了"线程持有者"和"计数器"两个信息.

1.如果某个线程加锁的时候,发现锁已经被人占用,但是恰好占用的正是自己,那么仍然可以继续获取到锁,并让计数器自增.

2.解锁的时候计数器递减为0 的时候,才真正释放锁.(才能被别的线程获取到)

1.2 synchronized使用

synchronized修饰普通方法,相当于给this加锁

synchronized修饰静态方法,相当于给类对象加锁

a)修饰代码块:明确指定锁那个对象
java 复制代码
锁任意对象
public class Synchronized{
    private Object locker=new Object();
    public void method(){
    synchronized(locker){
        }
    }
}


锁当前对象
public class Synchronized{
    public void method(){
    synchronized(this){
        }
    }
}
b)直接修饰普通方法:锁的Synchronized对象
java 复制代码
public class Synchronized{
    public synchronized void method(){
    }
}
c)修饰静态方法:锁的Synchronized类的对象
java 复制代码
 public class Synchronized{
    public synchronized static void method () {
     }
}

二,认识volatile关键字

引入volatile关键字来解决内存可见性问题,但不能保证原子性

解决内存可见性问题:被 volatile 修饰的变量,线程每次读取时,都会直接从主内存读取最新值,不会使用工作内存的缓存副本;每次修改后,会立即同步写回主内存,其他线程能马上看到最新值。加上volatile强制读写内存,速度虽然变慢了但是数据准确性更高

java 复制代码
class Counter{
    public volatile int flag=0;
 //此处加入volatile关键字
}
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关键字t1线程循环就不会结束,因为t1线程读的是自己工作内存中的内容,当t2对flag变量进行修改,此时t1感知不到flag的变化。但加入volatile后,t1线程循环能立即结束。

三,认识wait和notify

wait 和 notify是Java 中用于协调多线程执行顺序的核心方法。由于操作系统无法保证线程的调度顺序,我们可以让需要后执行的线程调用wait () 进入等待状态 ,待先执行的线程完成关键逻辑后,再调用 notify() 或 notifyAll() 唤醒等待的线程,从而实现线程间的协作与同步

wait ()和 notify() 必须在synchronized 同步块 / 方法中调用,且调用对象必须是同一个锁对象,否则会抛出 IllegalMonitorStateException。

且务必确保先wait后notify,如果有多个线程在同一对象上wait,进行notify时会随机唤醒其中一个线程,一次notify唤醒一个线程,而notifyAll一次唤醒所有线程。

3.1 wait( )方法

wait方法所作事情:1.使当前执行代码的线程进行等待

2.释放当前的锁

3.满足一定条件时被唤醒,重新尝试获取这个锁

wait结束的条件:1.其他线程调用该对象的notify()方法

2.wait等待时间超过wait方法所提供的带有timeout参数版本

3.其他线程调用该等待线程的interrupted方法,导致wait抛出异常

代码示例:

java 复制代码
public static void main(String[ ] args) throw InterruptedException{
    Object object=new Object();
    synchronized(object){
        System.out.println("等待中");
        object.wait();
        System.out.println("等待结束");
    }
}

代码在执行到object.wait()后就会一直进行等待,这时就需要使用notify()来唤醒。

3.1.1wait和sleep对比(本质上没有直接可比性)

**wait():****Object类的成员方法,用于线程间通信与协作,**让当前线程主动释放锁并进入等待状态,直到被其他线程唤醒或超时。

**sleep():****Thread类的静态方法,用于让线程暂停执行,**让当前线程休眠指定时间,时间结束后自动恢复执行。

|------|------------------------------|-----------------------------|
| | wait() | sleep() |
| 使用前提 | 必须在synchronized块中调用,且锁对象一致 | 无限制 |
| 唤醒方法 | 被其他线程通过notify()唤醒(或设置超时自动唤醒) | 休眠时间结束后自动恢复,或被interrupt()中断 |
| 锁行为 | 调用时会主动释放当前所持有的锁 | 调用时不会释放任何锁 |

3.2 notify( )方法

notify方法是唤醒等待的线程:

1.方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其他线程,对其发出通知notify,并使他们重新获得该对象的对象锁

2.如果有多个线程等待,则随机挑选出一个为wait状态的线程

3.在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完后才会释放对象锁

代码示例:

java 复制代码
//等待线程类wait
static class Wait implements Runnable {
    private Object locker;
    // 构造方法:传入统一的锁对象
    public WaitTask(Object locker) {
        this.locker = locker;
    }
    @Override
    public void run() {
        // 加锁:必须和 notify 使用同一个锁
        synchronized (locker) {
            while (true) { // 死循环,会一直等待、被唤醒、再等待
                try {
                    System.out.println("wait 开始");
                    // 1. 调用 wait():**释放锁**,线程进入等待池阻塞
                    locker.wait();
                    // 2. 被 notify 唤醒后,重新竞争锁,拿到锁才会走到这里
                    System.out.println("wait 结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
//唤醒线程类notify
static class Notify implements Runnable {
    private Object locker;
    public NotifyTask(Object locker) {
        this.locker = locker;
    }

    @Override
    public void run() {
        // 同样使用同一把锁
        synchronized (locker) {
            System.out.println("notify 开始");
            // 唤醒该锁上**一个**等待中的线程
            locker.notify();
            System.out.println("notify 结束");
        }
        // 走出同步块,**锁才会真正释放**
    }
}
//主线程
public static void main(String[] args) throws InterruptedException {
    // 唯一的锁对象,两个线程共用
    Object locker = new Object();
    Thread t1 = new Thread(new Wait(locker));
    Thread t2 = new Thread(new Notify(locker));

    t1.start();        // 先启动等待线程
    Thread.sleep(1000);// 主线程休眠1秒,保证 t1 先执行并进入 wait
    t2.start();        // 再启动唤醒线程
}

3.3 notifyAll( )方法

只需将上面示例代码中唤醒**线程类中notify()修改为notifyAll()**即可;注意:虽然可同时唤醒所有线程,但这些线程需要竞争锁,所以并不是同时执行,而是有先后顺序执行的

四,多线程案例

4.1 单例模式(是一种设计模式,单个实例)

4.1.1 饿汉模式(类加载的同时,创建实例)
java 复制代码
class Singleton {
    // 1. 静态私有实例对象
    private static Singleton instance = new Singleton();

    // 2. 私有构造方法,禁止外部new
    private Singleton() {}

    // 3. 公共静态获取实例方法,后续通过此方法获取这里的实例
    public static Singleton getInstance() {
        return instance;
    }
}
4.1.2 懒汉模式(第一次使用时,创建实例)

a)基础版

java 复制代码
class Singleton {
    // 静态实例,初始为 null,先不创建对象
    private static Singleton instance = null;

    // 私有构造方法:禁止外部 new
    private Singleton() {}

    // 对外获取实例的方法
    public static Singleton getInstance() {
        // 判断:如果还没创建实例,就 new 对象
        if (instance == null) {
            instance = new Singleton();
        }
        // 直接返回唯一实例
        return instance;
    }
}

b)pro版: 基础版的实现是线程不安全的

线程线程安全问题发生在首次创建实例时. 如果在多个线程中同时调用 getInstance 方法, 就可能导致创建出多个实例. 一旦实例已经创建好了, 后面再多线程环境调用 getInstance 就不再有线程安全问题了(不再修改instance 了)

++加上 synchronized 可以改善这里的线程安全问题.++

java 复制代码
class Singleton {
     private static Singleton instance = null;
     private Singleton() {}
     public synchronized static Singleton getInstance() {
         if (instance == null) {
             instance = new Singleton();
           }
       return instance;
     }
}

c)pro max版:在加锁基础上,还可进一步改进
++使用双重 if 判定, 降低锁竞争的频率; 给 instance 加上volatile++

java 复制代码
class Singleton {
    // volatile 关键字,重点
    private static volatile Singleton instance = null;

    // 私有构造:禁止外部 new
    private Singleton() {}

    public static Singleton getInstance() {
        // 第一层判断
        if (instance == null) {
            // 类对象锁。synchronized (Singleton.class) 就是用 Singleton 类的 Class 对象作为锁,实现全局互斥,保证在多线程环境下,只有一个线程能执行创建实例的代码,从而保证单例的线程安全。
            synchronized (Singleton.class) {
                // 第二层判断
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

理解双重 if 判定 / volatile:
加锁 / 解锁是⼀件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候. 因此后续使用的时候, 不必再进行加锁了. 外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了;同时为了避免 "内存可见性" 导致读取的 instance 出现偏差, 于是补充上 volatile . 当多线程首次调用 getInstance, 可能都发现 instance 为 null, 于是又继续往下执行来竞争锁, 其中竞争成功的线程, 再完成创建实例的操作. 当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例.
完整执行流程:

1.首次调用:instance = null,进入第一层判断。

2.线程争抢 Singleton.class 锁,抢到锁的线程进入同步块。

3.同步块内二次判断,确认无实例,执行 new 创建对象(volatile 保证顺序与可见性)。

4.创建完成,释放锁。

5.后续线程:第一层判断发现 instance != null,直接返回实例,不进锁

4.2 组赛队列(一种更为复杂的队列,主要场景为"生产者消费者模型")

阻塞特性:

1.队列为空,尝试出队列,出队列操作就会阻塞。直到其他线程添加元素

2.队列为满,尝试入队列,入队列操作也会阻塞。直到其他线程取走元素

|----|---------------------------|
| 优点 | a)解耦合 b)削峰填谷 |
| 缺点 | a)引入队列后,整体结构更为复杂 b)效率有所影响 |

java 复制代码
public void put(int value) throws InterruptedException {
    synchronized (this) { // 加锁:保证队列操作的原子性
        // 此处最好使用 while.
        // 否则 notifyAll 的时候, 该线程从 wait 中被唤醒,
        // 但是紧接着并未抢占到锁. 当锁被抢占的时候, 可能又已经队列满了
        // 就只能继续等待
        while (size == items.length) { // 队列已满,生产者需要等待
            wait(); // 释放锁并进入等待状态,直到被消费者唤醒
        }
        // 队列未满,执行入队操作
        items[tail] = value;          // 把数据放到队尾位置
        tail = (tail + 1) % items.length; // 队尾指针后移,循环队列处理
        size++;                       // 队列元素数量+1
        notifyAll();                  // 唤醒所有等待的线程(包括消费者)
    } // 同步块结束,自动释放锁
}

public int take() throws InterruptedException {
    int ret = 0;
    synchronized (this) { // 加锁:保证队列操作的原子性
        while (size == 0) { // 队列为空,消费者需要等待
            wait(); // 释放锁并进入等待状态,直到被生产者唤醒
        }
        // 队列不为空,执行出队操作
        ret = items[head];            // 取出队头元素
        head = (head + 1) % items.length; // 队头指针后移,循环队列处理
        size--;                       // 队列元素数量-1
        notifyAll();                  // 唤醒所有等待的线程(包括生产者)
    } // 同步块结束,自动释放锁
    return ret; // 返回取出的元素
}

4.3 线程池(工厂模式)

线程池包含的线程数量是可动态调整的 ,使用线程池可以省下应用程序切换到内核中运行这样的开销,让我们高效的创建销毁线程

标准库中的线程池:1.使用Executor.newFixedThreadPool( )来创建线程池

2.核心方法submit(Runnable)

3.通过Runnable描述一段要执行的任务

4.通过submit任务放到线程池中

5.此时线程池里的线程就会执行这样的任务

java 复制代码
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
     @Override
     public void run() {
         System.out.println("hello");
     }
});

Executors 本质上是 ThreadPoolExecutor 类的封装. ThreadPoolExecutor 提供了更多的可选参数, 可以进⼀步细化线程池行为的设定.

  • int corePoolSize (核心线程数,至少有多少个线程,线程池一创建这些线程也创建,直到线程池销毁,这些线程才销毁)
  • int maximumPoolSize(核心线程数+非核心线程数不忙就销毁,繁忙就创建
  • long keepAliveTime(非核心线程允许空闲最大时间)
  • TimeUnit unit(枚举常见时间单位,可指定long keepAliveTime 时间)
  • BlockingQueue<Runnable> workQueue(工作队列)
  • ThreadFactory threadFactory(工厂模式通过静态方法,把构造对象new的过程,各种属性初始化过程封装起来来弥补构造方法缺陷,)
  • RejectedExecutionHandler handler(拒绝策略)
    • AbortPolicy 线程池直接抛异常,线程池可能无法继续工作
    • CallerRunsPolicy 让调用submit的线程自行执行任务
    • DiscardoldestPolicy 丢队列最老任务
    • DiscardPolicy 丢队列最新任务

4.4 定时器

优先级队列(时间精度高),时间到了执行一些逻辑

实现:

1.创建一个类,表示一个任务

2.用集合类把多个任务管理起来

3.实现schedule方法,把任务添加到队列中

4.额外创建一个线程,负责执行队列中的任务

java 复制代码
import java.util.concurrent.PriorityQueue;

// 1. 创建一个类,表示一个任务
class MyTask implements Comparable<MyTask> {
    // 要执行的任务逻辑
    private Runnable runnable;
    // 任务的执行时间(毫秒时间戳)
    private long time;

    public MyTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        // 当前时间 + 延迟时间 = 任务执行时间
        this.time = System.currentTimeMillis() + delay;
    }

    public void run() {
        runnable.run();
    }

    public long getTime() {
        return time;
    }

    // 优先级队列需要实现 Comparable,让时间早的任务排在前面
    @Override
    public int compareTo(MyTask o) {
        // 时间小的任务优先级更高(排在队首)
        return Long.compare(this.time, o.time);
    }
}

// 定时器类
class MyTimer {
    // 2. 用集合类(优先级队列)管理多个任务
    private final PriorityQueue<MyTask> queue = new PriorityQueue<>();

    // 锁对象,用于线程间通信
    private final Object locker = new Object();

    public MyTimer() {
        // 4. 额外创建一个线程,负责执行队列中的任务
        Thread worker = new Thread(() -> {
            while (true) {
                try {
                    synchronized (locker) {
                        // 队列为空,等待任务加入
                        while (queue.isEmpty()) {
                            locker.wait();
                        }

                        // 取出队首任务(最早要执行的)
                        MyTask task = queue.peek();
                        long now = System.currentTimeMillis();

                        if (now >= task.getTime()) {
                            // 时间到了,执行任务
                            queue.poll();
                            task.run();
                        } else {
                            // 时间还没到,等待剩余时间
                            locker.wait(task.getTime() - now);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        });
        worker.start();
    }

    // 3. 实现schedule方法,把任务添加到队列中
    public void schedule(Runnable runnable, long delay) {
        MyTask task = new MyTask(runnable, delay);
        synchronized (locker) {
            queue.offer(task);
            // 添加任务后唤醒等待的线程,重新判断队首任务的时间
            locker.notify();
        }
    }
}

// 测试代码
public class TimerDemo {
    public static void main(String[] args) {
        MyTimer timer = new MyTimer();

        System.out.println("开始执行定时任务:" + System.currentTimeMillis());

        // 延迟 1000ms 执行的任务
        timer.schedule(() -> System.out.println("任务1执行:" + System.currentTimeMillis()), 1000);
        // 延迟 2000ms 执行的任务
        timer.schedule(() -> System.out.println("任务2执行:" + System.currentTimeMillis()), 2000);
        // 延迟 500ms 执行的任务
        timer.schedule(() -> System.out.println("任务3执行:" + System.currentTimeMillis()), 500);
    }
}

五,保证线程安全的思路

**1.**使用没用共享资源的模型

**2.**适用共享资源只读,不写的模型

a,不需要写共享资源的模型

b,使用不可变对象

3.直面线程安全

a,保证原子性

b,保证顺序性

c,保证可见性

六,线程和进程对比

6.1 线程的优点

1.创建一个新线程的代价比创建一个新进程的代价小很多

2.与进程间的切换相比,线程间的切换要操作系统做的工作很少

3.线程占用资源远小于进程

4.线程可以充分利用处理器的并行计算能力

5.可在等待慢速I/O时,执行其他任务(解决I/O阻塞问题)

6.计算密集型应用,可拆分任务,适配多处理器系统

7.I/O密集型应用,将I/O操作重叠,提高并发处理能力

6.2 线程和进程的区别

1.线程是操作系统中调度的基本单位,进程是资源分配的基本单位,线程共享进程的地址空间和资源

2.进程有自己的内存地址空间(独立),线程只独享指令流执行的必要资源(共享+私有)如寄存器和栈。

  1. 由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信。

  2. 线程的创建/切换/终止效率更高

七,小结

又拖拖拖,最近只忙着赶紧往后赶进度又好久没写博客了,心碎心碎。本来准备带小咪去绝育但是查出来小咪身体有点小问题更是心塞。我从明天开始必须好好沉淀,暑假找到实习,我还要考六级。我的天,为什么之前对自己那么好,现在一座座大山压下来要命了,沉淀沉淀吧,大家都加油都要好好敲代码!!!其实离我写前半段话已经过了十几天了哈哈哈,前面一段时间只顾着看课看课了,现在才写完,后面十几天会疯狂输出已经学了的,敬请期待吧!!!

相关推荐
悟乙己1 小时前
因果推断方法实践:Python实现合成控制法
开发语言·python
147API1 小时前
Claude Opus 4.8 接口与工程落地分析:长任务调用链应该怎么设计
java·前端·数据库
lulu12165440782 小时前
Claude钩子系统架构设计:从执行时序到扩展机制
java·人工智能·python·ai编程
.千余2 小时前
【C++】C++核心语法:函数重载与缺省参数原理与避坑
c语言·开发语言·c++·经验分享·笔记·git·学习
DreamLife☼2 小时前
OpenBCI-Python与OpenBCI:实时脑电信号采集实战
开发语言·python·硬件·选型·openbci·cyton·ganglion
AI行业学习2 小时前
CC-Switch 下载、安装与使用配置指南【2026.5.29】
java·开发语言·vscode·python·eclipse·laravel
许彰午2 小时前
03_Java流程控制详解
java·开发语言·python
霍格沃兹测试学院-小舟畅学2 小时前
接口自动化测试的下一个十年:从脚本到Skills,让AI学会“如何测”
java·前端·人工智能
SoftLipaRZC2 小时前
C语言内存函数完全指南:memcpy/memmove/memset/memcmp
c语言·开发语言