线程代码同步与线程锁

为什么要有同步代码块?

  线程同步的出现是为了解决多个线程对统一资源操作而引发的数据混乱问题。这里引用一个经典demo-银行转账操作,场景如下,小明的账户目前有1000人民币,他在商场买衣服给商家转账500元,而就在同一时间小明的朋友小张给小明转账500让他帮忙也买一件衣服带给他,如下面代码。

 package cn.wz.traditional.wf;

 /**
* 模拟银行转帐的demo
* Created by WangZhe on 2017/5/5.
*/
public class BankTransferDemo {
public static void main(String[] args) { final Account xiaoMing=new Account("小明",Double.valueOf(1000));
final Account shangJia=new Account("商家",Double.valueOf(5000));
final Account xiaoZhang=new Account("小张",Double.valueOf(3000));
new Thread(new Runnable() {
public void run() {
//模拟小明向商家付款
boolean falg = xiaoMing.transfer(shangJia, Double.valueOf(500));
if(falg){
System.out.println("转账成功");
}else {
System.out.println("系统异常转账失败");
}
}
}).start();
new Thread(new Runnable() {
public void run() {
//模拟小张向小明转账
boolean falg = xiaoZhang.transfer(xiaoMing, Double.valueOf(500));
if(falg){
System.out.println("转账成功");
}else {
System.out.println("系统异常转账失败");
}
}
}).start();
try {
Thread.currentThread().sleep(100);//主线程休眠1秒,等转账操作完成之后在输出小明的账户余额
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("转账后小明的账务余额为:"+xiaoMing.getMoney()); } /**
* 账户类
* Created by WangZhe on 2017/5/5.
*/
static class Account{ public Account() {
} /**
* 创建Account对象实例并进行初始化
* @param name 账户名称
* @param money 账户金额
*/
public Account(String name, Double money) {
this.name = name;
this.money = money;
} /**
* 账户名称
*/
private String name;
/**
* 账户金额
*/
private Double money; /**
* 转账操作
* @param a 被转账的账户实体
* @param money 转账金额
* @return
*/
public boolean transfer(Account a,Double money){
Double thisMoney = getMoney();//获取当前账户余额
Double aMoney=a.getMoney();
try{
Double newMoney= this.getMoney()-money;
a.setMoney(a.getMoney()+money);//给被转入的账户金额上加上转账金额
this.setMoney(newMoney);//减去需要转出的金额 return true;
}catch (Exception e){
//系统错误操作取消
this.setMoney(thisMoney);//恢复转账用户的账户金额
a.setMoney(aMoney);//恢复被转账用户的账户金额
return false;
}
}
public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} public Double getMoney() {
return money;
} public void setMoney(Double money) {
this.money = money;
}
} }

模拟银行转账demo(非同步)

我们预期的效果是转账后小明的账户金额还有1000元,但是运行上面代码后有两种可能的结果如图所示:

其实还有第三种结果就是转账后余额为500,但大多数情况下其余额为1000,后两种情况出现的几率非常小(基本上和中彩票差不多),但是这种逻辑错误虽然出现的概率很低,但依然是错误,还是要解决的。而这种错误引发的原因就是因为多个线程同时操作一个资源数据而引发的(因为线程占用cpu资源执行的时间、顺序等都是不固定的,有可能该程拿到的数据时cpu刚好调度另一个线程执行,对数据进行更改,再到该线程执行时,其已经拿到了数据但并不知到该数据已经被更改过所以就会引发逻辑错误)

这时就需要对访问竞争资源的代码块进行更改,使当一个线程访问该资源时对该资源进行标记其他线程无法访问该资源只能等该资源访问结束后在进行访问,这样就不会出现上面的情况了。 Java 提供了synchronized 关键字来解决上述问题。用synchronized关键字标示代码我们称之为同步代码块,标示的方法为同步方法。下面是使用synchronized关键字后的代码示例

synchronized 同步关键字

 package cn.wz.traditional.wf;

 /**
* 模拟银行转帐的demo
* Created by WangZhe on 2017/5/5.
*/
public class BankTransferDemo {
public static void main(String[] args) {
final Account xiaoMing=new Account("小明",Double.valueOf(1000));
final Account shangJia=new Account("商家",Double.valueOf(5000));
final Account xiaoZhang=new Account("小张",Double.valueOf(3000));
new Thread(new Runnable() {
public void run() {
//模拟小张向商家付款
boolean falg = xiaoMing.transfer(shangJia, Double.valueOf(500));
if(falg){
System.out.println("转账成功");
}else {
System.out.println("系统异常转账失败");
}
}
}).start();
new Thread(new Runnable() {
public void run() {
//模拟小张向小明转账
boolean falg = xiaoZhang.transfer(xiaoMing, Double.valueOf(500));
if(falg){
System.out.println("转账成功");
}else {
System.out.println("系统异常转账失败");
}
}
}).start();
try {
Thread.currentThread().sleep(100);//主线程休眠1秒,等转账操作完成之后在输出小明的账户余额
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("转账后小明的账务余额为:"+xiaoMing.getMoney()); } /**
* 账户类
* Created by WangZhe on 2017/5/5.
*/
static class Account{ public Account() {
} /**
* 创建Account对象实例并进行初始化
* @param name 账户名称
* @param money 账户金额
*/
public Account(String name, Double money) {
this.name = name;
this.money = money;
} /**
* 账户名称
*/
private String name;
/**
* 账户金额
*/
private Double money; /**
* 转账操作
* @param a 被转账的账户实体
* @param money 转账金额
* @return
*/
public synchronized boolean transfer(Account a,Double money){
synchronized (a) {
Double thisMoney = getMoney();//获取当前账户余额 Double aMoney = a.getMoney();
try {
Double newMoney = this.getMoney() - money;
a.setMoney(a.getMoney() + money);//给被转入的账户金额上加上转账金额
this.setMoney(newMoney);//减去需要转出的金额 return true;
} catch (Exception e) {
//系统错误操作取消
this.setMoney(thisMoney);//恢复转账用户的账户金额
a.setMoney(aMoney);//恢复被转账用户的账户金额
return false;
}
}
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} public Double getMoney() {
return money;
} public void setMoney(Double money) {
this.money = money;
}
} }

模拟银行转账代码(同步)

从上面代码中我们可以看到,synchronized关键字有两种写法,一种是直接在方法上另一种是在方法内部单独形成代码块,其实这两种方式是由区别的,其锁定的资源对象是不同的。下面介绍synchronized关键字作用在不同地方是锁定的资源对象。

作用在成员方法上:

  synchronized作用在成员方法上锁定的是当前对象的实例

  synchronized作用在类方法上锁定的是类的字节码

  synchronized单独形成功代码块是锁定的是其后面笑小括号()中的资源对象。

死锁

  sychronized同步代码块虽然可以解决因资源竞争所引发的数据混乱为题,但使用syschronized时一定要小心造成死锁,那什么是死锁呢?简单来说就是线程A拥有资源A的锁,线程B拥有线程B的锁

然后线程A在不释放资源A的锁的情况下尝试获取资源B的锁,因为资源B的锁被线程B占用所以线程A只能等待线程B释放资源B的锁之后才能继续执行,而同时线程B在不释放资源B的前提下尝试获取资源A的锁,但资源A被线程A占用,线程B只能等线程A释放资源A的锁再能继续执行,就这样两个线程一直互相等待对方释放锁无法继续执行,从而引发系统崩溃。如下面代码:

 package cn.wz.traditional.wf;

 /**
* 模拟银行转帐的demo
* Created by WangZhe on 2017/5/5.
*/
public class BankTransferDemo {
public static void main(String[] args) {
final Account xiaoMing=new Account("小明",Double.valueOf(1000));
final Account shangJia=new Account("商家",Double.valueOf(5000));
final Account xiaoZhang=new Account("小张",Double.valueOf(3000));
new Thread(new Runnable() {
public void run() {
//模拟小张向商家付款
boolean falg = xiaoMing.transfer(xiaoZhang, Double.valueOf(500));
if(falg){
System.out.println("转账成功");
}else {
System.out.println("系统异常转账失败");
}
}
}).start();
new Thread(new Runnable() {
public void run() {
//模拟小张向小明转账
boolean falg = xiaoZhang.transfer(xiaoMing, Double.valueOf(500));
if(falg){
System.out.println("转账成功");
}else {
System.out.println("系统异常转账失败");
}
}
}).start();
try {
Thread.currentThread().sleep(10000);//主线程休眠1秒,等转账操作完成之后在输出小明的账户余额
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("转账后小明的账务余额为:"+xiaoMing.getMoney()); } /**
* 账户类
* Created by WangZhe on 2017/5/5.
*/
static class Account{ public Account() {
} /**
* 创建Account对象实例并进行初始化
* @param name 账户名称
* @param money 账户金额
*/
public Account(String name, Double money) {
this.name = name;
this.money = money;
} /**
* 账户名称
*/
private String name;
/**
* 账户金额
*/
private Double money; /**
* 转账操作
* @param a 被转账的账户实体
* @param money 转账金额
* @return
*/
public synchronized boolean transfer(Account a,Double money){
try {
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a) {
Double thisMoney = getMoney();//获取当前账户余额 Double aMoney = a.getMoney();
try {
Double newMoney = this.getMoney() - money;
this.setMoney(newMoney);//减去需要转出的金额
a.setMoney(a.getMoney() + money);//给被转入的账户金额上加上转账金额 return true;
} catch (Exception e) {
//系统错误操作取消
this.setMoney(thisMoney);//恢复转账用户的账户金额
a.setMoney(aMoney);//恢复被转账用户的账户金额
return false;
}
}
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} public Double getMoney() {
return money;
} public void setMoney(Double money) {
this.money = money;
}
} }

死锁效果效果演示代码

死锁发生的概率是极低的,真要让你认真的写出一个死锁的代码可能还真不一定能写出来,但是一个完整的程序还是要尽可能的避免死锁的出现。

补充

Object对象改变线程状态的三个方法

  在之前提到过Object对象有三个方法可以改变线程的状态,分别是wait、notify和notifyall。这里需要注意的是这三个方法只能在同步代码块儿里进行调用,不然将引发异常(具体什么异常自己试下就知道了)。然后notify和notifyall方法虽然会唤醒线程,但并不会释放锁。

 package cn.wz.traditional.wf;

 import java.util.Date;

 /**
* Created by WangZhe on 2017/5/4.
*/
public class WaitAndNotify {
public static void main(String[] args) {
final WaitAndNotify.A a=new WaitAndNotify().new A(4);
new Thread(new Runnable() {
public void run() {
a.sayHi();
}
}).start(); new Thread(new Runnable() {
public void run() {
try {
synchronized (a){
System.out.println("您好!");
a.notify();
System.out.println(System.currentTimeMillis());//记录唤醒线程的时间
Thread.currentThread().sleep(10000);//使当前想成持有对象a的锁睡眠10秒钟
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
class A{ private Integer data;
public A(Integer data){
this.data=data;
}
public void sayHi(){
synchronized (this){
System.out.println("输出data:"+data);
System.out.println("调用wait方法");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (this){
this.data+=1;
System.out.println(System.currentTimeMillis());//记录该线程再次获取对象所得时间
System.out.println("更改data后输出:"+data);
}
} }
}

notify方法是否会释放锁演示代码

java线程(三)的更多相关文章

  1. java线程——三种创建线程的方式

    前言 线程,英文Thread.在java中,创建线程的方式有三种: 1.Thread 2.Runnable 3.Callable 在详细介绍下这几种方式之前,我们先来看下Thread类和Runnabl ...

  2. java 线程三种实现方式

    1继承thread public class MultiThread1 extends Thread{ public void run(){ for(int i=0; i<7; i++){ Sy ...

  3. Java线程专栏文章汇总(转)

    原文:http://blog.csdn.net/ghsau/article/details/17609747 JDK5.0之前传统线程        Java线程(一):线程安全与不安全 Java线程 ...

  4. Java线程专栏文章汇总

        转载自 http://blog.csdn.net/ghsau/article/details/17609747 JDK5.0之前传统线程        Java线程(一):线程安全与不安全 J ...

  5. java线程(3)——详解Callable、Future和FutureTask

    回顾: 接上篇博客 java线程--三种创建线程的方式,这篇博客主要介绍第三种方式Callable和Future.比较继承Thread类和实现Runnable接口,接口更加灵活,使用更广泛.但这两种方 ...

  6. java线程——详解Callable、Future和FutureTask

    回顾: 接上篇博客 java线程--三种创建线程的方式,这篇博客主要介绍第三种方式Callable和Future.比较继承Thread类和实现Runnable接口,接口更加灵活,使用更广泛.但这两种方 ...

  7. Java线程池ThreadPoolExecutor使用和分析(三) - 终止线程池原理

    相关文章目录: Java线程池ThreadPoolExecutor使用和分析(一) Java线程池ThreadPoolExecutor使用和分析(二) - execute()原理 Java线程池Thr ...

  8. Java线程的三种方式

    创建线程有三种方式: 1.继承Thread类 2.实现Runnable接口 3.使用Callable和Future创建线程 三种方式详解如下: ---------------------------- ...

  9. java 并发(三)---Thread 线程

    Thread 的状态 线程共有五种状态.分别是: (1)新建 (2)就绪 (3)运行 (4)阻塞 (5)死亡 ,下面列列举的状态需要结合状态示意图更好理解.  新建状态(New): 新创建了一个线程对 ...

  10. java多线程三之线程协作与通信实例

    多线程的难点主要就是多线程通信协作这一块了,前面笔记二中提到了常见的同步方法,这里主要是进行实例学习了,今天总结了一下3个实例: 1.银行存款与提款多线程实现,使用Lock锁和条件Condition. ...

随机推荐

  1. 《连载 | 物联网框架ServerSuperIO教程》- 16.OPC Server的使用步骤。附:3.3 发布与版本更新说明。

    1.C#跨平台物联网通讯框架ServerSuperIO(SSIO)介绍 <连载 | 物联网框架ServerSuperIO教程>1.4种通讯模式机制. <连载 | 物联网框架Serve ...

  2. jquery实现名单滚动

    转:http://www.qdfuns.com/notes/25341/917d9cb031f835a086dd445b77b6e04e.html 介绍:记录滚动特效.就是那一排文字不停地滚啊滚啊滚得 ...

  3. 【iOS】7.4 定位服务->2.1.3.2 定位 - 官方框架CoreLocation 功能2:地理编码和反地理编码

    本文并非最终版本,如果想要关注更新或更正的内容请关注文集,联系方式详见文末,如有疏忽和遗漏,欢迎指正. 本文相关目录: ================== 所属文集:[iOS]07 设备工具 === ...

  4. ASP.NET使用ajax实现分页局部刷新页面

    listview列表实现分页是非常容易的.ListView分页是非常简单的,加上一个DataPager控件,把ListView的ID赋予就可以了.最开始我就是这么写的.(网上有人说这样是伪分页?) & ...

  5. 深入理解Stream流水线

    前面我们已经学会如何使用Stream API,用起来真的很爽,但简洁的方法下面似乎隐藏着无尽的秘密,如此强大的API是如何实现的呢?Pipeline是怎么执行的,每次方法调用都会导致一次迭代吗?自动并 ...

  6. 模块“XXX.dll”加载失败

    具体问题:模块“XXX.dll”加载失败 请确保该二进制存储在指定的路径中,或者调试它以检查该二进制或相关的.DLL文件是否有问题  找不到指定的模块. 1.在安装C++软件的时候,有时候安装失败提示 ...

  7. JS——操作内容、操作相关元素

    操作内容:普通元素.innerHTML = "值": 会把标记执行渲染普通元素.innerText = "值": 将值原封不动的展示出来,即使里面有标记 var ...

  8. 老李分享:Web Services 组件 1

    老李分享:Web Services 组件   poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.如果对课程感兴趣,请大家咨询qq:9 ...

  9. ElasticSearch查询 第五篇:布尔查询

    布尔查询是最常用的组合查询,不仅将多个查询条件组合在一起,并且将查询的结果和结果的评分组合在一起.当查询条件是多个表达式的组合时,布尔查询非常有用,实际上,布尔查询把多个子查询组合(combine)成 ...

  10. jQuery基础学习(二)—jQuery选择器

    一.jQuery基本选择器 1.CSS选择器     在学习jQuery选择器之前,先介绍一下之前学过的CSS选择器. 选择器 语法 描述 示例   标签选择器 E {                 ...