本文主要接着前面多线程的两篇文章总结Java多线程中的线程安全问题。

一.一个典型的Java线程安全例子

 public class ThreadTest {

     public static void main(String[] args) {
Account account = new Account("123456", 1000);
DrawMoneyRunnable drawMoneyRunnable = new DrawMoneyRunnable(account, 700);
Thread myThread1 = new Thread(drawMoneyRunnable);
Thread myThread2 = new Thread(drawMoneyRunnable);
myThread1.start();
myThread2.start();
} } class DrawMoneyRunnable implements Runnable { private Account account;
private double drawAmount; public DrawMoneyRunnable(Account account, double drawAmount) {
super();
this.account = account;
this.drawAmount = drawAmount;
} public void run() {
if (account.getBalance() >= drawAmount) { //
System.out.println("取钱成功, 取出钱数为:" + drawAmount);
double balance = account.getBalance() - drawAmount;
account.setBalance(balance);
System.out.println("余额为:" + balance);
}
}
} class Account { private String accountNo;
private double balance; public Account() { } public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
} public String getAccountNo() {
return accountNo;
} public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
} public double getBalance() {
return balance;
} public void setBalance(double balance) {
this.balance = balance;
} }

上面例子很容易理解,有一张银行卡,里面有1000的余额,程序模拟你和你老婆同时在取款机进行取钱操作的场景。多次运行此程序,可能具有多个不同组合的输出结果。其中一种可能的输出为:

 取钱成功, 取出钱数为:700.0
余额为:300.0
取钱成功, 取出钱数为:700.0
余额为:-400.0

也就是说,对于一张只有1000余额的银行卡,你们一共可以取出1400,这显然是有问题的。

经过分析,问题在于Java多线程环境下的执行的不确定性。CPU可能随机的在多个处于就绪状态中的线程中进行切换,因此,很有可能出现如下情况:当thread1执行到//1处代码时,判断条件为true,此时CPU切换到thread2,执行//1处代码,发现依然为真,然后执行完thread2,接着切换到thread1,接着执行完毕。此时,就会出现上述结果。

因此,讲到线程安全问题,其实是指多线程环境下对共享资源的访问可能会引起此共享资源的不一致性。因此,为避免线程安全问题,应该避免多线程环境下对此共享资源的并发访问。

二.同步方法

对共享资源进行访问的方法定义中加上synchronized关键字修饰,使得此方法称为同步方法。可以简单理解成对此方法进行了加锁,其锁对象为当前方法所在的对象自身。多线程环境下,当执行此方法时,首先都要获得此同步锁(且同时最多只有一个线程能够获得),只有当线程执行完此同步方法后,才会释放锁对象,其他的线程才有可能获取此同步锁,以此类推...

在上例中,共享资源为account对象,当使用同步方法时,可以解决线程安全问题。只需在run()方法前加上synshronized关键字即可。

 public synchronized void run() {

     // ....

 }

三.同步代码块

正如上面所分析的那样,解决线程安全问题其实只需限制对共享资源访问的不确定性即可。使用同步方法时,使得整个方法体都成为了同步执行状态,会使得可能出现同步范围过大的情况,于是,针对需要同步的代码可以直接另一种同步方式——同步代码块来解决。

同步代码块的格式为:

 synchronized (obj) {

     //...

 }

其中,obj为锁对象,因此,选择哪一个对象作为锁是至关重要的。一般情况下,都是选择此共享资源对象作为锁对象。

如上例中,最好选用account对象作为锁对象。(当然,选用this也是可以的,那是因为创建线程使用了runnable方式,如果是直接继承Thread方式创建的线程,使用this对象作为同步锁会其实没有起到任何作用,因为是不同的对象了。因此,选择同步锁时需要格外小心...)

四.Lock对象同步锁

上面我们可以看出,正因为对同步锁对象的选择需要如此小心,有没有什么简单点的解决方案呢?以方便同步锁对象与共享资源解耦,同时又能很好的解决线程安全问题。

使用Lock对象同步锁可以方便的解决此问题,唯一需要注意的一点是Lock对象需要与资源对象同样具有一对一的关系。Lock对象同步锁一般格式为:

 class X {

     // 显示定义Lock同步锁对象,此对象与共享资源具有一对一关系
private final Lock lock = new ReentrantLock(); public void m(){
// 加锁
lock.lock(); //... 需要进行线程安全同步的代码 // 释放Lock锁
lock.unlock();
} }

 五.wait()/notify()/notifyAll()线程通信

在博文《Java总结篇系列:java.lang.Object》中有提及到这三个方法,虽然这三个方法主要都是用于多线程中,但实际上都是Object类中的本地方法。因此,理论上,任何Object对象都可以作为这三个方法的主调,在实际的多线程编程中,只有同步锁对象调这三个方法,才能完成对多线程间的线程通信。

wait():导致当前线程等待并使其进入到等待阻塞状态。直到其他线程调用该同步锁对象的notify()或notifyAll()方法来唤醒此线程。

notify():唤醒在此同步锁对象上等待的单个线程,如果有多个线程都在此同步锁对象上等待,则会任意选择其中某个线程进行唤醒操作,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。

notifyAll():唤醒在此同步锁对象上等待的所有线程,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。

 package com.qqyumidi;

 public class ThreadTest {

     public static void main(String[] args) {
Account account = new Account("123456", 0); Thread drawMoneyThread = new DrawMoneyThread("取钱线程", account, 700);
Thread depositeMoneyThread = new DepositeMoneyThread("存钱线程", account, 700); drawMoneyThread.start();
depositeMoneyThread.start();
} } class DrawMoneyThread extends Thread { private Account account;
private double amount; public DrawMoneyThread(String threadName, Account account, double amount) {
super(threadName);
this.account = account;
this.amount = amount;
} public void run() {
for (int i = 0; i < 100; i++) {
account.draw(amount, i);
}
}
} class DepositeMoneyThread extends Thread { private Account account;
private double amount; public DepositeMoneyThread(String threadName, Account account, double amount) {
super(threadName);
this.account = account;
this.amount = amount;
} public void run() {
for (int i = 0; i < 100; i++) {
account.deposite(amount, i);
}
}
} class Account { private String accountNo;
private double balance;
// 标识账户中是否已有存款
private boolean flag = false; public Account() { } public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
} public String getAccountNo() {
return accountNo;
} public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
} public double getBalance() {
return balance;
} public void setBalance(double balance) {
this.balance = balance;
} /**
* 存钱
*
* @param depositeAmount
*/
public synchronized void deposite(double depositeAmount, int i) { if (flag) {
// 账户中已有人存钱进去,此时当前线程需要等待阻塞
try {
System.out.println(Thread.currentThread().getName() + " 开始要执行wait操作" + " -- i=" + i);
wait();
//
System.out.println(Thread.currentThread().getName() + " 执行了wait操作" + " -- i=" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 开始存钱
System.out.println(Thread.currentThread().getName() + " 存款:" + depositeAmount + " -- i=" + i);
setBalance(balance + depositeAmount);
flag = true; // 唤醒其他线程
notifyAll(); //
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-- 存钱 -- 执行完毕" + " -- i=" + i);
}
} /**
* 取钱
*
* @param drawAmount
*/
public synchronized void draw(double drawAmount, int i) {
if (!flag) {
// 账户中还没人存钱进去,此时当前线程需要等待阻塞
try {
System.out.println(Thread.currentThread().getName() + " 开始要执行wait操作" + " 执行了wait操作" + " -- i=" + i);
wait();
System.out.println(Thread.currentThread().getName() + " 执行了wait操作" + " 执行了wait操作" + " -- i=" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 开始取钱
System.out.println(Thread.currentThread().getName() + " 取钱:" + drawAmount + " -- i=" + i);
setBalance(getBalance() - drawAmount); flag = false; // 唤醒其他线程
notifyAll(); System.out.println(Thread.currentThread().getName() + "-- 取钱 -- 执行完毕" + " -- i=" + i); //
}
} }

上面的例子演示了wait()/notify()/notifyAll()的用法。部分输出结果为:

 取钱线程 开始要执行wait操作 执行了wait操作 -- i=0
存钱线程 存款:700.0 -- i=0
存钱线程-- 存钱 -- 执行完毕 -- i=0
存钱线程 开始要执行wait操作 -- i=1
取钱线程 执行了wait操作 执行了wait操作 -- i=0
取钱线程 取钱:700.0 -- i=1
取钱线程-- 取钱 -- 执行完毕 -- i=1
取钱线程 开始要执行wait操作 执行了wait操作 -- i=2
存钱线程 执行了wait操作 -- i=1
存钱线程 存款:700.0 -- i=2
存钱线程-- 存钱 -- 执行完毕 -- i=2
取钱线程 执行了wait操作 执行了wait操作 -- i=2
取钱线程 取钱:700.0 -- i=3
取钱线程-- 取钱 -- 执行完毕 -- i=3
取钱线程 开始要执行wait操作 执行了wait操作 -- i=4
存钱线程 存款:700.0 -- i=3
存钱线程-- 存钱 -- 执行完毕 -- i=3
存钱线程 开始要执行wait操作 -- i=4
取钱线程 执行了wait操作 执行了wait操作 -- i=4
取钱线程 取钱:700.0 -- i=5
取钱线程-- 取钱 -- 执行完毕 -- i=5
取钱线程 开始要执行wait操作 执行了wait操作 -- i=6
存钱线程 执行了wait操作 -- i=4
存钱线程 存款:700.0 -- i=5
存钱线程-- 存钱 -- 执行完毕 -- i=5
存钱线程 开始要执行wait操作 -- i=6
取钱线程 执行了wait操作 执行了wait操作 -- i=6
取钱线程 取钱:700.0 -- i=7
取钱线程-- 取钱 -- 执行完毕 -- i=7
取钱线程 开始要执行wait操作 执行了wait操作 -- i=8
存钱线程 执行了wait操作 -- i=6
存钱线程 存款:700.0 -- i=7

由此,我们需要注意如下几点:

1.wait()方法执行后,当前线程立即进入到等待阻塞状态,其后面的代码不会执行;

2.notify()/notifyAll()方法执行后,将唤醒此同步锁对象上的(任意一个-notify()/所有-notifyAll())线程对象,但是,此时还并没有释放同步锁对象,也就是说,如果notify()/notifyAll()后面还有代码,还会继续进行,知道当前线程执行完毕才会释放同步锁对象;

3.notify()/notifyAll()执行后,如果右面有sleep()方法,则会使当前线程进入到阻塞状态,但是同步对象锁没有释放,依然自己保留,那么一定时候后还是会继续执行此线程,接下来同2;

4.wait()/notify()/nitifyAll()完成线程间的通信或协作都是基于不同对象锁的,因此,如果是不同的同步对象锁将失去意义,同时,同步对象锁最好是与共享资源对象保持一一对应关系;

5.当wait线程唤醒后并执行时,是接着上次执行到的wait()方法代码后面继续往下执行的。

当然,上面的例子相对来说比较简单,只是为了简单示例wait()/notify()/noitifyAll()方法的用法,但其本质上说,已经是一个简单的生产者-消费者模式了。

Java总结篇系列:Java多线程(三)的更多相关文章

  1. Java总结篇系列:Java多线程(二)

    本文承接上一篇文章<Java总结篇系列:Java多线程(一)>. 四.Java多线程的阻塞状态与线程控制 上文已经提到Java阻塞的几种具体类型.下面分别看下引起Java线程阻塞的主要方法 ...

  2. Java总结篇:Java多线程

    Java总结篇系列:Java多线程 多线程作为Java中很重要的一个知识点,在此还是有必要总结一下的. 一.线程的生命周期及五种基本状态 关于Java中线程的生命周期,首先看一下下面这张较为经典的图: ...

  3. Java总结篇系列:Java String

    String作为Java中最常用的引用类型,相对来说基本上都比较熟悉,无论在平时的编码过程中还是在笔试面试中,String都很受到青睐,然而,在使用String过程中,又有较多需要注意的细节之处. 1 ...

  4. java提高篇-----理解java的三大特性之封装

    在<Think in java>中有这样一句话:复用代码是Java众多引人注目的功能之一.但要想成为极具革命性的语言,仅仅能够复制代码并对加以改变是不够的,它还必须能够做更多的事情.在这句 ...

  5. Maven提高篇系列之(三)——使用自己的Repository(Nexus)

    这是一个Maven提高篇的系列,包含有以下文章: Maven提高篇系列之(一)——多模块 vs 继承 Maven提高篇系列之(二)——配置Plugin到某个Phase(以Selenium集成测试为例) ...

  6. Java总结篇系列:Java多线程(四)

    ThreadLocal是什么 早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路.使用这个工具类可以很简洁地 ...

  7. Java总结篇系列:java.lang.Object

    从本篇开始,将对Java中各知识点进行一次具体总结,以便对以往的Java知识进行一次回顾,同时在总结的过程中加深对Java的理解. Java作为一个庞大的知识体系,涉及到的知识点繁多,本文将从Java ...

  8. java基础篇---I/O技术(三)

    接上一篇java基础篇---I/O技术(二) Java对象的序列化和反序列化 什么叫对象的序列化和反序列化 要想完成对象的输入或输出,还必须依靠对象输出流(ObjectOutputStream)和对象 ...

  9. Java面试题系列 ----- Java基础面试题(91道)

    更多详情点击查看,点这里!这里!!这里!!! 文末获取所有面试PDF文档! Java概述 1. 何为编程 编程就是让计算机为解决某个问题而使用某种程序设计语言编写程序代码,并最终得到结果的过程. 为了 ...

随机推荐

  1. django开发个人简易Blog——数据模型

    提到数据模型,一定要说一下MVC,MVC框架是现代web开发中最流行的开发框架,它将数据与业务逻辑分开,减小了应用之间的高度耦合.个人非常喜欢MVC开发框架,除了具有上述特性,它使得web开发变得非常 ...

  2. web系统架构设计中需要知道的点(前端篇)

    上周没写东西,这周写点互联网系统开发中需要了解的技术点,每个点都可以发散出去,连接更多的知识点,打算做个逐步细化的记录. 一个应用的整个生命周期中(生,老,病,死)都需要有一个整体规划. 前期 评估需 ...

  3. JS中的宽高(基础知识很重要)

    IE中:document.body.clientWidth ==> BODY对象宽度document.body.clientHeight ==> BODY对象高度document.docu ...

  4. Atitit.架构设计趋势 设计模式 ---微服务架构  soa

    Atitit.架构设计趋势 设计模式 ---微服务架构  soa 什么是微服务架构?1 .微服务与SOA的关系 :微服务架架构师面向服务架构(SOA)的一种特定实现1 微服务与康威定律2 微服务的一些 ...

  5. Java六大问题你都懂了吗?

    这些问题对于认真学习java的人都要必知的,当然如果你只是初学者就没必要那么严格了,那如果你认为自己已经超越初学者了,却不很懂这些问题,请将你自己重归初学者行列. 一.到底要怎么样初始化! 本问题讨论 ...

  6. django创建项目

    django创建项目 安装django pip install django==1.9 Note: C:\Python34\Scripts\pip.exe 创建项目 django-admin star ...

  7. Android入门(十四)内容提供器-实现跨程序共享实例

    原文链接:http://www.orlion.ga/661/ 打开SQLite博文中创建的 DatabaseDemo项目,首先将 MyDatabaseHelper中使用 Toast弹出创建数据库成功的 ...

  8. 动态分配的顺序线性表的十五种操作—C语言实现

    线性表 定义:是最常用的,也是最简单的数据结构,是长度为n个数据元素的有序的序列. 含有大量记录的线性表叫文件 记录:稍微复杂的线性表里,数据元素为若干个数据项组成,这时把一个数据元素叫记录 结构特点 ...

  9. 百度地图JavaScript API覆盖物旋转时出现偏移

    在项目中,调用百度地图JavaScript API,做覆盖物的旋转再添加到地图上,结果出现偏移了. 调试过程中的效果图: 发现图片的旋转并不是按车子的中心来的,而是之外的一个点.最后发现犯了一个很细节 ...

  10. Android基于mAppWidget实现手绘地图(五)--如何创建地图资源

    地图资源可以通过Slicing Tool工具生成,教程如下: 1.打开Eclipse标准版4.3.2,以Java项目形式导入”slicingtool“项目,运行.(必须是eclipse4.3.2及以上 ...