谈谈线程安全问题及其解决方法

本文讲述一下线程的安全问题及解决方法。

一 线程安全问题举例说明
在电影院售票的场景,假使有100张票待售,有3个窗口在售票,如图所示:
在这里插入图片描述

三个窗口都卖出票1,一个票被卖了3次,多线程访问共享数据“票”,出现了线程安全问题。
问题代码见下:

public class SellTicketImpl1 implements Runnable {
    
    
    private int ticket=100;
    @Override
    public void run() {
    
    
        while (true){
    
    
            if(ticket>0){
    
    
                System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
                ticket--;
            }
        }
    }
}
public class ThreadSafeDemo1 {
    
    
    public static void main(String[] args) {
    
    
        SellTicketImpl1 sellTicket=new SellTicketImpl1();
        Thread t1=new Thread(sellTicket);
        Thread t2=new Thread(sellTicket);
        Thread t3=new Thread(sellTicket);
        t1.start();
        t2.start();
        t3.start();
    }

}

运行后出现下图所示:
在这里插入图片描述

二 解决线程安全问题三种方式

为了保证每个线程都能正常执行原子操作,java引入了线程同步机制。
有三种方式完成同步操作:

  1. 同步代码块
  2. 同步方法
  3. 锁机制

下面依次讲述三种同步操作:

1 同步代码块

1)说明:
解决线程安全问题的第一种方案是使用同步代码块,同步代码块的格式见下:

synchronized(锁对象){
    
    
       可能会出现线程安全问题的代码(访问了共享数据的代码)
   }

注意:

通过代码块中的锁对象,可以使用任意的对象,例如Object类对象;
但是必须保证多个线程使用的锁对象是同一个锁对象:把同步代码块锁住,只让一个线程在同步代码块中执行。
2)代码

public class SellTicketImpl2 implements Runnable {
    
    

    private int ticket=100;
    //创建一个Object类对象,当做锁对象
    Object obj=new Object();
    @Override
    public void run() {
    
    
        while (true){
    
    
            //使用synchronized同步代码块
            synchronized (obj) {
    
    
                if (ticket > 0) {
    
    
                    try {
    
    
                        //让它睡眠1秒
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                    ticket--;
                }
            }
        }
    }
}
public class ThreadSafeDemo2 {
    
    
    public static void main(String[] args) {
    
    
        SellTicketImpl2 sellTicket=new SellTicketImpl2();
        Thread t1=new Thread(sellTicket);
        Thread t2=new Thread(sellTicket);
        Thread t3=new Thread(sellTicket);
        t1.start();
        t2.start();
        t3.start();
    }

}
  1. 同步方法

1)说明
解决线程安全问题的第二种方案是使用同步方法,同步方法的格式见下:

 修饰符 synchronized 返回值类型 方法名(参数列表){
    
    
     可能会出现线程安全问题的代码(访问了共享数据的代码)
 }

2)代码

public class SellTicketImpl3 implements Runnable  {
    
    

    //定义一个多个线程共享的票源
    private int ticket=100;
    //设置线程任务:卖票
    @Override
    public void run() {
    
    
        while (true) {
    
    
            sellTicket();
        }
    }

    //定义同步方法
    public synchronized void sellTicket(){
    
    
        //使用死循环,让卖票重复操作执行
        while(true){
    
    
            //先判断票是否存在
            if(ticket>0){
    
    
                try {
    
    
                    //提高安全问题出现的概率,让程序睡眠
                    Thread.sleep(1);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                //如果票存在,卖票ticket
                System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
                ticket--;
            }
        }
    }
}
public class ThreadSafeDemo3 {
    
    
    public static void main(String[] args) {
    
    
        SellTicketImpl3 sellTicket=new SellTicketImpl3();
        Thread t1=new Thread(sellTicket);
        Thread t2=new Thread(sellTicket);
        Thread t3=new Thread(sellTicket);
        t1.start();
        t2.start();
        t3.start();
    }
}

3 Lock锁

1)说明:解决线程安全问题的第三种方案是使用Lock锁,即java.util.concurrent.locks.Lock接口。
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
Lock 接口中的方法有:
void lock() 获取锁
void unlock() 释放锁

2)使用步骤:

(1)在成员位置创建一个ReentrantLock 对象;
(2)在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁;
(3)在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁;

3)代码

public class SellTicketImpl4 implements Runnable {
    
    

    //定义一个多个线程共享的票源
    private int ticket=100;
    //1.在成员位置创建一个ReentrantLock对象
    //使用多态
    Lock lk=new ReentrantLock();
    //设置线程任务:卖票
    @Override
    public void run() {
    
    
        while (true){
    
    
            //使用死循环,让卖票重复操作执行
            while(true){
    
    
                //2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
                lk.lock();
                //先判断票是否存在
                if(ticket>0){
    
    
                    try {
    
    
                        //提高安全问题出现的概率,让程序睡眠
                        Thread.sleep(10);
                        //如果票存在,卖票ticket
                        System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
                        ticket--;
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }finally {
    
    
                        //3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
                        lk.unlock();   //无论是否出现异常,都释放锁
                    }

                }

            }
        }
    }
}
public class ThreadSafeDemo4 {
    
    
    public static void main(String[] args) {
    
    
        SellTicketImpl4 sellTicket=new SellTicketImpl4();
        Thread t1=new Thread(sellTicket);
        Thread t2=new Thread(sellTicket);
        Thread t3=new Thread(sellTicket);
        t1.start();
        t2.start();
        t3.start();
    }
}

运行后,如下图所示:
在这里插入图片描述

三 同步技术原理:

n个线程t1,t2,t3,…,tn,一起抢夺cpu的执行权,谁抢到了谁执行run方法进行卖票。假设t1抢到了cpu的执行权,执行run方法,遇到synchronized代码块,这时t1会检查synchronized代码块是否有锁对象(上述的obj),如果有,就会获取到锁对象,进入到同步执行;这时候t2也抢到了cpu执行权但是没有检查到锁对象,那么t2就会进入阻塞状态,会一直等待t1线程归还锁对象,一直到t1线程执行完同步中的代码,会把对象归还给synchronized代码块,t2才能获取到锁对象进入到同步中执行。

总结:同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步。同步保证了只能有一个线程在同步中执行共享数据。

猜你喜欢

转载自blog.csdn.net/helloworldchina/article/details/128758549