1. 线程简介及安全性
1.1 线程的优点
- 发挥多处理器的强大功能
- 建模的简单性 为模型中的每种类型任务都分配一个专门的线程
- 异步事件的简化处理
- 响应更灵敏的用户界面
1.2 线程带来的风险
- 安全性问题
- 活跃性问题 如发生死锁,无限等待
- 性能问题 如频繁的上下文切换:保存与恢复上下文,丢失局部性,CPU花在线程切换上更多开销而不是线程上;如同步机制,抑制某些编译器优化,使内存缓存区内存无效,增加共享内存总线流量
1.3 线程安全性
如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式可以修复这个问题:
√ 不在线程之间共享该变量
√ 将状态变量修改为不可变变量
√ 在访问状态变量时使用同步
无状态对象一定是线程安全的。
无状态对象:他既不包含任何域,也不包含对其他类中域的引用
竞态条件: 由于不恰当的执行时序而出现的不正确的结果
竞态条件出现常见操作: 先检查后执行,如延迟初始化; 读取-修改-写入, 如i++
1.3.1 内置锁与重入
JAVA中提供了一种内置锁机制支持原子性:同步代码块
同步代码块分为两部分:
1)锁的对象引用 synchronized (lock) { }
2)锁保护的代码块
public void synchronized count() {
.....
}
内置锁是可重入的,如果某个线程试图获得该线程已经获得的锁,那么该请求会成功
重入实现方式: 为每个锁关联一个获取计数器与一个所有者线程,线程进入则+1,线程退出则-1。当计数器为0,该锁将被释放
1.3.2 活跃性与性能
当实现某个同步策略时,一定不要盲目为了性能而牺牲简单性(这可能会破环安全性)
当执行时间较长的计算或者可能无法完成的操作时(例如,网络I/O或控制台I/O),一定不要持有锁
2. CompletionService优点及其应用
2.1 需求描述
某一图片网站首页有许多图片,渲染时间较长,给用户带来较差体验,为提高用户体验度,图片需缓存且无需等待所有图片全部准备完毕后,进行渲染
2.2 问题分析与实现
显而易见,我们可以想到一边获取图片,一边进行渲染,并行操作。但是每张图片获取的时间无法预知,即任务的执行时长不一致,有的可能几毫秒,有的可能几秒, 我们如何才能做到先获取的图片先进行渲染呢?幸好CompletionService适合该场景: 将Executor与BlockingQueue的功能融合在一起,将Callable任务提交给它执行,然后类似队列中的take与poll获取已经完成的任务。
2.3 CompletionService源码分析
public interface CompletionService<V> {
Future<V> submit(Callable<V> task);
Future<V> take() throws InterruptedException;
Future<V> poll();
......
}
CompletionService唯一实现类: ExecutorCompletionService。 在构建函数中创建一个BlockingQueue保存结果。任务提交后,将任务包装成QueueingFuture(FutureTask的一个子类),任务完成后,调用QueueingFuture的done方法,将结果放入BlockingQueue中。
public class ExecutorCompletionService<V> implements CompletionService<V> {
private final BlockingQueue<Future<V>> completionQueue;
public ExecutorCompletionService(Executor executor) {
if (executor == null)
throw new NullPointerException();
this.executor = executor;
this.aes = (executor instanceof AbstractExecutorService) ?
(AbstractExecutorService) executor : null;
this.completionQueue = new LinkedBlockingQueue<Future<V>>();
}
public Future<V> submit(Callable<V> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<V> f = newTaskFor(task);
executor.execute(new QueueingFuture(f)); // 任务包装成QueueingFuture
return f;
}
private class QueueingFuture extends FutureTask<Void> {
QueueingFuture(RunnableFuture<V> task) {
super(task, null);
this.task = task;
}
protected void done() { completionQueue.add(task); } // 任务完成后,调用done方法,放入队列
private final Future<V> task;
}
public Future<V> take() throws InterruptedException {
return completionQueue.take();
}
public Future<V> poll() {
return completionQueue.poll();
}
}
2.4 代码实现
/**
* 使用CompletionService实现页面渲染器
*/
public class Renderer {
private ExecutorService executor;
public Renderer(ExecutorService executor) {
this.executor = executor;
}
public void renderPage(CharSequence source) {
List<ImageInfo> info = scanImageInfo(source);
CompletionService<ImageData> completionService = new ExecutorCompletionService<>(executor);
for (final ImageInfo imageInfo : info) {
completionService.submit(() -> {
return imageInfo.downloadImage();
});
}
renderText(source);
try {
for (int i = 0; i < info.size(); i++) {
Future<ImageData> future = completionService.take();
ImageData imageData = future.get();
renderImage(imageData);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
3. CopyOnWriteArrayList实现原理
3.1 实现原理
CopyOnWriteArrayList是线程安全且读操作无锁的ArrayList,写操作则通过创建底层数组的新副本来实现,是一种读写分离的并发策略。对于写操作,比如向容器中添加一个元素,首先将当前容器复制一份,然后在新副本上执行写操作,然后将原容器的引用指向新容器
3.2 优点
读操作不加锁,性能很高,适合读多写少的并发场景,使用迭代器遍历时候,不会抛出ConcurrentModificationException异常
3.3 缺点
1)内存占用问题 每次写操作都要创建底层数组的新副本,当数据量大时,内存压力较大,可能引起频繁GC
2)无法保证实时性 写和读操作作用在不同的容器上,在写做操作执行的过程中,读操作不会阻塞但是会读取到老容器的数据
3.4 源码分析
add/remove方式使用ReentrantLock
,操作前lock(),操作时Arrays.copyOf进行数组拷贝,操作后unlock()
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
读操作时无需加锁,直接读取
private E get(Object[] a, int index) {
return (E) a[index];
}