重温《并发编程实战》---基础模块

来源:互联网 发布:淘宝的林国庆 编辑:程序博客网 时间:2024/05/30 12:31

 

1同步容器类的问题:

 

同步容器类包括VectorHashtable,这些类实现线程安全的方式是:把他们的状态封装起来,并对每个共有方法都进行同步,使得每次只有一个线程能访问容器的状态。

 

首先,同步容器类都是线程安全的,但是在某些情况下需要额外的客户端加锁来保护符合操作,例如迭代、跳转、以及条件运算。

 

比如有2个方法:

 

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);

}

当我们有2个线程交替工作,线程A得到listsize10,线程B也得到listsize10

当线程A想要getLast的时候,如果线程B先执行了deleteLast,线程AgetLast将抛出异常ArrayIndexOutOfBoundsException异常。

 

迭代同理,在迭代下一个元素的时候,如果其他线程给这个元素删除了,也会抛出异常。

此时的问题不是同步容器类,而是复合操作导致的问题,我们的结果依赖了时序,解决办法可以客户端加锁,使用synchronized(list)

 

 

 

2.fast-fail机制 :

http://blog.csdn.net/mypromise_tfs/article/details/70142997

 

这篇博客已经说的很棒,自己再补充2点。

我们在迭代器里面有expectedCount,它最初等于容器.Count,当容器进行结构的修改的时候,可能会导致expectedCount!=同步容器类.Count,此时迭代器里的hasNext或者next将抛出ConcurrentModificationException。然而expectedCount!=同步容器类.Count这个检查是在没有同步的情况下进行的,所以可能看到失效的技术器,并且导致抛出异常的时机不是很及时,但是它降低了并发修改操作的检测代码对程序性能的影响,所以是可以接受的,也就是抛出ConcurrentModificationException的时机可能延后。下面是没有使用同步的检查源码。

 

public E next() {  

checkForComodification();  

            /** 省略此处代码 */  

        }  

  

public void remove() {  

   if (this.lastRet < 0)  

throw new IllegalStateException();  

    checkForComodification();  

            /** 省略此处代码 */  

   }  

  

final void checkForComodification() {  

            if (ArrayList.this.modCount == this.expectedModCount)  

    return;  

throw new ConcurrentModificationException();  

 }  

 }  

 

 

要避免出现ConcurrentModificationException,解决办法:

a.)迭代过程中持有容器的锁,缺点是如果容器规模较大,持有锁的时间就会比较长,将产生严重的锁竞争,降低吞吐量和cpu的利用率。

b.)”克隆”容器:我们可以克隆一个容器,并且在副本上进行迭代,因为副本是被封闭在线程中的,因此其他线程不会在迭代期间对其进行修改,这样也避免抛出了ConcurrentModificationException

 

 

3.隐藏的迭代器:

标准容器的toString方法会迭代容器并在每个元素上调用toString

当容器作为另一个容器的元素或者键值的时候,就会出现这种情况。

containsAll,removeAll,retainAll等方法。

把容器作为参数的构造函数。

 

以上这些方法都会隐含的迭代容器,就有可能抛出ConcurrentModificationException,注意隐含迭代器。

 

 

4.Cppy-On-Write容器:

http://blog.csdn.net/mypromise_tfs/article/details/70146941

这个博客写的很好。

Copy-On-Write容器适合多读少写应用场景,例如黑名单,但是不能保证数据实时一致性,并且内存占用问题也是一个问题,因为在内存中会有旧的容器和新的容器。

 

 

 

5.BlockingQueue:

    http://blog.csdn.net/mypromise_tfs/article/details/70147288 

阻赛的puttake方法,以及定时的offerpoll方法。

offer方法:如果数据项不能被添加到队列中,那么将返回一个失败状态。此时数据不能添加到队列中,说明此时负荷过载了,通过这个失败状态,我们可以做一些机制减轻负载,例如将多余的工作项序列化并写入磁盘,或者减少生产者线程,或者通过某种方式来抑制生产者线程。

 

 

    直接交付方式:SynchronousQueue实际上不是真正的队列,在它内部不会为队列中元素维护存储空间,实际上它维护一组线程,这些线程在等着把元素直接的放入或者移除队列。这个队列可以直接交付工作,从而减少了将数据从生产者移动到消费者的延迟。直接交付方式还会将更多关于任务状态的信息反馈给生产者,我们可以得到更多信息。

最常见的生产者-消费者设计模式就是线程池与工作队列的组合,在Executor任务执行框架体现了这种模式。

 

6.串行线程封闭技术:生产者-消费者与阻塞队列,促进了线程封闭技术。我们可以通过安全的方式发布对象来”转移”对象的所有权,比如从生产者线程转移到消费者线程,当对象被发布到另一个线程,原来的线程就无法访问这个对象了,这个对象完全的封闭在新的线程当中,实现了线程封闭,不过是从A线程封闭转移到了B线程封闭,因此称为串行线程封闭。

 

 

7.双端队列和工作密取:

Java6增加了2种容器类型,DequeBlockingDeque,他们是双端队列,实现了在队列头和队列尾的高效插入和移除,具体实现包括ArrayDequeLinkedBlockingDeque

 

工作密取模式与生产者-消费者模式不同,工作密取的每个消费者都有一个自己的双端队列,即一个消费者线程对应一个自己的任务队列(双端队列),如果一个消费者完成了自己的全部任务,那么他们会从其他消费者双端队列末位秘密地获取工作。

 

工作密取模式的优点:它有更好的可伸缩性,因为它避免了所有消费者线程从一个公共并且共享的队列获取任务从而产生的竞争,并且当一个消费者线程完成自己的任务的时候,它会从其他消费者线程的工作队列的末位获取任务,这同样也减少了队列上的竞争。

 

工作密取模式的适用场景:工作密取模式非常使用即使消费者,也是生产者的问题,即当执行某个工作时可能导致出现更多的工作。例如在网页爬虫程序中处理一个页面,可能产生更多的页面。(一个消费者产生了更多的消费者,每个页面的处理可以看成是一个消费者线程,有自己的工作队列,如果我们使用生产者-消费者模式,我们可能有产生好几个线程要在公共的页面进行竞争,这显然没有每个消费者线程处理自己的页面显得合理。)类似还有搜索图的算法。当双端队列为空的时候,它会在另一个线程的队列尾查找新的任务,从而确保每个线程都保持忙碌状态。

 

 

 

 

 

 

9.同步工具类:

同步工具类可以是任何一个对象,只要它根据其自身的状态来协调线程的控制流。阻塞队列可以作为同步工具类(满了阻塞put,空了阻塞take),其他类型的同步工具类还包括Semaphore-信号量,Barrier-栅栏,以及Latch-闭锁。

 

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

 

a.)闭锁:闭锁相当于一扇门,在闭锁没有到达结束状态时候(Count不为0),这扇门一直是关闭的,线程无法通过这扇门,当闭锁到达结束状态的时候,这扇门会打开并允许所有的线程通过。当闭锁到达结束状态后,将不会再改变状态。闭锁可以用来确保一些活动等待一些条件或者活动完成后才继续执行:比如某个计算在它需要的所有资源初始化之后才开始计算,某个服务在它所有依赖的服务运行之后才开始运行,或者等待知道某个操作的所有参与者都就绪再继续执行。

 

         CountDownLatch是闭锁的灵活实现,CountDownLatch(int n)n决定计数器的数值,countDown方法递减计数器,await方法等待计数器达到0await抛出InterruptedException

 

 

 

闭锁示例:

 

 

public static long timeTask(int n,Runnabletask)throws InterruptedException{

final CountDownLatchstart =new CountDownLatch(1);

final CountDownLatchend =new CountDownLatch(n);

 for(int i=0;i<n;i++){

 Thread t = new Thread(new Runnable() {

@Override

public void run() {

try {

start.await();

task.run();

} catch (InterruptedExceptione) {

// TODO Auto-generated catch block

e.printStackTrace();

}finally {

end.countDown();

}

 

}

});

 

 t.start();

 }

 

 long starttime = System.nanoTime();

 start.countDown();

 end.await();

 long endtime = System.nanoTime();

 return endtime =starttime;

}

public static void main(String[]args)throws InterruptedException{

Runnable task = new Runnable() {

@Override

public void run() {

try {

Thread.sleep(1000);

System.out.println("线程"+ Thread.currentThread().getName()+"正在运行");

} catch (InterruptedExceptione) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

};

int n = 5;

long result =timeTask(n,task);

System.out.println("结果"+result);

 

 

}

 

 

 

线程Thread-0正在运行

线程Thread-1正在运行

线程Thread-2正在运行

线程Thread-3正在运行

线程Thread-4正在运行

结果11533772160646

 

 

 

b.)java多线程之Future和FutureTask

Executor框架使用Runnable 作为其基本的任务表示形式。Runnable是一种有局限性的抽象,然后可以写入日志,或者共享的数据结构,但是他不能返回一个值。

许多任务实际上都是存在延迟计算的:执行数据库查询,从网络上获取资源,或者某个复杂耗时的计算。对于这种任务,Callable是一个更好的抽象,他能返回一个值,并可能抛出一个异常。Future表示一个任务的周期,并提供了相应的方法来判断是否已经完成或者取消,以及获取任务的结果和取消任务。

public interface Callable<V> {

    

    V call() throws Exception;

}

public interface Future<V> {

 

    boolean cancel(boolean mayInterruptIfRunning);

  

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit)

        throws InterruptedException, ExecutionException, TimeoutException;

}

可以通过多种方法来创建一个Future来描述任务。ExecutorService中的submit方法接受一个Runnable或者Callable,然后返回一个Future来获得任务的执行结果或者取消任务。------注意,使用Future来描述任务。

<T> Future<T> submit(Callable<T> task);

<T> Future<T> submit(Runnable task, T result);

 Future<?> submit(Runnable task);

另外ThreadPoolExecutor中的newTaskFor(Callable<T> task)可以返回一个FutureTask

    假设我们通过一个方法从远程获取一些计算结果,假设方法是  List getDataFromRemote(),如果采用同步的方法,代码大概是List data = getDataFromRemote(),我们将一直等待getDataFromRemote返回,然后才能继续后面的工作,这个函数是从远程获取计算结果的,如果需要很长时间,后面的代码又和这个数据没有什么关系的话,阻塞在那里就会浪费很多时间。我们有什么办法可以改进呢???

    能够想到的办法是调用函数后,立即返回,然后继续执行,等需要用数据的时候,再取或者等待这个数据。具体实现有两种方式,一个是用Future,另一个是回调。

    

Future<List> future = getDataFromRemoteByFuture();

        //do something....

//在这里我们做很多工作然后在我们需要的时候再获得数据

        List data = future.get();

可以看到我们返回的是一个Future对象,然后接着自己的处理后面通过future.get()来获得我们想要的值。也就是说在执行getDataFromRemoteByFuture的时候,就已经启动了对远程计算结果的获取,同时自己的线程还继续执行不阻塞。知道获取时候再拿数据就可以。看一下getDataFromRemoteByFuture的实现:

private Future<List> getDataFromRemoteByFuture() {

 

        return threadPool.submit(new Callable<List>() {

            @Override

            public List call() throws Exception {

                return getDataFromRemote();

            }

        });

    }

我们在这个方法中调用getDataFromRemote方法,并且用到了线程池。把任务加入线程池之后,理解返回Future对象。Futureget方法,还可以传入一个超时参数,用来设置等待时间,不会一直等下去。

也可以利用FutureTask来获取结果: 

     

FutureTask<List> futureTask = new FutureTask<List>(new Callable<List>() {

    @Override

    public List call() throws Exception {

return getDataFromRemote();

    }

});

 

threadPool.submit(futureTask);

 

 

futureTask.get();

FutureTask是一个具体的实现类,ThreadPoolExecutorsubmit方法返回的就是一个Future的实现,这个实现就是FutureTask的一个具体实例,FutureTask帮助实现了具体的任务执行,以及和Future接口中的get方法的关联。FutureTask除了帮助ThreadPool很好的实现了对加入线程池任务的Future支持外,也为我们提供了很大的便利,使得我们自己也可以实现支持Future的任务调度。

 

 

 

 

通过上述描述,我们来理清一些概念:

a.)FutureTask实现了Future接口,Future表示一种抽象的可生成结果的计算。

理解:首先,我们的程序会做一些任务,而Future可能更好的帮助我们来描述我们的任务,通过Future,我们可以判断任务的状态,例如(isDone,isCancel),我们可以取消任务(cancel),最重要的是,我们的任务可以自己运行,在必要的时候或者需要的时候可以使用Future.get()方法来获得任务的结果或者等待结果直到计算完成并得到结果。

 

FutureTaskFuture的实现类,FutureTask是通过Callable来实现的。

 

b.)Callable是什么东西?

callableRunnable的升级版(个人理解),Runnable不能返回值,而Callable可以返回一个值或者可能抛出一个异常,这能更好的使我们对任务进行了解,如果如Runnable什么都不返回,我们可能只能看着任务执行,你说呢?

 

c.)ExecutorService submit方法通过往框架服务中提交一个代表任务的Runnable或者Callable,返回一个用来描述任务和对任务进行操作的Future,看,通过ExecutorService,我们使得我们的RunnableCallable变得可了解和可操纵了。

例子:ThreadPoolExecutorsubmit方法返回一个Future的实现类,即FutureTask

 

 

 

 

c.)信号量=Semaphore:用来控制同时访问某个特定资源的操作数量,或者执行某个制定操作的数量,Semaphopre维护了一组许可集,许可数量可以通过构造函数指定,要执行某个操作必须从Semaphore获得许可,并可以在恰当的时候释放许可。

acquire()获得许可,release()释放许可。

例子:

 

 

ExecutorService exec = Executors.newCachedThreadPool();  

        // 只能5个线程同时访问

        final Semaphoresemp =new Semaphore(5);  

        // 模拟20个客户端访问

        for (int index = 0;index < 20;index++) {

            final int NO =index;  

            Runnable run = new Runnable() {  

                public void run() {  

                    try {  

                        // 获取许可

                        semp.acquire();  

                        System.out.println("Accessing: " +NO);  

                        Thread.sleep((long) (Math.random() * 10000));  

                        System.out.println("释放 " +NO);

                        // 访问完后,释放 ,如果屏蔽下面的语句,则在控制台只能打印5条记录,之后线程一直阻塞

                        semp.release();  

                    } catch (InterruptedExceptione) {  

                    }  

                }  

            };  

            exec.execute(run);  

        }  

        // 退出线程池

        exec.shutdown();  

    }  

 

 

Accessing: 0

Accessing: 1

Accessing: 2

Accessing: 3

Accessing: 4

释放 0

Accessing: 5

释放 1

Accessing: 6

释放 2

Accessing: 7

释放 5

Accessing: 8

释放 4

Accessing: 9

释放 3

Accessing: 10

释放 7

Accessing: 11

释放 6

Accessing: 12

释放 9

Accessing: 13

释放 10

Accessing: 14

释放 11

Accessing: 15

释放 8

Accessing: 16

释放 15

Accessing: 17

释放 14

Accessing: 18

释放 12

Accessing: 19

释放 17

释放 16

释放 13

释放 19

释放 18

 

 

从结果可以看出,我们线程池一次只能处理5个任务,这是个很好的机制。

 

 

 

 

d.)栅栏(Barrier):典型实现是CyclicBarrier,它类似闭锁,但是和闭锁不同,下面是他们两个的区别,我们来分析一下。

 

 

栅栏和闭锁的区别:

首先,闭锁类似一扇大门,只有等待某个或某些事件的“开启命令”才能开启这扇大门,当这扇大门紧闭的时候,所有线程都不能通过这扇大门,

这种情况就包括:1.)部分线程已经在门这里阻塞了。  2.)部分线程还没到这个门这里,它们没被阻塞,而当他们到门这里的时候,闭锁已经接到了”开启命令”,所以这些线程直接通过这扇大门。

 

其实上述描述也不够准确,但是比较形象,实际上闭锁用于等待事件,例如我们想用一个数据源,它依赖很多其他的数据源,只有这些子数据源的初始化事件完成后,我们才能使用最终的数据源,闭锁只能使用一次,大门开了之后无法再关闭。闭锁可以用来确保一些活动在某个事件发生后执行。

 

 

而栅栏用于等待线程,只有所有线程的任务都完成之后,到达了我们的栅栏处,我们才能让这些线程继续执行。通常使用栅栏的方法是将一个问题分解成多个子问题,等所有的的子问题解决好,到达栅栏处,再一起继续执行。当线程到达汇集地后调用await,await方法会出现阻塞直至其他线程也到达汇集地。如果所有的线程都到达就可以通过栅栏,也就是所有的线程得到释放,而且栅栏也可以被重新利用。

 

 

记住,闭锁用于等待事件,只有某个或者某些事件完成,总事件才可以继续执行,例如初始化。

栅栏用于等待所有线程,可以理解为所有子任务都到达一处才继续执行,在模拟程序当中通常需要使用栅栏,比如我们一个需要多次更新的程序,每次更新操作都有许多子更新操作,我们可以通过在每两次更新之间等待栅栏,能够确保在第K步中的所有子更新操作都已经更新完毕,才进入第k+1步更新操作。

 

 

最后一点,new CyclicBarrier(int parties, Runnable task)parties代表一共是几个线程,Runnable代表当所有线程集合在栅栏处的时候,执行Runnable并将所有线程继续执行。

 

 

例子如下:

我们模仿10个人去春游,当10个人都到达集合点的时候,10个人才开始继续走。

 

 

 

 

public class Test {

//我们创建个10个人去春游的示例,只有所有人都到达正确的地点才能一起继续往前走:

private static class PersonRunnableimplements Runnable{

public int id;

public CyclicBarrierbarrier;

public PersonRunnable(int id,CyclicBarrierbarrier) {

this.id =id;

this.barrier =barrier;

}

@Override

public void run() {

System.out.println("第"+id+"个人开始走向集合点");

Random r = new Random();

try {

Thread.sleep(r.nextInt(2000));

System.out.println("第"+id+"个人走到了集合点");

barrier.await();

System.out.println("集合完毕,第"+id+"个人和大家一起走");

} catch (InterruptedExceptione) {

e.printStackTrace();

} catch (BrokenBarrierExceptione) {

e.printStackTrace();

}

}

}

 

public static void main(String[]args)throws InterruptedException{

int num = 10;

CyclicBarrier barrier = new CyclicBarrier(num,new Runnable() {

@Override

public void run() {

System.out.println("大家集合完毕,开始向前出发");

}

});

for(int i=0;i<num;i++){

new Thread(new PersonRunnable(i,barrier)).start();

}

    }  

 

 

 

输出示例:

2个人开始走向集合点

3个人开始走向集合点

1个人开始走向集合点

4个人开始走向集合点

5个人开始走向集合点

6个人开始走向集合点

7个人开始走向集合点

9个人开始走向集合点

8个人开始走向集合点

0个人开始走向集合点

 

8个人走到了集合点

4个人走到了集合点

5个人走到了集合点

7个人走到了集合点

9个人走到了集合点

2个人走到了集合点

1个人走到了集合点

0个人走到了集合点

3个人走到了集合点

6个人走到了集合点

 

大家集合完毕,开始向前出发

 

集合完毕,第6个人和大家一起走

集合完毕,第8个人和大家一起走

集合完毕,第5个人和大家一起走

集合完毕,第9个人和大家一起走

集合完毕,第4个人和大家一起走

集合完毕,第0个人和大家一起走

集合完毕,第1个人和大家一起走

集合完毕,第2个人和大家一起走

集合完毕,第7个人和大家一起走

集合完毕,第3个人和大家一起走

 

注意:a.)我们看到当所有线程到达集合点的时候先执行我们执行的Runnable,告诉我们大家集合完毕

 

      b.)等待几个人是在new CyclicBarrier(int parties, Runnable task)中的parties中指定的,如果我们修改上面的程序:

 

public static void main(String[]args)throws InterruptedException{

int num = 10;

CyclicBarrier barrier =new CyclicBarrier(num+1,new Runnable() {

@Override

public void run() {

System.out.println("大家集合完毕,开始向前出发");

}

});

for(int i=0;i<num;i++){

new Thread(new PersonRunnable(i,barrier)).start();

}

    }  

我们把栅栏等待线程数+1,即等待11个线程,我们还是10个人春游,看看会发生什么。

 

0个人开始走向集合点

1个人开始走向集合点

2个人开始走向集合点

3个人开始走向集合点

4个人开始走向集合点

5个人开始走向集合点

6个人开始走向集合点

7个人开始走向集合点

8个人开始走向集合点

9个人开始走向集合点

8个人走到了集合点

2个人走到了集合点

4个人走到了集合点

0个人走到了集合点

9个人走到了集合点

1个人走到了集合点

7个人走到了集合点

3个人走到了集合点

6个人走到了集合点

5个人走到了集合点

 

看,10个人走到集合点之后一直等待在集合点,无法继续往前走。