线程安全问题

线程卖票案例分析

有线程安全问题代码

java 复制代码
public class Ticket implements Runnable {
    //成员变量
    private int ticketCount = 100;
    @Override
    public void run() {
        while (true) {
            if (ticketCount > 0) {
                //模拟出票时间,让当前线程休息100毫秒
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + "售出票号:" + ticketCount);
                ticketCount--;//票数减一
            }
            if(ticketCount==0){
                break;//票数为0时退出循环
            }
        }
    }
}


public class TicketDemo {
    public static void main(String[] args) {
        //创建线程任务对象
        Ticket ticket = new Ticket();

        //创建三个买票窗口(三个线程在执行同一个任务)
        Thread t1 = new Thread(ticket, "窗口1 - ");
        Thread t2 = new Thread(ticket, "窗口2 - ");
        Thread t3 = new Thread(ticket, "窗口3 - ");

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

线程安全问题:卖票案例中重复票出现的原因,负数票出现的原因

发生线程安全问题的原因: 多个线程对同一个数据,进行读写操作,造成数据错乱


线程安全问题的解决

基本思想:

让共享数据存在安全的环境中 , 当某一个线程访问共享数据时其他线程是无法操作的

怎么实现呢?

  • 把多条线程操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
  • java语言基于线程安全问题,提供了:同步机制

线程的同步

java允许多线程并发执行,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证该变量的唯一性和准确性。

三种实现同步的方式:【同步机制之所以能够实现,是因为其加了同步锁】

  1. 同步代码块

  2. 同步方法

  3. 锁机制。Lock锁

1. 同步代码块

格式:

java 复制代码
synchronized(任意对象) { 
               多条语句操作共享数据的代码 
       }
synchronized:关键字,表示同步
任意对象:锁对象

a.默认情况锁是打开的,只要有一个线程进去执行代码了,锁就会关闭
b.当线程执行完出来了,锁才会自动打开
c.锁对象可以是任意对象 , 但是多个线程必须使用同一把锁
解决上面的售票安全问题:将对共享数据操作的代码都放到同步代码块中
java 复制代码
public class Ticket implements Runnable {
    //成员变量
    private int ticketCount = 100;
    @Override
    public void run() {
        while (true) {
            //使用同步代码块解决,线程安全问题
//            this:锁对象,是任意类型的对象,也可以是"锁对象"在括号中
            synchronized (this) {
                if (ticketCount > 0) {
                    //模拟出票时间,让当前线程休息100毫秒
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + "售出票号:" + ticketCount);
                    ticketCount--;//票数减一
                }
                if (ticketCount == 0) {
                    break;//票数为0时退出循环
                }
            }
        }
    }
}

public class TicketDemo {
    public static void main(String[] args) {
        //创建线程任务对象
        Ticket ticket = new Ticket();

        //创建三个买票窗口(三个线程在执行同一个任务)
        Thread t1 = new Thread(ticket, "窗口1 - ");
        Thread t2 = new Thread(ticket, "窗口2 - ");
        Thread t3 = new Thread(ticket, "窗口3 - ");

        t1.start();
        t2.start();
        t3.start();
    }
}
同步的好处和弊端

好处:解决了多线程的数据安全问题

弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

2. 同步方法

同步方法:就是把synchronized关键字加到方法上,保证线程执行该方法的时候,其他线程只能在方法外等着【该方法多个线程并发访问时只能唯一访问】

格式:

java 复制代码
格式:
       修饰符 synchronized 返回值类型 方法名(方法参数) {    }

 同步方法也有对象锁,不需要我们自己写:
    1. this锁  (当前对象锁)
    2. 类名.class (静态方法上的对象锁)

注意 : 同步方法时不能指定锁对象的 , 但是有默认存在的锁对象的。

  • 对于非static方法,同步锁就是this。
  • 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。 Class类型的对象
解决上面的售票安全问题:使用同步方法解决问题
java 复制代码
//线程任务类
public class Ticket implements Runnable {
    //成员变量
    private int ticketCount = 100;
    @Override
    public void run() {
        //模拟卖票
        while(true){
            if(ticketCount <= 0){
                break;
            }
            method();
        }
    }
    //使用同步方法解决线程安全问题(方法中的代码都是同步代码)
    public synchronized void method(){
        if(ticketCount > 0){
            //模拟出票时间
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "出售票号:" + ticketCount);
            ticketCount--;
        }
    }
}

public class TicketDemo {
    public static void main(String[] args) {
        //创建线程任务对象
        Ticket ticket = new Ticket();

        //创建三个买票窗口(三个线程在执行同一个任务)
        Thread t1 = new Thread(ticket, "窗口1 - ");
        Thread t2 = new Thread(ticket, "窗口2 - ");
        Thread t3 = new Thread(ticket, "窗口3 - ");

        t1.start();
        t2.start();
        t3.start();
    }
}
同步代码块和同步方法的区别:

同步代码块可以锁住指定代码,同步方法是锁住方法中所有代码

同步代码块可以指定锁对象,同步方法不能指定锁对象

3. 锁机制。Lock锁

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

Lock中提供了获得锁和释放锁的方法
java 复制代码
void lock():获得锁
void unlock():释放锁
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
java 复制代码
ReentrantLock的构造方法
    ReentrantLock():创建一个ReentrantLock的实例
注意:多个线程使用相同的Lock锁对象,需要多线程操作数据的代码放在lock()和unLock()方法之间。一定要确保unlock最后能够调用
解决上面的售票安全问题:使用Lock锁解决问题
java 复制代码
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Ticket implements Runnable {
    //成员变量
    private int ticketCount = 100;
    //锁对象
    Lock l = new ReentrantLock();
    @Override
    public void run() {
        while (true) {
            //使用Lock锁解决,线程安全问题
            //获取锁
            l.lock();
                if (ticketCount > 0) {
                    //模拟出票时间,让当前线程休息100毫秒
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + "售出票号:" + ticketCount);
                    ticketCount--;//票数减一
                }
                if (ticketCount == 0) {
                    break;//票数为0时退出循环
                }
            //释放锁
            l.unlock();
        }
    }
}

public class TicketDemo {
    public static void main(String[] args) {
        //创建线程任务对象
        Ticket ticket = new Ticket();

        //创建三个买票窗口(三个线程在执行同一个任务)
        Thread t1 = new Thread(ticket, "窗口1 - ");
        Thread t2 = new Thread(ticket, "窗口2 - ");
        Thread t3 = new Thread(ticket, "窗口3 - ");

        t1.start();
        t2.start();
        t3.start();
    }
}
使用Lock锁实现同步安全的步骤

1.创建Lock锁对象

2.调用lock()方法获得锁

3.调用unLock()释放锁

相关推荐
怀旧诚子3 小时前
timeshift之Fedora43设置,已在VM虚拟机验证,待真机验证。
java·服务器·数据库
1104.北光c°3 小时前
滑动窗口HotKey探测机制:让你的缓存TTL更智能
java·开发语言·笔记·程序人生·算法·滑动窗口·hotkey
for_ever_love__4 小时前
Objective-C学习 NSSet 和 NSMutableSet 功能详解
开发语言·学习·ios·objective-c
云原生指北6 小时前
GitHub Copilot SDK 入门:五分钟构建你的第一个 AI Agent
java
似水明俊德10 小时前
02-C#.Net-反射-面试题
开发语言·面试·职场和发展·c#·.net
Leinwin10 小时前
OpenClaw 多 Agent 协作框架的并发限制与企业化规避方案痛点直击
java·运维·数据库
薛定谔的悦10 小时前
MQTT通信协议业务层实现的完整开发流程
java·后端·mqtt·struts
enjoy嚣士10 小时前
springboot之Exel工具类
java·spring boot·后端·easyexcel·excel工具类
Thera77711 小时前
C++ 高性能时间轮定时器:从单例设计到 Linux timerfd 深度优化
linux·开发语言·c++
罗超驿11 小时前
独立实现双向链表_LinkedList
java·数据结构·链表·linkedlist