Java多线程之线程的同步
Java多线程之线程的同步
实际开发中我们也经常提到说线程安全问题,那么什么是线程安全问题呢?
线程不安全就是说在多线程编程中出现了错误情况,由于系统的线程调度具有一定的随机性,当使用多个线程来访问同一个数据时,非常容易出现线程安全问题。具体原因如下:
1,多个线程同时访问一个数据资源(该资源称为临界资源),形成数据发生不一致和不完整。
2,数据的不一致往往是因为一个线程中的多个关联的操作(这几个操作合成原子操作)未全部完成。
关于线程安全问题,有一个经典的情景:银行取钱。代码如下:
/**
*
* @version 1L
* @author LinkinPark
* @since 2015-2-4
* @motto 梦似烟花心似水,同学少年不言情
* @desc ^ 账户类,封装账户编号、账户余额两个Field
*/
public class Account
{
private String accountNo;//账户编号
private double balance;//账户余额 public Account()
{
} public Account(String accountNo, double balance)
{
this.accountNo = accountNo;
this.balance = balance;
} public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
} public String getAccountNo()
{
return this.accountNo;
} public void setBalance(double balance)
{
this.balance = balance;
} public double getBalance()
{
return this.balance;
} public int hashCode()
{
return accountNo.hashCode();
} public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj != null && obj.getClass() == Account.class)
{
Account target = (Account) obj;
return target.getAccountNo().equals(accountNo);
}
return false;
} }
/**
*
* @version 1L
* @author LinkinPark
* @since 2015-2-4
* @motto 梦似烟花心似水,同学少年不言情
* @desc ^ 取钱的线程类
*/
public class DrawThread extends Thread
{
private Account account;// 模拟用户账户
private double drawAmount;// 当前取钱线程所希望取的钱数 public DrawThread(String name, Account account, double drawAmount)
{
super(name);
this.account = account;
this.drawAmount = drawAmount;
} // 当多条线程修改同一个共享数据时,将涉及数据安全问题。比如说:2条线程进来取钱了,一条取完钱了但是还没有修改余额,那么另外一条就又进来了,本来是钱不够了,但是他还以为钱够,所以逻辑上就有错了
public void run()
{
// 账户余额大于取钱数目
if (account.getBalance() >= drawAmount)
{
System.out.println(getName() + "取钱成功!取出的钱是:" + drawAmount);
try
{
Thread.sleep(1000);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t余额为: " + account.getBalance());
}
else
{
System.out.println(getName() + "取钱失败!余额不足!");
}
}
}
public class DrawTest
{
public static void main(String[] args)
{
// 创建一个账户
Account acct = new Account("15158117453", 1000);
// 模拟两个线程对同一个账户取钱
new DrawThread("LinkinPark", acct, 800).start();
new DrawThread("NightWish", acct, 800).start();
}
}
运行上面的程序,控制台结果如下:
很明显出错了,正常的应该是LinkinPark可以取钱,取完钱余额变成了200,NightWish就不能再次取钱了。。。
既然如此,那么如何避免以上的问题呢?加锁。具体有2种方式:1,同步代码库 2,同步方法
使用第一种方式解决上面的问题,我们只需要修改取钱的那个线程类,给取钱的那个线程类中的那个账户添加一个同步监视器就可以了。
/**
*
* @version 1L
* @author LinkinPark
* @since 2015-2-4
* @motto 梦似烟花心似水,同学少年不言情
* @desc ^ 取钱的线程类:“加锁 → 修改 → 释放锁”
*/
public class DrawThread extends Thread
{
// 模拟用户账户
private Account account;
// 当前取钱线程所希望取的钱数
private double drawAmount; public DrawThread(String name, Account account, double drawAmount)
{
super(name);
this.account = account;
this.drawAmount = drawAmount;
} // 当多条线程修改同一个共享数据时,将涉及数据安全问题。
public void run()
{
// 使用account作为同步监视器,任何线程进入下面同步代码块之前,必须先获得对account账户的锁定——其他线程无法获得锁,也就无法修改它
synchronized (account)
{
// 账户余额大于取钱数目
if (account.getBalance() >= drawAmount)
{
// 吐出钞票
System.out.println(getName() + "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t余额为: " + account.getBalance());
}
else
{
System.out.println(getName() + "取钱失败!余额不足!");
}
}
//同步代码块结束,该线程释放同步锁
}
}
现在结果正确了。
使用第2种方式解决上面的问题,代码如下:
/**
*
* @version 1L
* @author LinkinPark
* @since 2015-2-4
* @motto 梦似烟花心似水,同学少年不言情
* @desc ^ 账户类,封装账户编号、账户余额两个Field,另外还有取钱的方法
*/
public class Account
{
// 封装账户编号、账户余额两个Field
private String accountNo;
private double balance; public Account()
{
} // 构造器
public Account(String accountNo, double balance)
{
this.accountNo = accountNo;
this.balance = balance;
} // accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
} public String getAccountNo()
{
return this.accountNo;
} // 因此账户余额不允许随便修改,所以只为balance提供getter方法,
public double getBalance()
{
return this.balance;
} // 提供一个线程安全draw()方法来完成取钱操作
public synchronized void draw(double drawAmount)
{
// 账户余额大于取钱数目
if (balance >= drawAmount)
{
// 吐出钞票
System.out.println(Thread.currentThread().getName() + "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
balance -= drawAmount;
System.out.println("\t余额为: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName() + "取钱失败!余额不足!");
}
} // 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
} public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj != null && obj.getClass() == Account.class)
{
Account target = (Account) obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
/**
*
* @version 1L
* @author LinkinPark
* @since 2015-2-4
* @motto 梦似烟花心似水,同学少年不言情
* @desc ^ 取钱的线程类:“加锁 → 修改 → 释放锁”
*/
public class DrawThread extends Thread
{
// 模拟用户账户
private Account account;
// 当前取钱线程所希望取的钱数
private double drawAmount; public DrawThread(String name, Account account, double drawAmount)
{
super(name);
this.account = account;
this.drawAmount = drawAmount;
} // 当多条线程修改同一个共享数据时,将涉及数据安全问题。
public void run()
{
// 直接调用account对象的draw方法来执行取钱
// 同步方法的同步监视器是this,this代表调用draw()方法的对象。
// 也就是说:线程进入draw()方法之前,必须先对account对象的加锁。
account.draw(drawAmount);
}
}
关于上面这2种做法的总结:
第一种做法使用synchronized将run方法里的方法体修改成为同步代码块,去实现"加锁--修改完成--释放锁"的逻辑。这里有一个问题就是如何确定同步代码块中的那个同步监视器是那个对象呢?记住就可以了:这个对象就是可能被并发访问的共享资源。
第二种做法使用synchronized来修饰某个方法,使用这种方式不需要显式指定同步监视器,其实同步监视器也就是他自己了,也就是this。值得注意的是:synchronized关键字可以修饰方法,可以修饰代码块,但是不能修饰构造器和属性。加锁了以后效率肯定会受到影响的,所以我们要有选择的去针对那些操作存在竞争的资源的方法来加锁,不要随便加。
对比上面2种加锁的做法,我们不难发现,其实第2种的设计是比较好的,因为这种做法更符合面向对象设计规则。面向对象里面有一种流行的设计方式,叫做领域驱动设计(DDD),说白了就是说对每一个对象都实现良好的封装,关于操作这个对象的属性或者方法都应该写在这个对象里面,不应该写在别的类中。
- 释放同步监视器的锁定
经过前面的整理,我们已经知道了,任何线程在进入同步代码块或者是同步方法之前,都会先获得对同步监视器的锁定,那么何时才会去释放这个锁定呢:
1,代码执行结束
2,代码遇见了retrue,break
3,代码中出现了未处理的Error和Exception
4,代码执行同步监视器的wait方法。
注意的是:程序调用Thread的sleep方法和yield方法,或者其他线程调用了该线程的suspend和resume方法都不会释放同步监视器的锁。
- 最后介绍2个知识点,实际编码中使用到的情景并不多。
1,同步锁(Lock)
JDK1.5后,提供了一个Lock接口。Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。常用的实现类是ReentrantLock。API中是这么介绍ReentrantLock的:一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
建议总是 立即实践,使用 lock 块来调用 try,在之前/之后的构造中,最典型的代码如下:
class X {
private final ReentrantLock lock = new ReentrantLock();
// ... public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
2,死锁
死锁就是说当2个线程互相等待对方来释放同步监视器。值得注意的是:死锁不报错的,整个程序不会异常,也不会给出任何的提示,只是所有的线程都处于阻塞状态,无法继续了。下面给出一个例子:
/**
*
* @version 1L
* @author LinkinPark
* @since 2015-2-4
* @motto 梦似烟花心似水,同学少年不言情
* @desc ^ 出现死锁的一个例子
*/
class A
{
public synchronized void foo(B b)
{
System.out.println("当前线程名: " + Thread.currentThread().getName() + " 进入了A实例的foo方法");
try
{
Thread.sleep(200);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用B实例的last方法");
b.last();
} public synchronized void last()
{
System.out.println("进入了A类的last方法内部");
}
} class B
{
public synchronized void bar(A a)
{
System.out.println("当前线程名: " + Thread.currentThread().getName() + " 进入了B实例的bar方法");
try
{
Thread.sleep(200);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用A实例的last方法");
a.last();
} public synchronized void last()
{
System.out.println("进入了B类的last方法内部");
}
} public class DeadLock implements Runnable
{
A a = new A();
B b = new B(); public void init()
{
Thread.currentThread().setName("主线程");
// 调用a对象的foo方法
a.foo(b);
System.out.println("进入了主线程之后");
} public void run()
{
Thread.currentThread().setName("副线程");
// 调用b对象的bar方法
b.bar(a);
System.out.println("进入了副线程之后");
} public static void main(String[] args)
{
DeadLock dl = new DeadLock();
// 以dl为target启动新线程
new Thread(dl).start();
// 调用init()方法
dl.init();
}
}
Java多线程之线程的同步的更多相关文章
- java多线程之线程的同步与锁定(转)
一.同步问题提出 线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏. 例如:两个线程ThreadA.ThreadB都操作同一个对象Foo对象,并修改Foo对象上的数据. publicc ...
- 关于Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇高质量的博文)
Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇质量高的博文) 前言:在学习多线程时,遇到了一些问题,这里我将这些问题都分享出来,同时也分享了几篇其他博客主的博客,并且将我个人的理解也分享 ...
- Java多线程02(线程安全、线程同步、等待唤醒机制)
Java多线程2(线程安全.线程同步.等待唤醒机制.单例设计模式) 1.线程安全 如果有多个线程在同时运行,而这些线程可能会同时运行这段代码.程序每次运行结果和单线程运行的结果是一样的,而且其他的变量 ...
- Java多线程与线程同步
六.多线程,线程,同步 ①概念: 并行:指两个或多个在时间同一时刻发生(同时发生) 并发:指两个或多个事件在同一时间段内发生 具体概念: 在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多 ...
- Java多线程之线程其他类
Java多线程之线程其他类 实际编码中除了前面讲到的常用的类之外,还有几个其他类也有可能用得到,这里来统一整理一下: 1,Callable接口和Future接口 JDK1.5以后提供了上面这2个接口, ...
- Java多线程之线程的通信
Java多线程之线程的通信 在总结多线程通信前先介绍一个概念:锁池.线程因为未拿到锁标记而发生的阻塞不同于前面五个基本状态中的阻塞,称为锁池.每个对象都有自己的锁池的空间,用于放置等待运行的线程.这些 ...
- Java多线程之线程的控制
Java多线程之线程的控制 线程中的7 种非常重要的状态: 初始New.可运行Runnable.运行Running.阻塞Blocked.锁池lock_pool.等待队列wait_pool.结束Dea ...
- JAVA多线程之线程间的通信方式
(转发) 收藏 记 周日,北京的天阳光明媚,9月,北京的秋格外肃穆透彻,望望窗外的湛蓝的天,心似透过栏杆,沐浴在这透亮清澈的蓝天里,那朵朵白云如同一朵棉絮,心意畅想....思绪外扬, 鱼和熊掌不可兼得 ...
- java多线程与线程间通信
转自(http://blog.csdn.net/jerrying0203/article/details/45563947) 本文学习并总结java多线程与线程间通信的原理和方法,内容涉及java线程 ...
随机推荐
- python 中文编码
import sys sys.setdefaultencoding('utf-8') 保存为:sitecustomize.py 将文件放至: /Library/Frameworks/Python.fr ...
- 【整理】REACT一些自己感觉需要记的东西
REACT生命周期: 组件的生命周期可分成三个状态: Mounting:已插入真实 DOM Updating:正在被重新渲染 Unmounting:已移出真实 DOM 生命周期的方法有: compon ...
- springboot(十七):使用Spring Boot上传文件
上传文件是互联网中常常应用的场景之一,最典型的情况就是上传头像等,今天就带着带着大家做一个Spring Boot上传文件的小案例. 1.pom包配置 我们使用Spring Boot最新版本1.5.9. ...
- Android基础_web通信
一.发展史 1G 模拟制式手机,只能进行语音通话2G 数字制式手机,增加接收数据等功能3G 智能手机,它已经成了集语音通信和多媒体通信相结合,并且包括图像.音乐.网页浏览.电话会议以及其它一些信息服务 ...
- Java下使用Apache POI生成具有三级联动下拉列表的Excel文档
使用Apache POI生成具有三级联动下拉列表的Excel文档: 具体效果图与代码如下文. 先上效果图: 开始贴代码,代码中部分测试数据不影响功能. 第一部分(核心业务处理): 此部分包含几个方面: ...
- 2017 Multi-University Training Contest - Team 9 1002&&HDU 6162 Ch’s gift【树链部分+线段树】
Ch’s gift Time Limit: 6000/3000 MS (Java/Others) Memory Limit: 65536/65536 K (Java/Others)Total S ...
- bzoj:4762: 最小集合
原题链接:http://www.lydsy.com/JudgeOnline/problem.php?id=4762 mark一下,有空要好好弄懂 #include<cstdio> #inc ...
- bzoj:3616: War
Description 小x所在的世界正在经历一场在k个阵营之间的战争.每个阵营有若干个炮塔,每个炮塔由攻击系统和防御系统组成.第i个炮塔可以攻击到离它欧几里德距离小于等于ri 或者曼哈顿距离小于等于 ...
- 我的第六个网页制作:table标签
<!doctype html> <html> <head> <meta charset="utf-8"> <title> ...
- C语言中%d,%p,%u,%lu等都有什么用处
%d 有符号10进制整数(%ld 长整型,%hd短整型 )%hu 无符号短整形(%u无符号整形,%lu无符号长整形)%i 有符号10进制整数 (%i 和%d 没有区别,%i 是老式写法,都是整型格式) ...