Java并发编程之线程安全、线程通信
Java多线程开发中最重要的一点就是线程安全的实现了。所谓Java线程安全,可以简单理解为当多个线程访问同一个共享资源时产生的数据不一致问题。为此,Java提供了一系列方法来解决线程安全问题。
synchronized
synchronized用于同步多线程对共享资源的访问,在实现中分为同步代码块和同步方法两种。
同步代码块
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;
}
@Override
public void run() {
//使用account作为同步代码块的锁对象
synchronized(account) {
if (account.getBalance() >= drawAmount) {
System.out.println(getName() + "取款成功, 取出:" + drawAmount);
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.setBalance(account.getBalance() - drawAmount);
System.out.println("余额为: " + account.getBalance());
} else {
System.out.println(getName() + "取款失败!余额不足!");
}
}
}
}
同步方法
使用同步方法,即使用synchronized关键字修饰类的实例方法或类方法,可以实现线程安全类,即该类在多线程访问中,可以保证可变成员的数据一致性。
同步方法中,隐式的锁对象由锁的是实例方法还是类方法确定,分别为该类对象或类的Class对象。
public class SyncAccount {
private String accountNo;
private double balance;
//省略构造器、getter setter方法
//在一个简单的账户取款例子中, 通过添加synchronized的draw方法, 把Account类变为一个线程安全类
public synchronized void draw(double drawAmount) {
if (balance >= drawAmount) {
System.out.println(Thread.currentThread().getName() + "取款成功, 取出:" + drawAmount);
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= drawAmount;
System.out.println("余额为: " + balance);
} else {
System.out.println(Thread.currentThread().getName() + "取款失败!余额不足!");
}
}
//省略HashCode和equals方法
}
同步锁(Lock、ReentrantLock)
Java5新增了两个用于线程同步的接口Lock和ReadWriteLock,并且分别提供了两个实现类ReentrantLock(可重入锁)和ReentrantReadWriteLock(可重入读写锁)。
相比较synchronized,ReentrantLock的一些优势功能:
1. 等待可中断:指持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待。
2. 公平锁:多个线程等待同一个锁时,必须按照申请锁的时间顺序依次获取。synchronized是非公平锁,ReentrantLock可以通过参数设置为公平锁
3. 多条件锁:ReentrantLock可通过Condition类获取多个条件关联
Java 1.6以后,synchronized性能提升较大,因此一般的开发中依然建议使用语法层面上的synchronized加锁。
Java8新增了更为强大的可重入读写锁StampedLock类。
比较常用的是ReentrantLock类,可以显示地加锁、释放锁。下面使用ReentrantLock重构上面的SyncAccount类。
public class RLAccount {
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
private String accountNo;
private double balance;
//省略构造方法和getter setter
public void draw(double drawAmount) {
//加锁
lock.lock();
try {
if (balance >= drawAmount) {
System.out.println(Thread.currentThread().getName() + "取款成功, 取出:" + drawAmount);
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= drawAmount;
System.out.println("余额为: " + balance);
} else {
System.out.println(Thread.currentThread().getName() + "取款失败!余额不足!");
}
} finally {
//通过finally块保证释放锁
lock.unlock();
}
}
}
死锁
当两个线程相互等待地方释放锁的时候,就会产生死锁。关于死锁和线程安全的深入分析,将另文介绍。
线程通信方式之wait、notify、notifyAll
Object类提供了三个用于线程通信的方法,分别是wait、notify和notifyAll。这三个方法必须由同步锁对象来调用,具体来说:
1. 同步方法:因为同步方法默认使用所在类的实例作为锁,即this,可以在方法中直接调用。
2. 同步代码块:必须由锁来调用。
wait():导致当前线程等待,直到其它线程调用锁的notify方法或notifyAll方法来唤醒该线程。调用wait的线程会释放锁。
notify():唤醒任意一个在等待的线程
notifyAll():唤醒所有在等待的线程
/*
* 通过一个生产者-消费者队列来说明线程通信的基本使用方法
* 注意: 假如这里的判断条件为if语句,唤醒方法为notify, 那么如果分别有多个线程操作入队\出队, 会导致线程不安全.
*/
public class EventQueue { private final int max; static class Event{ }
//定义一个不可改的链表集合, 作为队列载体
private final LinkedList<Event> eventQueue = new LinkedList<>(); private final static int DEFAULT_MAX_EVENT = 10; public EventQueue(int max) {
this.max = max;
} public EventQueue() {
this(DEFAULT_MAX_EVENT);
} private void console(String message) {
System.out.printf("%s:%s\n",Thread.currentThread().getName(), message);
}
//定义入队方法
public void offer(Event event) {
//使用链表对象作为锁
synchronized(eventQueue) {
//在循环中判断如果队列已满, 则调用锁的wait方法, 使线程阻塞
while(eventQueue.size() >= max) {
try {
console(" the queue is full");
eventQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
console(" the new event is submitted");
eventQueue.addLast(event);
this.eventQueue.notifyAll();
}
}
//定义出队方法
public Event take() {
//使用链表对象作为锁
synchronized(eventQueue) {
//在循环中判断如果队列已空, 则调用锁的wait方法, 使线程阻塞
while(eventQueue.isEmpty()) {
try {
console(" the queue is empty.");
eventQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Event event = eventQueue.removeFirst();
this.eventQueue.notifyAll();
console(" the event " + event + " is handled/taked.");
return event;
}
}
}
线程通信方式之Condition
如果使用的是Lock接口实现类来同步线程,就需要使用Condition类的三个方法实现通信,分别是await、signal和signalAll,使用上与Object类的通信方法基本一致。
/*
* 使用Lock接口和Condition来实现生产者-消费者队列的通信
*/
public class ConditionEventQueue {
//显示定义Lock对象
private final Lock lock = new ReentrantLock();
//通过newCondition方法获取指定Lock对象的Condition实例
private final Condition cond = lock.newCondition();
private final int max;
static class Event{ }
//定义一个不可改的链表集合, 作为队列载体
private final LinkedList<Event> eventQueue = new LinkedList<>();
private final static int DEFAULT_MAX_EVENT = 10;
public ConditionEventQueue(int max) {
this.max = max;
} public ConditionEventQueue() {
this(DEFAULT_MAX_EVENT);
} private void console(String message) {
System.out.printf("%s:%s\n",Thread.currentThread().getName(), message);
}
//定义入队方法
public void offer(Event event) {
lock.lock();
try {
//在循环中判断如果队列已满, 则调用cond的wait方法, 使线程阻塞
while (eventQueue.size() >= max) {
try {
console(" the queue is full");
cond.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
console(" the new event is submitted");
eventQueue.addLast(event);
cond.signalAll();;
} finally {
lock.unlock();
} }
//定义出队方法
public Event take() {
lock.lock();
try {
//在循环中判断如果队列已空, 则调用cond的wait方法, 使线程阻塞
while (eventQueue.isEmpty()) {
try {
console(" the queue is empty.");
cond.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Event event = eventQueue.removeFirst();
cond.signalAll();
console(" the event " + event + " is handled/taked.");
return event;
} finally {
lock.unlock();
}
}
}
Java 1.5开始就提供了BlockingQueue接口,来实现如上所述的生产者-消费者线程同步工具。具体介绍将另文说明。
Java并发编程之线程安全、线程通信的更多相关文章
- Java并发编程:如何创建线程?
Java并发编程:如何创建线程? 在前面一篇文章中已经讲述了在进程和线程的由来,今天就来讲一下在Java中如何创建线程,让线程去执行一个子任务.下面先讲述一下Java中的应用程序和进程相关的概念知识, ...
- Java 并发编程——Executor框架和线程池原理
Eexecutor作为灵活且强大的异步执行框架,其支持多种不同类型的任务执行策略,提供了一种标准的方法将任务的提交过程和执行过程解耦开发,基于生产者-消费者模式,其提交任务的线程相当于生产者,执行任务 ...
- 【转】Java并发编程:如何创建线程?
一.Java中关于应用程序和进程相关的概念 在Java中,一个应用程序对应着一个JVM实例(也有地方称为JVM进程),一般来说名字默认是java.exe或者javaw.exe(windows下可以通过 ...
- [Java并发编程(二)] 线程池 FixedThreadPool、CachedThreadPool、ForkJoinPool?为后台任务选择合适的 Java executors
[Java并发编程(二)] 线程池 FixedThreadPool.CachedThreadPool.ForkJoinPool?为后台任务选择合适的 Java executors ... 摘要 Jav ...
- [Java并发编程(一)] 线程池 FixedThreadPool vs CachedThreadPool ...
[Java并发编程(一)] 线程池 FixedThreadPool vs CachedThreadPool ... 摘要 介绍 Java 并发包里的几个主要 ExecutorService . 正文 ...
- Java 并发编程——Executor框架和线程池原理
Java 并发编程系列文章 Java 并发基础——线程安全性 Java 并发编程——Callable+Future+FutureTask java 并发编程——Thread 源码重新学习 java并发 ...
- 2、Java并发编程:如何创建线程
Java并发编程:如何创建线程? 在前面一篇文章中已经讲述了在进程和线程的由来,今天就来讲一下在Java中如何创建线程,让线程去执行一个子任务.下面先讲述一下Java中的应用程序和进程相关的概念知识, ...
- 原创】Java并发编程系列2:线程概念与基础操作
[原创]Java并发编程系列2:线程概念与基础操作 伟大的理想只有经过忘我的斗争和牺牲才能胜利实现. 本篇为[Dali王的技术博客]Java并发编程系列第二篇,讲讲有关线程的那些事儿.主要内容是如下这 ...
- Java并发编程的艺术(六)——线程间的通信
多条线程之间有时需要数据交互,下面介绍五种线程间数据交互的方式,他们的使用场景各有不同. 1. volatile.synchronized关键字 PS:关于volatile的详细介绍请移步至:Java ...
- Java并发编程:进程和线程之由来
Java多线程基础:进程和线程之由来 在前面,已经介绍了Java的基础知识,现在我们来讨论一点稍微难一点的问题:Java并发编程.当然,Java并发编程涉及到很多方面的内容,不是一朝一夕就能够融会贯通 ...
随机推荐
- [Mark]Windows Server 2008 R2 防火墙之SQL Server 2008 R2
今天新装了一个DBServer (Windows Server 2008 R2),但是在客户端跑之前的应用时却发现出错了,Debugg后发现的错误如下: 由InnerException可知时DB ...
- 学生管理之Bootstrap初体验
Bootstrap,来自 Twitter,是目前比较受欢迎的前端框架.Bootstrap 是基于 HTML.CSS.JAVASCRIPT 的,它简洁灵活,使得 Web 开发更加快捷. Bootstra ...
- 深入浅出js中的this
Q:this是什么? A:this是Javascript语言的一个关键字,它代表函数运行时,自动生成的一个内部对象,在每个 function 中自动根据作用域(scope) 确定, 指向的是此次调用者 ...
- 初学者浅度剖析eShopOnContainers 里面用到的MediatR .
一.介绍 简单了解下开源项目 MedatR, eShopOnContainers, MediatR作者Jimmy Bogard : Simple mediator implementation in ...
- python爬虫beautifulsoup4系列1
前言 以博客园为例,爬取我的博客上首页的发布时间.标题.摘要,本篇先小试牛刀,先了解下它的强大之处,后面讲beautifulsoup4的详细功能. 一.安装 1.打开cmd用pip在线安装beauti ...
- Unity摄像机围绕物体旋转两种实现方式
第一种,使用Transform 函数 RotateAround. 代码如下: public Transform target;//获取旋转目标 private void camerarotate() ...
- python中web应用与mysql数据库交互
7使用数据库 具体使用python的DB-API,这一章里介绍如何编写代码与MYSQL数据库技术交互,这里使用一个通用的数据库API,名为DB-API. 7.1基于数据库的web应用 之前我们把日志数 ...
- vue 组件-父组件传值给子组件
父组件通过属性,传值给子组件,子组件通过,props数组里的名称来接受父组件传过来的值. HTML部分: <div id="app"> <tmp1 :parent ...
- 【LDAP安装】在已编译安装的PHP环境下安装LDAP模块
在已编译安装的PHP环境下安装LDAP模块 (乐维温馨提示:其他模块也能以这个方式安装) 1.在PHP源码包内找到ldap模块文件 cd php-5.6.37 cd ext/ldap/ 2.phpiz ...
- 2017年10月WEB前端开发实习生面试题总结
从大一开始学习前端,今年大三,10月份开始投简历,陆续收到很多家公司的面试,目前为止的面试通过率是百分之百,总结下面试题. 不定期更新中... 百度第一次 一面 1.AJAX流程 2.promise简 ...