Java多线程之同步与阻塞队列
多线程对共享数据的读写涉及到同步问题,锁和条件是线程同步的强大工具。锁用来保护代码片段(临界区),任何时刻只能有一个线程执行被保护的代码。条件对象用来管理那些已经进入被保护的代码段但还不能运行的线程。
竞争条件
各线程访问数据的次序不同,可能会产生不同的结果。下面的程序可以实现两个账户之间的转账,正常情况下所有账户的总金额应该是不变的。
public void transfer(int from, int to, double amount) {if (accounts[from] < amount) {return;}accounts[from] -= amount;accounts[to] += amount;System.out.printf(" Total Balance %10.2f\n", getTotalBalance());
}
但是在上面程序的运行中发现输出的总金额是变化的,这是因为transfer()
方法执行的过程中会被中断,可能存在几个线程同时读写账户余额。问题的根源在于转账这一系列动作不是原子操作,并且没有使用同步。当然同步使用不当也会造成死锁(所有线程都阻塞的状态)。
锁对象
可以使用锁和条件对象实现同步数据存取。锁能够保护临界区,确保只有一个线程执行。
注意,在finally
子句中不要忘记解锁操作。若因异常抛出释放,对象可能受损。
互斥锁
ReentrantLock
类能够有效防止代码块受并发访问的干扰。
private Lock bankLock;
private Condition sufficientFunds;
public void transfer(int from, int to, double amount) throws InterruptedException {bankLock.lock();try {while (accounts[from] < amount) {sufficientFunds.await();}accounts[from] -= amount;accounts[to] += amount;System.out.printf(" Total Balance %10.2f\n", getTotalBalance());sufficientFunds.signalAll();} finally {bankLock.unlock();}
}
每一个Bank
对象有自己的ReentrantLock
对象,如果两个线程试图访问同一个Bank
对象,那么锁以串行方式提供服务。但是如果两个线程访问的是不同的Bank
对象,两个线程都不会发生阻塞。
对于所有账户总金额的获取方法也需要加锁才能保证正确执行。锁是可重入的,也就是说同一个线程可以重复的获得已经持有的锁。锁保持一个持有计数来跟踪嵌套获得锁的次数,当持有计数变为0时,线程释放锁。
public double getTotalBalance() {bankLock.lock();try {double sum = 0;for (double a : accounts) {sum += a;}return sum;} finally{bankLock.unlock();}
}
测试锁
tryLock()
方法用于尝试获取锁而没有发生阻塞。如果未获得锁,线程可以立即离开,去做别的事。
if(myLock.tryLock()) {try {do something} finally {myLock.unlock();}
} else {do something else
}
调用带有超时参数的tryLock()
,线程可以在等待获取锁的过程中被中断,抛出InterruptedException
异常。从而允许程序打破死锁,类似于lockInterruptibly()
。
读写锁
java.util.concurrent.locks
包定义了两个锁类:ReentrantLock
类和ReentrantReadWriteLock
类。在读多写少(很多线程从一个数据结构读取数据,很少线程修改其中数据)的情形中,ReentrantReadWriteLock
类是十分实用的。
读锁,允许多个读,排斥所有写;写锁,排斥所有读和写。
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();
条件对象
条件对象用来管理那些已经获得锁但不能工作的线程。比如当账户中没有足够余额时,需等待别的线程的存款操作。
一个锁对象可以有一个或多个相关的条件对象。当一个线程调用await()
等待方法时,它将进入该条件的等待集。当一个线程转账完成时会调用sufficientFunds.signalAll()
方法,重新激活因为sufficientFunds
这一条件而等待的所有线程,使这些线程从等待集中移出,状态变为可运行。当一个线程处于等待集中时,只能靠其他线程来重新激活自己。
synchronized关键字
使用synchronized
关键字声明的方法,对象的锁将保护整个方法,其实就是隐式的使用了一个内部对象锁。内部对象锁只有一个条件对象,使用wait()
/notifyAll()
/notify()
操作。
public synchronized void myMethod() {while (! (ok to proceed)) {wait();}do somethingnotifyAll();
}
注意,signal()
和notify()
都是随机选择一个线程,解除其阻塞状态,可能会造成死锁。
对于sychronized
修饰的方法,显式使用锁对象和条件对象,形式如下。
public void myMethod() {this.intrinsic.lock();try {while(! (ok to proceed)) {condition.await();}do somethingcondition.signalAll();} finally {this.intrinsic.unlock();}
}
为了保证操作的原子性,可以安全地使用AtomicInteger
作为共享计数器而无需同步,这个类提供方法incrementAndGet()
和decrementAndGet()
完成自增自减操作。
Volatile域
使用volatile
关键字同步读写的必要性:
由于寄存器或缓存的存在同一内存地址可能会取到不同的值;
编译器优化中假定内存中的值仅在代码中有显式修改指令时会改变。
volatile
关键字为实例域的同步访问提供了一种免锁机制,当被声明为volatile
域时,编译器和虚拟机就知道该域可能被另一个线程并发更新。使用锁或volatile
修饰符,多个线程可以安全地读取一个域,但volatile
不提供原子性。。另外,将域声明为final
,也可以保证安全的访问这个共享域。
线程局部变量
在线程间共享变量时有风险的,可以使用ThreadLocal
辅助类为各个线程提供各自的实例。比如,SimpleDateFormat
类不是线程安全的,内部数据结构会被下面形式的并发访问破坏。
public static final SimpleDateFormat dataFormat = new SimpleDateFormat("yyyy-MM-dd");
String dateStamp = dateFormat.format(new Date());
如果不使用synchronized
或锁等开销较大的同步,可以使用线程局部变量ThreadLocal
解决变量并发访问的问题。
public static final ThreadLocal<SimpleDateFormat> dateFormat =new ThreadLocal<SimpleDateFormat>() {protected SimpleDateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd");}};
String dateStamp = dateFormat.get().format(new Date());
在一个线程中首次调用get()
时,会调用initialValue()
方法,此后会返回属于当前线程的实例。
对于java.util.Random
类,虽是线程安全的,但多线程共享随机数生成器却是低效的。可以使用上面提到的ThreadLocal
为各个线程提供一个单独的生成器,还可以使用ThreadLocalRandom
这个便利类。
int random = ThreadLocalRandom.current().nextInt(upperBound);
阻塞队列
上面关于同步的实现方式是Java并发程序设计基础的底层构建块,在实际的编程使用中,使用较高层次的类库会相对安全方便。对于典型的生产者和消费者问题,可以使用阻塞队列解决,这样就不用考虑锁和条件的问题了。
生产者线程向队列插入元素,消费者线程从队列取出元素。当添加时队列已满或取出时队列为空,阻塞队列导致线程阻塞。将阻塞队列用于线程管理工具时,主要用到put()
和take()
方法。对于offer()
、poll()
、peek()
方法不能完成时,只是给出一个错误提示而不会抛出异常。
java.util.concurrent
包提供了几种形式的阻塞队列:
LinkedBlockingQueue
:无容量限制,链表实现;LinkedBlockingDeque
:双向队列,链表实现;ArrayBlockingQueue
:需指定容量,可指定公平性,循环数组实现;PriorityBlockingQueue
:无边界优先队列,用堆实现。
这里有一个用阻塞队列控制一组线程的示例,实现的功能是搜索指定目录及子目录中的所有文件并找出含有查询关键字的行。里面有个小技巧,一个线程搜索完毕时向阻塞队列填充DUMMY,让所有线程能停下来。
Java多线程之同步与阻塞队列相关推荐
- Java线程详解(15)-阻塞队列和阻塞栈
Java线程:新特征-阻塞队列 阻塞队列是Java5线程新特征中的内容,Java定义了阻塞队列的接口java.util.concurrent.BlockingQueue,阻塞队列的概念是,一个指定长度 ...
- java实验多线程机制_使用Java多线程的同步机制编写应用程序 PDF 下载
使用Java多线程的同步机制编写应用程序 PDF 下载 本站整理下载: 相关截图: 主要内容: 一. 实验名称 使用Java多线程的同步机制编写应用程序 二. 实验目的及要求 1.理解并行/并发的概念 ...
- Java多线程的同步机制(synchronized)
一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在 java里边就是拿到某个同步对象的锁(一个对象只有一把锁): 如果这个时候同步对象的锁被其他线程拿走了,他(这个 ...
- java多线程的同步
http://www.blogjava.net/jadmin/archive/2010/03/18/360497.html 多线程的同步依靠的是对象锁机制,synchronized关键字的背后就是利用 ...
- java queue通信_Java -- 使用阻塞队列(BlockingQueue)控制线程通信
BlockingQueeu接口是Queue的子接口,但是它的主要作用并不是作为容器,而是作为线程同步的工具. 特征: 当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程 ...
- Java并发编程之LinkedTransferQueue阻塞队列详解
简介 LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列.相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和tran ...
- java多线程、同步、异步
1.多线程.并发.异步.并行: 多线程是对cpu剩余劳动力的压榨,是一种技术.想想web server 需要处理大量并发请求的场景,是你同时给A,B,C...打电话(你的思维在不断切换,如一边给女朋友 ...
- Java多线程的同步优化的6种方案
概述 处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存. 加入高速缓存带来了一个新的问题:缓存一致性.如果多个缓存共享同一块主内存区域,那么多个缓存的数据 ...
- java 多线程的同步问题_java多线程解决同步问题的几种方式,原理和代码
wait()/notify()方法 await()/signal()方法 BlockingQueue阻塞队列方法 PipedInputStream/PipedOutputStream 阻塞队列的一个简 ...
- Java多线程与同步
文章目录 1. 多线程概述 1.1 多线程引入 1.2 多线程概述 1.2.1 什么是进程? 1.2.2 多进程有什么意义呢? 1.2.3 什么是线程? 1.2.4 多线程有什么意义呢? 1.2.5 ...
最新文章
- python高并发的解决方案
- 高可用软件heartbeat服务章节目录(草稿)
- 去掉动网广告“国内使用量最大的动网论坛”的方法
- 作业三--简单四则运算
- Web开发的历史发展技术演变
- 利用11行Python代码,盗取了室友的U盘,内容十分刺激!
- crash recovery mysql_MySQL · 源码分析 · binlog crash recovery
- apk反编译、smali修改、回编译笔记
- python从语音生成语谱图
- 简单使用hibernate(idea中使用)
- Python 3.7.1 模块 文本处理 正则表达式 re
- 基于go语言的牛牛游戏服务器搭建
- 概率图--贝叶斯网络、马尔可夫网络
- JavaWeb(10.21)
- 谷歌翻译字数限制_如何始终在Google文档中显示字数统计
- FTTH网速慢解决方案
- 联想为什么不卖X系列服务器,IBM欲向联想出售x86服务器业务,涉及System x产品线...
- 使用Google Colab运行项目
- Sublime Emmet 插件安装教程 Tab 快捷键无法使用问题解决
- SVN 服务端 和 客户端(转)