线程安全

  • 当多线程程序访问了共享的数据的时候,就会出现线程安全的问题。

  • 下面以一个买票的案例来说明一下线程安全的问题,运行后会发现同一张票被多个窗口一起卖,这是不合理的不安全的。产生这个问题的原因是多个线程都一起执行到了买票的那条语句,但是都没有执行到ticket–这条语句,导致三个窗口一起卖同一张票。

//接口的实现类
public class Runnableimpl implements Runnable{
    private int ticket=100;
    public void run(){
        while(ticket>0) {

            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
            ticket--;
        }

    }
}


//测试类
public class Test {
    public static void main(String[] args) {
        Runnableimpl el=new Runnableimpl();
        Thread th1=new Thread(el);
        Thread th2=new Thread(el);
        Thread th3=new Thread(el);
        th1.start();
        th2.start();
        th3.start();
    }
}

  • 可以引入三种方法来解决这个问题
  1. 同步代码块

    • 格式:synchronized(同步锁){ 需要同步操作的代码 }

    • 同步锁是一个任意的对象,但是要保证几个线程使用的对象是同一个,为了保证是同一个对象并且不占用更多的内存,我们可以使用this对象,this是调用当前run方法的对象。当然,也可以自己new一个Object

    • 锁对象的作用是同时只让一个线程执行同步代码块里的代码

    • 这个放在synchronized括号里的对象叫做锁对象,也叫同步锁、对象锁、对象监视器。main方法中开启三个线程之后,哪个线程抢到了cpu执行权,哪个就先执行run方法,这个时候加入线程1抢到了执行权,那么线程1就会进入到synchronized代码块之前,然后先检查代码块是否有锁对象,如果有,就会获取锁对象,如果没有,就会阻塞等待拿到锁对象,之后执行代码块。这个时候线程2跟线程3也来到synchronized代码块前面了,也都检测了锁对象是否存在,发现都不存在,然后线程2跟线程3因为拿不到锁对象而阻塞。线程1执行完synchronized中的代码之后归还锁对象,然后重新等待获取锁对象(因为ticket>0 ,还要继续执行代码),线程2和线程3其中一个拿到锁对象,以此类推。

      //接口实现类
      public class Runnableimpl implements Runnable{
          private int ticket=100;
          Object obj=new Object();
          public void run() {
                  while (ticket > 0) {
                      synchronized (obj) {
                          if(ticket>0) {
                              System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                              ticket--;
                          }
                      }
                  }
              }
      
      }
      
      //测试类
      public class Test {
          public static void main(String[] args) {
              Runnableimpl el=new Runnableimpl();
              Thread th1=new Thread(el);
              Thread th2=new Thread(el);
              Thread th3=new Thread(el);
              th1.start();
              th2.start();
              th3.start();
          }
      }
      
      
    • 这里大家可能会疑惑,我为什么要在while循环里面判断了ticket>0之后还要在同步的代码块那里再判断一次,这是因为synchronized锁住的代码块只允许一个线程正在执行,但是其他代码是可以几个线程一起执行的,这就导致了当ticket=1的时候,线程1正在执行锁里面的代码,而线程2和线程3已经在锁外面等着执行了(因为线程1还没执行到ticket–的操作,ticket还是为1,所以线程2线程3通过了while循环的ticket>1的检测),当线程1执行完ticket–的操作之后ticket=0,此时线程2跟线程3还是执行了卖票和ticket–的操作,所以卖出的票会出现负数的情况。而如果我在锁里面添加了一个if判断,就可以避免这个问题。

    • 最后执行代码之后还会发现,只有两个或者一个线程卖出了票,其他线程都没机会卖出票,票就卖光了。这是因为极短时间内线程1和线程2就卖出去了很多票,接着又继续跟线程3抢着卖,导致可能线程3完全抢不到。所以我们可以让某个线程在卖完票之后“休息”一段时间,再接着卖票。这就需要sleep函数。只需要修改接口实现类

      public class Runnableimpl implements Runnable{
          private int ticket=100;
          Object obj=new Object();
          public void run() {
                  while (ticket>0) {
                      try {
                          Thread.sleep(20);
                      } catch (InterruptedException e) {
                          throw new RuntimeException(e);
                      }
                      synchronized (obj) {
      
                          if(ticket>0) {
                              System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                              ticket--;
                          }
                      }
                  }
              }
      
      }
      
      
  2. 同步方法

    • 把访问了共享数据的代码抽取出来,单独放在一个方法里面

    • 在方法中添加synchronized修饰符

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

    • 使用了同步方法之后,当前的锁对象默认就是this

      public class Runnableimpl implements Runnable{
          private int ticket=100;
          public synchronized void payTicket(){
              if(ticket>0) {
                  System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                  ticket--;
              }
          }
          public void run() {
                  while (ticket>0) {
                      try {
                          Thread.sleep(30);
                      } catch (InterruptedException e) {
                          throw new RuntimeException(e);
                      }
                      payTicket();
      
                  }
              }
      
      }
      
      
  3. 锁机制

    • Lock锁机制,比synchronized锁更加灵活更加强大,可以手动上锁和释放锁对象。

    • Lock是一个接口,我们需要使用它的实现类ReentrantLock

    • 步骤:创建一个ReentrantLock对象,在同步代码之前获取对象锁,在同步代码之后释放对象锁

      import java.util.concurrent.locks.Lock;
      import java.util.concurrent.locks.ReentrantLock;
      
      public class Runnableimpl implements Runnable{
          private int ticket=100;
          public void run() {
              Lock lock=new ReentrantLock();
              while (ticket>0) {
                  try {
                      Thread.sleep(20);
                  } catch (InterruptedException e) {
                      throw new RuntimeException(e);
                  }
      
                  lock.lock(); //上锁
                  //下面的代码会出现同步问题
                      if(ticket>0) {
                          System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                          ticket--;
                      }
                 lock.unlock();  //释放锁
      
              }
          }
      
      }
      
      

你可能感兴趣的:(Java学习笔记,安全,java,jvm)