同步容器类(每个公有方法进行同步,每次一个线程访问容器状态)
容器常见的符合操作:迭代(反复访问元素),跳转(根据指定顺序找到当前元素下一元素),条件运算(若没有则添加),当这些情况下,另一个线程并发修改容器时,可能会出现问题。
public static Object getLast(Vector list){
int lastIndex = list.size() - 1; //取出最后位置索引
return list.get(lastIndex); //获取
}
public static void deleteLast(Vector list){
int lastIndex = list.size() - 1; //取出最后位置索引
list.remove(lastIndex); // 移除
}
//这段代码是线程不安全的
//加锁后安全
synchronized(list){
//这里执行上面的代码
}
当A线程(getLast操作)获取到size为10时,B线程(deleteLast)获取size为10,两者的lastIndex都为9,然而B接着执行remove操作,删除后,A开始执行get(9)操作,那么就会产生错误.
解决方法:使用synchronized加锁, 锁对象为Vector对象(list).
由于迭代操作额能会产生问题,所有要上锁,这样可以防止其他线程在迭代期间修改Vector
ConcurrentModificationException
设计同步容器类的迭代器时并没有考虑并发修改问题,表现出的行为“fail-fast”(及时失败)的。
当发现容器在迭代过程中修改时,会抛出一个ConcurrentModificationException异常。
实现方式:每一个容器的添加删除操作都会有一个变量modcount对操作进行++,每执行一次就增加1,这样,如果迭代期间这个值发生变化那么就会抛出异常。
对于for-each循环语法,它将变为使用hasNext和next的迭代器来进行使用。
如果不希望在迭代期间对容器进行加锁,那么可以“克隆”一个容器,从而在副本上进行迭代操作。
隐藏迭代器
//例如
System.out.println("hello"+set);
//编译器将字符串操作转换为StringBuilder.append(Object)->这个方法又会调用toString生成容器内部格式化表示
containsAll,removeAll,retainAll都会调用容器进行迭代。
并发容器
first
ConcurrentHashMap,用来替代同步且基于散列的Map,以及CopyOnWriteArrayList。同时增加一些复合操作,“若没有则添加”,替换,以及条件删除。
通过并发容器来代替同步容器,可以极大地提高伸缩性并降低风险。
同步容器在执行每个操作的期间都持有一个锁(可能会花费很长时间查找对象)
ConcurrentHashMap使用粒度更细的加锁机制(分段锁),这样任意数量的读取线程可以并发地访问Map,执行读取操作的线程和执行写入操作的线程可以并发地访问Map,并且一定数量写入线程可以并发修改Map。它提供的迭代器不会抛出ConcurrentModificationException异常。(弱一致性的迭代器)创建迭代器时会遍历已有的元素,并可以在迭代器被构造后修改操作反映给容器。
ConcurrentMap相关结构
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 replave(K key,V oldValue,V newValue);
//仅当K被映射到某个值时才替换为newValue
V replace(K key,V newValue);
}
second
CopyOnWriteArrayList,在迭代期间不需要对容器进行加锁或者复制。
“写入时复制(Copy-On-Write)”容器的线程安全性在于,发布事实不可变对象,访问对象不需同步。在每次修改时,都会创建并重新发布一个新的容器副本,从而实现可变性。不会抛出ConcurrentModificationException异常。仅当迭代操作远远多于修改操作时,才应该使用“写入时复制”容器。例如,事件通知系统,注册和注销操作远小于接受时间通知操作。
线程的状态
public enum State {
NEW, //没有调用start的线程状态
RUNNABLE,//调用start后线程在执行run方法且没有阻塞时状态
BLOCKED, //阻塞
WAITING, //阻塞
TIMED_WAITING, //阻塞
TERMINATED; //运行结束
}
双端队列和工作密取
JAVA6 增加了两种容器类型,Deque(发音为“deck”)和BlockingDeque他们分别对Queue和BlockingQueue进行扩展。Deque是一个双端队列,实现了在队列头和队列尾的高效插入和移除。具体实现包括ArrayDeque和LinkedBlockingDeque。
工作密取,使用于既是生产者也是消费者问题-执行某个工作时可能导致出现更多的工作。(例如网页爬虫处理一个页面,发现更多页面要处理)类似还有图算法,垃圾回收阶段对堆进行标记,都可以通过工作密取机制来实现高效并行。
阻塞方法和中断方法
阻塞或暂停执行,原因:等待I/O操作结束,等待获得一个锁,等待从Thread.sleep方法中醒来,或等待另一个线程的计算结果。当线程阻塞,被挂起,并处于某种阻塞状态(BLOCKED、WAITING或TIMED WAITING)。被阻塞的线程必须等待某个不受它控制的时间发生后才继续执行,例如等待I/O操作完成,等待某个锁变成可用,或者等待外部计算的结束。当某个外部事件发生时,线程被置回RUNNABLE状态,并可以再次被调度执行。
Thread提供的interrupt方法,用于中断线程或者查询线程是否已经被中断。
中断是一种协作机制。当在代码中调用了一个将抛出InterruptedException异常的方法时,自己的方法会变成一个阻塞方法,必须要处理对中断的响应。2种选择
1.传递InterruptedException
2.恢复中断 调用interrupt,将异常抛给上一层
同步工具类
1.闭锁
闭锁相当于一扇门,当闭锁到达结束状态之前,这扇门一直是关闭的(所有线程不能通过),当到达状态后,这扇门会打开并允许所有线程通过。闭锁可以用来确保某些活动直到其他活动完成后才继续执行。
例如三国杀当所有游戏玩家准备后,房主才能点开始。
CountDownLatch是一种灵活的闭锁实现。可以使一个或多个线程等待一组事件发生。闭锁包含一个计数器,初始化为一个整数(表示需要等待的事件数量),countDown方法递减计数器,await方法等待计数器到达零(表示等待的事件都已经发生)。如果非0那么await会阻塞知道计数器为零。
FutureTask也可以用做闭锁。可以处于3种状态:等待运行,正在运行,运行完成。get方法会阻塞知道任务完成,可能返回结果,也可能抛出异常。抛出ExecutionExecption异常,Throwable cause = e.getCause();
//使用FutureTask提前加载稍后需要数据
public class Preloader{
private final FutureTask<ProductInfo> future = new FutureTask<ProductInfo>(new Callable<ProductInfo>(){
public ProducInfo call() throws DataLoadException{
return loadProductInfo();
}
});
private final Thread thread = new Thread(future);
public ProductInfo get() throws DataLoadException{
try{
returnn future.get();
}catch(ExecutionException e){
Throwable casue = e.getCasue();
...
}
}
}
2.信号量
用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定的操作数量,还可以实现某种资源池(数据库连接池).
relase方法将返回一个许可给信号量,acquire方法获取一个许可。
在构造阻塞对象池时,简单的方法可以使用BlockingQueue来保存池的资源。
//使用semaphore为容器设置边界
public class BoundedHashSet<T>{
private final Set<T> set;
private final Sempahore sem;
public BoundedHashSet(int bound){
this.set = Collections.synchronizedSet(new HashSet<T>);
sem = new Semaphore(bound); //设置边界值
}
public boolean add(T o)throws InterrupedException{
sem.acquire(); //申请获取许可,如果没有得到那么一直阻塞下去
bollean wasAdded = false;;
try{
wasAdd = set.add(o);
return wasAdded;
}finally{
if(!wasAdded)
sem.release(); //如果添加失败,则释放许可,许可增加1个
}
}
}
栅栏
它类似于闭锁,它能阻塞线程知道某个事件发生。区别在于,所有线程必须同时到达栅栏位置,才能继续执行(这个应该是GC里头安全点的应用,设置安全点标记,当线程到达后等待,当所有线程都到达此安全点后,那么开始清理回收)
CyclicBarrier可以使一定数量的参与方反复地在栅栏位置汇集。可以给CyclicBarrier传递一个Runnable给构造函数,当成功通过栅栏会执行它(执行Runnable)
尽量将域声明为final类型
不可变对象一定是线程安全的
封装有助于管理复杂性
用锁来保护每个可变对象
保护同一个不变性条件中的所有变量时,要使用同一个锁