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并发编程涉及到很多方面的内容,不是一朝一夕就能够融会贯通 ...
随机推荐
- 实验二:ICMP重定向攻击
-:实验原理 ICMP重定向信息是路由器向主机提供实时的路由信息,当一个主机收到ICMP重定向信息时,它就会根据这个信息来更新自己的路由表.由于缺乏必要的合法性检查,如果一个黑客想要被攻击的主机修改它 ...
- 菜鸟vimer成长记——第2.4章、cmd-line模式
cmd-line模式又有3个类型:Ex 命令(ex commands).查找模式(Search patterns).Filter 命令(Filter commands).本文主要重点的是Ex 命令和S ...
- tabindex 属性
tabindex 属性规定元素的 tab 键控制次序(当 tab 键用于导航时). 语法:<element tabindex="number">规定元素的 tab 键控 ...
- (webapp)微信和safri 对于html5 部分功能不兼容,多选或单选下拉框去除边框无效果。
1 appearance:none; 2 -moz-appearance:none; /* Firefox */ 3 -webkit-appearance:none; /* Safari 和 Chro ...
- 使用VMWareWorkstation10搭建学习环境笔记
第一节:介绍虚拟化技术 虚拟化技术1.在一台计算机运行多个操作系统2.教学环境 测试环境3.和硬件无关4.P to V 物理机->虚拟机(Physical to Virtual) V ...
- loadrunner11和https
最近做了一个接口测试的项目,json格式,https协议,使用postman调试这个接口,在postman中写好三个表头Authorization.sessionIndex.Content-Type和 ...
- ci框架学习整理
-- -- 表的结构 `yi_article` -- CREATE TABLE IF NOT EXISTS `yi_article` ( `id` int(11) unsigned NOT NULL ...
- c语言数字图像处理(十):阈值处理
定义 全局阈值处理 假设某一副灰度图有如下的直方图,该图像由暗色背景下的较亮物体组成,从背景中提取这一物体时,将阈值T作为分割点,分割后的图像g(x, y)由下述公式给出,称为全局阈值处理 多阈值处理 ...
- GlusterFS分布式存储集群-1. 部署
参考文档: Quick Start Guide:http://gluster.readthedocs.io/en/latest/Quick-Start-Guide/Quickstart/ Instal ...
- 使用Zabbix的SNMP trap监控类型监控设备的一个例子
本文以监控绿盟设备为例. 1.登录被监控的设备的管理系统,配置snmptrap地址指向zabbix服务器或代理服务器. snmptrap地址也叫陷阱. 2.验证是否能在zabbix服务器或代理服务器上 ...