多线程同步问题及解决

多线程安全问题案例

线程在执行的时候,cpu的执行权有可能随时被抢走;

1、所以如果是在多线程的环境下,会出现一些安全的问题。 以下面的需求为例:

某电影院目前正在上映国产大片,共有100张票,而它有3个窗口买票,请设计一个程序模拟该电影院买票。

这里面的安全保证得是:每一张票是由某个窗口卖出去的,而且卖了就没有了。这都得用同步机制才能实现。 如果不用锁机制实现线程同步,上面的安全问题就无法得到保障。

例如下面的代码:

java 复制代码
public class UnsafeTicketSale extends Thread {
    // 共享资源:100张票
    private static int ticket = 0;
​
    @Override
    public void run() {
        while (true) {
            if (ticket < 100) {
                try {
                    // 模拟网络延迟,放大线程安全问题
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket++;
                System.out.println(getName() + " 卖出第 " + ticket + " 张票");
            } else {
                break;
            }
        }
    }
​
    public static void main(String[] args) {
        UnsafeTicketSale window1 = new UnsafeTicketSale();
        UnsafeTicketSale window2 = new UnsafeTicketSale();
        UnsafeTicketSale window3 = new UnsafeTicketSale();
​
        window1.setName("窗口1");
        window2.setName("窗口2");
        window3.setName("窗口3");
​
        window1.start();
        window2.start();
        window3.start();
    }
}

部分结果是:

复制代码
窗口1 卖出第 2 张票
窗口2 卖出第 2 张票
窗口3 卖出第 3 张票

可以看到,第 2 张票被两个窗口同时卖出,出现了线程安全问题。

使用同步代码块解决

我们可以 使用同步代码块解决线程安全问题 即可以使用一个锁对象 SafeTicketSaleWithBlock.class(类对象),保证三个线程共用同一把锁。 当一个线程进入同步代码块时,其他线程必须等待锁释放,确保 ticket++ 操作的原子性。 从而运行结果中不会出现重复卖票或超卖的情况。

java 复制代码
public class SafeTicketSaleWithBlock extends Thread {
    private static int ticket = 0;
​
    @Override
    public void run() {
        while (true) {
            // 同步代码块:锁对象为类对象,保证所有线程共用同一把锁
            synchronized (SafeTicketSaleWithBlock.class) {
                if (ticket < 100) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket++;
                    System.out.println(getName() + " 卖出第 " + ticket + " 张票");
                } else {
                    break;
                }
            }
        }
    }

使用同步方法解决

同步方法的核心就是在方法声明中加上 synchronized 关键字,它会自动对整个方法体进行加锁。

  1. 非静态同步方法

    java 复制代码
    修饰符 synchronized 返回值类型 方法名(参数列表) {
     // 线程安全的代码
    }
    ​

    锁对象:默认是 this,也就是调用该方法的对象本身。

  2. 静态同步方法

    java 复制代码
    修饰符 static synchronized 返回值类型 方法名(参数列表) {
     // 线程安全的代码
    }

    锁对象:默认是当前类的字节码对象(类名.class),这个对象在全局是唯一的。

解决示例:

java 复制代码
public class SafeTicketSaleWithMethod implements Runnable {
    private int ticket = 0;
​
    // 同步方法:锁对象为 this(当前对象)
    private synchronized void sellTicket() {
        if (ticket < 100) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticket++;
            System.out.println(Thread.currentThread().getName() + " 卖出第 " + ticket + " 张票");
        }
    }
​
    @Override
    public void run() {
        while (ticket < 100) {
            sellTicket();
        }
    }
​
    public static void main(String[] args) {
        SafeTicketSaleWithMethod task = new SafeTicketSaleWithMethod();
​
        Thread window1 = new Thread(task, "窗口1");
        Thread window2 = new Thread(task, "窗口2");
        Thread window3 = new Thread(task, "窗口3");
​
        window1.start();
        window2.start();
        window3.start();
    }
}

讨论

1、可以看到synchronized (SafeTicketSaleWithBlock.class) 使用的锁对象是字节码文件;这个类可以是任意的类;因为无论哪个类的字节码文件都是唯一的,而我们只需要保证锁这个资源是唯一的就行,无其他要求

2、为什么类的字节码对象(XXX.class)是唯一的?

当一个类被 JVM 加载完成后,会在内存中生成一个对应的「Class 对象」(也就是你说的字节码文件对象),这个对象全局唯一,无论你创建多少个该类的实例,对应的 Class 对象只有一个。

3、 锁的范围

同步方法的锁范围是整个方法体,无法像同步代码块那样精确控制锁的范围。

如果方法体中只有一小部分代码需要同步,使用同步代码块性能更高。

特性 同步方法 同步代码块
语法 简单,只需加 synchronized 关键字 需显式指定锁对象 synchronized(锁对象){...}
锁对象 固定(this类名.class 可以自定义,更灵活
锁范围 整个方法体 可以精确控制到代码块
性能 略低(锁范围大) 更高(锁范围小)
可读性 高(一眼看出是同步方法) 稍低(需看锁对象)

s

相关推荐
gugucoding几秒前
38. 【Java】Stream API(下):高级操作与性能
java·开发语言
Lhan.zzZ8 分钟前
在 Visual Studio 2022 中打造可扩展的动态链接库模块:从零搭建到原理解析
开发语言·c++·visual studio
心平气和量大福大28 分钟前
android-实例-蒲公英-更新与安装-5-签名与生成APK
android·java·开发语言
码哥DFS42 分钟前
构造函数、实例对象、对象原型 ----三者关系
开发语言·javascript·原型模式
952361 小时前
Spring-cloud配置中心
java·spring·spring cloud·微服务
quantdash_cc1 小时前
告别自建 Requests/BS4 网页爬虫:基于 QuantDash 搭建零维保的高性能量化行情流水线
开发语言·爬虫·python·pandas·量化·quantdash
ydd1001001 小时前
寻找两个正序数组的中位数 Java 题解,二分分割详解
java·开发语言
半亩码田2 小时前
C#转Python第3.1篇:Python 的 class 没有访问修饰符?面向对象的另一条路
开发语言·python·c#
真上帝的左手2 小时前
10. 软件设计&架构-Spring Security 7 整合CAS SSO 单点登录
java·spring·架构·sso
Wzx1980123 小时前
python沙箱和docker沙箱你选对了吗?
开发语言·python·docker