• Java线程安全问题


    Java 线程安全问题

    当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题。但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。

    实际举例

    开启三个窗口售票,总票数为100张。(相当于多线程

    //实现方式一: 实现Runnable接口
    public class WindowTest {
    public static void main(String[] args) {
      SaleTicket s = new SaleTicket();
      Thread t1 = new Thread(s);
      Thread t2 = new Thread(s);
      Thread t3 = new Thread(s);
      t1.setName("窗口1");
      t2.setName("窗口2");
      t3.setName("窗口3");
      t1.start();
      t2.start();
      t3.start();
    }
    }
    class SaleTicket implements Runnable{
    int ticket = 100;
    @Override
    public void run(){
      while (true){
          if(ticket > 0){
              System.out.println(Thread.currentThread().getName()+"售票,票号为"+ticket);
              ticket --;
          }else{
              break;
          }
      }
    }
    }
    //输出结果
    //存在不同窗口卖出相同票号的问题
    //出现了线程的安全问题
    
    //实现方式二: 继承Thread类
    public class WindowTest1 {
    //使用继承Thread类实现
    public static void main(String[] args) {
      Window w1 = new Window();
      Window w2 = new Window();
      Window w3 = new Window();
      w1.setName("窗口1");
      w2.setName("窗口2");
      w3.setName("窗口3");
      w1.start();
      w2.start();
      w3.start();
    }
    
    }
    class Window extends Thread{
    static int ticket = 100;
    @Override
    public void run() {
      while(true){
          if(ticket > 0){
              System.out.println(Thread.currentThread().getName()+"售票,票号为"+ticket);
              ticket--;
          }else{
              break;
          }
      }
    }
    }
    //即使使用static 修饰ticket 仍然存在同票现象
    

    归纳问题

    • 多线程卖票出现的问题

      出现了同票和错票现象

    • 什么原因导致?

      线程1操作ticket的过程中,尚未结束的情况下,其他线程也参与进来,对ticket进行操作

    • 如何解决?

      必须保证一个线程A在操作ticket的过程中,其他线程必须等待,直到线程A操作ticket结束以后,其他线程才能进来继续操作ticket。

    • Java是如何解决线程安全问题的?

      使用线程的同步机制同步代码块同步方法

    同步代码块

    同步代码块概述

    synchronized 关键字可以用于某个区块前面,表示只对这个区块的资源实行互斥访问。

    synchronized(同步监视器){	//多个线程必须共用同一个同步监视器,当线程类只有一个实例化时候,可以使用this
        //需要被同步的代码
    }
    

    说明

    • 需要被同步的代码,即为操作共享数据的代码共享数据,即多个线程都需要操作的数据
    • 需要被同步的代码,在被synchronized包裹以后,就使得一个线程在操作这些代码的过程中,其他线程必须等待。
    • 同步监视器,俗称锁。哪个线程获得了锁,哪个线程就能执行需要被同步的代码。
    • 同步监视器,可以使用任何一个类的对象充当。但是,多个线程必须共用同一个同步监视器。

    使用同步代码块修改程序

    使用同步代码块修改上述实现Runnalbe接口的线程问题

    //使用同步代码块解决上述线程问题
    class SaleTicket implements Runnable{
        int ticket = 100;
    
        @Override
        public void run() {
            while(true){
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (this){	//SaleTicket只有唯一类,可以使用this
                    if(ticket > 0){
                        System.out.println(Thread.currentThread().getName()+"售票,票号为"+ticket);
                        ticket--;
                    }
                    else {
                        break;
                    }
                }
            }
        }
    }
    public class Test{
        public static void main(String []args){
            SaleTicket p = new SaleTicket();
            Thread t1 = new Thread(p);
            Thread t2 = new Thread(p);
            Thread t3 = new Thread(p);
            t1.setName("窗口1");
            t2.setName("窗口2");
            t3.setName("窗口3");
            t1.start();
            t2.start();
            t3.start();
        }
    }
    

    使用同步代码块解决继承Thread类的线程安全问题

    //使用继承Thread类的方式,实现卖票
    //使用同步代码块解决卖票的线程安全问题
    public class Test2 {
        public static void main(String[] args) {
            SaleTicket2 t1 = new SaleTicket2();
            SaleTicket2 t2 = new SaleTicket2();
            SaleTicket2 t3 = new SaleTicket2();
            t1.setName("窗口1");
            t2.setName("窗口2");
            t3.setName("窗口3");
            t1.start();
            t2.start();
            t3.start();
        }
    }
    class SaleTicket2 extends Thread{
        static int ticket = 100;    //ticket必须为静态变量
        public void run() {
            while(true){
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (SaleTicket2.class){
                    if(ticket > 0){
                        System.out.println(Thread.currentThread().getName()+"售票,票号为"+ticket);
                        ticket--;
                    }
                    else {
                        break;
                    }
                }
            }
        }
    
    }
    

    同步方法

    同步方法概述

    同步方法说明

    如果操作共享数据的代码完整的声明在了一个方法中,那么我们就可以将此方法声明为同步方法即可。

    **同步方法:**synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能进入这个方法,其他线程在外面等着。

    public synchronized void method(){
        //可能会产生线程安全问题的代码
    }
    //声明了方法后,要在run()方法中调用method()
    
    使用同步方法修改程序

    实现Runnable接口

    //使用实现Runnable接口的方式 实现多窗口卖票问题
    //使用同步方法解决多窗口中的线程安全问题
    public class WindowTest1 {
        public static void main(String[] args) {
            Window p = new Window();
            Thread t1 = new Thread(p);
            Thread t2 = new Thread(p);
            Thread t3 = new Thread(p);
            t1.setName("窗口1");
            t2.setName("窗口2");
            t3.setName("窗口3");
            t2.start();
            t1.start();
            t3.start();
        }
    }
    class Window implements Runnable{
        int ticket = 100;
        boolean isFlag = true;
        public synchronized void show(){
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(ticket > 0){
                System.out.println(Thread.currentThread().getName()+"售票,票号为"+ticket);
                ticket--;
            }
            else {
                isFlag = false;
            }
        }
    
        @Override
        public void run() {
            while(isFlag){
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                show();
            }
        }
    }
    

    继承Thread类方式

    //使用继承Thread类的方式,实现多窗口售票的模式
    //使用同步方法,解决线程安全问题
    public class WindowTest2 {
        public static void main(String[] args) {
            Window2 t1 = new Window2();
            Window2 t2 = new Window2();
            Window2 t3 = new Window2();
            t1.setName("窗口1");
            t2.setName("窗口2");
            t3.setName("窗口3");
            t1.start();
            t2.start();
            t3.start();
        }
    }
    class Window2 extends Thread{
        static boolean isFlag = true;
        static int ticket = 100;
        //使用继承Thread类的方式,则多线程会声明多个实例化对象,ticket必须使用static修饰
    
        @Override
        public void run() {
            while(isFlag){
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                    show();
            }
        }
        public static synchronized void show(){     //必须使用static的同步监视器
            //此时的同步监视器是 当前类本身(加了static)
            if(ticket > 0){
                System.out.println(Thread.currentThread().getName()+"售票,票号为"+ticket);
                ticket--;
            }else{
                isFlag = false;
            }
        }
    }
    
    说明
    • 如果操作共享数据的代码完整的声明在了一个方法中,那么就可以将此方法声明为同步方法即可
    • 非静态的同步方法,默认同步监视器是this ; 静态的同步方法,默认同步监视器是当前类本身

    汇总说明

    线程创建的方式有两种,分别为实现Runnalbe接口继承Thread类,对于不同的线程创建类,使用的同步方法和同步代码块也有不同

    • 当线程创建为继承Thread类时,需要注意:

    • 使用实现Runnalbe接口和同步代码块时:

      synchronized (this) 同步代码块中同步监视器可以使用this替代

    • 使用实现Runnable接口和同步方法时:

      将公共代码部分,封装到一个方法中,方法格式为(可修改)public synchronized void method() ,并在pubilc void run()中调用即可

    • 使用继承Thread类时:

      公共的属性需要用static修饰 例如 static int ticket = 100

      synchronized ()同步监视器中不能使用this, 可以使用synchronized(类名.class)替代,例如synchronized (SaleTicket2.class)

      • 若继续使用同步方法时,同步方法也需要使用static进行修饰

    synchronized利弊分析

    • 好处,解决了线程安全问题
    • 弊端,在操作共享数据时,多线程其实时串行执行的,意味着性能的降低。
  • 相关阅读:
    某上市环境公司:汇聚18个系统,看数据治理如何反哺业务
    环保行业智能供应链系统加快企业数字化转型,增强企业核心竞争力
    C语言二维数组编程练习集
    重温设计模式之什么是设计模式?
    小米路由器如何设置去广告功能,如何设置小米路由器的自定义Hosts(小米路由器如何去除小米广告、去除小米电视盒子开屏广告、视频广告)
    STM32 float浮点数转换成四个字节
    Springboot毕设项目美食点评系统gb9o5(java+VUE+Mybatis+Maven+Mysql)
    后期学习计划
    吃透Spring源码分析专题
    VS2019中使用printf函数报错处理方法
  • 原文地址:https://blog.csdn.net/qq_35899077/article/details/139456416