线程安全问题

需求:某电影院目前正在上映国产大片,共100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票

public class MyThread extends Thread{
  //表示这个类的所有对象都共享ticket
  static int ticket=0; //0~99
  
  @Override
  public void run(){
    while(true){
      if(ticket<100){
        try{
          Thread.sleep(100);
        }catch(InterruptedException e){
          e.printStackTrace();
        }
        ticket++:
        System.out.println(getName()+"正在卖第"+ticket+"张票");
      }else{
        break;
      }
    }
  }
}

public static void main(String[] args){
  //创建线程对象
  MyThread t1 = new Thread();
  MyThread t2 = new Thread();
  MyThread t3 = new Thread();

  //起名字
  t1.setName("窗口1");
  t2.setName("窗口2");
  t3.setName("窗口3");

  //开启线程
  t1.start();
  t2.start();
  t3.start();
}

此时的代码会有一个问题,3个窗口的票数不是多了就是重复了
多个线程操作同一个数据时,就会产生问题
问题1:相同的票出现了多次
问题2:出现了超出范围的票

原因:线程执行时,有随机性,多个线程操作同一个共享变量
如果线程执行到ticket++;还没有打印,cpu的执行权就被抢走了,就可能会打印相同的数据
//本地内存->主内存

可以用同步代码块解决
同步代码块:把操作共享数据的代码锁起来
格式

synchronized(锁对象){
  操作共享数据的代码
}

//锁对象没有要求,但是要确保唯一
//例如static Object obj =new Object();
特点:默认锁打开,有一个线程进去了,锁自动关闭
特点2:里面的代码全部执行完毕,线程出来,锁自动打开

解决后的代码

public class MyThread extends Thread{
  //表示这个类的所有对象,都共享ticket数据
  static int ticket =0;
  
  //锁对象,一定要是唯一的
  static Object obj = new Object();

  @Override
  public void run(){
    while(true){
      synchronized(obj){
        if(ticket<100){
          try{
            Thread.sleep(10);
          }catch(InterruptedException e){
            e.printStackTrace();
          }
          ticket++:
          System.out.println(getName()+"正在卖第"+ticket+"张票");
        }else{
          break;
        }
      }
    }
  }
}

同步代码中的小细节
1.写在循环里面
2.锁对象,一定要是唯一的
一般书写为当前类的字节码文件
格式:类名.class

同步方法
把synchronized关键字加到方法上
格式:
修饰符 synchronized 返回值类型 方法名(方法参数){...}

特点1:同步方法时锁住方法里面的所有代码
特点2:锁对象不能自己指定
非静态:this
静态:当前类的字节码文件对象 类型.class

技巧:同步代码块完成后->写到同步方法

public class ThreadDemo{
  public static void main(String[] args){
    /*需求:
    *100张票,3个窗口卖票 利用同步方法
    *技巧: 同步代码块
    */
    
    MyRunnable mr = new MyRunnable();
    Thread t1 = new Thread(mr);
    Thread t2 = new Thread(mr);
    Thread t3 = new Thread(mr);
    
    t1.setName("窗口1");
    t2.setName("窗口2");
    t3.setName("窗口3");

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

public class MyRunnable implements Runnable{
   int ticket=0;
    @Override
    public void run(){
      //1.循环
      //2.同步代码块(同步方法)
      //3.判断共享数据是否到了末尾,如果到了末尾
      //4.判断共享数据是否到了末尾,如果没有到末尾

      while(true){
        synchronized(MyRunnable.class){
          if(ticket==100){
              break;
          }else{
            try{
              Thread.sleep(10);
            }catch(InterruptedException e){
              e.printStackTrace();
            }
            ticket++;
            System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
          }
        }
      }
    }
}

//选中然后ctrl+alt+m就可以抽取成一个方法

//抽取方法后
public class MyRunnable implements Runnable{
  int kicket=0;

  @Override
  public void run(){
    //1.循环
    while(true){
      //2.同步方法
      if(method())break;
    }
  }

  //非静态方法 锁对象是this
  privare synchronized boolean method(){
    //3.判断共享数据是否到了末尾,如果到了末尾
    if(ticket==100){
      return true;
    }else{
      //4.判断共享数据是否到了末尾,如果没有到末尾
      try{
        Thread.sleep(10);
      }catch(InterruptedException e){
        e.printStackTrace();
      }
      ticket++;
      System.out.println(Thread.currentThread().getName()+"在卖第"+ticket+"张票");
    }
  }
  return false;
}

StringBuild和StringBuffer的方法是一样的
但是StringBuffer的方法都加上了synchronized都是同步方法
如果单线程可以用StringBuild,多线程用StringBuffer

Lock锁
虽然可以理解同步代码块和同步方法的锁对象问题
但是并没有直接看到锁
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

Lock实现提供比使用synchronized方法和语句,可以获得更广泛的锁定操作
Lock中提供了获得锁和释放锁的方法
void lock() 获得锁
void unlock() 释放锁
手动上锁,手动释放锁

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
ReentrantLock的构造方法
ReentrantLock() 创建一个RenntrantLock的实例

public class MyThread extends Thread{
  static int ticket = 0;
  static Lock lock = new ReentrantLock();

  @Override
  public void run(){
    //1.循环
    while(true){
      //2.同步代码块
      //synchronized(MyThread.class)
      lock.lock();
      //3.判断
      try{
        Thread.sleep(10);
      }catch(InterruptedException e){
        e.printStackTrace();
      }

      ticket++;
      System.out.println(getName()+"正在卖第"+ticket+"张票");
    }
    lock.unlock();
  }
}

public class ThreadDemo{
  public static void main(String[] args){
    MyThread t1 = new MyThread();
    MyThread t2 = new MyThread();
    MyThread t3 = new MyThread();

    t1.setName("窗口1");
    t2.setName("窗口2");
    t3.setName("窗口3");

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

此时还会有一个问题,虽然票卖完了,但是程序不会停止
原因是第100次之后结束循环后,锁还没有关
解决办法用try{...}catch{...}finally{关锁}
finally一定会被执行

更新后的代码

public class MyThread extends Thread{
  static int ticket = 0;
  static Lock lock = new ReentrantLock();
  
  @Override
  public void run(){
    //1.循环
    while(true){
      //2.同步代码块
      lock.lock();
      try{
        //3.判断
        if(ticket==100){
          break;
        }else{
           //4.判断
           Thread.sleep(10);
            ticket++;
            System.out.println(getName()+"在卖第"+ticket+"张票");
        }
      }catch(InterruptedException e){
        e.printStackTrace();
      }finally{
        lock.unlock();
      }
    }
  }
}

死锁(应该避免的错误)
死锁:指的是两个或多个线程无限期地互相等待对方所持有的资源,导致程序无法继续执行下去的状态

1.大锁套小锁可以解决死锁的循环,但是性能更低
2.最好的办法就是不要嵌套两个锁

posted @ 2024-01-29 14:09  狗狗没有坏心眼  阅读(1)  评论(0编辑  收藏  举报