首页 > 其他 > 详细

并发系列(二)线程安全之synchronized

时间:2014-03-06 07:11:37      阅读:501      评论:0      收藏:0      [点我收藏+]

前述

并发系列是我个人的学习笔记,主要参考书籍<<java并发编程实战>>,<<java虚拟机并发编程>>,<<java 虚拟机规范(J2SE-7)>>,参考网站:并发编程网(强烈建议大家去瞄一瞄)

先看下面一段简单的代码

bubuko.com,布布扣
public class AccountUnSafe {

private String accountId;
private Double money;

//转账
public void tranferMoney(AccountUnSafe from,AccountUnSafe to,double amontMoney){
if(from.getMoney()<amontMoney) throw new IllegalArgumentException("账户余额不足");
from.decreace(amontMoney);
to.increace(amontMoney);
//...日志记录等等操作
}

//取款
public double drawMoney(double toDrawAmont){
if(getMoney()<toDrawAmont) throw new IllegalArgumentException("账户余额不足");
decreace(toDrawAmont);
return toDrawAmont;
}


private double getMoney() {
return money;
}

private void increace(double money) {
this.money += money;
}

private void decreace(double money) {
this.money -= money;
}

}

bubuko.com,布布扣

我想你很乐意银行去这么实现转账业务.假如现在我去给你转账10000,这时你又在取钱10000.恰巧系统同时开启了两个线程,我给你的转账线程T,你的取钱线程G.T和G同时获得了你的账户余额10000元.之后G线程先更新了你的账户余额为0,T却接着increace了你的账户,由于T拿到的你的账户时的余额是1万元.我又打给你1万元.此时你的账户余额已经是2万了,而事实是只应该是一万.

这就是多线程操作共享数据造成的数据的二异性,这样的线程是不安全的,不符合我们的业务逻辑.在java并发编程实战中作者是这样叙述什么是线程安全的:

当多个线程访问某个类的时候,不管运行时环境采用何种调度方式,或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的

根据上面的线程安全定义,当T,G线程同时访问你的账户的时候,获得了相同的账户余额,接着G更新了账户余额,之后T又更新了余额.不管是T先更新,还是G先更新,都不能表现出正确的行为,都违反了此例的业务逻辑.我个人觉得线程安全就是涉及对象数据的回写操作时,在多线程环境下回写数据对于所有线程都可见,不出现数据二异性.这样的类就是线程安全的

读取账户余额->更新账户余额,是一个原子性的行为.对于原子性行为我的理解就是不可分隔,不可干扰的定义的最小行为单位.它可以是一个action,也可以是一系类action的复合操作

上述情形在内存的表现

bubuko.com,布布扣
bubuko.com,布布扣
bubuko.com,布布扣

对于线程不安全的类,可以通过加锁使其在多线程环境下对原子性的复合操作串行执行

大家看代码-1:

bubuko.com,布布扣
private synchronized void increace(double money) {
this.money += money;
// synchronized (this) {
// this.money += money;
// }
}

private synchronized void decreace(double money) {
this.money -= money;
// synchronized (this) {
// this.money -= money;
// }
}

bubuko.com,布布扣

代码-1使用了synchronized关键字来同步两个私有的方法,这样是能解决上述转账,取钱的线程安全问题,但是如果换成另外一种业务场景就会存在非常严重的缺陷,大家可以考虑一下存在什么问题.先回到synchronized,其特性如下:

1. 一个方法声明为synchronized后,任何时刻仅且只能有一个线程访问此方法直到线程释放此方法持有的锁.

2. 锁是谁?任何对象都可以是锁,此例中的锁为账户对象,切记共享变量必须要求用同一个锁来同步.

3. 一个类中如果有多个非静态的synchronized方法,任何时刻仅且只能有一个线程访问此类的一个synchronized方法直到线程释放此类的对象锁.此时锁为对象本身

4. 一个类如果有多个静态synchronized方法同3,此时锁为这个类本身,也就是class文件加载到内存生成的Class对象.但是其他线程还是可以访问此类的非静态synchronized方法的.

那么synchronized块和synchronized方法的锁机制在JVM底层到底是怎么实现的呢?来看一段虚拟机规范中的例子说明

<<java 虚拟机规范(J2SE-7)>>例子

bubuko.com,布布扣
void onlyMe(Foo f) { 
synchronized(f) {
doSomething();
}
}
//编译后的代码如下:
Method void onlyMe(Foo) 
0 aload_1 // 参数f入栈
1 dup // 在栈中复制f为f1
2 astore_2 // f1存储到栈的局部变量表
3 monitorenter // 管程进入并与f1关联起来
4 aload_0 // 持有当前管程
5 invokevirtual #5 // 调用doSomething()方法
8 aload_2 // f1从局部变量表加载到操作栈
9 monitorexit // 与f1关联的管程退出
10 goto 18 // 方法正常调用完成
13 astore_3 // 如果方法出现异常
14 aload_2 f1从局部变量表加载到操作栈
15 monitorexit // 与f1关联的管程退出
16 aload_3 // 异常对象入栈
17 athrow // 异常抛给调用者
18 return // 方法返回
Exception table:
FromTo Target Type
4 10 13 any
13 16 13 any

bubuko.com,布布扣

java虚拟机规范说明:monitorenter和monitorexit指令用于实现同步语句块,方法中调用过的每条monitorenter指令都必须有执行其对应monitorexit指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时monitorenter和monitorexit指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执monitorexi指令。

可以看出,同步代码块是由monitorenter持有关联锁对象的管程(moniter)和monitorexit释放当前持有的管程实现同步的.对于synchronized修饰的同步方法java虚拟机规范说明如下:

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先持有管程,然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放管程.--注:此段描述如果想深入了解的猿友可以去阅读java虚拟机规范

至于管程大家可以去维基百科.相信看了之后会对加锁机制明白不少

synchronized加锁机制我的理解就是让并行执行中的线程在操作共享变量的时候串行化,按先入锁先执行的顺序进行.最后实现共享数据的可见性.

synchronized同步带来的问题-死锁

简单的顺序死锁

bubuko.com,布布扣
public class DeadLock {
Object a = new Object();
Object b = new Object();

public void get(){
synchronized (a) {
synchronized (b) {
//doSomething
}
}
}

public void eat(){
synchronized (b) {
synchronized (a) {
//doSomething
}
}
}

}

bubuko.com,布布扣

活动的死锁.还记得代码-1吗?这样的改进就有可能带来活动死锁,假设现在Tom,Jim相互转账.存在A(Tom),B(Jim)两线程他们同时进入了tranferMoney方法.A线程from.decreace获得了Tom账户的锁,B的from.decreace获得了Jim账户的锁.问题就出现了,两个线程的to.increace调用都在对方持有的锁释放.

协作死锁. 这种死锁的产生一般是在A类中一个同步的方法调用了另一个类B的同步方法,但是B中也可能出现有同步的方法调A的某个同步法.

就像GC stop the world 一样synchronized会阻塞住除了获得了锁的线程之外所有试图访问此锁的线程(呵呵,这里把synchronized和GC的STW问题相比较本意就是为了说明synchronized是同步方法中非常消耗性能的)这种同步叫做互斥同步.它会导致线程在挂起和运行态之间来回切换.这是一种耗性能操作.

并发系列(二)线程安全之synchronized,布布扣,bubuko.com

并发系列(二)线程安全之synchronized

原文:http://www.cnblogs.com/huavben/p/3582612.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!