当前位置: 首页 > news >正文

Java多线程——线程同步

线程同步

基本概念

线程同步指的是在多线程环境下,通过某种机制来协调多个线程对共享资源的访问,确保在同一时刻只有一个线程能够访问共享资源,从而避免数据不一致和其他并发问题。可以将其类比为多个线程在使用同一台打印机,为了避免打印内容混乱,需要规定一次只能有一个线程使用打印机,这个规定的过程就是线程同步。

存在以下问题

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起。
  • 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题。

产生问题的原因

当多个线程同时访问和修改共享资源时,可能会出现以下问题:

  • 数据不一致:例如,一个线程正在读取共享变量的值,而另一个线程同时在修改这个变量的值,就可能导致读取到的数据是错误的。
  • 竞态条件:多个线程对共享资源的操作顺序不确定,可能会导致程序的执行结果出现不可预测的情况。

实现线程同步的方式

1.synchronized 关键字(本质利用队列和锁)
  • 修饰实例方法:当 synchronized 修饰实例方法时,同一时刻只有一个线程能够访问该方法所属对象的这个方法。锁对象是当前对象实例。(一般是修饰那些会修改共享资源的方法,其他只读的方法可以不用修饰)

“锁对象是当前对象实例”,这句话的意思是把当前这个对象实例上锁,操作这个对象都要有锁才行,当有线程对这个对象进行操作时,这个对象就被锁上了,只有等这个线程结束操作这个对象才会释放锁,别的线程才能够对这个对象进行操作。

class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

在上述代码中,increment() 方法被 synchronized 修饰,当一个线程进入该方法时,会自动获取 Counter 对象的锁,其他线程必须等待该线程执行完方法并释放锁后才能进入。

  • 修饰静态方法:如果 synchronized 修饰的是静态方法,那么锁对象是该类的 Class 对象。所有线程在访问该静态方法时,都需要竞争同一个锁。

回想之前的线程休眠,是不是提到过:"每一个对象都有一个锁,sleep不会释放锁“,这句话的意思就是假设a线程占领了这个锁,就算此时a线程调用sleep方法休眠了,但是由于锁没有释放,其他需要该锁的线程仍然不能执行。

class StaticCounter {
    private static int count = 0;

    public static synchronized void increment() {
        count++;
    }

    public static int getCount() {
        return count;
    }
}

这里的 increment() 静态方法被 synchronized 修饰,所有线程调用该方法时都要竞争 StaticCounter 类的锁。

  • 修饰代码块:可以指定一个对象作为锁,只有获取到该对象锁的线程才能执行代码块中的内容。

当使用 synchronized 修饰实例方法时,锁的对象是当前对象实例(this);修饰静态方法时,锁的对象是类的 Class 对象。如果共享资源不属于当前对象实例或者类,而是其他对象的数据,使用 synchronized 修饰方法可能无法达到预期的同步效果,因为此时锁错了对象。而修饰代码块可以自己指定锁的对象。

例如,有两个类 AB,类 B 中有一个共享资源,而类 A 中的方法需要操作类 B 的这个共享资源。如果在类 A 的方法上加 synchronized,锁的是类 A 的实例对象,而不是类 B 的实例对象,其他线程仍然可以同时操作类 B 的共享资源,无法实现同步。

class B {
    int sharedResource = 0;
}

class A {
    B b;

    public A(B b) {
        this.b = b;
    }

    // 错误示例:锁的是A的实例对象,但是我们其实操作的共享数据是B类里面的,无法对B的共享资源同步,
    public synchronized void wrongModify() {
        b.sharedResource++;
        System.out.println(b.sharedResource);
    }

    // 正确示例:使用synchronized代码块,锁B的实例对象
    // 其实区别不大,就是把原来修饰方法的synchronized关键字,放到方法内部去修饰块,而块里的内容就是原来方法体里的代码,只是主要把要锁的对象给synchronized关键字
    public void correctModify() {
        synchronized (b) {
            b.sharedResource++;
            System.out.println(b.sharedResource);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        B b = new B();
        A a = new A(b);

        // 创建两个线程调用wrongModify方法,无法同步
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.wrongModify();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.wrongModify();
            }
        });

        // 创建两个线程调用correctModify方法,可以同步
        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.correctModify();
            }
        });
        Thread t4 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.correctModify();
            }
        });

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

在上述代码中,wrongModify 方法使用 synchronized 修饰,锁的是 A 的实例对象,无法对 B 的共享资源进行同步;而 correctModify 方法使用 synchronized 代码块,锁的是 B 的实例对象,可以实现对 B 的共享资源的同步访问。

2.ReentrantLock(可重入锁)
  • 简介ReentrantLockjava.util.concurrent.locks 包下的一个类,它是一个可重入的互斥锁,功能与 synchronized 类似,但提供了更灵活的锁机制。是显式的加锁,看起来更好理解。
  • 使用方式:
    • 加锁与解锁:通过 lock() 方法获取锁,unlock() 方法释放锁。为确保锁最终能被释放,通常将 unlock() 方法放在 finally 块中。
    • 可中断锁:支持可中断的锁获取模式,通过 lockInterruptibly() 方法实现,当线程在等待锁的过程中可以被中断。
    • 尝试获取锁:使用 tryLock() 方法可以尝试获取锁,如果锁可用则获取并返回 true,否则返回 false,不会阻塞线程。
  • 示例代码
package com.demo01;

import java.util.concurrent.locks.ReentrantLock;

public class ThreadLock {
    public static void main(String[] args) {
        station station = new station();
        // 开启三个线程
        new Thread(station).start();
        new Thread(station).start();
        new Thread(station).start();

    }
}
class station implements Runnable {
    private int ticketNumes = 10;
    private final ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while(true) {
            try{
            lock.lock();
            if(ticketNumes > 0) {
                System.out.println(ticketNumes--);

                    Thread.sleep(1000);

            }else{
                break;
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }finally {
                lock.unlock();
            }
        }
    }
}
  1. 整体架构ThreadLock 类的 main 方法作为程序入口,创建 station 类实例并启动三个线程,让它们共享该实例执行售票任务。
  2. station:实现 Runnable 接口,包含车票数量 ticketNumesReentrantLock 类型的锁 lock
  3. run 方法逻辑
    • 进入无限循环,先调用 lock.lock() 获取锁,保证同一时刻只有一个线程能执行后续操作。
    • 检查车票数量,若大于 0 则打印车票号并减 1,模拟售票,线程休眠 1 秒模拟处理时间。
    • 若车票售罄,跳出循环结束线程。
    • 无论是否异常,在 finally 块调用 lock.unlock() 释放锁,确保锁资源正确释放。

注意事项:

在这段代码中,ReentrantLocklock 方法锁的不是传统意义上类似 synchronized 中隐式的 this 对象,它锁的是创建的 ReentrantLock 实例 lock 本身所代表的锁资源。所以当锁(即ReentrantLock lock)已经被其他线程持有时,当前线程会被阻塞,进入等待状态。它会一直等待,直到持有锁的线程释放锁,然后当前线程才有机会获取到锁并继续执行后续代码。

Lock 锁的加锁 lock() 方法和解锁 unlock() 方法一般和 try{} catch{} finally{} 语句块一起使用,这样不管是否有异常发生,都能保证在加锁后,即使在临界区代码执行过程中抛出异常,也可以在 finally 块中调用 unlock() 方法来释放锁,避免因异常导致锁无法释放而造成死锁,从而确保其他线程能够正常获取该锁并继续执行后续操作。

http://www.dtcms.com/a/71323.html

相关文章:

  • 【DeepSeek应用】DeepSeek模型本地化部署方案及Python实现
  • 从零实现Kafka延迟队列:Spring Boot整合实践与原理剖析
  • Golang倒腾一款简配的具有请求排队功能的并发受限服务器
  • 【mysql】centOS7安装mysql详细操作步骤!—通过tar包方式
  • 系统架构设计师—案例分析—数据库篇—关系型数据库设计
  • 蓝桥杯Python赛道备赛——Day5:算术(一)(数学问题)
  • NO.39十六届蓝桥杯备战|结构体八道练习|加号小于号运算符重载|自定义排序(C++)
  • 如何设计可扩展、高可靠的移动端系统架构?
  • 选择循环汇编
  • 2023华东师范大学计算机复试上机真题
  • PHP中的命令行工具开发:构建高效的脚本与工具
  • 具身沟通——机器人和人类如何通过物理交互进行沟通
  • C# 模块里cctor函数: mono_runtime_run_module_cctor
  • c语言笔记 字符串函数---strcmp,strncmp,strchr,strrchr
  • Django REST Framework 中 ModelViewSet 的接口方法及参数详解,继承的方法和核心类方法,常用查询方法接口
  • UDP Socket
  • 复试不难,西电马克思主义学院—考研录取情况
  • vanna+deepseekV3+streamlit本地化部署
  • harmony Next 基础知识点1
  • 以太网 MAC 帧格式
  • P1540 [NOIP 2010 提高组] 机器翻译
  • RTDETR融合[CVPR2025]ARConv中的自适应矩阵卷积
  • .NET Framework华为云流水线发布
  • MKS HA-MFV:半导体制造中的高精度流量验证技术解析
  • 如何撰写一份清晰专业的软件功能测试报告
  • Next.js项目MindAI教程 - 第一章:环境准备与项目初始化
  • 硬件与软件的边界-从单片机到linux的问答详解
  • python速通小笔记-------1.容器
  • 全网第一提出:WIFI 透传串口模块都可以用于px4连接QGC上位机调试。
  • 论Linux进程间通信