Java并发编程实战——基础知识3

5 基础构建模块

5.1 同步容器类

例如Vector和HashTable

5.1.1 同步容器类的问题

5.1.2 迭代器与ConcurrentModificationException

5.1.3 隐藏迭代器

如果状态与保护它的同步代码之间相隔越远,那么开发人员就越容易忘记在访问状态时使用正确的同步。

正如封装对象的状态有助于维持不变性条件一样,封装对象的同步机制同样有助于确保实施同步策略。

5.2 并发容器

并发容器代替同步容器,可以极大地提高伸缩性并降低风险。

5.2.1 ConcurrentHashMap

5.2.2 额外的原子Map操作

public interface ConcurrentMap<K, V> extends Map<K, V> {
	// 仅当K没有相应的映射值时才插入
	V putIfAbsent(K key, V value);

	// 仅当K被映射到V时才移除
	boolean remove(K key, V value);

	// 仅当K被映射到oldValue时才替换为newValue
	boolean replace(K key, V oldValue, V newValue);

	// 仅当K被映射到某个值时才替换为newValue
	V replace(K key, V newValue); 
}

5.2.3 CopyOnWriteArrayList

仅当迭代操作远远多于修改操作时,才应该使用CopyOnWrite的容器。这个准则很契合事件通知系统,在分发通知时需要迭代已注册的监听器列表,并调用每一个监听器,在大多数情况下,注册和注销事件监听器的操作远少于接收事件通知的操作。

5.3 阻塞队列和生产者-消费者模式

阻塞队列提供了可阻塞的put和take方法。如果队列已经满了,那么put方法将阻塞直到有空间可用;如果队列为空,那么take方法将会阻塞直到有元素可用。队列可以使有界的也可以是无界的,无界队列永远不会充满,因此无界队列上的put方法永远不会阻塞。

在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具:它们能抑制并防止产生过多的工作项,使应用程序在负荷过载的情况下变得更加健壮。

一个特殊的BlockingQueue是SynchronousQueue,SynchronousQueue实际上并不是一个真正的队列,因为它不会为队列中的元素维护存储空间。SynchronousQueue维护一组线程,这些线程在等待着把元素加入或者移出队列。因为SynchronousQueue没有存储功能,因此put和take会一直阻塞,直到有另一个线程已经准备好参与到交付过程中。仅当有足够多的消费者,并且总是有一个消费者准备好获取交付的工作时,才适合使用同步队列。

5.3.1 示例:桌面搜索


import java.io.File;
import java.io.FileFilter;
import java.util.concurrent.BlockingQueue;

/**
 * 生产者,在某个文件层次中搜索符合索引标准的文件,并将文件放入工作队列
 */
public class FileCrawler implements Runnable {
    private final BlockingQueue<File> fileQueue;
    private final FileFilter fileFilter;
    private File root;

    public FileCrawler(BlockingQueue fileQueue, FileFilter fileFilter, File root) {
        this.fileQueue = fileQueue;
        this.fileFilter = fileFilter;
        this.root = root;
    }

    @Override
    public void run() {
        try {
            crawl(root);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    private void crawl(File root) throws InterruptedException {
        File[] entries = root.listFiles(fileFilter);
        if (entries != null) {
            for (File entry : entries) {
                if (entry.isDirectory()) {
                    crawl(entry);
                } else if (!alreadyIndexed(entry)) {
                    fileQueue.put(entry);
                }
            }
        }

    }
}

import java.io.File;
import java.util.concurrent.BlockingQueue;

/**
 * 消费者。从队列中取出文件并建立索引。
 */
public class Indexer implements Runnable {
    private final BlockingQueue<File> queue;

    public Indexer(BlockingQueue<File> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            while (true) {
                indexFile(queue.take());
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
import java.io.File;
import java.io.FileFilter;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * 启动桌面搜索
 */
public class Client {
    public static void main(String[] args) {

    }

    public static void startIndexing(File[] roots) {
        BlockingQueue<File> queue = new LinkedBlockingQueue<>(BOUND);
        FileFilter fileFilter = new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return true;
            }
        };

        for (File root : roots) {
            new Thread(new FileCrawler(queue, fileFilter, root)).start();
        }

        for (int i = 0; i < N_CONSUMERS; i++) {
            new Thread(new Indexer(queue)).start();
        }
    }
}

5.3.2 串行线程封闭

5.3.3 双端队列与工作密取

双端队列适用于工作密取(Work Stealing)模式。

工作密取适用于既是消费者也是生产者的问题——当执行某个工作时导致出现更多的工作。

5.4 阻塞方法与中断方法

当在代码中调用了一个会抛出InterruptedException的方法时,这个调用程序就成为了阻塞方法,并且必须要处理对中断的响应。对于库代码来说,有两种基本选择:
传递InterruptedException。 把这个异常传递给方法的调用者通常是最明智的选择。传递InterruptedException的方法包括:根本不捕获该异常,或者捕获该异常,然后在执行某种简单的清理工作后再次抛出这个异常。
恢复中断。 有时候不能抛出InterruptedException,例如代码是Runnable的一部分。在这种情况下就必须要捕获InterruptedException,并通过调用当前线程上的interrupt方法恢复中断状态,这样在调用栈中更高层的代码将看到引发了一个中断。
恢复中断不是“从中断中恢复”的意思,而是再次将线程的中断状态设置为true。参考:
https://blog.csdn.net/u014745069/article/details/87889222

程序清单 5-4-1 恢复中断状态以避免屏蔽中断

public class TaskRunnable implements Runnable {
	BlockingQueue<Task> queue;

	public void run() {
		try {
			processTask(queue.take());
		} catch (InterruptedException e) {
			// 恢复被中断的状态
			Thread.currentThread().interrupt();
		}
	}
}

切忌捕获了中断异常但是不作出任何处理。这将使调用栈上更高层的代码无法对中断采取处理措施,因为线程被中断的证据已经丢失。

5.5 同步工具类

同步工具类可以是任何一个对象,只要它根据其自身的状态来协调线程的控制流。常见的同步工具类有:阻塞队列、信号量(Semaphore)、栅栏(Barrier)和闭锁(Latch)。

所有的同步工具类都包含一些特定的结构化属性:它们封装了一些状态,这些状态决定执行同步工具类的线程是继续执行还是等待。 此外还提供了一些方法对状态进行操作,以及另一些方法来高效地等待同步工具类进入到预期状态。

5.5.1 闭锁

CountDownLatch是一种灵活的闭锁实现。
程序清单 5-5-1-1 在计时测试中使用CountDownLatch启动和停止线程

public class TestHarness {
	public long timeTask(int nThreads, final Runnable task) throws InteruptedException {
		final CountDownLatch startGate = new CountDownLatch(1);
		final CountDownLatch endGate = new CountDownLatch(nThreads);

		for (int i = 0; i < nThreads; i++) {
			Thread t = new Thread() {
				public void run() {
					try {
						startGate.await();
						try {
							task.run();
						} finally {
							endGate.countDown();
						}
					} catch(InterruptedException e) {
						
					}
					
				}
			};
			t.start();
		}

		long start = System.nanoTime();
		startGate.countDown();
		endGate.await();
		long end = System.nanoTime();
		return end - start;
	}
}

5.5.2 FutureTask

public class Preloader {
    private final FutureTask<ProductInfo> future = new FutureTask<ProductInfo>(new Callable<ProductInfo>() {
        @Override
        public ProductInfo call() throws Exception {
            return loadProductInfo();
        }
    });

    private final Thread thread = new Thread(future);

    public void start() {
        thread.start();
    }

    public ProductInfo get() {
        try {
            return future.get();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            Throwable cause = e.getCause();
            if (cause instanceof DataLoadException) {
                throw (DataLoadException) cause;
            } else {
                throw new RuntimeException(e);
            }
        }
    }
}

5.5.3 信号量

程序清单 5-5-3-1 使用Semaphore为容器设置边界

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Semaphore;

public class BoundedHashSet<T> {
    private final Set<T> set;
    private final Semaphore semaphore;

    public BoundedHashSet(int bound) {
        this.set = Collections.synchronizedSet(new HashSet<>());
        semaphore = new Semaphore(bound);
    }

    public boolean add(T o) throws InterruptedException {
        semaphore.acquire();
        boolean wasAdded = false;
        try {
            wasAdded = set.add(o);
            return wasAdded;
        } finally {
            if (!wasAdded) {
                semaphore.release();
            }
        }
    }
    
    public boolean remove(Object o) {
        boolean wasRemoved = set.remove(o);
        if (wasRemoved) {
            semaphore.release();
        }
        return wasRemoved;
    }
}

5.5.4 栅栏

程序清单 5-5-4-1 通过CyclicBarrier协调细胞自动衍生系统中的计算

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

/**
 * 工作线程都为各自子问题中的所有细胞计算新值。当所有的工作线程都到达栅栏时,
 * 栅栏会把这些新值提交给数据模型。在栅栏的操作执行完以后,工作线程将开始下
 * 一步的计算。
 */
public class CellularAutomata {
    private final Board mainBoard;
    private final CyclicBarrier barrier;
    private final Worker[] workers;

    public CellularAutomata(Board board) {
        this.mainBoard = board;
        int count = Runtime.getRuntime().availableProcessors();
        this.barrier = new CyclicBarrier(count, new Runnable() {
            @Override
            public void run() {
                mainBoard.commitNewValues();
            }
        });
        this.workers = new Worker[count];
        for (int i = 0; i < count; i++) {
            workers[i] = new Worker(mainBoard.getSubBoard(count, i));
        }
    }

    public void start() {
        for (int i = 0; i < workers.length; i++) {
            new Thread(workers[i]).start();
        }

        mainBoard.waitForConvergence();
    }

    private class Worker implements Runnable {
        private final Board board;

        public Worker(Board board) {
            this.board = board;
        }

        @Override
        public void run() {
            while(!board.hasConverged()) {
                for (int x = 0; x < board.getMaxX(); x++) {
                    for (int y = 0; y < board.getMaxY(); y++) {
                        board.setNewValue(x, y, computeValue(x, y));
                    }
                }

                try {
                    barrier.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } catch (BrokenBarrierException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

5.6 构建高效可伸缩的结果缓存

程序清单 5-6-1 缓存的最终实现

public interface Computable<A, V> {
    V compute(A arg) throws InterruptedException;
}
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

public class Memoizer<A, V> implements Computable<A, V> {
    private final ConcurrentMap<A, Future<V>> cache = new ConcurrentHashMap<>();
    private final Computable c;

    public Memoizer(Computable<A, V> c) {
        this.c = c;
    }

    @Override
    public V compute(final A arg) throws InterruptedException {
        while (true) {
            Future<V> f = cache.get(arg);
            if (f == null) {
                Callable<V> eval = new Callable<V>() {
                    @Override
                    public V call() throws Exception {
                        return c.compute(arg);
                    }
                };
                FutureTask<V> ft = new FutureTask<>(eval);
                f = cache.putIfAbsent(arg, ft);
                if (f == null) {
                    f = ft;
                    ft.run();
                }
            }

            try {
                return f.get();
            } catch (ExecutionException e) {
                throw launcherThrowable(e.getCause());
            }
        }
    }
}
import java.math.BigInteger;

@TreadSafe
public class Factorizer implements Servlet {
    private final Computable<BigInteger, BigInteger[]> c = new Computable<BigInteger, BigInteger[]>() {
        @Override
        public BigInteger[] compute(BigInteger arg) throws InterruptedException {
            return factor(arg);
        }
    };

    private final Computable<BigInteger, BigInteger[]> cache = new Memoizer<>(c);

    public void service(ServletRequest req, ServletResponse resp) {
        try {
            BigInteger i = extracFromRequest(req);
            encodeToResponse(resp, cache.compute(i));
        } catch (InterruptedException e) {
            encodeError(resp, "factorization interrupted.");
        }
    }
}

基础知识小结

  • 可变状态是至关重要的
    所有的并发问题都可以归结为如何协调对并发状态的访问。可变状态越少,就越容易确保线程安全性。
  • 尽量将域声明为final类型,除非需要它们是可变的。
  • 不可变对象一定是线程安全的。
    不可变对象能极大地降低并发编程复杂性。它们更为简单而且安全,可以任意共享而无需使用加锁或者保护性复制等机制。
  • 封装有助于管理复杂性。
    在编写线程安全的程序时,虽然可以将所有的数据都保存在全局变量中,但是为什么要这么做?将数据封装在对象中,更易于维持不变性条件:将同步机制封装在对象中,更易于遵循同步策略。
  • 用锁来保护每个可变变量。
  • 当保护同一个不变性条件中的所有变量时,要使用同一个锁。
  • 在执行复合操作期间,要持有锁。
  • 如果从多个线程中访问同一个可变变量时没有同步机制,那么程序会出现问题。
  • 不要故作聪明地推断不需要使用同步
  • 在设计过程中考虑线程安全,或者在文档中明确地指出它不是线程安全的。
  • 将同步策略文档化。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值