JAVA并发编程学习笔记(一)

同步容器类(每个公有方法进行同步,每次一个线程访问容器状态)

容器常见的符合操作:迭代(反复访问元素),跳转(根据指定顺序找到当前元素下一元素),条件运算(若没有则添加),当这些情况下,另一个线程并发修改容器时,可能会出现问题。

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类型
不可变对象一定是线程安全的
封装有助于管理复杂性
用锁来保护每个可变对象
保护同一个不变性条件中的所有变量时,要使用同一个锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值