Java核心技术 并发2

同步阻塞

每个Java对象有一个锁。线程可以通过同步方法获得锁。还有通过进入一个同步阻塞获得锁。当线程进入如下形式的阻塞:

// 这是同步块的语法
synchronized (obj) {}

于是它获得obj的锁。
有时会发现“特殊的”锁:

public class Bank {
	...
	private Object lock = new Object();
	...
	public void transfer(int from, int to, int amount) {
		synchronized(lock){ //临时锁
			accounts[from] -= amount;
			accounts[to] += amount;
		}
	}
	...
}

在此,lock对象被创建仅仅是用来使用每个Java对象持有的锁。

有时使用一个对象锁来实现额外的原子操作,实际上称为客户端锁定(client-side locking)。Vector< Double >存储银行余额,它的方法是同步的:

public void transfer(Vector<Double> accounts, int from, int to, int amount) {
	accounts.set(from, accounts.get(from) - amount);
	accounts.set(to, accounts.get(to) + amount);
	System.out.println(...);
}

Vector类的get和set方法都是同步的,但并没有什么帮助。第一次对get调用完成之后,完全可能在transfer方法中被剥夺运行权。
可以截获这个锁:

public void transfer(Vector<Double> accounts, int from, int to, int amount) {
	synchronized(accounts) {
		accounts.set(from, accounts.get(from) - amount);
		accounts.set(to, accounts.get(to) + amount);
		System.out.println(...);
	}
}

这个方法可以工作,但完全依赖于Vector类对自己的所有可修改方法都是用内部锁(Vector类的文档没有给出这样的承诺)。客户端锁定是非常脆弱的,不推荐使用。

监视器概念

监视器(monitor)不需要考虑如何加锁的情况下,就可以保证多线程的安全性。
用Java术语来讲,监视器具有以下特性:
1.监视器是只包含私有域的类
2.每个监视器类的对象有一个相关的锁
3.使用该锁对所有的方法进行加锁
4.该锁可以有任意多个相关条件
Java设计者以不是很精确的方式采用了监视器概念,Java中的每一个对象有一个内部的锁和内部的条件。如果一个方法用synchronized关键字声明,那么它表现的就像一个监视器方法。通过调用wait/notify/notifyAll来访问条件变量。
在3个方面Java对象不同于监视器,从而使得线程安全性下降:
1.域不要求必须是private
2.方法不要求必须是synchronized
3.内部锁对客户是可用的

Volatile域

volatile关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。
假定一个布尔标记done,它的值被一个线程设置却别另一个线程查询,可以使用锁:

private boolean done;
public synchronized boolean isDone() {return done;}
public synchronized void setDone() {done = true;}

使用内部锁,如果另一个线程已经对该对象加锁,isDone和setDone方法可能阻塞。如果注意到这个方面,一个线程可以为这一变量使用独立的Lock。但是这也会带来很多麻烦。
可以将域声明为bolatile:

private volatile boolean done;
public boolean isDone() {return done;}
public void setDone() {done = true;}

volatile不能提供原子性,例如done = !done,不能确保翻转域中的值,不能保证读取、翻转和写入不被中断。

final变量

除非使用锁或volatile修饰符,否则无法从多个线程安全地读取一个域。
还有一种情况可以安全的访问共享域,即这个域声明为final:

final Map<String, Double> accounts = new HashMap<>();

其他线程会在构造函数完成之后才看到这个accounts变量。
当然,对这个映射表的操作并不是线程安全的,如果多个线程在读写这个映射表,仍然需要进行同步。

原子性

对共享数据除了赋值之外并不完成其他操作,可以将这些共享变量声明为volatile。
java.util.concurrent.atomic包中有很多类使用了很高效的机器级指令(不是锁)来保证其他操作的原子性。
AtomicInteger类提供了incrementAndGet和decrementAndGet,它们分别以原子方式将一个整数自增或自减。
incrementAndGet以原子方式将AtomicLong自增,并返回自增后的值。获得值、增1并设置然后生成新值的操作不会中断。可以保证即使是多个线程并发访问同一个实例,也会计算并返回争取的值。
如果希望完成更复杂的更新,就必须使用compareAndSet,例如希望跟踪不同线程观察的最大值:

// 不可行的
public static AtomicLong largest = new AtomicLong();
largest.set(Math.max(largest.get(). observed));
// 上述更新不是原子的,应当在一个循环中计算新值和使用comapreAndSet
do {
	oldValue = largest.get();
	newValue = Math.max(oldValue, observed);
} while (!largest.compareAndSet(oldValue, newValue));

如果另一个线程也在更新largest,就可能阻止这个线程更新。compareAndSet会返回false,而不会设置新值。循环会更次尝试,读取更新后的值,并尝试修改。最终它会成功地用新值替换原来的值。听起来有些麻烦,不过compareAndSet会映射到一个处理器操作,比使用锁速度更快。
Java SE 8中,不再需要编写这样的循环代码,可以提供一个lambda表达式:

largest.updateAndGet(x -> Math.max(x, observed));
// 或
largset.accumulateAndGet(observed, Math::max);

accumulateAndGet方法利用一个二元操作符来合并原子值和所提供的参数。还有getAndUpdate和getAndAccumulate方法可以返回原值。
类AtomicInteger、AtomicIntegerArray、AtomicIntegerFieldUpdater、AtomicLongArray、AtomicLongFieldUpdater、AtomicReference、AtomicReferenceArray和AtomicReferenceFieldUpdater也提供这些方法。
如果有大量线程要访问原子值,性能会大幅下降,因为乐观更细需要太多次尝试。Java SE 8提供了LongAdder和LongAccumulator类来解决问题。
LongAdder包含多个变量(加数),总和为当前值。可以有多个线程更新不同的加数,线程个数增加时会自动提供新的加数。通常只有当所有工作完成之后才需要总和的值。
如果认为可能存在大量竞争,只需要使用LongAdder而不是AtomicLong。方法名稍有区别,调用increment让计数器自增,或者调用add增加一个量,或调用sum获取总和。

final LongAdder adder = new LongAdder();
for (...){
	pool.submit(() -> {
		while(...) {
			...
			if(...) adder.increment();
		}
	});
}
...
long total = adder.sum();

LongAccumolator将这种思想推广到任意的累加操作。在构造器中,可以提供这个操作以及它的零元素。要加入新值,调用accumulate。调用get来获得当前值:

LongAccumulator adder = new LongAccumulator(Long::sum, 0);
adder.accumulate(value);

在内部,这个累加器包含变量a1、a2…an,每个变量初始化为零元素。
调用accumulate并提供值v时,其中一个变量会以原子方式更新为ai=ai op v,op是中缀形式的累加操作。
get的结果是a1 op a2 op … an。如果选择一个不同的操作,可以计算最小值和最大值。这个操作必须满足结合律和交换律,这说明,最终结果必须独立于所结合的中间值的顺序。
DoubleAdder和DoubleAccumulator也有类似方式。

死锁

Java编程语言没有任何东西可以避免或打破死锁现象,必须仔细设计程序,以确保不会出现死锁。

线程局部变量

有时要避免共享变量,使用ThreadLocal辅助类为各个线程提供各自的实例。
SimpleDateFormat类不是线程安全的:

public static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
String dateStamp = dateFormat.format(new Date());

两个线程都执行,结果可能很混乱,dateFormat使用的内部数据结构可能会被并发的访问所破坏。当然可以使用同步,但开销很大。
为每个线程构造一个实例:

public static final ThreadLocal<SimpleDateFormat> dateFormat =
                ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
String dateStamp = dateFormat.get().format(new Date());

在一个给定线程中首次调用get时,会调用initialValue方法,在此之后,get方法会返回属于当前线程的那个实例。
在多个线程生成随机数也存在类似问题。java.util.Random类是线程安全的,但如果多个线程需要等待一个共享的随机数生成器会很低效。可以使用ThreadLocal为各个线程提供单独的生成器:

int random = ThreadLocalRandom.current().nextInt(upperBound);

ThreadLocalRandom.current()调用会返回特定于当前线程的Random类实例。

锁测试与超时

线程在调用lock方法来获得另一个线程所持有的锁的时候,很可能阻塞。tryLock方法试图申请一个锁,在成功获得锁后返回true,否则立即返回false,而且线程可以立即离开去做任何其他事情。

if (myLock.tryLock()) {
	try{...}
	finally{myLock.unlock();}
} else {
	...
}

可以调用tryLock,使用超时参数myLock.tryLock(100, TimeUnit.MILLISENCONDS),TimeUnit是一个枚举类型,可以取的值包括SECONDS、MILLISECONDS、MICROSECONDS和NANOSECONDS。
lock方法不能被中断。如果一个线程在等待获得一个锁时被中断,中断程序在获得锁之前一直处于阻塞状态,如果出现死锁,lock将无法终止。
带有超时参数的tryLock如果在线程等待期间被中断,将抛出InterruptedException,这是一个非常有用的特性,因为运行程序打破死锁。
lockInterruptibly方法等同于超时为无限的tryLock。
在等待一个条件时,也可以提供超时:

myCondition.await(100, TimeUnit.MILLISECONDS);

如果一个线程被siganlAll或signal激活,或者超时时限已达到,或者线程被中断,那么await方法将返回。
如果等待的线程被中断,await方法将抛出InterruptedException异常。如果希望出现这种情况时线程继续等待,可以使用awaitUninterruptibly替代await。

读/写锁

java.util.concurrent.locks定义的另一个锁类,ReentrantReadWriteLock类。如果很多线程从一个数据结构读取数据而很少线程修改的话,十分有用。在这种情况下,允许读者线程共享访问是合适的。但写者线程必须是互斥访问:

// 构造一个ReentrantReadWriteLock对象
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
// 抽取读锁和写锁
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();
// 所有获取方法加读锁
public double getTotalBalance() {
    readLock.lock();
    try {
        ...
    } finally {
        readLock.unlock();
    }
}
// 所有修改方法加写锁
public void transfer(...) {
    writeLock.lock();
    try {
        ...
    } finally {
        writeLock.unlock();
    }
}

为什么弃用stop和suspend方法

stop方法用来终止一个线程;
suspend方法用来阻塞一个线程,直至另一个线程调用resume。共同点:都试图控制一个给定线程的行为。
stop、suspend、resume方法已经弃用。stop方法天生不安全,经验证明suspend方法会经常导致死锁。

stop方法终结所有未结束的方法,包括run方法。当线程被终止,立即释放被它锁住的所有对象的锁。这会导致对象处于不一致状态。一个账户想另一个账户转账过程中被终止,钱款已转出,却没有转入目标账户。
当线程要终止另一个线程时,无法知道什么时候调用stop方法是安全的,因此该方法被弃用了。

suspend挂起一个持有锁的线程,那么该锁在恢复之前是不可用的。如果调用suspend方法的线程试图获得同一个锁,那么程序死锁:被挂起的线程等着被恢复,而将挂起的线程等待获得锁。

6.阻塞队列

对于实际编程,应该尽可能远离Java并发程序设计基础的底层构建块。使用由并发处理的专业人士实现的较高层次的结构要方便得多,安全得多。
对于许多线程问题,通过使用一个或多个队列以优雅且安全的方式将其形式化。生产者线程向队列插入元素,消费者线程则取出它们。使用线程可以安全地从一个线程向另一个线程传递数据。
银行转账程序,转账线程将转账指令对象插入一个队列中,而不是直接访问银行对象。另一个线程从队列中取出指令执行转账。只有该线程可以访问银行对象内部。因此不需要同步。
当试图向队列添加元素而队列已满,或想从队列移除元素而队列为空时,阻塞队列(blocking queue)导致线程阻塞。在协调多个线程之间的合作时,工作者线程可以周期性的将中间结果存储在阻塞队列中。其他的工作者线程移除中间结果并进一步加以修改。队列会自动地平衡负载。如果第一个线程集运行的比第二个慢,第二个线程集在等待结果时会阻塞。如果第一个线程集运行得快,它将等待第二个队列集赶上来。
在这里插入图片描述
阻塞队列方法分为以下3类:
1.将队列当作线程管理工具使用,将要用到put和take方法
2.向满的队列中添加或从空的队列中移除元素时,add、remove和element操作抛出异常
3.一个多线程程序中,队列会在任何时候空或满,一定要使用offer、poll和peek方法替代,这些方法如果不能完成任务,这会给出错误提示而不会抛出异常
还有带有超时offer方法和poll方法的变体:

boolean success = q.offer(x, 100, TimeUnit.MILLISECONDS);

尝试在100毫秒的时间内在队列的尾部插入一个元素。如果成功返回true;否则,达到超时时,返回false。类似地q.poll(100, TimeUnit.MILLISECONDS)。
java.util.concurrent包提供了阻塞队列的几个变种:
LinkedBlockingQueue在默认情况下容量是没有上边界的,也可以选择指定最大容量;
LinkedBlockingDeque是一个上端版本;
ArrayBlockingQueue在构造时需要指定容量,并且有一个可选的参数来指定是否需要公平性。若设置了公平参数,则那么等待了最长时间的线程会优先得到处理(通常公平性会降低性能)。
PriorityBlockingQueue是一个带优先级的队列,而不是先进先出队列。元素按照优先级顺序被移除。该队列没有容量上限。但如果队列为空,取元素的操作会阻塞。
DelayQueue包含实现Delayed接口的对象:

interface Delayed extends Comparable<Delayed>{
	long getDelay(TimeUnit unit);
}

getDelay方法返回对象的残留延迟。负值表示延迟已经结束。元素只能在延迟用完的情况下才能从DelayQueue移除。还必须实现comparaTo方法。DelayQueue使用该方法对元素进行排序。
Java SE 7增加了一个TransferQueue接口,允许生产者线程等待,知道消费者准备就绪可以接收一个元素。如果生产者调用q.transfer(item),这个调用会阻塞,直到另一个线程将元素item删除。LinkedTransferQueue类实现了这个接口。

public class BlockingQueueTest {
    private static final int EILE_QUEUE_SIZE = 10;
    private static final int SEARCH_THREADS = 100;
    private static final File DUMMY = new File("");
    private static BlockingQueue<File> queue = new ArrayBlockingQueue<>(EILE_QUEUE_SIZE);
    public static void main(String[] args) {
        try (Scanner in = new Scanner(System.in)) {
            System.out.print("Enter base directory (e.g. /opt/jdk1.8.0/src):");
            String directory = in.nextLine();
            System.out.print("Enter keyword (e.g. volatile):");
            String keyword = in.nextLine();
            Runnable enumerator = () -> {
                try {
                    enumerate(new File(directory));
                    queue.put(DUMMY);
                }catch (InterruptedException e){}
            };
            new Thread(enumerator).start();
            for (int i = 1; i <= SEARCH_THREADS; i++) {
                Runnable searcher = () -> {
                    try {
                        boolean done = false;
                        while (!done) {
                            File file = queue.take();
                            if (file == DUMMY) {
                                queue.put(file);
                                done = true;
                            } else {
                                search(file, keyword);
                            }
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (InterruptedException e) {}
                };
                new Thread(searcher).start();
            }
        }
    }
    public static void enumerate(File directory) throws InterruptedException {
        File[] files = directory.listFiles();
        for (File file : files) {
            if (file.isDirectory()) {
                enumerate(file);
            } else {
                queue.put(file);
            }
        }
    }
    public static void search(File file, String keyword) throws IOException {
        try (Scanner in = new Scanner(file, "UTF-8")) {
            int lineNumber = 0;
            while (in.hasNextLine()) {
                lineNumber++;
                String line = in.nextLine();
                if (line.contains(keyword)) {
                    System.out.printf("%s:%d:%s%n", file.getPath(), lineNumber, line);
                }
            }
        }
    }
}

生产者线程枚举在所有子目录下的所有文件并把它们放到一个阻塞队列中。
启动大量搜索线程。每个线程从队列中取一个文件,打开它,打印所有包含该关键字的行,然后取出下一个文件。
为了发出完成信号,枚举线程放置一个虚拟对象到队列中(向行礼输送带上当一个写着最后一个包的虚拟包)。当搜索线程取到这个虚拟对象时,将其放回并终止。
不需要显示的线程同步,这个程序使用队列数据结构作为一种同步机制。

线程安全的集合

高效的映射、集和队列

java.util.concurrent包提供了映射、有序集和队列的高效实现:concurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和ConcurrentLinkedQueue。
这些集合使用了复杂的算法,通过允许并发地访问数据结构的不同部分来使竞争极小化。
size通常不必在常量时间内操作,确定集合当前大小通常需要遍历。有些映射过于庞大(20亿),JavaSE8引入了mappingCount方法可以把大小做为long返回。

集合返回弱一致性(weakly consistent)的迭代器,意味着迭代器不一定能反映出它们被构造之后的所有修改,但它们不会将同一个值返回两次,也不会抛出ConcurrentModificationException异常(java.util包的迭代器会抛出)。
并发的散列映射表,可高效地支持大量读者和一定量写者。默认情况下,可以有多达16个写者线程同时执行。可以有更多的写者线程,但如果多于16个,其他线程将暂时被阻塞,可以指定更大数目的构造器,然而没必要。

映射条目的原子更新

ConcurrentHashMap原来的版本没有很多方法实现原子更新,举例多个线程会遇到单词,想统计它们的频率:
传统做法是使用replace操作,它会以原子方法用一个新值替换原值,前提是之前没有其他线程把原值替换为其他值,必须一直这么做,知道replace成功:

do {
	oldValue = map.get(word);
	newValue = oldValue == null ? 1 : oldValue + 1;
} while(!map.relace(word, oldValue, newValue));

或使用ConcurrentHashMap< String, AtomicLong >,或者Java SE8可以使用ConcurrentHashMap< String, LongAdder >:

map.putIfAbsent(word, new LongAdder());
map.get(word).increment();

Java SE 8提供了compute方法,可以提供一个键和一个计算新值的函数。这个函数接收键和相关联的值,会计算新值:

map.compute(word, (k, v) -> v ==null ? 1 : v + 1);

ConcurrentHashMap不允许有null值,很多方法都是用null来指示映射中某个给定的键不存在。
另外还有computeIfPresent和computeIfAbsent方法,分别只在已有原值情况下计算新值,或者只有没有原值情况下计算新值。

map.computeIfAbsent(word, k -> new LongAdder()).increment();

首次添加一个键时通常需要特殊处理,利用merge方法可以方便的处理,这个方法有一个参数表示键不存在时使用的初始值。否则,就会调用提供的函数来结合原值与初始值(compute不处理键):

map.merge(word, 1L, (existingValue, newValue) -> existingValue + newValue);
// 或
map.merge(word, 1L, Long::sum);

对并发散列映射的批操作

Java SE 8为并发散列映射提供了批操作,即使有其他线程在处理映射,也能安全地执行。
有3种不同的操作:
1.搜索(search)为每个键或值提供一个函数,直到函数生成一个非null的结果。然后搜索终止,返回这个函数的结果。
2.归约(reduce)组合所有键或值,这里使用所提供的一个累加函数
3.forEach为所有键或值提供一个函数
每个操作都有4个版本:
1.operationKeys:处理键
2.operationValues:处理值
3.operation:处理键和值
4.operationEntries:处理Map.Entry对象
需要指定一个参数化阈值(parallelism threshold)。如果包含的元素多于这个阈值,就会并行完成。如果希望批操作在一个线程中运行,可以使用阈值Long.MAX_VALUE。如果希望尽可能多的线程运行,阈值可以为1。
search的方法:
U searchKeys(long threshold, BiFunction< ? super K, ? extends U > f)
U searchValues(long threshold, BiFunction< ? super K, ? extends U > f)
U search(long threshold, BiFunction< ? super K, ? extends U > f)
U searchEntries(long threshold, BiFunction< ? super K, ? extends U > f)

希望找出一个第一次出现次数超过1000次的单词。需要搜索键和值:

String result = map.search(threshold, (k, v) -> v > 1000 ? k : null);

forEach方法有两种形式:

// 1.为各个映射条目提供一个消费者函数
map.forEach(threshold, (k, v) -> System.out.println(k + " -> " + v));
// 2.有一个转换器函数,要先提供,其结果会传递到消费者
map.forEach(threshold, (k, v) -> k + " -> " + v, System.out::println);

转换器可以用作为一个过滤器,只要转换器返回null,这个值就会被悄无声息地跳过。例如,下面只打印有大值的条目:

map.forEach(threshold, (k, v) -> v > 1000 ? k + " -> " + v : null, System.out::println);

reduce操作用一个累加函数组合其输入。例如,可以计算所有值的总和:

Long sum = map.reduceValues(threshold, Long::sum);
// 与forEach类似,也可以提供一个转换器函数,计算最长的键的长度:
Integer maxlength = map.reduceKeys(threshold, String::length, Integer::max);

转换器可以作为一个过滤器,通过返回null来排除不想要的输入。
统计多少个条目的值>1000:

Long count = map.reduceValues(threshold, v -> v > 1000 ? 1L : null, Long::sum);

对于int、long、double输出还有特殊化操作,分别有后缀ToInt、ToLong和ToDouble。需要把输入转换为一个基本类型值,并指定一个默认值和一个累加器函数。映射为空时返回默认值。

long sum = map.reduceValuesToLong(threshold, Long::longValue, 0, Long::sum);

并发集视图

并没有一个线程安全的集,ConcurrentHashMap的静态newKeySet方法会生成一个Set< K >,实际上是ConcurrentHashMap< K, Boolean>的包装器(所有的映射值都是Boolean.TRUE)。

Set<String> words = ConcurrentHashMap.<String>newKeySet();

如果删除这个集的元素,这个键会从映射中删除。不能向键集增加元素,因为没有相应的值可以增加。
Java SE 8增加了第二个keySet方法,包含一个默认值,可以在为集增加元素时使用:

Set<String> words = map.keySet(1L);
words.add("Java");
// 如果Java在words中不存在,现在它会有一个值1

写数组的拷贝

CopyOnWriteArrayList和CopyOnWriteArraySet是线程安全的集合,其中所有的修改线程对底层数组进行复制。如果在集合上进行迭代的线程数超过修改线程数,这样的安排是有用的。当构建一个迭代器的时候,它包含一个对当前数组的引用。如果数组后来被修改了,迭代器仍然引用旧数组,但是集合的数组已经被替换了。因而,旧的迭代器拥有一致的视图,访问它无须任何同步开销。

并行数组算法

Arrays提供了大量并行化操作。静态Arrays.parallelSort方法可以对一个基本类型或对象的数组进行排序:

String contents = new String(Files.readAllBytes(Paths.get("alice.txt")), StandardCharsets.UTF_8);
String[] words = contents.split("[\\P{L}]+");
Arrays.parallelSort(words);
// 对对象排序时,可以提供Comparator
Arrays.parallelSort(words, Comparator.comparing(String::length));
// 对于所有方法都可以提供一个范围的边界
values.parallelSort(values.length / 2, values.length);

API设计者希望通过parallel方法名指出排序是并行化的,用户就会注意避免使用有副作用的比较器。

parallelSetAll方法会用由一个函数计算得到的值填充另一个数组。这个函数接收元素索引,然后计算相应位置上的值:

Arrays.parallelSetAll(values, i -> i % 10);
// 0 1 2 3 4 5 6 7 8 9 0 1 2...

parallelPrefix方法,会用对应一个给定结合操作的前缀的累加结果替换各个数组元素。考虑数组[1,2,3,4,…]和×操作。

Arrays.parallelPrefix(values, (x, y) -> x * y);
// 函数包含
[1, 1*2, 1*2*3, 1*2*3*4,...]

可能很奇怪,不过这个计算确实可以并行化。可以在不同的数组区中并行完成计算。log(n)步之后完成,如果有足够多的处理器,这会远远胜过直接的线性计算。

较早的线程安全集合

Vector和Hashtable类是Java初始版本提供的线程安全的动态数组和散列表,这些类已经被弃用。取而代之的是ArrayList和HashMap类,这些类不是线程安全的,而集合库提供了不同的机制。任何集合类都可以通过同步包装器(synchronization wrapper)变成线程安全的:

List<E> synchArrayList = Collections.synchronizedList(new ArrayList<E>);
Map<K, V> synchHashMap = Collections.synchronizedMap(new HashMap<K, V>);

结果集合的方法使用锁加以保护,提供了线程安全的访问。
应该确保没有任何线程通过原始的非同步方法访问数据结构。最便利的方法是确保不保存任何执行原始对象的引用,简单地构造一个集合并立即传递给包装器。
如果在另一个线程可能进行修改时要对集合进行迭代,仍然需要使用客户端锁定:

synchronized(synchHashMap) {
	Iterator<K> iter = synchHashMap.keySet().iterator();
	while(iter.hasNext()) {...}
}

如果使用for each循环必须使用同样的代码,因为循环使用了迭代器。如果在迭代过程中,别的线程修改了集合,迭代器会失败,抛出ConcurrentModificationException异常。同步仍然是需要的,因此并发的修改可以被可靠地检测出来。
最好使用java.util.concurrent包中定义的集合,不要使用同步包装器。有一个例外是经常被修改的数组列表,同步的ArrayList可以胜过CopyOnWriteArrayList。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值