Java并发编程实战笔记—— 并发编程4
1、同步容器类
同步容器类都是线程安全的,但在某些情况下可能需要额外的客户端加锁保护复合操作。
容器上常见的复合操作包括但不限于:迭代(反复访问数据,直到遍历完容器中所有的元素为止)、跳转(根据指定顺序找到当前元素的下一个元素)以及条件运算(例如:如果没有则添加)。
这些复合操作在没有客户端加锁的情况下仍然是线程安全的,但是当其他现场并发的修改容器时,它们就可能出现意料之外的行为。
public static Object getLast(Vector list){
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}
public static void deleteLast(Vector list){
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
这两个方法分别实现了查询最后一个元素的索引然后返回或是删除的操作,单独一个操作可以说是线程安全的,因为Vector是线程安全的类,它的get()、remove()方法都是线程安全的,但是如果线程A执行getLast操作,刚获得索引线程B执行deleteLast操作且执行完成,这是线程A的get()操作就会出错,或者说是其他线程调用了其他方法修改了这个list,都会导致错误。所以需要进行客户端加锁:
public static Object getLast(Vector list) {
synchronized (list) {
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}
}
public static void deleteLast(Vector list) {
synchronized (list) {
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
}
这样锁住list,就可以避免执行getLast或是deleteLast这种复合操作的时,因为list被修改而导致的错误。
在调用size()和get()的时候,Vector的长度可能会发生变化,
public static void outList(Vector list){
for(int i = 0; i < list.size(); i++){
System.out.println(list.get(i));
}
}
这样的遍历的正确性要依赖于运气,期望在size和get之间永远没有线程改变了Vector的长度,然而这是不可能的,所以这同样通过客户端加锁解决。
public static void outList(Vector list){
synchronized (list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
}
2、并发容器
- ConcurrentHashMap
ConcurrentHashMap和HashMap一样是一个基于散列的Map,但是它是用来不一样的加锁策略,ConcurrentHashMap它并不是将每一个方法都在同一个锁使得每一次都次能有一个线程访问容器,它采用的分段锁的机制,在这种机制中,任意数量读取线程可以并发的访问Map,执行读取操作的线程和执行写入的线程可以并发的访问Map,而且一定数量的写入操作可以并发的修改Map,它不需要在迭代过程中对容器加锁
- CopyOnWriteArrayList
CopyOnWriteArrayList用于替代同步List,迭代期间不需要进行加锁或复制,“CopyOnWrite”代表的是“写入时复制”,意思就是只要正确地发布一个事实不可变的对象,那么在访问这个对象的时候就不需要进一步的同步了,在每一次修改的时候,都会创建并重新发布一个新的容器副本。也就是说其实CopyOnWriteArrayList是一个读写分离的容器,所以CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器,仅当迭代的操作远远多于修改的操作时适合使用这个容器。
3、阻塞队列和生产者 — 消费者模式
阻塞队列提供了可阻塞的put和take方法,以及支持定时的offer和poll方法,如果队列已经满了,使用put将阻塞到有空间有用;如果队列为空,那么take方法将阻塞到有元素可用。
生产者—消费者模式将“找出需要执行的工作”和“执行工作”这两个过程分离开来,并把工作放入一个“待完成”列表中以便随后处理,而不是找出立即处理。
基于阻塞队列实现的生产者—消费者模式,当数据生成的时候,生产者将数据放入队列,而当消费者准备处理数据的时候,从队列中获取数据。生产者不需要知道消费者的标识和数量,只需要吧数据放入队列即可,消费者也不需要知道生产者是,数据来自那里。
4、串行线程封闭
对于可变对象,生产者——消费者模式这种设计与阻塞队列一起,促进了串行线程关闭,将对象的所有权从生产者处交付给消费者。线程封闭对象只能由单个线程拥有,但可以通过安全的发布对于新的所有者是可见的,并且最初的所有者不能
会再访问他。
5、双端队列和工作密取
Deque和BlockingDeque是一种双端队列,实现了在队列头和队列尾的高效插入和移除。
工作密取设计中,每个消费者都有各自的双端队列,如果某个消费者完成了自己双端队列中的的全部工作,那么它将可以从其他消费者双端队列秘密的获取工作。
6、阻塞方法与中断方法
线程可能会阻塞或暂停,原因又很多:等待I/O操作结束,等待某个锁,等待从Thread.sleep方法中醒来,或是等待另一个线程的计算结果。
BlockingQueue的put和take等方法会抛出受检查异常InterruptedException,当某个方法抛出这个异常的时候,表示该方法是一个阻塞方法,如果这个方法被中断,那么它将努力提前结束阻塞状态。
Thread提供了interrupt方法,用于中断线程或者检查线程是否已经被中断。
当代码中调用了一个将要抛出InterruptedException异常的方法时,就必须要处理对中断的反应了:
- 传递InterruptedException,自需要把这个异常传递给这个方法的调用者
- 恢复中断,通过捕获这个中断,并调用当前线程上的interrupt方法恢复中断状态,这样调用栈中更高层的代码将看到引发一个中断
public void run() {
try{
aTask(queue.take());
}catch (InterruptedException e){
Thread.currentThread().interrupt();
}
}
7、同步工具类
同步工具类可以使任何一个对象,只要它根据其自身的状态来协调线程的控制流,阻塞队列可以作为同步工具类,其他类型的同步工具类还包括信号量、栅栏以及闭锁。
所有的同步工具类都包括一些特定的结构化属性:它们封装了一些状态,这些状态将决定执行同步工具类的线程是继续执行还是等待,此外还提供一些方法对状态进行操作,以及另一些方法用于高效地等待同步工具类进入到预期状态
8、闭锁
闭锁是一种同步工具类,可以延迟线程的进度直到到达终止状态,它就像一扇门,在闭锁达到结束之前这扇门一直是关闭的,没有任何线程能通过,到达结束状态的时候,这扇门会打开并允许所有的线程通过。
闭锁的用处有:
- 确保某个计算在其需要的所有资源都背初始化之后再继续进行
- 确保某个服务在其依赖的所有服务都已经启动之后再启动
- 等待等到所有的参与者都就绪在继续执行
CountDownLatch,一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
countDown方法,当前线程调用此方法,则计数减一
awaint方法,调用此方法会一直阻塞当前线程,直到计时器的值为0
private static long timeTasks(int nThreads, final Runnable tasks)throws InterruptedException{
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(nThreads);
for(int i = 0; i < nThreads; i++){
Thread t = new Thread(){
@Override
public void run() {
try{
startGate.await();
try{
tasks.run();
}finally {
endGate.countDown();
}
} catch(InterruptedException e){}
}
};
t.start();
}
long start = System.nanoTime();
startGate.countDown();
long end = System.nanoTime();
return end-start;
}
这是一个计时测试的方法,使用了两个CountDownLatch,一个startGate的初始计数是1,用来开启所有的线程,一个endGate的计数是nThreads,用来阻塞nThreads个线程。这个程序新创建了nThreads个线程每个线程都被startGate阻塞,这里新建的每个线程在被startGate阻塞后将启动我们想要测试的线程,然后最后每个新建的线程都将执行finally中的内容将endGate的计数减1。。。。。然后我就发现问题了如果不使用endGate也是可以实现的。。
只使用startGate:
private static long timeTasks(int nThreads, final Runnable tasks)throws InterruptedException{
final CountDownLatch startGate = new CountDownLatch(1);
for(int i = 0; i < nThreads; i++){
Thread t = new Thread(){
@Override
public void run() {
try{
startGate.await();
tasks.run();
} catch(InterruptedException e){
}
}
};
t.start();
}
long start = System.nanoTime();
startGate.countDown();
long end = System.nanoTime();
return end-start;
}
因为要新创建线程来运行想要测试的线程,所以循环确定测几个就好了。
换个实例吧。。
public class MonitorVehicleTracker {
final static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch=new CountDownLatch(2);//两个工人的协作
Worker worker1=new Worker("one", 5000, latch);
Worker worker2=new Worker("two", 8000, latch);
worker1.start();//
worker2.start();//
latch.await();//等待所有工人完成工作
System.out.println("all work done at "+sdf.format(new Date()));
} static class Worker extends Thread{
private String workerName;
private int workTime;
private CountDownLatch latch;
public Worker(String workerName ,int workTime ,CountDownLatch latch){
this.workerName=workerName;
this.workTime=workTime;
this.latch=latch;
}
public void run(){
System.out.println("Worker " + workerName + " do work begin at " + sdf.format(new Date()));
doWork();
System.out.println("Worker " + workerName + " do work complete at " + sdf.format(new Date()));
latch.countDown();//完成工作后,计数器减一 }
private void doWork(){
try {
Thread.sleep(workTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
输出:
Worker li si do work begin at 2017-11-23 15:59:47
Worker zhang san do work begin at 2017-11-23 15:59:47
Worker zhang san do work complete at 2017-11-23 15:59:52
Worker li si do work complete at 2017-11-23 15:59:55
all work done at 2017-11-23 15:59:55
FutureTask也可以用作闭锁,FutureTask可以处于一下3种状态:等待运行,正在运行,运行完成。Future.get的行为取决于FutureTask的执行情况,如果任务完成那么get立即就会返回结果,否则get将阻塞到直到任务完成,然后返回结果或是抛出异常。
public class MonitorVehicleTracker {
final static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private static final FutureTask<Integer> future = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
Thread.currentThread().setName("Thread(1)");
System.out.println(Thread.currentThread().getName() + "start" + sdf.format(new Date()));
return getAResult();
}
});
private static final Thread thread = new Thread(future); private static void start(){ thread.start();} private static Integer getAResult() throws InterruptedException{
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "over" + sdf.format(new Date()));
return 1;
}
private static Integer get(){
System.out.println(Thread.currentThread().getName() + "start" + sdf.format(new Date()));
try{
return future.get();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
public static void main(String[] args) throws InterruptedException {
start();
new Thread(){ @Override
public void run() {
Thread.currentThread().setName("Thread(2)");
System.out.println(Thread.currentThread().getName() + "get(): " + get() + " "+ sdf.format(new Date()));
}
}.start();
new Thread(){
@Override
public void run() {
Thread.currentThread().setName("Thread(3)");
System.out.println(Thread.currentThread().getName() + "get(): " + get() + " "+ sdf.format(new Date()));
}
}.start();
}
}
这里定义了三个线程,执行FutureTask的线程和,执行Future.get的两个线程。
输出:
Thread(1)start2017-11-23 17:06:53
Thread(2)start2017-11-23 17:06:53
Thread(3)start2017-11-23 17:06:53
Thread(1)over2017-11-23 17:06:58
Thread(2)get(): 1 2017-11-23 17:06:58
Thread(3)get(): 1 2017-11-23 17:06:58
从结果可以看出来如果FutureTask没有完成任务的话,的确是会阻塞了线程2和线程3,直到完成了线程2和线程3才继续执行的,还有就是FutureTask只会执行一次,所以,多次调用get方法,返回的值始终是一样的。
9、信号量
计数信号量用来控制同时访问某个特定资源的操作数量,或者执行某个操作的数量。
Semaphore中管理着一组虚拟的许可,许可的初始数量可通过构造函数来指定,再执行操作时可以首先获得许可(如果还有剩余的话),并在使用后释放许可,如果没有许可,那么acquire将阻塞到有许可为止。
Semaphore.acquire()方法将取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。获取一个许可(如果提供了一个)并立即返回,将可用的许可数减 1。
Semaphore.release()释放一个许可,将其返回给信号量。释放一个许可,将可用的许可数增加 1。
class BoundedHashSet<T>{
private final Set<T> set;
private final Semaphore sem; private void BoundedHashSet(int bound){
this.set = Collections.synchronizedSet(new HashSet<T>());
sem = new Semaphore(bound);
} public boolean add(T o) throws InterruptedException {
sem.acquire();
boolean wasAdded = false;
try {
wasAdded = set.add(o);
return wasAdded;
}
finally {
if(!wasAdded){
sem.release();
}
}
}
public boolean remove(Object o){
boolean wasRemoved = set.remove(o);
if(wasRemoved){
sem.release();
}
return wasRemoved;
}
}
这里实现了一个有界的HashMap,每次的添加add操作都将先调用acquire()方法获取一个许可如果没有获取到将被阻塞,直到获取到许可,如果添加失败将释放这个许可。
每次的删除remove操作如果成功都将释放一个许可。
10、栅栏
栅栏类似于闭锁,它能阻塞一组线程直到某个事件的发生,栅栏和闭锁的区别就是,所有的线程必须同时达到栅栏的位置,才会继续运行,闭锁用于等待时间,而栅栏用于等待其他线程。
CylicBarrier可以是一定数量的参与方式反复地在栅栏位置汇聚,当所有线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都达到栅栏位置,当所有线程都达到栅栏位置时调用await方法,那么栅栏将被打开,所有线程都将释放,而栅栏将重置以便下次使用。
如果对await的调用超时,或者await阻塞的线程被中断,那么栅栏就认为被打破了,所有阻塞的await调用都将内终止并抛出BrokenBarrierException。
如果成功通过栅栏,将为每个线程返回唯一的到达索引号,我们可以利用这些索引号来“选举”产生一个领导线程,在下一次的迭代中由该领导执行一些特殊的工作,
public class test {
public static void main(String[] args){
int count = 4;
CyclicBarrier barrier = new CyclicBarrier(count);
for(int i = 0; i < count; i++){
new Writer(barrier).start();
}
}
static class Writer extends Thread{
private CyclicBarrier cyclicBarrier;
public Writer(CyclicBarrier cyclicBarrier){
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println("线程" + Thread.currentThread().getName() + "正在写入数据");
try {
Thread.sleep(5000);//用睡眠来模拟写入数据操作
System.out.println("线程" + Thread.currentThread().getName() + "写入数据完毕");
cyclicBarrier.await();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
CylicBarrierx的await方法将阻塞线程,直到阻塞了初始化时确定的数量的线程就一起释放。
Java并发编程实战笔记—— 并发编程4的更多相关文章
- Java并发编程实战笔记—— 并发编程1
1.如何创建并运行java线程 创建一个线程可以继承java的Thread类,或者实现Runnabe接口. public class thread { static class MyThread1 e ...
- Java并发编程实战笔记—— 并发编程2
1.ThreadLocal Java中的ThreadLocal类可以让你创建的变量只被同一个线程进行读和写操作.因此,尽管有两个线程同时执行一段相同的代码,而且这段代码又有一个指向同一个ThreadL ...
- Java并发编程实战笔记—— 并发编程3
1.实例封闭 class personset{ private final Set<Person> myset = new HashSet<Person>(); public ...
- Java并发编程实战.笔记十一(非阻塞同步机制)
关于非阻塞算法CAS. 比较并交换CAS:CAS包含了3个操作数---需要读写的内存位置V,进行比较的值A和拟写入的新值B.当且仅当V的值等于A时,CAS才会通过原子的方式用新值B来更新V的值,否则不 ...
- Java并发编程实战 01并发编程的Bug源头
摘要 编写正确的并发程序对我来说是一件极其困难的事情,由于知识不足,只知道synchronized这个修饰符进行同步. 本文为学习极客时间:Java并发编程实战 01的总结,文章取图也是来自于该文章 ...
- 【ARM-Linux开发】OpenACC并行编程实战笔记
今年运气比较好,学了cuda之后,了解到了gpu的另两种使用语言opencl和openacc, opencl(Open Computing Language ,开放计算语言)是面向异构系统的并行编程 ...
- 多线程-java并发编程实战笔记
线程安全性 编写线程安全的代码实质上就是管理对状态的访问,而且通常都是共享的,可变的状态. 一个对象的状态就是他的数据,存储在状态变量中,比如实例域或静态域.所谓共享是指一个对象可以被多个线程访问:所 ...
- java并发编程实战笔记---(第四章)对象的组合
4.1设计线程安全的类 包含三个基本要素: 1.找出构成对象状态的所有变量 2.找出约束状态变量的不变性条件 2.简历对象状态的并发访问管理策略 对象的状态: 域 基本类型所有域, 引用类型包括被引用 ...
- java并发编程实战笔记---(第三章)对象的共享
3.1 可见性 synchronized 不仅实现了原子性操作或者确定了临界区,而且确保内存可见性. *****必须在同步中才能保证:当一个线程修改了对象状态之后,另一个线程可以看到发生的状态变化. ...
随机推荐
- django基础知识之POST属性:
POST属性 QueryDict类型的对象 包含post请求方式的所有参数 与form表单中的控件对应 问:表单中哪些控件会被提交? 答:控件要有name属性,则name属性的值为键,value属性的 ...
- scrapy基础知识之 Scrapy 和 scrapy-redis的区别:
Scrapy 和 scrapy-redis的区别 Scrapy 是一个通用的爬虫框架,但是不支持分布式,Scrapy-redis是为了更方便地实现Scrapy分布式爬取,而提供了一些以redis为基础 ...
- 你必须知道的Docker镜像仓库的搭建
近期工作中发现用到的容器镜像越来越多(不多的时候没考虑过镜像仓库的问题),同一个容器镜像也存在多个版本,那么镜像仓库的搭建需求就涌现出来,本文就目前的几个常用镜像仓库的搭建进行介绍,我们可以根据需要选 ...
- sql server 2008 NULL值
SQL支持用NULL符号来表示缺少的值,它使用的是三值谓词逻辑,计算结果可是以TURE.FALSE或UNKNOWN. SQL中不同语言元素处理NULL和UNKNOWN的方式也有所不同,如果逻辑表达式只 ...
- NOIP2018普及T2暨洛谷P5016 龙虎斗
题目链接:https://www.luogu.org/problemnew/show/P5016 分析: 这是一道模拟题.看到题目,我们首先要把它细致的读明白,模拟题特别考察细节,往往会有想不到的坑点 ...
- 个人永久性免费-Excel催化剂功能第38波-比Vlookup更好用的查找引用函数
谈起Excel的函数,有一个函数生来自带明星光环,在表哥表姐群体中无人不知,介绍它的教程更是铺天盖地,此乃VLOOKUP函数也.今天Excel催化剂在这里冒着被火喷的风险,大胆地宣布一个比VLOOKU ...
- MYSQL数据库的安装,配置文件,登入
07.13自我总结 MYSQL数据库 一.MYQL数据库的安装 可以去mysql官网下载mysql压缩包 运行程序:在bin文件夹中,其中客户端运行文件是mysql.exe,服务端运行文件为mysql ...
- 浅谈redis
1.Redis简介: Redis是一个开源的使用ANSI C语言编写,遵守BSD协议.支持网络.可基于内存亦可持久化的日志型.Key-Value数据库,并提供多种语言的API.它通常被称为数据结构服务 ...
- JDK(Windows)
百度云:链接:http://pan.baidu.com/s/1dEEsIUd 密码:15cn 官网下载网址:http://www.oracle.com/technetwork/java/jav ...
- <<Modern CMake>> 翻译 2. CMake 基础
<<Modern CMake>> 翻译 2. CMake 基础 最低版本 这是每个 CMakeLists.txt 文件的第一行.CMakeLists.txt 是 CMake 所 ...