Java线程安全问题

Java 线程安全问题

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

实际举例

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

java 复制代码
//实现方式一: 实现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;
      }
  }
}
}
//输出结果
//存在不同窗口卖出相同票号的问题
//出现了线程的安全问题
java 复制代码
//实现方式二: 继承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 关键字可以用于某个区块前面,表示只对这个区块的资源实行互斥访问。

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

说明

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

使用同步代码块修改程序

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

java 复制代码
//使用同步代码块解决上述线程问题
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类的线程安全问题

java 复制代码
//使用继承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 关键字直接修饰方法,表示同一时刻只有一个线程能进入这个方法,其他线程在外面等着。

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

实现Runnable接口

java 复制代码
//使用实现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类方式

java 复制代码
//使用继承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利弊分析

  • 好处,解决了线程安全问题
  • 弊端,在操作共享数据时,多线程其实时串行执行的,意味着性能的降低。
相关推荐
百事老饼干1 分钟前
Java[面试题]-真实面试
java·开发语言·面试
customer089 分钟前
【开源免费】基于SpringBoot+Vue.JS医院管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·开源·intellij-idea
千澜空16 分钟前
celery在django项目中实现并发任务和定时任务
python·django·celery·定时任务·异步任务
2402_8575893619 分钟前
SpringBoot框架:作业管理技术新解
java·spring boot·后端
HBryce2422 分钟前
缓存-基础概念
java·缓存
斯凯利.瑞恩24 分钟前
Python决策树、随机森林、朴素贝叶斯、KNN(K-最近邻居)分类分析银行拉新活动挖掘潜在贷款客户附数据代码
python·决策树·随机森林
一只爱打拳的程序猿37 分钟前
【Spring】更加简单的将对象存入Spring中并使用
java·后端·spring
杨荧39 分钟前
【JAVA毕业设计】基于Vue和SpringBoot的服装商城系统学科竞赛管理系统
java·开发语言·vue.js·spring boot·spring cloud·java-ee·kafka
minDuck40 分钟前
ruoyi-vue集成tianai-captcha验证码
java·前端·vue.js