java并发原理实战(12)--同步并发容器

1.fork/join框架

  • 多线程的目的不仅仅是提高程序运行的性能。

  • 但是可以充分利用cpu资源。
    在这里插入图片描述

示例代码:

计算1+2+3+…+100的和:

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;

public class Demo extends RecursiveTask<Integer> {
    private int begin;
    private int end;

    public Demo(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        //拆分任务
        if (end - begin <= 2) {
            //计算
            for (int i = begin; i <= end; i++) {
                sum += i;
            }
        } else {
            Demo d1 = new Demo(begin, (begin + end) / 2);
            Demo d2 = new Demo((begin + end) / 2 + 1, end);
            //执行任务
            d1.fork();
            d2.fork();

            Integer a = d1.join();
            Integer b =d2.join();
            sum = a + b;
        }
        //拆分
        return sum;
    }

    public static void main(String[] args) throws Exception{
        ForkJoinPool pool = new ForkJoinPool();
        Future<Integer> future = pool.submit(new Demo(1, 100));
        System.out.println("....");
        System.out.println("计算的值为: " + future.get());
    }
      /**
     * JDK8 的写法
     */
    @Test
    public void test3() {
        long start = System.currentTimeMillis();

        Long sum = LongStream.rangeClosed(0L, 100).parallel().sum();
        System.out.println(sum);

        long end = System.currentTimeMillis();
        System.out.println("耗费的时间为: " + (end - start)); 
    }

}

运行结果:
在这里插入图片描述

2.同步容器和并发容器

①同步容器:

在 Java 中,同步容器主要包括 2 类:

Vector、Stack、HashTable
Vector 实现了 List 接口,Vector 实际上就是一个数组,和 ArrayList 类似,但是 Vector 中的方法都是 synchronized 方法,即进行了同步措施。
Stack 也是一个同步容器,它的方法也用 synchronized 进行了同步,它实际上是继承于 Vector 类。
HashTable 实现了 Map 接口,它和 HashMap 很相似,但是 HashTable 进行了同步处理,而 HashMap 没有。
Collections 类中提供的静态工厂方法创建的类(由 Collections.synchronizedXxxx 等方法)

原文链接:https://blog.csdn.net/qq_20499001/article/details/89031480

示例代码:

public class Demo {
    public static void main(String[] args) {
       List<String> s =  new ArrayList<>();
       //转成list的同步容器
        List<String> strings = Collections.synchronizedList(s);
        HashMap<String, Object> res = new HashMap<>();
        //转成map的同步容器
        Map<String, Object> stringObjectMap = Collections.synchronizedMap(res);
    }
}

查看collections的源代码,发现增删改查的方法都是同步的方法。
在这里插入图片描述

扫描二维码关注公众号,回复: 9895921 查看本文章

同步容器的缺陷
同步容器的同步原理就是在方法上用 synchronized 修饰。那么,这些方法每次只允许一个线程调用执行。

性能问题

由于被 synchronized 修饰的方法,每次只允许一个线程执行,其他试图访问这个方法的线程只能等待。显然,这种方式比没有使用 synchronized 的容器性能要差。

安全问题

同步容器真的一定安全吗?

答案是:未必。同步容器未必真的安全。在做复合操作时,仍然需要加锁来保护。

常见复合操作如下:

迭代:反复访问元素,直到遍历完全部元素;
跳转:根据指定顺序寻找当前元素的下一个(下 n 个)元素;
条件运算:例如若没有则添加等;

原文链接:https://blog.csdn.net/qq_20499001/article/details/89031480

②并发容器

并发容器是在多线程情况下,解决同步容器性能差的问题。

并发容器
JDK 的 java.util.concurrent 包(即 juc)中提供了几个非常有用的并发容器。

CopyOnWriteArrayList - 线程安全的 ArrayList
CopyOnWriteArraySet - 线程安全的 Set,它内部包含了一个 CopyOnWriteArrayList,因此本质上是由 CopyOnWriteArrayList 实现的。
ConcurrentSkipListSet - 相当于线程安全的 TreeSet。它是有序的 Set。它由 ConcurrentSkipListMap 实现。
ConcurrentHashMap - 线程安全的 HashMap。采用分段锁实现高效并发。
ConcurrentSkipListMap - 线程安全的有序 Map。使用跳表实现高效并发。
ConcurrentLinkedQueue - 线程安全的无界队列。底层采用单链表。支持 FIFO。
ConcurrentLinkedDeque - 线程安全的无界双端队列。底层采用双向链表。支持 FIFO 和 FILO。
ArrayBlockingQueue - 数组实现的阻塞队列。
LinkedBlockingQueue - 链表实现的阻塞队列。
LinkedBlockingDeque - 双向链表实现的双端阻塞队列。

ConcurrentHashMap

要点

  • 作用:ConcurrentHashMap 是线程安全的 HashMap。
  • 原理:JDK6 与 JDK7 中,ConcurrentHashMap 采用了分段锁机制。JDK8 中,摒弃了锁分段机制,改为利用 CAS 算法。

原文链接:https://blog.csdn.net/qq_20499001/article/details/89031480

CopyOnWriteArrayList

要点

作用:CopyOnWrite 字面意思为写入时复制。CopyOnWriteArrayList 是线程安全的 ArrayList。
原理:
在 CopyOnWriteAarrayList 中,读操作不同步,因为它们在内部数组的快照上工作,所以多个迭代器可以同时遍历而不会相互阻塞(1,2,4)。
所有的写操作都是同步的。他们在备份数组(3)的副本上工作。写操作完成后,后备阵列将被替换为复制的阵列,并释放锁定。支持数组变得易变,所以替换数组的调用是原子(5)。
写操作后创建的迭代器将能够看到修改的结构(6,7)。
写时复制集合返回的迭代器不会抛出 ConcurrentModificationException,因为它们在数组的快照上工作,并且无论后续的修改(2,4)如何,都会像迭代器创建时那样完全返回元素。

添加操作

  • 添加的逻辑很简单,先将原容器 copy 一份,然后在新副本上执行写操作,之后再切换引用。当然此过程是要加锁的。

删除操作

  • 删除操作同理,将除要删除元素之外的其他元素拷贝到新副本中,然后切换引用,将原容器引用指向新副本。同属写操作,需要加锁。

读操作

  • CopyOnWriteArrayList 的读操作是不用加锁的,性能很高。

CopyOnWriteArraySet

当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后向新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为在当前读的容器中不会添加任何元素。所以CopyOnWrite容器是一种读写分离的思想,读和写对应不同的容器。
原文链接:https://blog.csdn.net/wang7807564/article/details/80048576

BlockingQueue

这种并发容器,会自动实现阻塞式的生产者/消费者模式。使用队列解耦合,在实现异步事物的时候很有用。下面的例子,实现了阻塞队列:

LinkedBlockingQueue

static BlockingQueue<String> strs = new LinkedBlockingQueue<>(10);
strs.put("a" + i); //加入队列,如果满了,就会等待
strs.take(); //取出队列元素,如果空了,就会等待

在实例化时,可以指定具体的队列容量。
在加入成员的时候,除了使用put方法还可以使用其他方法:

Str.add(“aaa”);
/* add如果在队列满了之后,再加入成员会抛出异常,而这种情况下,put方法会一直等待被消费掉。
*/
Str.offer(“aaa”);
/* offer添加成员的时候,会有boolean类型的返回值,如果添加成功,会返回true,如果添加失败,会返回false.除此之外,offer还可以按时段进行添加,例如:
*/
strs.offer("aaa", 1, TimeUnit.SECONDS);
/*
如果队列满了,等待1秒,再进行成员的添加,如果添加失败了,则返回false.
*/

ArrayBlockingQueue

static BlockingQueue<String> strs = new ArrayBlockingQueue<>(10);

对象的方法和上面的BlockingQueue是一样的,用法也是一样的。
二者的区别主要是:

  1. LinkedBlockingQueue是一个单向链表实现的阻塞队列,在链表一头加入元素,如果队列满,就会阻塞,另一头取出元素,如果队列为空,就会阻塞。
  2. LinkedBlockingQueue内部使用ReentrantLock实现插入锁(putLock)和取出锁(takeLock)。

相比于数组实现的ArrayBlockingQueue的有界情况,我们称之为有界队列,LinkedBlockingQueue可认为是无界队列。当然,也可以向上面那样指定队列容量,但是这个参数常常是省略的,多用于任务队列。

DelayQueue

DelayQueue也是一个BlockingQueue,其特化的参数是Delayed。
Delayed扩展了Comparable接口,比较的基准为延时的时间值,Delayed接口的实现类getDelay()的返回值应为固定值(final).DelayQueue内部是使用PriorityQueue实现的,即:

DelayQueue = BlockingQueue + PriorityQueue + Delayed

可以说,DelayQueue是一个使用优先队列(PriorityQueue)实现的BlockingQueue,优先队列的比较基准值是时间。这是一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。这种队列是有序的,即队头对象的延迟到期时间最长。但是要注意的是,不能将null元素放置到这种队列中。
Delayed,一种混合风格的接口,用来标记那些应该在给定延迟时间之后执行的对象。此接口的实现类必须重写一个 compareTo() 方法,该方法提供与此接口的 getDelay()方法一致的排序。
DelayQueue存储的对象是实现了Delayed接口的对象,在这个对象中,需要重写compareTo()和getDelay()方法,例如:

static class MyTask implements Delayed {
long runningTime;
MyTask(long rt) {
this.runningTime = rt;
}
@Override
public int compareTo(Delayed o) {
if(this.getDelay(TimeUnit.MILLISECONDS) <  o.getDelay(TimeUnit.MILLISECONDS))
return -1;
else if(this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS))
return 1;
else 
return 0;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(runningTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
}

因此,当我们在main()函数中,向该队列加入元素后再取出元素的过程,就会存在延时,可以这样验证:

long now = System.currentTimeMillis();
MyTask t1 = new MyTask(now + 1000);
MyTask t2 = new MyTask(now + 2000);
MyTask t3 = new MyTask(now + 1500);
MyTask t4 = new MyTask(now + 2500);
MyTask t5 = new MyTask(now + 500);
tasks.put(t1);
tasks.put(t2);
tasks.put(t3);
tasks.put(t4);
tasks.put(t5);
System.out.println(tasks);
for(int i=0; i<5; i++) {
System.out.println(tasks.take());
}

注意:为了方便查看到效果,可以重写toString()函数,来保证打印出来的结果有意义:
例如:

@Override
public String toString() {
	return "" + runningTime;
}

DelayQueue可以用在诸如用监控线程来轮询是否有超时任务出现,来处理某些具有等待时延的情况,这样,可以避免由于数量巨大造成的轮询效率差的问题。例如:

  1. 关闭空闲连接:服务器中,有很多客户端的连接,空闲一段时间之后需要关闭他们。
  2. 缓存:缓存中的对象,超过了空闲时间,需要从缓存中移出。
  3. 任务超时处理:在网络协议滑动窗口请求应答式交互时,处理超时未响应的请求。

LinkedTransferQueue

TransferQueue是一个继承了BlockingQueue的接口,并且增加若干新的方法。LinkedTransferQueue是TransferQueue接口的实现类,其定义为一个无界的队列,具有先进先出(FIFO)的特性。
TransferQueue接口含有下面几个重要方法:

  1. transfer(E e)
    若当前存在一个正在等待获取的消费者线程,即立刻移交之;否则,会插入当前元素e到队列尾部,并且等待进入阻塞状态,到有消费者线程取走该元素。
  2. tryTransfer(E e)
    若当前存在一个正在等待获取的消费者线程(使用take()或者poll()函数),使用该方法会即刻转移/传输对象元素e;若不存在,则返回false,并且不进入队列。这是一个不阻塞的操作。
  3. tryTransfer(E e,long timeout,TimeUnit unit)
    若当前存在一个正在等待获取的消费者线程,会立即传输给它;否则将插入元素e到队列尾部,并且等待被消费者线程获取消费掉;若在指定的时间内元素e无法被消费者线程获取,则返回false,同时该元素被移除。
  4. hasWaitingConsumer()
    判断是否存在消费者线程。
  5. getWaitingConsumerCount()
    获取所有等待获取元素的消费线程数量。
  6. size()
    因为队列的异步特性,检测当前队列的元素个数需要逐一迭代,无法保证原子性,可能会得到一个不太准确的结果,尤其是在遍历时有可能队列发生更改。
    使用方法:
LinkedTransferQueue<String> strs = new LinkedTransferQueue<>();//实例化

如果当前没有消费者线程(存在take方法的线程):

strs.transfer("aaa");

该方法会一直阻塞在这里,知道有消费者线程存在。
而如果使用传统的put()方法来加入元素的话,则不会发生阻塞现象。

strs.take()

同样,获取队列中元素的方法take()也是阻塞在这里等待获取新的元素的。

SynchronousQueue

SynchronousQueue也是一种BlockingQueue,是一种无缓冲的等待队列。所以,在某次添加元素后必须等待其他线程取走后才能继续添加;可以认为SynchronousQueue是一个缓存值为0的阻塞队列(也可以认为是1),它的isEmpty()方法永远返回是true,remainingCapacity()方法永远返回是0.
remove()和removeAll() 方法永远返回是false,iterator()方法永远返回空,peek()方法永远返回null.
在使用put()方法时,会一直阻塞在这里,等待被消费:

BlockingQueue strs = new SynchronousQueue<>();//实例化
strs.put(“aaa”); //阻塞等待消费者消费
strs.add(“aaa”);//会产生异常,提示队列满了
strs.take();//该方法可以取出元素,同样是阻塞的,需要在线程中去实现他,作为消费者.

原文链接:https://blog.csdn.net/wang7807564/article/details/80048576


发布了246 篇原创文章 · 获赞 29 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/baidu_21349635/article/details/104168163