在多线程环境下,我们常常需要让多个线程同时去操作同一资源。在某些情况下,这种情形会导致程序的运行结果出现差错。专业上的,当多个线程在执行同一段代码的时候,每次的执行结果和单线程执行的结果都是一样的,不存在执行结果的二义性,就可以称作是线程安全的。具体的说,线程安全问题,其实是指多线程环境下对共享资源的访问可能会引起此共享资源的不一致性。线程安全问题多是由全局变量和静态变量引起的,当多个线程对共享数据只执行读操作,不执行写操作时,一般是线程安全的;当多个线程都执行写操作时,就需要考虑线程同步来解决线程安全问题。所谓线程同步,是指将操作共享数据的代码行作为一个整体,同一时间只允许一个线程执行,执行过程中其他线程不能参与执行。目的是为了防止多个线程访问一个数据对象时,对数据造成的破坏。线程同步其实是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程才能去使用。这就好比我们现实生活中去食堂排队打饭,每个人一窝蜂涌在窗口肯定会引起混乱,排好队一个个来大家才能吃上饭。在Java中,实现线程同步的手段简单说,就是队列+锁。这一机制可以用synchronized关键字实现。下面我们举若干例子介绍synchronized的使用如何解决了线程安全的问题。

一、案例一:抢票——使用synchronized修饰方法形成同步方法

考虑这样一个场景,在售票站有10张票,现在来了3个人打算抢这10张票。我们使用Java编程模拟这一过程:

public class Main {public static void main(String[] args) {TicketStation ticketStation = new TicketStation(10);new Thread(ticketStation, "Alice").start();new Thread(ticketStation, "Bob").start();new Thread(ticketStation, "Cathy").start();}
}class TicketStation implements Runnable {private int numberOfTicket;private boolean hasTicket;public TicketStation(int numberOfTicket) {this.numberOfTicket = numberOfTicket;hasTicket = true;}@Overridepublic void run() {while (hasTicket) {buyTicket();}}private void buyTicket() {if (numberOfTicket <= 0) {hasTicket = false;} else {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " buy ticket #" + numberOfTicket--);}}
}

程序的运行结果如下:

Bob buy ticket #10
Cathy buy ticket #9
Alice buy ticket #8
Alice buy ticket #7
Bob buy ticket #6
Cathy buy ticket #5
Alice buy ticket #4
Bob buy ticket #3
Cathy buy ticket #2
Alice buy ticket #1
Cathy buy ticket #-1
Bob buy ticket #0

我们惊讶的发现,结果的末尾,Cathy居然抢到了第0张票,Bob抢到了第-1张票,这显然出现了错误。具体的原因是,在3个线程同时运行的过程中,它们都先执行条件判断“numberOfTicket <= 0”是否成立,每个线程读取到的numberOfTicket都是1,所以条件成立。但是之后,Alice线程率先执行了“numberOfTicket--”,抢到了第1张票,并将剩余票数减为0。然后,Bob线程也去执行“numberOfTicket--”,即先读取到当前剩余票数为0,然后减1,变成了-1。类似的,Cathy线程最后执行“numberOfTicket--”,读取到当前剩余票数为-1,然后减1,变成了-2。综上所述,这程序是线程不安全的。

为了解决这一问题,我们可以使用synchronized关键字对方法进行修饰,使之变为同步方法,例如

public synchronized void method(int arg) { ... }

被synchronized修饰的方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法必须获得调用该方法的对象的锁才能执行,否则线程会阻塞。方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。

针对上面抢票的例子,我们在buyTicket方法前加上了synchronized关键字,修改后的程序为:

public class Main {public static void main(String[] args) {TicketStation ticketStation = new TicketStation(10);new Thread(ticketStation, "Alice").start();new Thread(ticketStation, "Bob").start();new Thread(ticketStation, "Cathy").start();}
}class TicketStation implements Runnable {private int numberOfTicket;private boolean hasTicket;public TicketStation(int numberOfTicket) {this.numberOfTicket = numberOfTicket;hasTicket = true;}@Overridepublic void run() {while (hasTicket) {buyTicket();try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}private synchronized void buyTicket() {if (numberOfTicket <= 0) {hasTicket = false;} else {System.out.println(Thread.currentThread().getName() + " buy ticket #" + numberOfTicket--);}}
}

程序的执行结果为:

Alice buy ticket #10
Cathy buy ticket #9
Bob buy ticket #8
Cathy buy ticket #7
Alice buy ticket #6
Bob buy ticket #5
Alice buy ticket #4
Bob buy ticket #3
Cathy buy ticket #2
Cathy buy ticket #1

此时,3个人排好队依次购票,就没有出现之前混乱的场面了。

细心的读者可能发现,修改后的程序中线程休眠的语句从buyTicket方法体中挪到了run方法中。这是出于什么考虑呢?假设我们依然将线程休眠的语句放在buyTicket方法体内,即

private synchronized void buyTicket() {if (numberOfTicket <= 0) {hasTicket = false;} else {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " buy ticket #" + numberOfTicket--);}}

那么程序的运行结果很大可能是这样的:

Alice buy ticket #10
Alice buy ticket #9
Alice buy ticket #8
Alice buy ticket #7
Alice buy ticket #6
Alice buy ticket #5
Alice buy ticket #4
Alice buy ticket #3
Alice buy ticket #2
Alice buy ticket #1

即这10张票都被同一个人买走了!这是为什么呢?由于线程休眠时,调用对象不会释放锁,所以在休眠100ms时Alice线程依旧把持着锁(或者说占据着售票窗口),其它线程是没有机会进入buyTicket方法买票的。当Alice线程买好一张票后,buyTicket方法执行结束,Alice线程释放了锁。但是CPU依然让Alice线程执行run方法中的while判断,发现依然有票,于是获得锁又去执行buyTicket方法。在这极短暂的时间内,其它线程根本没有机会抢占到时间执行buyTicket方法。因此,为了让3个线程都有机会买到票,我们将线程休眠语句从同步方法buyTicket中拿了出来。于是,当一个线程在执行buyTicket方法买票时,其它线程没有机会也进入buyTicket方法购票;而当一个线程执行完毕buyTicket方法并释放锁后,该线程会休眠一段时间,这就给了其它线程机会来拿到锁,执行buyTicket方法进行购票。

二、案例二:取款——使用synchronized修饰代码块形成同步块。

使用synchronized关键字修饰方法存在着一个弊端:若将一个很大/复杂的方法声明为同步方法,那么它将大量霸占计算资源,严重影响程序运行效率。举个不是很文雅的例子,假如现在有5个人要排队上厕所,其中4个人只是尿尿(时间很短,不会长期占用厕所),1个人则是拉肚子(时间很长,会长期霸占厕所)。假如那一名拉肚子的朋友进入了厕所,关起了门(拿到锁),其它4个人本来一会就能完事,如今却要等上半天,憋的痛不欲生。这样子的安排显然不甚合理。所以,在实践中,我们会使用synchronized关键字仅修饰方法中的一小段程序,即修饰代码块。语法格式为:

synchronized (obj) { ... }

其中花括号内的为代码块,括号内的obj称为“同步监视器”,它可以是任何对象,但一般推荐是共享资源(即要在代码块中被修改的共享资源,若仅是读取则无需设立同步代码块)。

下面我们以模拟银行账户取款为例进行讲解。假设我们在银行内有一个账户,现在同时在手机银行和线下ATM机上执行取款操作,利用程序进行模拟如下:

public class Main {public static void main(String[] args) {Account account = new Account("Research Funding", 100);new WithdrawMoney(account, "APP", 50).start();new WithdrawMoney(account, "ATM", 100).start();}
}class Account {private String name;private int money;public Account(String name, int money) {this.name = name;this.money = money;}public String getName() { return this.name; }public int getMoney() { return this.money; }public void setName(String name) { this.name = name; }public void setMoney(int money) { this.money = money; }
}class WithdrawMoney extends Thread {private Account account;private int moneyToDraw;public WithdrawMoney(Account account, String threadName, int moneyToDraw) {super(threadName);this.account = account;this.moneyToDraw = moneyToDraw;}@Overridepublic void run() {withdraw();}private void withdraw() {if (account.getMoney() < moneyToDraw) {System.out.println(account.getName() + ": no enough money");} else {// It takes a while to withdraw moneytry {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}account.setMoney(account.getMoney() - moneyToDraw);System.out.println(Thread.currentThread().getName() + ": draw money " + moneyToDraw);System.out.println(account.getName() + ": balance is " + account.getMoney());}}
}

程序的运行结果可能是:

ATM: draw money 100
Research Funding: balance is 0
APP: draw money 50
Research Funding: balance is -50

显然,银行账户内的余额不应该是-50。出行这种现象的原因是,两个线程都先后执行withdraw中的if判断,此时都读取到当前账户余额为100,并不低于待取金额,因此打算执行else中的语句。然而,ATM线程先执行该语句块,将钱全部取出,于是账户余额清零。接着APP线程也过来执行这一段话,于是取走50后账户余额就变成了-50。显然,这是线程不安全的实现。

假如,我们跟之前一样在withdraw方法前添加synchronized关键字,并不会解决线程不安全的问题。因为synchronized方法默认的同步监视器(或者说共享资源)是this(或者说是这个类的对象自己)。在本例中,我们的共享资源是Account类对象,而非WithdrawMoney类对象。在上面的程序中,我们创建了两个WithdrawMoney线程对象,两者是相互独立的。就算用synchronized修饰了withdraw方法,那么APP线程锁住跟ATM线程锁住没有一点关系,APP线程获得锁并不妨碍ATM线程执行它自己的withdraw方法。于是,两个线程还是能同时操作Account对象。因此,我们就需要在withdraw方法中使用同步语句块。当一个线程中操作account时,其它线程就只能排队等待,这个逻辑才是正确的。

public class Main {public static void main(String[] args) {Account account = new Account("Research Funding", 100);new WithdrawMoney(account, "APP", 50).start();new WithdrawMoney(account, "ATM", 100).start();}
}class Account {private String name;private int money;public Account(String name, int money) {this.name = name;this.money = money;}public String getName() { return this.name; }public int getMoney() { return this.money; }public void setName(String name) { this.name = name; }public void setMoney(int money) { this.money = money; }
}class WithdrawMoney extends Thread {private Account account;private int moneyToDraw;public WithdrawMoney(Account account, String threadName, int moneyToDraw) {super(threadName);this.account = account;this.moneyToDraw = moneyToDraw;}@Overridepublic void run() {withdraw();}private void withdraw() {synchronized (account) {if (account.getMoney() < moneyToDraw) {System.out.println(account.getName() + ": no enough money");} else {// It takes a while to withdraw moneytry {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}account.setMoney(account.getMoney() - moneyToDraw);System.out.println(Thread.currentThread().getName() + ": draw money " + moneyToDraw);System.out.println(account.getName() + ": balance is " + account.getMoney());}}}
}

这时,程序的运行结果为

APP: draw money 50
Research Funding: balance is 50
Research Funding: no enough money

Java多线程编程——线程同步与线程安全问题及synchronized关键字相关推荐

  1. Java多线程编程-(4)-线程间通信机制的介绍与使用

    上一篇: Java多线程编程-(1)-线程安全和锁Synchronized概念 Java多线程编程-(2)-可重入锁以及Synchronized的其他基本特性 Java多线程编程-(3)-线程本地Th ...

  2. java 多线程编程(包括创建线程的三种方式、线程的生命周期、线程的调度策略、线程同步、线程通信、线程池、死锁等)

    1 多线程的基础知识 1.1 单核CPU和多核CPU 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务.微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那 ...

  3. Java多线程编程-(1)-线程安全和锁Synchronized概念

    ##一.进程与线程的概念 ## (1)在传统的操作系统中,程序并不能独立运行,作为资源分配和独立运行的基本单位都是进程. 在未配置 OS 的系统中,程序的执行方式是顺序执行,即必须在一个程序执行完后, ...

  4. java多线程编程(三)- 线程的创建

    一:线程说明 1,Java虚拟机允许应用程序并发的运行多个执行线程. 2,线程都有自己的优先级,新线程会继承创建它的线程优先级. 3,线程可以为守护线程和用户线程,如java资源回收线程为守护线程.当 ...

  5. java多线程编程(六)-线程间通信

    一:线程通信介绍 线程通信是通过主动放弃对资源的使用,而让给其它线程的过程.合理的安排多个线程对同一资源的使用,即设计线程间的通信,可以完成很多复杂的任务. 二:线程通信实现 1,java.lang. ...

  6. Java多线程初学者指南(10):使用Synchronized关键字同步类方法

    要想解决"脏数据"的问题,最简单的方法就是使用synchronized关键字来使run方法同步,代码如下: public synchronized void run() {     ...

  7. Java多线程编程-(5)-使用Lock对象实现同步以及线程间通信

    前几篇: Java多线程编程-(1)-线程安全和锁Synchronized概念 Java多线程编程-(2)-可重入锁以及Synchronized的其他基本特性 Java多线程编程-(3)-线程本地Th ...

  8. Java多线程编程-(6)-两种常用的线程计数器CountDownLatch和循环屏障CyclicBarrier

    前几篇: Java多线程编程-(1)-线程安全和锁Synchronized概念 Java多线程编程-(2)-可重入锁以及Synchronized的其他基本特性 Java多线程编程-(3)-线程本地Th ...

  9. Java多线程编程(四)——死锁问题

    死锁 什么是死锁? 什么情况下会产生死锁? 生产者与消费者 什么是生产者与消费者? Object类的等待和唤醒方法 生产者-消费者案例(唤醒机制) 基本写法 代码书写技巧与"套路" ...

  10. java多线程编程—高级主题_Java day20 高级编程【第一章】Java多线程编程

    [第一章]Java多线程编程 一.进程与线程 多个时间段会有多个程序依次执行,但是同一时间点只有一个进程执行 线程是在进程基础之上划分的更小的程序单元 ,线程是在进程基础上创建并且使用的,所以线程依赖 ...

最新文章

  1. pyqt5如何循环遍历控件名_利用Python的PyQt5编写GUI界面教学,QT5还是比较难的
  2. rs485调试软件_【乐创“芯”说】你想知道的RS485
  3. 使用Java泛型实现快速排序(快排,Quicksort)
  4. 开启虚拟化技术之旅---1什么是虚拟化?
  5. SecureCRT连接Linux终端中文乱码解决方法
  6. QT的QBufferDataGenerator类的使用
  7. centos7启动与切换图形界面
  8. SQL-ALTER-change和modify区别
  9. 在公司交了十年社保了,退休了,自己还要补交六、七万元社保,你觉得该不该补交?
  10. Win32 Thread Information Block
  11. python 拼音输入法_隐马尔科夫模型python实现简单拼音输入法
  12. 先进驾驶员辅助系统ADSA
  13. 身份证真伪辨别 Python
  14. 如何查看和修改Windows的主机名
  15. 一口气说出 OAuth2.0 的四种授权方式,面试官会高看一眼
  16. python学习笔记9——第八章 异常
  17. Python3.5 处理excel_1(删除多余行)
  18. 2020JDK1.8安装教程,配有每一步的图文安装细节,一次就可安装成功!
  19. 利用GDAL(python)读取Landsat8数据
  20. 2018中国软件和信息技术服务综合竞争力百强企业

热门文章

  1. 圆周率一千万亿位_圆周率被算到31.4万亿位,它的终点是宇宙奇点?爱因斯坦说对了?...
  2. 中国十大可行性研究报告公司
  3. 双耳节拍 枕头_枕头2015年报告
  4. 内置 DSP,回音消除,噪音抑制全双工通话芯片—ATH8809
  5. PostgreSQL 源码解读(203)- 查询#116(类型转换实现)
  6. ERP系统对接方案,API接口封装系列(高并发)
  7. Java:用递归计算n!
  8. EC MCAD Connector 3010 AdminGuide-2
  9. 微信小程序获取手机号的乱码问题
  10. 分类统计字符—Python