`

<java并发编程实践>(1~5)读书笔记

阅读更多

第一章:介绍

进程的资源比如内存,文件句柄,安全证书,由操作系统分配。进程通过Socket,信号处理,共享内存,信号量通信。

 

线程共享进程的资源,每个线程有自己的程序计数器,栈(stack)和本地变量。

 

第二章:线程安全

编写正确的并发程序的关键在于对共享的,可变的状态进行访问管理

synchronized,一方面保证操作的原子性,一方面保证操作的可见性。

 

耗时的计算或操作,比如网络或控制台I/O,难以快速完成,执行这些操作期间不要占有锁。

 

第三章:共享对象

在没有同步的情况下,编译器,处理器,运行时安排操作的执行顺序可能完全出人意料。在没有进行适当同步的多线程程序中,尝试推断那些“必然”发生在内存中的动作时,你总是会判断错误。

 

锁可以保证可见性和原子性,而volatile变量只能保证可见性。

 

不安全的发布对象,导致this溢出。

 

public class ThisEscape { public ThisEscape(EventSource source) { source.registerListener( new EventListener() { public void onEvent(Event e) { doSomething(e); } }); } }


 

第四章 组合对象

 

设计线程安全的类的过程包括三个基本要素:

1)确定对象状态是由哪些变量构成的。

2)确定限制状态变量的不变约束。

3)制定一个管理并发访问对象状态的策略。

 

如果一个类由多个彼此独立的线程安全状态变量组成,并且类的操作不包含任何无效状态转换时,可以将线程安全委托给这些状态变量。

 

 

线程安全

public class VisualComponent { private final List<KeyListener> keyListeners = new CopyOnWriteArrayList<KeyListener>(); private final List<MouseListener> mouseListeners = new CopyOnWriteArrayList<MouseListener>(); public void addKeyListener(KeyListener listener) { keyListeners.add(listener); } public void addMouseListener(MouseListener listener) { mouseListeners.add(listener); } public void removeKeyListener(KeyListener listener) { keyListeners.remove(listener); } public void removeMouseListener(MouseListener listener) { mouseListeners.remove(listener); } }


 

 

 

  非线程安全

public class NumberRange { // INVARIANT: lower <= upper private final AtomicInteger lower = new AtomicInteger(0); private final AtomicInteger upper = new AtomicInteger(0); public void setLower(int i) { // Warning -- unsafe check-then-act if (i > upper.get()) throw new IllegalArgumentException( "can't set lower to " + i + " > upper"); lower.set(i); } public void setUpper(int i) { // Warning -- unsafe check-then-act if (i < lower.get()) throw new IllegalArgumentException( "can't set upper to " + i + " < lower"); upper.set(i); } public boolean isInRange(int i) { return (i >= lower.get() && i <= upper.get()); } }


 

前五章是并发编程的理论基础,我读的很痛苦

在第五章的最后介绍了几种JDK新加的synchronizer。

1)阻塞队列

2)闭锁,latch:他可以延迟线程的进度直到线程到达终止(terminal)状态。一个闭锁工作起来就像一道大门:直到闭锁达到终点状态之前,门一直是关闭的,没有线程能够通过,在终点状态到来的时候,门开了,允许所有线程都通过。一旦闭锁到达了终点状态,他就不能够再改变状态了,所以他会永远保持敞开状态,闭锁可以用来确保特定活动直到其他的活动完成后才发生,比如:

   1、确保一个计算不会执行,直到他需要的资源被初始化。

   2、确保一个服务不会开始,直到他依赖的其他服务都已经开始。

   3、等待,直到活动的所有部分都为继续处理做好充分准备。

  场景:一个服务中包含一个相关的两元闭锁,开启服务S会首先开始等待闭锁S中所依赖的其他服务,在启动结束后,会释放闭锁S,这样所依赖S的服务也开始处理了。

  实例:CountDownLatch,FutureTask

java.util.concurrent.CountDownLatch它是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。用给定的计数初始化 CountDownLatch。在调用countDown() 方法,使当前计数减一,且当前计数到达零之前,await 方法会一直受阻塞。当前计数到达零之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。 CountDownLatch 是一个通用同步工具,它有很多用途。将计数 1 初始化的 CountDownLatch 用作一个简单的开/关锁存器,或入口:在通过调用 countDown() 的线程打开入口前,所有调用 await 的线程都一直在入口处等待。用 N 初始化的 CountDownLatch 可以使一个线程在 N 个线程完成某项操作之前一直等待,或者使其在某项操作完成 N 次之前一直等待。 CountDownLatch的一个有用特性是,它不要求调用 countDown 方法的线程等到计数到达零时才继续,调用countDown 方法的线程并不会阻塞。CountDownLatch调用await方法将阻塞当前线程,直到其他线程调用countDown 方法,使其计数到达零时才继续。 CountDownLatch主要有以下三种用法 用法1:将计数1初始化的 CountDownLatch 用作一个简单的开/关锁存器,或入口:在通过调用 countDown() 的线程打开入口前,所有调用 await 的线程都一直在入口处等待。 用法2:用N初始化的 CountDownLatch 可以使一个线程在 N 个线程完成某项操作之前一直等待,或者使其在某项操作完成 N 次之前一直等待。 用法3:它不要求调用 countDown 方法的线程等到计数到达零时才继续,而在所有线程都能通过之前,它只是阻止任何线程继续通过一个await。注意2:用法3其实对用法1和用法2的混合使用。它的实质是一个线程来阻止其他线程通过(用法1), 但是该线程要完成N个操作(用法2),才让其他线程通过。注意3:理解的CountDownLatch含义,可以灵活的使用它,而不用拘于上述三种形式。 注意4:计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。 示例1 startSignal是一个启动信号,在 driver 为继续执行 worker 做好准备之前,它会阻止所有的 worker 继续执行。 doneSignal是一个完成信号,它允许 driver 在完成所有 worker 之前一直等待。 class Driver { // ... void main() throws InterruptedException { CountDownLatch startSignal = new CountDownLatch(1); CountDownLatch doneSignal = new CountDownLatch(N); for (int i = 0; i < N; ++i) // create and start threads new Thread(new Worker(startSignal, doneSignal)).start(); doSomethingElse(); // don't let run yet startSignal.countDown(); // let all threads proceed doSomethingElse(); doneSignal.await(); // wait for all to finish } } class Worker implements Runnable { private final CountDownLatch startSignal; private final CountDownLatch doneSignal; Worker(CountDownLatch startSignal, CountDownLatch doneSignal) { this.startSignal = startSignal; this.doneSignal = doneSignal; } public void run() { try { startSignal.await(); doWork(); doneSignal.countDown(); } catch (InterruptedException ex) {} // return; } void doWork() { ... } } 另一种典型用法是,将一个问题分成N个部分,用执行每个部分并让锁存器倒计数的Runnable来描述每个部分,然后将所有 Runnable 加入到 Executor 队列。当所有的子部分完成后,协调线程就能够通过 await。(当线程必须用这种方法反复倒计数时,可改为使用 CyclicBarrier。) class Driver2 { // ... void main() throws InterruptedException { CountDownLatch doneSignal = new CountDownLatch(N); Executor e = ... for (int i = 0; i < N; ++i) // create and start threads e.execute(new WorkerRunnable(doneSignal, i)); doneSignal.await(); // wait for all to finish } } class WorkerRunnable implements Runnable { private final CountDownLatch doneSignal; private final int i; WorkerRunnable(CountDownLatch doneSignal, int i) { this.doneSignal = doneSignal; this.i = i; } public void run() { try { doWork(i); doneSignal.countDown(); } catch (InterruptedException ex) {} // return; } void doWork() { ... } } 内存一致性效果:线程中调用 countDown() 之前的操作 happen-before 紧跟在从另一个线程中对应 await() 成功返回的操作。主要函数 public CountDownLatch(int count) 构造一个用给定计数初始化的 CountDownLatch。 参数: count - 在线程能通过 await() 之前,必须调用 countDown() 的次数 抛出: IllegalArgumentException - 如果 count 为负 public void await() throws InterruptedException 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。 如果当前计数为零,则此方法立即返回。 如果当前计数大于零,则出于线程调度目的,将禁用当前线程,且在发生以下两种情况之一前,该线程将一直处于休眠状态: * 由于调用 countDown() 方法,计数到达零;或者 * 其他某个线程中断当前线程。 如果当前线程: * 在进入此方法时已经设置了该线程的中断状态;或者 * 在等待时被中断, 则抛出 InterruptedException,并且清除当前线程的已中断状态。 抛出: InterruptedException - 如果当前线程在等待时被中断注意:它会抛出InterruptedException,关于Interrupted,请参阅《JAVA线程的interrupt》 public boolean await(long timeout,TimeUnit unit) throws InterruptedException 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。 如果当前计数为零,则此方法立刻返回 true 值。 如果当前计数大于零,则出于线程调度目的,将禁用当前线程,且在发生以下三种情况之一前,该线程将一直处于休眠状态: * 由于调用 countDown() 方法,计数到达零;或者 * 其他某个线程中断当前线程;或者 * 已超出指定的等待时间。 如果计数到达零,则该方法返回 true 值。 如果当前线程: * 在进入此方法时已经设置了该线程的中断状态;或者 * 在等待时被中断, 则抛出 InterruptedException,并且清除当前线程的已中断状态。 如果超出了指定的等待时间,则返回值为 false。如果该时间小于等于零,则此方法根本不会等待。 参数: timeout - 要等待的最长时间 unit - timeout 参数的时间单位。 返回: 如果计数到达零,则返回 true;如果在计数到达零之前超过了等待时间,则返回 false 抛出: InterruptedException - 如果当前线程在等待时被中断 public void countDown() 递减锁存器的计数,如果计数到达零,则释放所有等待的线程。 如果当前计数大于零,则将计数减少。如果新的计数为零,出于线程调度目的,将重新启用所有的等待线程。 如果当前计数等于零,则不发生任何操作。 public long getCount() 返回当前计数。 此方法通常用于调试和测试。 返回: 当前计数

 


 

3)信号量 (计数信号量 Counting semaphore)用来控制能够同时访问某特定资源的活动的数量,或者同时执行某一给定操作的数量。计数信号量可以用来实现资源池或者给一个容器限定边界。

  一个Semaphore管理一个有效的许可集;许可的初始量通过构造函数传递给Semaphore。活动能够获得许可,并在使用之后释放许可。

 简单来说,信号量维护了一个许可集。通过acquire()来获得一个许可,如果有许可可用,就返回,否则阻塞直到有许可可用。通过release() 来释放一个许可。
import java.util.concurrent.Semaphore; public class SemaphoreDemo { public static void main(String[] args) { Pool sem = new Pool(); try { Object o1 = sem.getItem(); Object o2 = sem.getItem(); Object o3 = sem.getItem(); System.out.println("availablePermits:" + sem.availablePermits() + " before reduce"); sem.putItem(o1); System.out.println("availablePermits:" + sem.availablePermits() + " after reduce"); sem.putItem(o2); System.out.println("availablePermits:" + sem.availablePermits() + " after realese two"); sem.putItem(o3); System.out.println("availablePermits:" + sem.availablePermits() + " after realese another one"); } catch (InterruptedException e) { } } } class Pool { private static final int MAX_AVAILABLE = 10; private final Semaphore available = new Semaphore(MAX_AVAILABLE, true); public Pool() { for (int i = 0; i < MAX_AVAILABLE; i++) { items[i] = new String("" + i); } } public Object getItem() throws InterruptedException { available.acquire(); return getNextAvailableItem(); } public void putItem(Object x) { if (markAsUnused(x)) { available.release(); } } public int availablePermits() { return available.availablePermits(); } // Not a particularly efficient data structure; just for demo // whatever kinds of items being managed protected Object[] items = new String[MAX_AVAILABLE]; protected boolean[] used = new boolean[MAX_AVAILABLE]; protected synchronized Object getNextAvailableItem() { for (int i = 0; i < MAX_AVAILABLE; ++i) { if (!used[i]) { used[i] = true; return items[i]; } } return null; // not reached } protected synchronized boolean markAsUnused(Object item) { for (int i = 0; i < MAX_AVAILABLE; ++i) { if (item == items[i]) { if (used[i]) { used[i] = false; return true; } else { return false; } } } return false; } }



 

 

 

 

 

4)关卡 Barrier 类似于闭锁,他们都能够阻塞一组线程,直到某些事件发生。其中关卡和闭锁关键的不同在于,所有线程必须同时到达关卡点,才能继续处理。闭锁等待的是事件;关卡等待的是其他线程。关卡实现的协议,就像一些家庭成员指定商场中的集合地点:“我们6:00在麦当劳见,不见不散,之后我们再决定接下来做什么”。

public class CyclicBarrierDemo { public static void main(String[] args) { int number = 50; CyclicBarrier barrier = new CyclicBarrier(number); try { for (int i = 0; i < number; i++) { new CyclicBarrierDemoThread("thread" + i, barrier).start(); Thread.sleep(100); } } catch (InterruptedException e) { e.printStackTrace(); } try { for (int i = 0; i < number; i++) { new CyclicBarrierDemoThread("【new】" + i, barrier).start(); Thread.sleep(100); } } catch (InterruptedException e) { e.printStackTrace(); } } } class CyclicBarrierDemoThread extends Thread { private CyclicBarrier barrier; CyclicBarrierDemoThread(String name, CyclicBarrier barrier) { super(name); this.barrier = barrier; } public void run() { try { System.out.println(getName() + " has arrived"); int val = barrier.await(); System.out.println(getName() + " has crossd the barrier " + val); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } }

 


 

CyclicBarrier 和 CountDownLatch 的主要异同:

两者在构造的时候都必须指定线程数量,而且该数量在构造后不可修改。
前者可以传入一个 Runnable 对象,在任务完成后自动调用,执行者为某个子线程;后者可在 await 方法后手动执行一段代码实现相同的功能,但执行者为主线程。
前者在每个子线程上都进行阻塞,然后一起放行;后者仅在主线程上阻塞一次。
前者可以重复使用;后者的倒计数器归零后就作废了。
两者的内部实现完全不同。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics