JAVA线程安全问题解决方案

1,787 阅读7分钟

当多个线程访问同个一资源的时候,就会出现线程安全问题。比如售票系统,多个线程同时访问库存,如果没有做到线程的同步就很有可能会出现超卖(比如卖了-1这个号的票),或者重复卖(票号为2的卖了多次)的问题。

问题出现

**线程测试类**
package jd.com.ThreadSafe;

public class demo1ThreadSafe {
    public static void main(String[] args) {
        //生成一个实现类对象,只能生成一个实现类对象
        RunnableImpl run = new RunnableImpl();

        //启动3个线程
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        Thread t3 = new Thread(run);

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

**线程实现类**
package jd.com.ThreadSafe;

/*
模拟卖票
 */
public class RunnableImpl implements Runnable {
    int ticket = 10;
    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(10);
                if(ticket > 0){
                    System.out.println(Thread.currentThread().getName() + " saling : " + ticket + " ticket.");
                    ticket--;
                }
                else{
                    break;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

执行结果: 出现了重复卖的异常情况,因为CPU执行权是高速切换的,当线程A执行到sleep(10)时,此时ticket=10, CPU执行权切换到了线程B, 此时B拿到的ticket=10,线程B执行到sleep(10)时,切换到线程A,打印了ticket=10的记录(第10号票);再次切换CPU执行权到线程B时,B也打印了ticket=10的记录。
总结:因为共同操作了相同的资源,而没有进行有效的同步控制导致此现象发生。

线程安全解决方法一:同步代码块

  • 同步代码块的格式:
    synchronized(锁对象){
      把操作共享资源的代码放在此处
    }

  • 锁对象保证是唯一对象。多个线程抢唯一一个锁。

  • 注意:

    1. 通过代码块中的锁对象,可以使用任意的对象
    2. 但是必须保证多个线程使用的锁对象是同一个
    3. 锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行
public class RunnableImpl implements Runnable {
    int ticket = 10;
    Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {    //同步代码块
                if (ticket > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("saling : " + ticket + " ticket.");
                    ticket--;
                }
                else{
                    break;
                }
            }
        }
    }
}
    **执行结果**

线程安全解决方法二:使用同步方法

  • 使用步骤:
    1.把访问了共享数据的代码抽取出来,放到一个方法中
    2.在方法上添加synchronized修饰符

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

public class RunnableImpl implements Runnable {
    int ticket = 10;
    @Override
    public void run() {
        while (true) {
            int iRet = payTicket(); //调用同步方法
            if (iRet == 0) break;
        }
    }
    public synchronized int payTicket() {
        if (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " saling : " + ticket + " ticket.");
            ticket--;
            return 1;
        } else {
            return 0;
        }
    }
}
    执行结果:

线程安全解决方法三:静态的同步方法 (静态方法 + 同步代码块)

  • 静态的同步方法中同步代码注意:
    锁对象是谁? 不能是this,因为this是创建对象之后产生的,静态方法优先于对象
    静态方法的锁对象 是本类的class属性-->class文件对象(反射)
public class RunnableImpl implements Runnable {
    static int ticket = 10;        //被静态方法访问的静态成员
    @Override
    public void run() {
        while (true) {
           int iRet = payTicket();  //调用静态方法
           if( iRet == 0) break;
        }
    }

    public static /*synchronized*/ int payTicket() {    //静态方法
        synchronized (RunnableImpl.class){      //同步代码块
            if (ticket > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " saling : " + ticket + " ticket.");
                ticket--;
                return 1;
            }
            else{
                return 0;
            }
        }
    }
}
    执行结果:

线程安全解决方法四:lock 锁机制

  • java.util.concurrent.locks包中有一个Lock接口。
    Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作 此接口提供了两个方法:
    1. void lock() 获取锁
    2. void unlock() 释放锁
  • Lock接口的实现类有:java.util.concurrent.locks.ReentrantLock
  • 使用步骤:
    1.在成员位置创建一个ReentrantLock对象
    2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
    3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
public class RunnableImpl implements Runnable {
    static int ticket = 10;
    ReentrantLock lock1 = new ReentrantLock();

    @Override
    public void run() {
        //使用死循环,让卖票操作重复执行
        while (true) {
            //2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
            lock1.lock();
            if( ticket > 0){
                //提高安全问题出现的概率,让程序睡眠
                try {
                    Thread.sleep(10);
                    System.out.println(Thread.currentThread().getName() + "正在卖:" + ticket + "号票");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {                  //无论程序是否异常,都会把锁释放
                    lock1.unlock();
                }
            }
        }
    }
}

以上方法的测试类可以用第一个测试类

线程的唤醒机制

  • 这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是 故事的全部,但更多时 候你们更多是一起合作以完成某些任务。就是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后再将其唤醒(notify()); 在有多个线程进行等待时,如果需要,可以使用 notifyAll()来唤醒所有的等待线程。 wait/notify 就是线程间的一种协作机制.
  • 等待唤醒中的方法
    等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:
    1. wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列(ready queue)中
    2. notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座
    3. notifyAll:则释放所通知对象的 wait set 上的全部线程

** 注意:**
哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而 此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调 用 wait 方法之后的地方恢复执行。
总结如下:

  • 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
  • 否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态

调用wait和notify方法需要注意的细节

  1. wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对 象调用的wait方法后的线程。
  2. wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继 承了Object类的。
  3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方 法。

包子状态->没有包子->通知包子铺线程做包子->包子状态->有包子->通知吃货线程吃包子->包子状态->没有包子->...

1.包子类,资源
public class BaoZi {
    private String Pier;
    private String Xianer;
    boolean flag = false;   //包子资源状态

    public String getPier() {
        return Pier;
    }
}
2.包子铺线程类,负责做包子
public class BaoZiPu extends Thread {
    private BaoZi bz;

    public BaoZiPu(String name, BaoZi bz) {
        super(name);
        this.bz = bz;
    }

    @Override
    public void run() {
        int iCount = 0;
        while (true) {
            synchronized (bz) {  //同一个资源锁对象的操作放在同步代码块中
                if (bz.flag == true) {
                    try {
                        bz.wait();      //包子状态是有的。则等待吃货线程来唤醒做包子
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                System.out.println("开始做包子");
                if (iCount % 2 == 0) {
                    bz.setPier("薄皮");
                    bz.setXianer("水果");
                } else {
                    bz.setPier("冰皮");
                    bz.setXianer("三鲜");
                }
                iCount++;
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(bz.getPier() + bz.getXianer() + "馅的包子做好啦!!!");
                System.out.println("吃货来吃啦!!");
                bz.flag = true;
                bz.notify();        //唤醒吃货线程来吃包子
            }
        }
    }
}
3.吃货线程类
public class ChiHuo extends Thread {
    private BaoZi bz;

    public ChiHuo(String name, BaoZi bz) {
        super(name);
        this.bz = bz;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (bz) {
                if (bz.flag == false) {
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                System.out.println("吃货正在吃" + bz.getPier() + bz.getXianer() + "馅的包子。");
                try {
                    sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                bz.flag = false;
                bz.notify();
            }
        }
    }
}
4.线程测试类:
public class DemoTestWaitNotice {
    public static void main(String[] args) {
        BaoZi bz = new BaoZi();

        ChiHuo CH = new ChiHuo("吃货", bz);
        BaoZiPu BZP = new BaoZiPu("包子铺", bz);

        CH.start();
        BZP.start();
    }
}
  • 执行结果: