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程序中为什么会出现这种情况
- 在CPU线程的调度分类中,Java使用的是抢占式调度。
- 我们开启了三个线程,3个线程一起在抢夺CPU的执行权,谁能抢到谁就可以被执行。
- 从输出结果可以知道,刚开始抢夺CPU执行权的时候,线程0(window0窗口)先抢到,再到线程1(window1窗口)抢到,最后线程2(window2窗口)才抢到。
- 那么为什么100号票已经在0号窗口出售了,在1号窗口还会出售呢?其实很简单,线程0先抢到CPU执行权,于是有了执行权后,他就开始嚣张了,作为第一个它通过while判断,很自豪的拿着ticketNumber = 100进入while里面开始执行。
- 可线程0是万万没有想到,这时候的线程1,在拿到执行权后,在线程0刚刚实现print语句还没开始ticketNumber --的时候,线程1以ticketNumber = 100跑进了while里面。
- 线程2很遗憾,在线程0执行了ticketNumber --了才急匆匆的进入while里面,不过它也不甘落后,于是拼命追赶。终于,后来居上,在线程1还没开始print的时候,他就开始print了。于是便出现了控制台的前三条输出的情况。
- 售票窗口(window0)正在出售:100号电影票
- 售票窗口(window2)正在出售:99号电影票
- 售票窗口(window1)正在出售:100号电影票
window0、window1、window2分别对应线程0、线程1、线程2
- 售票窗口(window0)正在出售:100号电影票
- 以此类推,直到最后程序执行完毕。
解决情况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 多线程安全问题就到这里了,如果有什么不足、错误的地方,希望大佬们指正。
Java 多线程安全问题简单切入详细解析的更多相关文章
- Java多线程学习(吐血超详细总结)
Java多线程学习(吐血超详细总结) 林炳文Evankaka原创作品.转载请注明出处http://blog.csdn.net/evankaka 写在前面的话:此文只能说是java多线程的一个入门,其实 ...
- java基础知识回顾之java Thread类学习(四)--java多线程安全问题(锁)
上一节售票系统中我们发现,打印出了错票,0,-1,出现了多线程安全问题.我们分析为什么会发生多线程安全问题? 看下面线程的主要代码: @Override public void run() { // ...
- Java多线程——<三>简单的线程执行:Executor
一.概述 按照<Java多线程——<一><二>>中所讲,我们要使用线程,目前都是显示的声明Thread,并调用其start()方法.多线程并行,明显我们需要声明多个 ...
- 【JAVA多线程安全问题解析】
一.问题的提出 以买票系统为例: class Ticket implements Runnable { public int sum=10; public void run() { while(tru ...
- java 多线程安全问题-同步代码块
/* 多线程的安全问题: while(true) { if(tick>0) { //线程0,1,2,3在余票为1时,都停滞在这里,之后分别获得CPU执行权,打印出0,-1,-2等错票 Syste ...
- Java虚拟机堆和栈详细解析,以后面试再也不怕问jvm了!
堆 Java堆是和Java应用程序关系最密切的内存空间,几乎所有的对象都放在其中,并且Java堆完全是自动化管理,通过垃圾收集机制,垃圾对象会自动清理,不需自己去释放. 根据垃圾回收机制的不同,Jav ...
- Java——多线程安全问题
静态代码块中没有this /* * 线程安全问题产生的原因: * 1.多个线程操作共享的数据 * 2.操作共享数据的线程代码有多条 * * 当一个线程在执行操作共享数据的多条代码过程中,其他线程 ...
- Java多线程实现简单的售票程序
设计一个多线程程序如下:设计一个火车售票模拟程序.假如火车站要有100张火车票要卖出,现在有5个售票点同时售票,用5个线程模拟这5个售票点的售票情况 1.要求打印出每个售票点所卖出的票号 2.各售票点 ...
- java多线程安全问题-同步修饰符于函数
上一篇文章通过卖票使用同步代码块的方法解决安全问题本篇文章首先探讨如何找出这样的安全问题,并提出第二种方式(非静态函数synchronized修饰)解决安全问题 /* 需求: 银行有一个公共账号金库 ...
随机推荐
- Cookie内不能直接存入中文,cookie转码以及解码
如果在cookie中存入中文,极易出现问题. js在存入cookie时,利用escape() 函数可对字符串进行编码, 用unescape()进行解码 顺序是先把cookie用escape()函数编码 ...
- 关于Ping和Tracert命令原理详解
本文只是总结了两个常用的网络命令的实现原理和一点使用经验说明.这些东西通常都分布在各种书籍或者文章中的,我勤快那么一点点,总结一下,再加上我的一点理解和使用经验,方便大家了解.这些也是很基础的东西,没 ...
- git 回滚到某个版本
首先使用git log 显示最近的代码提交记录 commit后面的内容,就是回滚的记录名 增加了加载条显示,提高用户体验 commit 47f45668e72e4deeccae85e9767c250d ...
- 2018-2-13-win10-uwp-InkCanvas控件数据绑定
title author date CreateTime categories win10 uwp InkCanvas控件数据绑定 lindexi 2018-2-13 17:23:3 +0800 20 ...
- java笔试题及其答案
1:下列哪个工具可以编译源文件(A) A:javac B:jdb C:javadoc D:junit 2:String b = new String("1"+"2&quo ...
- linux 让出处理器
如我们已见到的, 忙等待强加了一个重负载给系统总体; 我们乐意找出一个更好的技术. 想到的第一个改变是明确地释放 CPU 当我们对其不感兴趣时. 这是通过调用调度函数而 实现地, 在 <linu ...
- CentOS yum有时出现“Could not retrieve mirrorlist ”的解决办法——resolv.conf的配置
国内服务器在运行命令yum -y install wget的时候,出现: Could not retrieve mirrorlist http://mirrorlist.centos.org/?rel ...
- SpringBoot的四种定时任务
定时任务实现的几种方式: Timer:这是java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务. 使用这种方式可以让你的程序按照某一个频度执行 ...
- Linux 内核控制 urb
控制 urb 被初始化几乎和 块 urb 相同的方式, 使用对函数 usb_fill_control_urb 的 调用: void usb_fill_control_urb(struct urb *u ...
- Vue学习笔记-目录结构
1.采用脚手架构建的项目基本目录结构 可能会有些许差别,但是大致基本目录都差不多 2.项目入口(index.html,main.js,App.vue) 一般情况下,我们都习惯性将 index.html ...