提升--12---并发容器历史


容器

在这里插入图片描述

java容器分2大类

  • 第一大类:Collection叫集合
  • 第二大类:Map

Collection集合

在这里插入图片描述

第一大类Collection叫集合。集合的意思是不管你这个容器是什么结构你可以把一个元素一个元素的往里面扔;

Map

在这里插入图片描述

第二大类Map:Map是一对一对的往里扔,kv结构。其实Map来说可以看出是Collection一个特殊的变种,你可以把一对对象看成一个entry对象,所以这也是一个整个对象。容器就是装一个一个对象的,这么一些个集合。严格来讲数组也属于容器。

从数据结构角度分:

从数据结构角度来讲在物理上的这种存储的数据结构其实只有两种,

  1. 一种是连续存储的数组Array
  2. 另一种就是非连续存储的一个指向另外一个的链表。在逻辑结构那就非常非常多了。

Collection 分三大类

  • list
  • set
  • Queue
    在这里插入图片描述

Queue队列

队列就是一对一队的,往这个队列里取数据的时候它和这个List、Set都不一样。大家知道List取的时候如果是Array的话还可以取到其中一个的。Set主要和其他的区别就是中间是唯一的,不会有重复元素,这个它最主要的区别。Queue实现了一个什么逻辑呢,实际上就是一个队列,

队列什么概念,有进有出,那么在这个基础之上它实现了很多的多线程的访问方法(比如说put阻塞式的放、take阻塞式的取),这个是在其他的List、Set里面都是没有的。队列最主要的原因是为了实现任务的装载的这种取和装这里面最重要的就是是叫做阻塞队列,它的实现的初衷就是为了线程池、高并发做准备的。原来的这些是容器是普通的为了装东西做准备的。

list,set和队列 Queue的区别

  1. 队列 Queue实现了很多的多线程的访问方法(比如说put阻塞式的放、take阻塞式的取),这个是在其他的List、Set里面都是没有的。
  2. 初衷不同:队列 它的实现的初衷就是为了线程池、高并发做准备的。原来的这些list,set容器是普通的为了装东西做准备的

JDK容器发展历史

  1. java1.0-----Vector、Hashtable(synchronized)
  2. list、set 、HashMap(没有锁)
  3. Collections工具类------SynchronizedList、synchronizedMap
  4. Queue、ConcurrentHashMap(高并发)

1. Vector、Hashtable

Vector 和 Hashtable 自带锁,基本不用,大家记住这个结论。
  • 最开始java1.0容器里只有两个,第一个叫Vector可以单独的往里扔,还有一个是Hashtable是可以一对一对往里扔的。Vector相对于实现了List接口,Hashtable实现了Map接口。但是这个两个容器在1.0设计的时候稍微有点问题,这两个容器设计成了所有方法默认都是加synchronized的,这是它最早设计不太合理的地方。多数的时候我们多数的程序只有一个线程在工作,所以在这种情况下你完全是没有必要加synchronized,因此最开始的时候设计的性能比较差,

2. list、set 、HashMap

  • 在Hashtable之后又添加了HashMap,HashMap就是完全的没有加锁,一个是二话没说就加锁,一个是完全没有加锁。那这两个除了这个加锁区别之外 还有其他的一些源码上的区别,所以Sun在那个时候就在这个HashMap的基础之上又添加了一个,说你用的这个新的HashMap比原来Hashtable好用,但是HashMap没有那些锁的东西,

3. SynchronizedList、synchronizedMap

Map<> map = Collections.synchronizedMap(new HashMap<>());
  • 那么怎么才可以让这个HashMap既可以用于这些不需要锁的环境,有可以用于需要锁的环境呢? 所以它又添加了一个方法叫做Collections相当于这个容器的工具类,这个工具类里有一个方法叫synchronizedMap,这个方法会把它变成加锁的版本。所以,HashMap有两个版本。

4. ConcurrentHashMap

map容器-----效率对比

那个效率高那个效率低不要想当然,务必要写程序来测试(压测

准备数据:100万个UUID对、100个线程

  • 容器装的都是Key,Value对,一对一对的。这个Key是UUID,Value也是UUID等于new一个Hashtable出来,这里面的UUID到底有多少个呢,我定义了两个常量,这两个常量在一个单独的类里,这个类叫Constants:100万个UUID对内容要装到容器里,会有100个线程,将来我们访问的时候会有100个线程来模拟。
public class Constants {
    public static final int COUNT = 1000000;
    public static final int THREAD_COUNT = 100;
}

程序设计:

  • 看程序,我先new出100万个Key和100万个Value来,然后把这些东西装到数组里面去,for循环(int i = 0; i < count;i++)。为什么先把这些UUID对准备好而不是我们装的时候现场生成?原因是我们写这个测试用例的时候前后用的是一样的,你往Hashtable里头装的时候也得是这100万对,同样的内容,但你要是每次都生成是不一样的内容,在这种情况下你测试就会有一些干扰因素,所以我们先准备好在往里扔。
  • 后面,写了一个线程类,叫MyThread,从Thread继承,start,gap是每个线程负责往里面装多少。线程开始往里面扔。在看后面主程序代码,记录起始时间,new出来一个线程数组,这个线程数据总共有100个线程,给它做初始化。由于你需要指定这个start值,所以这个MyThread启动的时候i乘以count(总而言之就是这个起始的值不一样,第一个线程是从0开始,第二个是从100000个开始)没用到也没关系,用到了就把他记录下来是一个好的习惯。然后让每一个线程启动,等待每一个线程结束,最后计算这个线程时间。
  • 现在我有一个Hashtable,里面装的是一对一对的内容,现在我们起了100个线程,这个100个线程去Key,Value取数据,一个线程取1万个数据,一共100万个数据,100个线程,每个线程取1万个数据往里插,整个程序模拟的是这么一个情形

TestHashtable

package c_023_02_FromHashtableToCHM;

import java.util.Hashtable;
import java.util.UUID;

public class T01_TestHashtable {

    static Hashtable<UUID, UUID> m = new Hashtable<>();

    static int count = Constants.COUNT;
    static UUID[] keys = new UUID[count];
    static UUID[] values = new UUID[count];
    static final int THREAD_COUNT = Constants.THREAD_COUNT;

    static {
        for (int i = 0; i < count; i++) {
            keys[i] = UUID.randomUUID();
            values[i] = UUID.randomUUID();
        }
    }

    static class MyThread extends Thread {
        int start;
        int gap = count/THREAD_COUNT;

        public MyThread(int start) {
            this.start = start;
        }

        @Override
        public void run() {
            for(int i=start; i<start+gap; i++) {
                m.put(keys[i], values[i]);
            }
        }
    }

    public static void main(String[] args) {

        long start = System.currentTimeMillis();

        Thread[] threads = new Thread[THREAD_COUNT];

        for(int i=0; i<threads.length; i++) {
            threads[i] =
                    new MyThread(i * (count/THREAD_COUNT));
        }

        for(Thread t : threads) {
            t.start();
        }

        for(Thread t : threads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        long end = System.currentTimeMillis();
        System.out.println("===Hashtable.put() 所花费时间===: "+ (end - start));

        System.out.println("===m.size()===: "+m.size());

        //-----------------------------------

        start = System.currentTimeMillis();
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(()->{
                for (int j = 0; j < 10000000; j++) {
                    m.get(keys[10]);
                }
            });
        }

        for(Thread t : threads) {
            t.start();
        }

        for(Thread t : threads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        end = System.currentTimeMillis();
        System.out.println("===Hashtable.get() 所花费时间===: "+(end - start));
    }
}

在这里插入图片描述

HashMap

  • 我们来看这HashMap,同学们想一下这个HashMap往里头插会不会有问题。因为HashMap没有锁啊,线程不安全,这个就没有意义了,这只是为了程序的完整性留在这,他虽然速度比较快,但是数据会出问题,还各种各样的报异常。主要是因为它内部会把这个变成TreeNode,我们先不去细究它,总而言之HashMap这个东西你往里扔的时候,由于它内部没有锁,所以你多线程访问的时候会出问题,这个你往里插的时候就没有实际意义了。
package c_023_02_FromHashtableToCHM;

import java.util.HashMap;
import java.util.UUID;

public class T02_TestHashMap {

    static HashMap<UUID, UUID> m = new HashMap<>();

    static int count = Constants.COUNT;
    static UUID[] keys = new UUID[count];
    static UUID[] values = new UUID[count];
    static final int THREAD_COUNT = Constants.THREAD_COUNT;

    static {
        for (int i = 0; i < count; i++) {
            keys[i] = UUID.randomUUID();
            values[i] = UUID.randomUUID();
        }
    }

    static class MyThread extends Thread {
        int start;
        int gap = count/THREAD_COUNT;

        public MyThread(int start) {
            this.start = start;
        }

        @Override
        public void run() {
            for(int i=start; i<start+gap; i++) {
                m.put(keys[i], values[i]);
            }
        }
    }

    public static void main(String[] args) {

        long start = System.currentTimeMillis();

        Thread[] threads = new Thread[THREAD_COUNT];

        for(int i=0; i<threads.length; i++) {
            threads[i] =
                    new MyThread(i * (count/THREAD_COUNT));
        }

        for(Thread t : threads) {
            t.start();
        }

        for(Thread t : threads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        long end = System.currentTimeMillis();
        System.out.println(end - start);

        System.out.println(m.size());
    }
}

SynchronizedHashMap

  • 我们在看第三个,用的是SynchronizedMap这个方法,给HashMap我们手动加锁,它的源码自己做了一个Object,然后每次都是SynchronizedObject,严格来讲他和那个Hashtable效率上区别不大。
  • 在这里插入图片描述
package c_023_02_FromHashtableToCHM;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class T03_TestSynchronizedHashMap {

    static Map<UUID, UUID> m = Collections.synchronizedMap(new HashMap<UUID, UUID>());

    static int count = Constants.COUNT;
    static UUID[] keys = new UUID[count];
    static UUID[] values = new UUID[count];
    static final int THREAD_COUNT = Constants.THREAD_COUNT;

    static {
        for (int i = 0; i < count; i++) {
            keys[i] = UUID.randomUUID();
            values[i] = UUID.randomUUID();
        }
    }

    static class MyThread extends Thread {
        int start;
        int gap = count/THREAD_COUNT;

        public MyThread(int start) {
            this.start = start;
        }

        @Override
        public void run() {
            for(int i=start; i<start+gap; i++) {
                m.put(keys[i], values[i]);
            }
        }
    }

    public static void main(String[] args) {

        long start = System.currentTimeMillis();

        Thread[] threads = new Thread[THREAD_COUNT];

        for(int i=0; i<threads.length; i++) {
            threads[i] =
                    new MyThread(i * (count/THREAD_COUNT));
        }

        for(Thread t : threads) {
            t.start();
        }

        for(Thread t : threads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        long end = System.currentTimeMillis();
        System.out.println("===SynchronizedHashMap.put() 所花费时间===: "+ (end - start));

        System.out.println("===m.size()===: "+m.size());

        //-----------------------------------

        start = System.currentTimeMillis();
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(()->{
                for (int j = 0; j < 10000000; j++) {
                    m.get(keys[10]);
                }
            });
        }

        for(Thread t : threads) {
            t.start();
        }

        for(Thread t : threads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        end = System.currentTimeMillis();
        System.out.println("===SynchronizedHashMap.get() 所花费时间===: "+(end - start));
    }
}

在这里插入图片描述

看结果,SynchronizedHashMap严格来讲他和那个Hashtable效率上区别不大。

ConcurrentHashMap

  • 这个第四个ConcurrentHashMap是多线程里面真正用的,以后我们多线程用的基本就是它,用Map的时候。并发的。
  • 这个ConcurrentHashMap提高效率主要提高在读上面,由于它往里插的时候内部又做了各种各样的判断,本来是链表的,到8之后又变成了红黑树,然后里面又做了各种各样的cas的判断,所以他往里插的数据是要更低一些的
  • HashMap和Hashtable虽然说读的效率会稍微低一些,但是它往里插的时候检查的东西特别的少,就加个锁然后往里一插。所以,关于效率,还是看你实际当中的需求。用几个简单的小程序来给大家列举了这几个不同的区别。
package c_023_02_FromHashtableToCHM;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

public class T04_TestConcurrentHashMap {

    static Map<UUID, UUID> m = new ConcurrentHashMap<>();

    static int count = Constants.COUNT;
    static UUID[] keys = new UUID[count];
    static UUID[] values = new UUID[count];
    static final int THREAD_COUNT = Constants.THREAD_COUNT;

    static {
        for (int i = 0; i < count; i++) {
            keys[i] = UUID.randomUUID();
            values[i] = UUID.randomUUID();
        }
    }

    static class MyThread extends Thread {
        int start;
        int gap = count/THREAD_COUNT;

        public MyThread(int start) {
            this.start = start;
        }

        @Override
        public void run() {
            for(int i=start; i<start+gap; i++) {
                m.put(keys[i], values[i]);
            }
        }
    }

    public static void main(String[] args) {

        long start = System.currentTimeMillis();

        Thread[] threads = new Thread[THREAD_COUNT];

        for(int i=0; i<threads.length; i++) {
            threads[i] =
                    new MyThread(i * (count/THREAD_COUNT));
        }

        for(Thread t : threads) {
            t.start();
        }

        for(Thread t : threads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        long end = System.currentTimeMillis();
        System.out.println("===ConcurrentHashMap.put() 所花费时间===: "+(end - start));

        System.out.println("===m.size()===: "+m.size());

        //-----------------------------------

        start = System.currentTimeMillis();
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(()->{
                for (int j = 0; j < 10000000; j++) {
                    m.get(keys[10]);
                }
            });
        }

        for(Thread t : threads) {
            t.start();
        }

        for(Thread t : threads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        end = System.currentTimeMillis();
        System.out.println("===ConcurrentHashMap.get() 所花费时间===: "+(end - start));
    }
}

在这里插入图片描述

集合容器-----效率对比

买票案例

  • 有N张火车票,每张票都有一个编号
  • 同时有10个窗口对外售票

ArrayList

  • 写法比较简单,我们先来用一个List把这些票全装进去,往里面装一万张票,然后10个线程也就是10个窗口对外销售,只要size大于零,只要还有剩余的票时我就往外卖,取一张往外卖remove。
  • 大家想象一下到最后一张票的时候,好几个线程执行到这里所以线程都发现了size大于零,所有线程都往外买了一张票,那么会发生什么情形,只有一个线程拿到了这张票,其他的拿到的都是空值,就是超卖的现象。没有加锁,线程不安全
import java.util.ArrayList;
import java.util.List;

public class TicketSeller1 {
    static List<String> tickets = new ArrayList<>();

    static {
        for(int i=0; i<10000; i++) tickets.add("票编号" + i);
    }



    public static void main(String[] args) {
        for(int i=0; i<10; i++) {
            new Thread(()->{
                while(tickets.size() > 0) {
                    System.out.println("销售了--" + tickets.remove(0));
                }
            }).start();
        }
    }
}

在这里插入图片描述

Vector

  • 我们来看最早的这个容器Vector,内部是自带锁的,你去读它的时候就会看到很多方法synchronized二话不说先加上锁在说,所以你用Vector的时候请放心它一定是线程安全的。100张票,10个窗口,读这个程序还是有问题的,还是不对。
  • 锁为了线程的安全,就是当我们调用size方法的时候他加锁了,调用remove的时候它也加锁了,可是很不幸的是在你这两个中间它没有加锁,那么,好多个线程还会判断依然这个size还是大于0的,大家伙又超卖了。
import java.util.Vector;
import java.util.concurrent.TimeUnit;

public class TicketSeller2 {
    static Vector<String> tickets = new Vector<>();


    static {
        for(int i=0; i<10000; i++) tickets.add("票编号" + i);
    }

    public static void main(String[] args) {

        for(int i=0; i<10; i++) {
            new Thread(()->{
                while(tickets.size() > 0) {

                    try {
                        TimeUnit.MILLISECONDS.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }


                    System.out.println("销售了--" + tickets.remove(0));
                }
            }).start();
        }
    }
}

在这里插入图片描述

正确写法

  • 虽然你用了这个加锁的容器了,由于在你调用这个并发容器的时候,你是调用了其中的两个原子方法,所以你在外层还得在加一把锁synchronized(tickets),继续判断size,售出去不断的remove,这个就没有问题了,它会踏踏实实的往外销售,但不是效率最高的方案
package c_023_02_FromHashtableToCHM;

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class TicketSeller3 {
    static List<String> tickets = new LinkedList<>();


    static {
        for(int i=0; i<1000; i++) tickets.add("票编号" + i);
    }

    public static void main(String[] args) {

        for(int i=0; i<10; i++) {
            new Thread(()->{
                while(true) {
                    synchronized(tickets) {
                        if(tickets.size() <= 0) break;

                        try {
                            TimeUnit.MILLISECONDS.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }

                        System.out.println("销售了--" + tickets.remove(0));
                    }
                }
            }).start();
        }
    }
}

在这里插入图片描述

Queue

高并发,多线程情况下 ,单个元素操作的时候多考虑Queue
  • 效率最高的就是这个Queue,这是最新的一个接口,他的主要目标就是为了高并发用的,就是为了多线程用的。所以,以后考虑多线程这种单个元素的时候多考虑Queue。
  • 看程序前面初始化不说了,这个使用的是ConcurrentLinkedQueue,然后里面并没有说加锁,我就直接调用了一个方法叫poll,poll的意思就是我从tickets去取值,这个值什么时候取空了就说明里面的值已经没了,所以这个while(true)不断的往外销售,一直到他突然发现伸手去取票的时候这里面没了,那我这个窗口就可以关了不用买票了。

poll()

  • poll的意思它加了很多对于多线程访问的时候比较友好的一些方法,它的源码,取一下去得到我们这个queue上的头部,脑袋上这个元素,得到并且去除掉这里面这个值,
  • 如果这个已经是空我就返回null值。
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

public class TicketSeller4 {
    static Queue<String> tickets = new ConcurrentLinkedQueue<>();


    static {
        for(int i=0; i<1000; i++) tickets.add("票编号" + i);
    }

    public static void main(String[] args) {

        for(int i=0; i<10; i++) {
            new Thread(()->{
                while(true) {
                    String s = tickets.poll();
                    if(s == null) break;
                    else System.out.println("销售了--" + s);
                }
            }).start();
        }
    }
}

在这里插入图片描述

小结:

所以刚才讲的这八个小程序,主要是为了给大家说明整体的这个演化的过程是一个什么样子的,从Map这个角度来讲最早是从Hashtable,二话不说先加锁到HashMap去除掉锁,再到synchronizedHashMap加一个带锁的版本,到ConcurrentHashMap多线程时候专用。

任何时候在你实际情况下都需要通过测试,压测来决定用哪种容器。
  • 注意,不是替代关系,这个归根结底还是会归到,到底cas操作就一定会比synchronized效率要高吗,不一定,要看你并发量的高低,要看你锁定之后代码执行的时间,任何时候在你实际情况下都需要通过测试,压测来决定用哪种容器。
    在这里插入图片描述

面向接口编程

  • 为什么要面向接口编程,如果说你在工作之中设计一个程序,这个程序你应该设计一个接口,这个接口里面只包括业务逻辑,取出学生列表,放好,但是这里列表具体的实现是放到Hashtable里面还是放到HashMap里面还是放到ConcurrentHashMap里面,你可以写好几个不同的实现,在不同的并发情况下采用不同的实现你的程序会更灵活,在这里你们能不能理解就是这种面向接口的编程和面向接口的设计它的微妙之所在。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值