`

《Java并发编程实践》读书笔记(1)

    博客分类:
  • Book
阅读更多


并发编程在编写高性能, 可伸缩应用的时候经常用到的一项技术, 也是相对来说比较高级的一项技术, 是每一个做后端开发的必备技能.这本书差不多是对Java并发包的一个非常详细的用法介绍.

在看的过程中做了一些笔记, 方便以后备忘.

并发基础

编写线程安全的代码, 本质上就是管理对状态的访问,而且通常都是共享的, 可变的状态.

通俗的说, 一个对象的状态就是他的数据.

所谓共享就是指一个变量可以被多个线程访问, 所谓可变是指变量的值在其生命周期内可以改变, 而真正要做到线程安全是在不可控的并发访问中保护数据

一个对象是否应该是线程安全取决于它是否被多个线程访问.

无论如何, 只要有多于一个的线程访问给定的状态变量, 而其中某个线程会写入该变量, 此时必须使用同步来协调线程对该变量的访问.

最简单的保证数据的线程安全:
  • 不要跨线程共享变量
  • 使状态变量为不可变
  • 在任何访问状态变量的时候使用同步.


竞争条件
最常见的一种竞争条件是"检查再运行(check-then-act)", 使用一个潜在的过期值作为下一步操作的依据.

检查再运行: 你观察到一些事情为真, 然后基于你的观察去执行一些动作, 但事实上, 从观察到执行操作的这段时间内, 观察结果可能已经无效了, 从而引发错误.

为了避免竞争条件, 必须阻止其他线程同时访问我们正在修改的变量, 让我们可以确保: 当其他线程想要查看或修改一个状态时, 必须在我们的线程开始之前或者完成之后, 而不能在操作过程中.

原子操作
假设有操作a和b, 如果从执行a的线程的角度看, 当其他线程执行b时, 要么b全部执行完成, 要么一点也没有执行, 那么a和b就互为原子操作.

为了保证线程安全, "检查再运行"操作和"读-改-写"操作必须是原子操作.

锁的可重入特点
线程在试图获得它自己占有的锁时, 请求线程将会成功, 重进入意味着所有的请求都是基于"每线程", 而不是基于"每调用".
可重入的例子:
public class Supper{
	public synchronized void doSomething(){...}
}
public class Child extends Super{
	public synchronized void doSomething(){
		...
		super.doSomething();
	}
}

如果内部锁是不可重入的, 那么super.doSomething()将永远也无法得到Super的锁, 因为锁已经被子类占用了, 而可重入则可以避免这种死锁.

共享对象
同步同样还有一个重要而微妙的方面: 内存可见性. 我们不仅希望能够避免一个线程修改其他线程正在使用的对象的状态, 而且希望确保当一个线程修改了对象的状态之后, 他线程能够真正看到改变.

JVM允许将64位的读写划分两个32位的操作, 如果读写发生在不同的线程, 这样情况读取一个非volatile类型long就可能会出现得到一个值的高32位和另一个值的低32位.

锁不仅是关于同步与互斥的, 也是关于内存可见的, 为了保证所有线程都能看到共享的, 可变变量的最新值, 读取和写入线程必须使用公共的锁进行同步.

volatile变量相对于synchronized而言, 只是轻量级的同步机制.

从内存可见性的角度来看, 写入volatile变量就像退出同步块, 读取volatile变量就像进入同步块.

加锁可以保证可见性与原子性; volatile变量只保证可见性.

逸出
如果一个对象还没有完成构造就发布了, 这种情况就是逸出.

不要让this引用在构造期间逸出.

一个导致this引用在构造期间逸出的常见错误, 是在构造函数中启动一个线程, 在构造函数中创建线程并没有错误, 但是最好不要立即启动它, 而是通过一个start()或者init()方法来启动之.

ThreadLocal
一个使用线程共享的方式使用Connection的例子:
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){
	public Connection initialValue(){
		return DriverManager.getConnection(url);
	}
}  
public static Connection getConnection(){
	return connectionHolder.get();
}


线程首次调用ThreadLocal.get()方法时, 会请求initialValue()提供一个初始值.

可以将ThreadLocal<T>看着map<Thread, T>它存储了与线程相关的值.

并发容器
采用并发容器替换同步容器, 这种做法以很小的风险带来了可扩展性显著提交

CopyOnWriteArrayList
CopyOnWriteArrayList是List相应的同步实现, 在多数操作为读取操作时会提高性能

CopyOnWriteArrayList是同步List的一个并发替代品(还有一种是CopyOnWriteSet), 通常情况下它提供了更好的并发性, 并避免了在迭代期间对容器的加锁和复制.多个线程可以对该容器进行迭代, 并且不会受到另一个或者多个想要修改容器的线程带来的干涉, 迭代的时候返回的元素严格与创建的时候一致, 不会考虑后续的修改.

在每次CopyOnWriteArrayList改变时都需要对底层数组进行一次复制, 因此当容器比较大时, 不是很合适, 而当容器迭代操作的频率远远高于对容器修改的频率2, 写入即复制容器是一个合适的选择.

CooncurrentMap
ConcurrentMap接口加入了对常见复合操作的支持, 比如"缺少即加入(putIfAbsent)", 替换和条件删除, 而且这些操作都是原子操作

CooncurrentMap使用了一个更加细化的锁机制, 名叫分离锁. 这个机制允许更深层次的共享访问. 任意数量的读取线程可以并发的访问Map, 读者和写者也可以并发的访问, 并且有限数量的写线程还可以并发修改Map, 结果是为并发带来更高的吞吐量, 同时几乎没有损失单线程访问的性能.

ConcurrentMap返回的迭代器具有弱一致性, 而并非及时失败的, 弱一致性的迭代器可以容许并发的修改, 当迭代器被创建时, 它会遍历已有的元素, 并且可以(但不保证)感应到在迭代器被创建后对容器的修改.

只有当你的程序需要独占访问中加锁时, ConcurrentMap 无法胜任.

Queue
BlockingQueue提供了可阻塞的put和take方法, 他们与可定时的offer和poll是等价的. 如果Queue已经满了, put方法会被阻塞直到有空间可用; 如果queue是空的, 那么take方法会被阻塞, 直到有元素可用. queue的长度可以有限, 也可以无限.

可以使用BlockingQueue的offer方法来处理这样一种场景: 如果条目不能被加入到队列里, 它会返回一个失败状态. 这样可以创建更多灵活的策略来处理超负荷工作, 比如减轻负载, 序列化剩余工作条目并写入硬盘, 减少生产者线程, 或者其他方法儿子生产者线程.

SynchronousQueue维护了一个没有存储空间的queue, 如果用洗盘子来比喻的话, 可以认为没有盘子架, 直接将洗好的盘子放到烘干机中. 因为是直接移交, 这样可以减少数据在生产者和消费者移动的延迟.

因为SynchronousQueue没有存储能力, 所以除非另一个线程已经准备好参与移交工作, 否则put和take会一直阻止, 这类队列只有在消费者充足的时候比较合适, 他们总是为下一个任务做好准备.

生产者-消费者模式带来了一些性能方面的提高. 生产者和消费者可以并发地执行, 如果一个受限于I/O, 另一个受限于CPU, 那么并发执行的全部产出会高于顺序执行的产出. 如果生产者和消费在不同层面并行执行, 那么紧密耦合会减弱并行性, 减少并行化的活动.

Deque(BlockingDeque)是一个双端队列是对Queue和BlockingQueue的扩展, 允许高效的在头和尾分别进行插入和删除, 其实现有ArrayDeque和LinkedBlockingDeque.

双端队列采用的是一种窃取的工作模式, 其原理是每一个消费者都有一个自己的双端队列, 如果一个消费者完成了自己的双端队列中的全部工作, 它可以偷取其他消费者的双端队列中末尾的任务. 由于消费者不会共享同一个队列, 因此相对于传统的生产者-消费者模式具有更高的可伸缩性. 而且即使一个工作者要访问另一个队列, 也是从末尾截取, 这样可以进一步降低对队列的争夺.
这里我google一个关于Deque的例子, 一看就明白了:
class Producer implements Runnable {
    private String name;

    private BlockingDeque<Integer> deque;

    public Producer(String name, BlockingDeque<Integer> deque) {
        this.name = name;
        this.deque = deque;
    }

    public synchronized void run() {

        for (int i = 0; i < 10; i++) {
            try {
                deque.putFirst(i);
                System.out.println(name + " puts " + i);
                Thread.sleep(300);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

class Consumer implements Runnable {
    private String name;

    private BlockingDeque<Integer> deque;

    public Consumer(String name, BlockingDeque<Integer> deque) {
        this.name = name;
        this.deque = deque;
    }

    public synchronized void run() {
        for (int i = 0; i < 10; i++) {
            try {
                int j = deque.takeLast();
                System.out.println(name + " takes " + j);
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class BlockingDequeTester {
    public static void main(String[] args) {
        BlockingDeque<Integer> deque = new LinkedBlockingDeque<Integer>(5);
        Runnable producer = new Producer("Producer", deque);
        Runnable consumer = new Consumer("Consumer", deque);
        new Thread(producer).start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(consumer).start();
    }
}


Synchronizer(同步器)
Synchronizer包括semaphore, barrier, latch. 他们封装了状态, 而这些状态决定着线程执行到某一点时是通过还是被迫等待; 他们还提供操控状态的方法, 以及高效地等待Synchronizer进入到期望状态的方法.

Latch(闭锁)
一个闭锁工作起来就像一道大门: 直到闭锁达到终点状态之前, 门一直是关闭的, 没有线程通过, 在终点状态到来的时候, 门开了, 允许所有的线程通过. 一旦闭锁到达了终点状态, 它就不能再改变状态了, 所以它会永远保持敞开的状态.

CountDownLatch是闭锁的一个实现, 它的状态包括一个计数器, 初始化为一个正数, 用来表现需要等待的事件数. countDown方法对计数器做减操作, 表示一个事件已经发生了, 而await方法等待计数器达到零, 此时所有需要等待的时间都已经发生. 如果计数器入口时值为非零, await会一直阻塞知道计数器为零, 或者等待线程中断以及超时.

闭锁适用这样一种场景:需要计算在n个线程并发的情况下执行一个任务的时间, 如果我们简单的创建并启动线程, 那么先启动的就比后启动的具有领先优势, 并且根据活动线程数的增加或者减少, 这样的竞争度也在不断改变, 开始阀门能让控制线程同时释放所有工作线程, 结束阀门让控制线程能够等待最后一个线程完成任务.而不是顺序等待每一个线程结束.
举个例子就明白了:
public class TestHarness {
    public long timeTasks(int n, final Runnable task) throws Exception {
        final CountDownLatch startGate = new CountDownLatch(1);
        final CountDownLatch endGate = new CountDownLatch(n);
        for (int i = 0; i < n; i++) {
            Thread t = new Thread() {
                public void run() {
                    try {
                        startGate.await(); // 所有线程运行到此被暂停, 等待一起被执行
                        try {
                            task.run();
                        } finally {
                            endGate.countDown();
                        }
                    } catch (Exception e) {
                    }
                };
            };
            t.start();
        }

        long start = System.nanoTime();
        startGate.countDown(); // 启动所有被暂停的线程
        endGate.await(); // 等待所有线程执行完
        long end = System.nanoTime();
        return end - start;
    }

    public static void main(String[] args) {
        TestHarness th = new TestHarness();
        Runnable r = new Runnable() {
            public void run() {
                System.out.println("running");
            }
        };
        try {
            th.timeTasks(10, r);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }



FutureTask
FutureTask的计算是通过Callable实现的, 它等价于一个可以携带结果的Runnable, 并且有三个状态:等待, 运行和完成. 完成包括所有计算以及任意的方式结束, 包括正常结束, 取消和异常, 一旦FutureTask进入完成状态, 它会永远停止这个状态上.

FutureTask.get()的行为依赖于任务的状态, 如果它已经完成, get可以立即结果, 否则会被阻塞知道任务转入完成状态, 然后会返回结果或者抛出异常.

Executor框架利用FutureTask来完成异步任务, 并可以用来进行任何潜在的耗时计算, 而且可以在真正需要计算结果之前就启动他们开始计算.
一个通过FutureTask来实现的高性能的缓存:
public interface Computable<K, V> {
    V compute(K arg);
}

public class ConcurrentCache<K, V> implements Computable<K, V> {
    public ConcurrentHashMap<K, Future<V>> cache = new ConcurrentHashMap<K, Future<V>>();
    public Computable<K, V> c;

    public ConcurrentCache(Computable<K, V> c) {
        this.c = c;
    }

    public V compute(final K arg) {
        Future<V> f = cache.get(arg);
        if (f == null) {
            Callable<V> eval = new Callable<V>() {
                public V call() throws Exception {
                    System.out.println("begin compute...");
                    return c.compute(arg);
                }
            };
            FutureTask< V> ft = new FutureTask<V>(eval);
            f = cache.putIfAbsent(arg, ft); // 在并发的情况下也只添加一个future
            if (f == null) {
                f = ft;
                ft.run();
            }
        }
        try {
            return f.get();
        } catch (Exception e) {
            // 如果执行失败, 必须干掉future
            cache.remove(arg);
        }
        return null;
    }

    public static void main(String[] args) {
        Computable<String, Integer> c = new Computable<String, Integer>() {

            public Integer compute(String arg) {
                System.out.println("computint...");
                return Integer.valueOf(arg);
            }
        };
        final ConcurrentCache<String, Integer> cc = new ConcurrentCache<String, Integer>(c);
        new Thread() {
            @Override
            public void run() {
                cc.compute("111");
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                cc.compute("111");
            }
        }.start();
    }

}


Semaphore(信号量)
计数信号量用来控制能够同时访问某特定资源的活动的数量或者同时执行某一给定操作的数量. 技术信号量可以用来实现资源池或者给一个容器设定边界.

一个Semaphore管理一个有效的许可集, 许可的初始量通过构造函数传递给Semaphore, 活动能够获得许可, 并在使用之后释放许可, 如果已经没有可用的许可了, 那么acquire会被阻塞, 直到有可用的为止(或者直到被中断或者操作超时). release方法向信号量返回一个许可. 一个初始值为1的Semaphore可以用来充当mutex(互斥锁).
一个信号量的例子, 看了就明白了:
public class BoundedHashSet <T>{
    private final Set<T> set;
    private final Semaphore sem;

    public BoundedHashSet(int n) {
        set = Collections.synchronizedSet(new HashSet<T>());
        sem = new Semaphore(n);
    }
    
    public boolean add(T element) {
        try {
            sem.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        boolean result = false;
        try {
            result = set.add(element);
        }finally {
            sem.release();
        }
        return result;
    }
    
    public void remove(T o) {
        boolean result = set.remove(o);
        if (result) {
            sem.release();
        }
    }

    public static void main(String[] args) {
        final BoundedHashSet<String> bhs = new BoundedHashSet<String>(3);
        for (int i = 0; i < 4; i++) {
            Thread t = new Thread() {
                @Override
                public void run() {
                    bhs.add(System.currentTimeMillis() + "");
                };
            };
            t.start();
        }
    }
}


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

CyclicBarrier允许一个给定数量的成员多次集中在一个关卡点, 这在并行迭代算法中非常有用, 这个算法会把一个问题拆分成一系列相互独立的子问题, 当线程到达关卡点时, 调用await, await会被阻塞, 直到所有线程到达关卡点.

关卡通常用来模拟这种情况, 一个步骤的计算可以并行完成, 但是要求必须完成所有与一个步骤相关的工作后才能进入下一步.

Exchanger是关卡的另外一种形式, 它是一种两步关卡, 在关卡交汇点会叫唤数据, 当两方进行的活动不对称时, Exchanger是非常有用的, 比如当一个线程向缓冲写入一个数据, 这是另一个线程充当消费者使用这个数据.
一个关于使用CyclicBarrier的例子, 看了就明白了:
public class Cellular {
    private CyclicBarrier cb;
    private Worker[] workers;

    public Cellular() {
        int count = Runtime.getRuntime().availableProcessors();
        workers = new Worker[count];
        for (int i = 0; i < count; i++) {
            workers[i] = new Worker();
        }

        cb = new CyclicBarrier(count, new Runnable() {
                public void run() {
                System.out.println("the workers is all end...");
                }
                        });
    }

    public void start() {
        for (Worker worker : workers) {
            new Thread(worker).start();
        }
    }

    private class Worker implements Runnable {
        public void run() {
            System.out.println("working...");
            try {
                cb.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Cellular c = new Cellular();
        c.start();
    }
}


笔记2
笔记3
分享到:
评论
3 楼 micro123 2013-04-25  
<a href='www.baidu.com'>很喜欢,你很棒</a>
2 楼 ribavnu 2013-01-06  
想看这本书,一直没时间
1 楼 qingtian16265 2010-07-12  
总结的很好,多谢了~~~最近正在看这本书!

相关推荐

Global site tag (gtag.js) - Google Analytics