Java 多线程安全问题简单切入详细解析

线程安全

假如Java程序中有多个线程在同时运行,而这些线程可能会同时运行一部分的代码。如果说该Java程序每次运行的结果和单线程的运行结果是一样的,并且其他的变量值也都是和预期的结果是一样的,那么就可以说线程是安全的。

解析什么是线程安全:卖电影票案例

假如有一个电影院上映《葫芦娃大战奥特曼》,售票100张(1-100号),分三种情况卖票:

情况1

该电影院开设一个售票窗口,一个窗口卖一百张票,没有问题。就如同单线程程序不会出现安全问题一样。

情况2

该电影院开设n(n>1)个售票窗口,每个售票窗口售出指定号码的票,也不会出现问题。就如同多线程程序,没有访问共享数据,不会产生问题。

情况3

该电影院开设n(n>1)个售票窗口,每个售票窗口出售的票都是没有规定的(如:所有的窗口都可以出售1号票),这就会出现问题了,假如三个窗口同时在卖同一张票,或有的票已经售出,还有窗口还在出售。就如同多线程程序,访问了共享数据,会产生线程安全问题。

卖100张电影票Java程序实现:出现情况3类似情况

public class MovieTicket01 implements Runnable {

    /**
     * 电影票数量
     */
    private static int ticketNumber = 100;

    /**
     * 在实现类中重写Runnable接口的run方法,并设置此线程要执行的任务
     */
    @Override
    public void run() {
        // 设置此线程要执行的任务
        while (ticketNumber > 0) {
            // 提高程序安全的概率,让程序睡眠10毫秒
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 电影票出售
            System.out.println("售票窗口(" + Thread.currentThread().getName() + ")正在出售:" + MovieTicket01.ticketNumber + "号电影票");
            ticketNumber --;
        }
    }
}
// 测试
public class Demo01MovieTicket {
    public static void main(String[] args) {
        // 创建一个 Runnable接口的实现类对象。
        MovieTicket01 movieTicket = new MovieTicket01();

        // 创建Thread类对象,构造方法中传递Runnable接口的实现类对象(三个窗口)。
        Thread window0 = new Thread(movieTicket);
        Thread window1 = new Thread(movieTicket);
        Thread window2 = new Thread(movieTicket);

        // 设置一下窗口名字,方便输出确认
        window0.setName("window0");
        window1.setName("window1");
        window2.setName("window2");

        // 调用Threads类中的start方法,开启新的线程执行run方法
        window0.start();
        window1.start();
        window2.start();
    }
}
控制台部分输出:
售票窗口(window0)正在出售:100号电影票
售票窗口(window2)正在出售:99号电影票
售票窗口(window1)正在出售:100号电影票
售票窗口(window0)正在出售:97号电影票
售票窗口(window2)正在出售:97号电影票
售票窗口(window1)正在出售:97号电影票
售票窗口(window1)正在出售:94号电影票
售票窗口(window2)正在出售:94号电影票
.
.
.
.
.
.
售票窗口(window0)正在出售:7号电影票
售票窗口(window2)正在出售:4号电影票
售票窗口(window0)正在出售:4号电影票
售票窗口(window1)正在出售:2号电影票
售票窗口(window1)正在出售:1号电影票
售票窗口(window2)正在出售:0号电影票
售票窗口(window0)正在出售:-1号电影票

可以看到,三个窗口(线程)同时出售不指定号数的票(访问共享数据),出现了卖票重复,和出售了不存在的票号数(0、-1)

Java程序中为什么会出现这种情况

  1. 在CPU线程的调度分类中,Java使用的是抢占式调度。
  2. 我们开启了三个线程,3个线程一起在抢夺CPU的执行权,谁能抢到谁就可以被执行。
  3. 从输出结果可以知道,刚开始抢夺CPU执行权的时候,线程0(window0窗口)先抢到,再到线程1(window1窗口)抢到,最后线程2(window2窗口)才抢到。
  4. 那么为什么100号票已经在0号窗口出售了,在1号窗口还会出售呢?其实很简单,线程0先抢到CPU执行权,于是有了执行权后,他就开始嚣张了,作为第一个它通过while判断,很自豪的拿着ticketNumber = 100进入while里面开始执行。
  5. 可线程0是万万没有想到,这时候的线程1,在拿到执行权后,在线程0刚刚实现print语句还没开始ticketNumber --的时候,线程1以ticketNumber = 100跑进了while里面。
  6. 线程2很遗憾,在线程0执行了ticketNumber --了才急匆匆的进入while里面,不过它也不甘落后,于是拼命追赶。终于,后来居上,在线程1还没开始print的时候,他就开始print了。于是便出现了控制台的前三条输出的情况。

    售票窗口(window0)正在出售:100号电影票
    售票窗口(window2)正在出售:99号电影票
    售票窗口(window1)正在出售:100号电影票

    window0、window1、window2分别对应线程0、线程1、线程2

  7. 以此类推,直到最后程序执行完毕。

解决情况3的共享数据问题

通过线程的同步,来解决共享数据问题。有三种方式,分别是同步代码块、同步方法、锁机制。

同步代码块

public class MovieTicket02 implements Runnable {

    /**
     * 电影票数量
     */
    private static int ticketNumber = 100;

    /**
     * 创建锁对象
     */
    Object object = new Object();

    /**
     * 在实现类中重写Runnable接口的run方法,并设置此线程要执行的任务
     */
    @Override
    public void run() {
        // 设置此线程要执行的任务
        synchronized (object) {
            // 把访问了共享数据的代码放到同步代码中
            while (ticketNumber > 0) {
                // 提高程序安全的概率,让程序睡眠10毫秒
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 电影票出售
                System.out.println("售票窗口(" + Thread.currentThread().getName() + ")正在出售:" + MovieTicket02.ticketNumber + "号电影票");
                ticketNumber --;
            }
        }
    }
}
// 进行测试
public class Demo02MovieTicket {
    public static void main(String[] args) {
        // 创建一个 Runnable接口的实现类对象。
        MovieTicket02 movieTicket = new MovieTicket02();

        // 创建Thread类对象,构造方法中传递Runnable接口的实现类对象(三个窗口)。
        Thread window0 = new Thread(movieTicket);
        Thread window1 = new Thread(movieTicket);
        Thread window2 = new Thread(movieTicket);

        // 设置一下窗口名字,方便输出确认
        window0.setName("window0");
        window1.setName("window1");
        window2.setName("window2");

        // 调用Threads类中的start方法,开启新的线程执行run方法
        window0.start();
        window1.start();
        window2.start();
    }
}
控制台输出:
售票窗口(window0)正在出售:100号电影票
售票窗口(window0)正在出售:99号电影票
售票窗口(window0)正在出售:98号电影票
售票窗口(window0)正在出售:97号电影票
售票窗口(window0)正在出售:96号电影票
.
.
.
.
.
.
售票窗口(window0)正在出售:5号电影票
售票窗口(window0)正在出售:4号电影票
售票窗口(window0)正在出售:3号电影票
售票窗口(window0)正在出售:2号电影票
售票窗口(window0)正在出售:1号电影票

这时候,控制台不再出售不存在的电影号数以及重复的电影号数了。

通过代码块中的锁对象,可以使用任意的对象。但是必须保证多个线程使用的锁对象是同一。锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行。

总结:同步中的线程,没有执行完毕,不会释放锁,同步外的线程,没有锁,进不去同步。

同步方法

public class MovieTicket03 implements Runnable {

    /**
     * 电影票数量
     */
    private static int ticketNumber = 100;

    /**
     * 创建锁对象
     */
    Object object = new Object();

    /**
     * 在实现类中重写Runnable接口的run方法,并设置此线程要执行的任务
     */
    @Override
    public void run() {
        // 设置此线程要执行的任务
        ticket();
    }

    public synchronized void ticket() {
        // 把访问了共享数据的代码放到同步代码中
        while (ticketNumber > 0) {
            // 提高程序安全的概率,让程序睡眠10毫秒
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 电影票出售
            System.out.println("售票窗口(" + Thread.currentThread().getName() + ")正在出售:" + MovieTicket03.ticketNumber + "号电影票");
            ticketNumber --;
        }
    }
}

测试与同步代码块一样。

锁机制(Lock锁)

在Java中,Lock锁机制又称为同步锁,加锁public void lock(),释放同步锁public void unlock()。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MovieTicket05 implements Runnable {

    /**
     * 电影票数量
     */
    private static int ticketNumber = 100;

    Lock reentrantLock = new ReentrantLock();

    /**
     * 在实现类中重写Runnable接口的run方法,并设置此线程要执行的任务
     */
    @Override
    public void run() {
        // 设置此线程要执行的任务
        while (ticketNumber > 0) {
            reentrantLock.lock();
            // 提高程序安全的概率,让程序睡眠10毫秒
            try {
                Thread.sleep(10);
                // 电影票出售
                System.out.println("售票窗口(" + Thread.currentThread().getName() + ")正在出售:" + MovieTicket05.ticketNumber + "号电影票");
                ticketNumber --;
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                reentrantLock.unlock();
            }
        }
    }
}
// 测试

public class Demo05MovieTicket {
    public static void main(String[] args) {
        // 创建一个 Runnable接口的实现类对象。
        MovieTicket05 movieTicket = new MovieTicket05();

        // 创建Thread类对象,构造方法中传递Runnable接口的实现类对象(三个窗口)。
        Thread window0 = new Thread(movieTicket);
        Thread window1 = new Thread(movieTicket);
        Thread window2 = new Thread(movieTicket);

        // 设置一下窗口名字,方便输出确认
        window0.setName("window0");
        window1.setName("window1");
        window2.setName("window2");

        // 调用Threads类中的start方法,开启新的线程执行run方法
        window0.start();
        window1.start();
        window2.start();
    }
}
控制台部分输出:
售票窗口(window0)正在出售:100号电影票
售票窗口(window0)正在出售:99号电影票
售票窗口(window0)正在出售:98号电影票
售票窗口(window0)正在出售:97号电影票
售票窗口(window0)正在出售:96号电影票
.
.
.
.
.
.
售票窗口(window1)正在出售:7号电影票
售票窗口(window1)正在出售:6号电影票
售票窗口(window1)正在出售:5号电影票
售票窗口(window1)正在出售:4号电影票
售票窗口(window1)正在出售:3号电影票
售票窗口(window2)正在出售:2号电影票
售票窗口(window1)正在出售:1号电影票

与前两种方式不同,前两种方式,只有线程0能够进入同步机制执行代码,Lock锁机制,三个线程都可以进行执行,通过Lock锁机制来解决共享数据问题。

Java 多线程安全问题就到这里了,如果有什么不足、错误的地方,希望大佬们指正。

原文地址:https://www.cnblogs.com/liyihua/p/12210893.html

时间: 2024-10-12 22:29:36

Java 多线程安全问题简单切入详细解析的相关文章

thread.join函数,java多线程中的join函数解析

join函数的作用,是让当前线程等待,直到调用join()的 线程结束或者等到一段时间,我们来看以下代码 1 package mian; 2 3 4 public class simpleplela { 5 static void threadMessage(String message) { 6 String threadName = 7 Thread.currentThread().getName(); 8 9 System.out.println(threadName+" "+m

java基础知识回顾之java Thread类学习(五)--java多线程安全问题(锁)同步的前提

这里举个例子讲解,同步synchronized在什么地方加,以及同步的前提: * 1.必须要有两个以上的线程,才需要同步. * 2.必须是多个线程使用同一个锁. * 3.必须保证同步中只能有一个线程在运行,锁加在哪一块代码 那么我们要思考的地方有:1.知道我们写的哪些是多线程代码 2.明确共享数据 3.明确多线程运行的代码中哪些语句是操作共享数据的.. 4.要确保使用同一个锁. 下面的代码:需求:两个存户分别往银行存钱,每次村100块,分三次存完. class bank{ private int

Java多线程之简单的线程同步实例

数据类: package Thread.MyCommon; public class Data { public int num = 0; public synchronized int getEven() { ++num; Thread.yield();//让另外线程先执行,加大测试效果几率 ++num; return num; } } 线程类: package Thread.MyCommon; public class myThread implements Runnable { priva

java基础知识回顾之java Thread类学习(四)--java多线程安全问题(锁)

上一节售票系统中我们发现,打印出了错票,0,-1,出现了多线程安全问题.我们分析为什么会发生多线程安全问题? 看下面线程的主要代码: @Override public void run() { // TODO Auto-generated method stub while(true){ if(ticket > 0){//当线程0被调起的时候,当执行到这条判断语句的时候,线程1被调起抢了CPU资源,线程0进入冻结状态. try { Thread.sleep(100);//中断当前活跃的线程,或者

Java 多线程IO简单实用Demo

多线程主要作用是充分利用Cpu,而不在于它的乱序性.本Demo不讲它竞争什么的.之前看过乱序打印ABC的例子什么的,那些有意义吗? 本Demo 是多线程打印文件夹下的文件,主要实现是用数组存放文件,一个游标遍历. 我们需要考虑在什么时候加互斥访问,本例用synchronized . 先考虑单线程的流程:客户端启动-->读取文件下的文件放到数组(IO)--> 取游标打印 ,游标加1 -- > 改文件写文件(IO) -- 重复上两步至越界 -- 结束 多线程时显然需要在"取游标打印

java基础知识回顾之java Thread类学习(七)--java多线程安全问题(死锁)

死锁:是两个或者两个以上的线程被无限的阻塞,线程之间互相等待所需资源. 线程死锁产生的条件: 当两个线程相互调用Join()方法. 当两个线程使用嵌套的同步代码块的时候,一个线程占用了另一个线程的锁,互相等待阻塞,就有可能产生死锁. 下面看代码: 代码1:死锁的案例 package com.lp.ecjtu.Thread; /* 死锁:常见情景之一:同步的嵌套. */ class Ticket implements Runnable { private int num = 100; Object

Java多线程——<三>简单的线程执行:Executor

一.概述 按照<Java多线程——<一><二>>中所讲,我们要使用线程,目前都是显示的声明Thread,并调用其start()方法.多线程并行,明显我们需要声明多个线程然后都调用他的start方法,这么一看,似乎有些问题:第一.线程一旦多了,声明势必是个问题:第二.多线程启动如果通过手动执行的话,那可能一个线程已经跑完了,另外一个还没起来(我推测可能会出现这个问题).所以,我们在想,如果有个管家,能够帮我们管理这么多线程,只需要把我们定义的任务交给管家,管家就能够帮我们

【JAVA多线程安全问题解析】

一.问题的提出 以买票系统为例: 1 class Ticket implements Runnable 2 { 3 public int sum=10; 4 public void run() 5 { 6 while(true) 7 { 8 if(sum>0) 9 { 10 System.out.println(Thread.currentThread().getName()+":"+sum--); 11 } 12 } 13 } 14 } 15 public class Demo

java 多线程安全问题-同步代码块

/* 多线程的安全问题: while(true) { if(tick>0) { //线程0,1,2,3在余票为1时,都停滞在这里,之后分别获得CPU执行权,打印出0,-1,-2等错票 System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--); } } 问题的原因: 当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行一部分,还没有执行完,停滞 另一个线程参与进来执行,导致共享数据