Java实现根据卖票案例分析线程安全原理以及解决线程安全的三种方法

一、线程安全

  • 如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
  • 我们通过一个案例,演示线程的安全问题:
    电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共100个(本场电影只能卖100张票)。
  • 我们来模拟电影院的售票窗口,实现多个窗口同时卖 “葫芦娃大战奥特曼”这场电影票(多个窗口一起卖这100张票)需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟
    在这里插入图片描述
/*实现卖票案例*/
class RunnableImpl implements Runnable{
    //定义一个多线程共享的票源
    private int ticket = 100;

    //设置线程任务:买票
    @Override
    public void run() {
        //使用死循环,让卖票操作重复执行
        while (true){
            //先判断票是否存在
            if (ticket>0){
                //提高安全问题出现的概率,让程序睡眠
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //票存在,卖票
                System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                ticket--;
            }
        }
    }
}

/*模拟卖票案例:创建3个线程,同时开启,对共享的票进行出售*/
public class Demo01Test{
    public static void main(String[] args) {
        //创建Runnable接口的实现类
        RunnableImpl run = new RunnableImpl();
        //创建Thread类对象,构造方法中传递Runnable接口的实现类对象
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        //调用start方法开启多线程
        t0.start();
        t1.start();
        t2.start();
    }
}

经过运行,发现程序出现了两个问题:1. 相同的票数,比如5这张票被卖了两回。2. 不存在的票,比如0票与-1票,是不存在的。这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。
在这里插入图片描述

1.1 线程同步

  • 当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。
  • 要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。
  • 有三种方式完成同步操作:①、 同步代码块。②、 同步方法。③、 锁机制。

(一)、解决线程安全问题的第一种方案:使用同步代码块

格式:

synchronized(锁对象){
	可能会出现线程安全问题的代码(访问了共享数据的代码)
}
  • 注意:
    ①、通过代码块中的锁对象,可以使用任意的对象。
    ②、但是必须保证多个线程使用的锁对象是同一个。
    ③、锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行
/*实现卖票案例*/
class RunnableImpl implements Runnable{
    //定义一个多线程共享的票源
    private int ticket = 100;

    //1、创建一个锁对象
    Object obj = new Object();

    //设置线程任务:买票
    @Override
    public void run() {
        //使用死循环,让卖票操作重复执行
        while (true){
            //2、创建同步代码块
            synchronized (obj){
                //先判断票是否存在
                if (ticket>0){
                    //提高安全问题出现的概率,让程序睡眠
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //票存在,卖票
                    System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                    ticket--;
                }
            }
        }
    }
}
/*模拟卖票案例:创建3个线程,同时开启,对共享的票进行出售*/
public class Demo01Test{
    public static void main(String[] args) {
        //创建Runnable接口的实现类
        RunnableImpl run = new RunnableImpl();
        //创建Thread类对象,构造方法中传递Runnable接口的实现类对象
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        //调用start方法开启多线程
        t0.start();
        t1.start();
        t2.start();
    }
}

同步技术的原理:
在这里插入图片描述

(二)、解决线程安全问题第二种方案:使用同步方法

  • 使用步骤:
    ①、把访问了共享数据的代码抽取出来,放到一个方法中。
    ②、在方法上添加synchronized修饰符
  • 格式:定义方法的格式
修饰符 synchronized 返回值类型 方法名(参数列表){
	可能会出现线程安全问题的代码(访问了共享数据的代码)
}

定义一个方法,同步方法也会把方法内部的代码锁住,只让一个线程执行,同步方法的锁对象是实现类对象new RunnableImpl()也就是this。

/*实现卖票案例*/
class RunnableImpl implements Runnable{
    //定义一个多线程共享的票源
    private int ticket = 100;
    //设置线程任务:买票
    @Override
    public void run() {
        System.out.println("this:"+this);
        //使用死循环,让卖票操作重复执行
        while (true){
            //2.调用同步方法
            payTicket();
        }
    }
    /*1.定义一个同步方法*/
    public synchronized void payTicket(){
        //先判断票是否存在
        if (ticket>0){
            //提高安全问题出现的概率,让程序睡眠
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //票存在,卖票
            System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
            ticket--;
        }
    }
}

/*模拟卖票案例:创建3个线程,同时开启,对共享的票进行出售*/
public class Demo01Test{
    public static void main(String[] args) {
        //创建Runnable接口的实现类
        RunnableImpl run = new RunnableImpl();
        System.out.println("run:"+run);
        //创建Thread类对象,构造方法中传递Runnable接口的实现类对象
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        //调用start方法开启多线程
        t0.start();
        t1.start();
        t2.start();
    }
}

下面验证同步方法的锁对象是实现类对象new RunnableImpl()也就是this;将上面的代码修改为下面的代码,照样可以运行

public static /*synchronized*/ void payTicketStatic(){
        synchronized (this){
            //先判断票是否存在
            if (ticket>0){
                //提高安全问题出现的概率,让程序睡眠
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //票存在,卖票
                System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                ticket--;
            }
        }
    }

静态的同步方法,锁对象不能是this,this是创建对象之后产生的,静态方法优先于对象,静态方法的锁对象就是本类的class属性—>class文件对象(反射)

class RunnableImpl implements Runnable{
    //2.定义一个多线程共享的静态的票源
    private static int ticket = 100;
    //设置线程任务:买票
    @Override
    public void run() {
        System.out.println("this:"+this);
        //使用死循环,让卖票操作重复执行
        while (true){
            //3.调用静态的同步方法
            payTicketStatic();
        }
    }
    /*1.定义一个静态的同步方法*/
    public static synchronized void payTicketStatic(){
        //先判断票是否存在
        if (ticket>0){
            //提高安全问题出现的概率,让程序睡眠
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //票存在,卖票
            System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
            ticket--;
        }
    }
}
/*模拟卖票案例:创建3个线程,同时开启,对共享的票进行出售*/
public class Demo01Test{
    public static void main(String[] args) {
        //创建Runnable接口的实现类
        RunnableImpl run = new RunnableImpl();
        System.out.println("run:"+run);
        //创建Thread类对象,构造方法中传递Runnable接口的实现类对象
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        //调用start方法开启多线程
        t0.start();
        t1.start();
        t2.start();
    }
}

下面验证静态方法的锁对象就是本类的class属性;将上面的代码修改为下面的代码,照样可以运行

public static /*synchronized*/ void payTicketStatic(){
        synchronized (RunnableImpl.class){
            //先判断票是否存在
            if (ticket>0){
                //提高安全问题出现的概率,让程序睡眠
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //票存在,卖票
                System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                ticket--;
            }
        }
    }

(三)、解决线程安全问题第三种方案:Lock锁

  • java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
  • Lock锁也称同步锁,加锁与释放锁方法化了,如下:
    public void lock() :加同步锁。
    public void unlock() :释放同步锁。
  • 实现步骤:
    ①、在成员位置创建一个ReentrantLock对象。
    ②、在可能会出现安全问题的代码前调用Lock接口中的方法Lock获取锁。
    ③、在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/*实现卖票案例*/
class RunnableImpl implements Runnable{
    //定义一个多线程共享的票源
    private  int ticket = 100;

    //1.在成员位置创建一个ReentrantLock对象
    Lock l = new ReentrantLock();

    //设置线程任务:买票
    @Override
    public void run() {
        //使用死循环,让卖票操作重复执行
        while (true){
            //2.在可能会出现安全问题的代码前调用Lock接口中的方法Lock获取锁。
            l.lock();
            //先判断票是否存在
            if (ticket>0){
                //提高安全问题出现的概率,让程序睡眠
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //票存在,卖票
                System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                ticket--;
            }
            //3.在可能会出现安全问题的代码后调用Lock接口中的方法unLock获取锁。
            l.unlock();
        }
    }
}

/*模拟卖票案例:创建3个线程,同时开启,对共享的票进行出售*/
public class Demo01Test{
    public static void main(String[] args) {
        //创建Runnable接口的实现类
        RunnableImpl run = new RunnableImpl();
        System.out.println("run:"+run);
        //创建Thread类对象,构造方法中传递Runnable接口的实现类对象
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        //调用start方法开启多线程
        t0.start();
        t1.start();
        t2.start();
    }
}

也可以把unlock放在finally块中,可以按照下面编写:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class RunnableImpl implements Runnable{
    private  int ticket = 100;
    //1.在成员位置创建一个ReentrantLock对象
    Lock l = new ReentrantLock();
    //设置线程任务:买票
    @Override
    public void run() {
        //使用死循环,让卖票操作重复执行
        while (true){
            //2.在可能会出现安全问题的代码前调用Lock接口中的方法Lock获取锁。
            l.lock();
            //先判断票是否存在
            if (ticket>0){
                //提高安全问题出现的概率,让程序睡眠
                try {
                    Thread.sleep(10);
                    //票存在,卖票
                    System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //3.在可能会出现安全问题的代码后调用Lock接口中的方法unLock获取锁。
                    l.unlock();//无论程序是否异常,都会把锁释放
                }
            }
        }
    }
}
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值