zoukankan      html  css  js  c++  java
  • 线程安全问题

    线程安全

    1.概念

    多个线程同时运行同一个实现了Runnable接口的类,程序每次运行结果和单线程运行结果是一样的,其他变量的值和预期的一样,就称之为线程安全的,反之则是不安全的

    2.问题演示

    如下模拟一个抢票系统:

    • 定义一个Ticket线程类

      public class Ticket implements Runnable{
          private int Count = 100;//100张票在售
          public void run() {
              while (true){
                  //有剩余票数
                  if (Count>=0){
                      //睡眠100毫秒模拟网络延迟
                      try {
                          Thread.sleep(100);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      //输出模拟抢票结果
                      String name = Thread.currentThread().getName();
                      System.out.println(name+"执行;剩余票数:"+ Count);
                      Count--;
                  }
              }
          }
      
      
    • 主程序里我们同时让三个线程调用同一个Ticket对象来进行抢票

      public class TicketSafe {
          public static void main(String[] args) {
              Ticket ticket = new Ticket();
              Thread thread = new Thread(ticket,"窗口一");
              Thread thread2 = new Thread(ticket,"窗口二");
              Thread thread3 = new Thread(ticket,"窗口三");
              thread.start();
              thread2.start();
              thread3.start();
          }
      }
      
    • 预期结果应该是三个窗口各自抢票,剩余票数要实时的进行减少,而以上代码的实际效果如下:

      窗口二执行;剩余票数:100
      窗口一执行;剩余票数:100
      窗口三执行;剩余票数:100
      窗口一执行;剩余票数:97
      窗口二执行;剩余票数:97
      窗口三执行;剩余票数:97
      窗口二执行;剩余票数:94
      窗口一执行;剩余票数:94
      窗口三执行;剩余票数:94
      窗口二执行;剩余票数:91
      窗口三执行;剩余票数:90
      窗口一执行;剩余票数:90
      窗口一执行;剩余票数:88
      窗口二执行;剩余票数:88
      ................
      

      可以看出,结果和我们预期的完全不同,分析可知,当多个线程一起对执行一个Runnable接口对象时,会出现以上情况,多个线程结果相同,不符合逻辑,致错原因如下所示:

    三个线程同时进入if方法,并发情况下先后打印了结果,其他线程还没来得及count--就打印了,所以下次三个线程打印了一样的结果,count--执行了三次,下次循环还是一样的问题,所以出现了以上结果

    • 总结问题:

      • 多个线程在操作共享的数据
      • 操作共享数据的线程代码有多条
      • 多个线程对共享数据有写操作

    3.实现线程安全

    1.思路

    • 只要在某个线程修改共享数据时,阻止其他要修改该共享数据的线程进行,等待修改结束完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证数据的同步性

    2.7种线程同步机制

    • 同步代码块(synchronized)
    • 同步方法(synchronized)
    • 同步锁(Lock)
    • 特殊域变量(volatile)
    • 局部变量(ThreadLocal)
    • 阻塞队列(LinkedBlockingQueue)
    • 原子变量(Atomic*)

    3.同步代码块(synchronized)

    • 定义一个锁对象,作为进入代码块的钥匙

    • 将需要保证线程安全的代码加入到synchronized下的代码块中,起到安全作用

          //创建锁对象:相当于打开代码块的钥匙
          private Object obj = new Object();
          public void run() {
              while (true){
                  //有剩余票数
                  //同步代码块,线程到这里的时候,都会去请求一个obj资源,只有一个线程可以拿到
                  //拿到obj的线程才能进入代码块,其他请求的线程只能继续等待,等待obj锁对象被释放
                  synchronized (obj){
                    .....................
                  }
              }
          }
      
    • 只有获取到obj的线程才能执行代码块,其余的线程必须等该线程运行完释放锁,再获取资源

    4.同步方法

    • 同步方法与同步代码块类似,使用的是synchronized关键字,不过他是基于方法层面的,关键字在方法上

    • synchronized加在线程要运行的方法上,java会自动给该方法加上一个锁对象,类似同步代码块中的obj

    • 只有线程拥有该锁对象时,才能运行方法,没有锁对象的方法需要在方法外等待

          //对于非static方法,调用该方法的Runnable实现类对象实例就是锁对象即this,注意对于多个线程来说,他们的this得是同一个实例对象,不然达不到互斥作用,相当于synchronized(new Ticket())
          //对于static方法,当前方法所在类的字节码对象就是锁对象,相当于synchronized(Ticket.class)
          private synchronized void threadSafe(){
              if (Count>=0){
                  //睡眠100毫秒模拟网络延迟
                  try {
                      Thread.sleep(100);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  //输出模拟抢票结果
                  String name = Thread.currentThread().getName();
                  System.out.println(name+"执行;剩余票数:"+ Count);
                  Count--;
              }
          }
          public void run() {
              while (true) {
                  threadSafe();
              }
          }
      

    5.同步锁(Lock)

    • java.util.Concurrent.locks.Lock 机制提供了比synchronized关键字更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,lock有更强大的功能,更体现面向对象

    • 同步锁的方法

      public void lock();   //加同步锁
      public void unlock();  //释放同步锁
      
    • 锁有多种,后面会有详细专题,这里先暂时使用以下重入锁(释放后还可以被调用的锁),以下为使用方式

      • 先创建Lock对象
      • 将需要加锁的代码块放在try中
      • 在try前加上锁,在finally中释放锁,以确保不会导致死锁
         //创建一个lock对象,重入锁实例
          //参数fair:
          //   true---公平锁:所有线程都能公平的得到机会
          //   false(默认)---独占锁:只有第一个得到的线程可以使用,除非它主动放弃或者释放
          Lock lock = new ReentrantLock(false);
      
          public void run() {
              while (true) {
                  lock.lock();//加上锁,只要有这个方法就一定要在某处有unlock,否则会导致死锁
                  //为保证unlock一定被执行,使用try finally来实现
                  try{
                      if (Count>=0){
                          //睡眠100毫秒模拟网络延迟
                          try {
                              Thread.sleep(100);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                          //输出模拟抢票结果
                          String name = Thread.currentThread().getName();
                          System.out.println(name+"执行;剩余票数:"+ Count);
                          Count--;
                      }
                  }finally {
                      //保证释放锁
                     lock.unlock();
                  }
              }
          }
      

    实验结果发现:当重入锁ReentrantLock的参数为true,即公平锁时,每个线程是公平的获得执行方法的权力,结果是非常有规律的1,2,3,1,2,3;而当定义为独占锁时,才有随机的效果,每个线程谁先获得锁,就可以执行。

    关于锁的小结

    • synchronized是java内置的关键字,在jvm层面;Lock是java类,在编码层面
    • synchronized无法获取锁的状态,Lock可以判断是否获取到锁
    • synchronized可以主动释放锁,Lock需要手动unlock
    • synchronized阻塞的线程获取不到锁就会一直等待一直阻塞,Lock阻塞的线程则不会,如果尝试获取不到锁,线程可以不用一直等待就结束了
    • synchronized锁可重入,不可判断,非公平,而Lock都可以自己定义
    • synchronized适合少量代码的同步问题,Lock适合大量同步的代码问题
  • 相关阅读:
    一天一道算法题---6.8--数学题
    TOJ----1037---最大独立集
    一天一道算法题---6.6---排列递推(我不会)
    一天一道算法题---6.4--中途相遇法
    一天一道算法题--6.5--数学题
    夜太美---酒不醉--人自醉
    SSH组合之Spring3整合Hibernate4(一)
    Hibernater入门
    Java微信公众平台进入开发模式
    新浪sae平台进行数据库的连接
  • 原文地址:https://www.cnblogs.com/JIATCODE/p/13276327.html
Copyright © 2011-2022 走看看