容器 订阅
容器是指用以容纳物料并以壳体为主的基本装置。常用作储存设备或其他化工设备的外壳。主要由壳体、封头、接管、法兰和支座组成。对容器的基本要求是:满足工艺需要;保证操作安全,包括具有足够的强度、刚度以及密封性;耐腐蚀,具有一定的使用寿命;便于制造、安装、维修和使用;成本低,材料节省,尤其要节约不锈钢和有色金属等贵重材料。容器在化工生产中应用广泛,按压力分有真空、常压、外压的压力容器,压力容器又可分低压、中压、高压及超高压容器;按温度分有常温、低温及高温容器;按筒体结构分有单层容器和多层容器;按工艺用途分有储存、分离、反应及换热容器;按厚度分有薄壁和厚壁容器。 [1] 展开全文
容器是指用以容纳物料并以壳体为主的基本装置。常用作储存设备或其他化工设备的外壳。主要由壳体、封头、接管、法兰和支座组成。对容器的基本要求是:满足工艺需要;保证操作安全,包括具有足够的强度、刚度以及密封性;耐腐蚀,具有一定的使用寿命;便于制造、安装、维修和使用;成本低,材料节省,尤其要节约不锈钢和有色金属等贵重材料。容器在化工生产中应用广泛,按压力分有真空、常压、外压的压力容器,压力容器又可分低压、中压、高压及超高压容器;按温度分有常温、低温及高温容器;按筒体结构分有单层容器和多层容器;按工艺用途分有储存、分离、反应及换热容器;按厚度分有薄壁和厚壁容器。 [1]
信息
定义1
包装或装载物品的贮存器
外文名
container;vessel
材    质
金属 非金属
用    途
装载物品
中文名
容器
定义2
能将其他控件放置在其上面的控件
容器计算机
能够将其他控件放置在其上面的控件,如VB中的Frame控件或PictureBox控件等。容器是应用服务器中位于组件和平台之间的接口集合。容器是伴随着瘦客户端系统的发展而诞生的。在开发瘦客户端系统时,开发人员要花费大量的精力去关注线程安全、事务、网络、资源等等细节,从而降低了开发效率。由于这些对这些细节的解决方法一般是固定不变,或者只有参数改变的,所以从代码重用和设计模式的角度出发,开发人员将这些底层细节提取出来,做成平台,并提供一定的接口。这样,业务开发人员就不需要在关注与这些底层细节的实现,而专注于业务逻辑的实现。容器一般位于应用服务器之内,由应用服务器负责加载和维护。一个容器只能存在于一个应用服务器之内,一个应用服务器可以建立和维护多个容器。容器一般遵守可配置的原则,即容器的使用者可以通过对容器参数的配置,来达到自己的使用需求,而不需要修改容器的代码。定义容器是用来存储和组织其他对象的对象。实现链表的类就是一个容器的示例。如vectormydata;//创建存储double类型值的容器mydata可以在容器中存储基本类型或任何类类型的条目。如果STL容器模板的类型实参是一个类类型,那么容器可以存储该类型的对象或者任何派生类类型的对象。通常,容器存储我们存储在其中的对象的副本,它们自动分配和管理对象占用的内存。当销毁某个容器的对象时,容器会负责销毁它包含的对象并释放它们占用的内存。使用STL容器存储对象的一个优点是我们不用费心管理它们的内存。 在实际的开发过程中,数据结构本身的重要性不会逊于操作于数据结构的算法的重要性,当程序中存在着对时间要求很高的部分时,数据结构的选择就显得更加重要。经典的数据结构数量有限,但是我们常常重复着一些为了实现向量、链表等结构而编写的代码,这些代码都十分相似,只是为了适应不同数据的变化而在细节上有所出入。STL容器就为我们提供了这样的方便,它允许我们重复利用已有的实现构造自己的特定类型下的数据结构,通过设置一些模版类,STL容器对最常用的数据结构提供了支持,这些模板的参数允许我们指定容器中元素的数据类型,可以将我们许多重复而乏味的工作简化。STL容器类的模板容器部分主要由头文件,,,,,组成。对于常用的一些容器和容器适配器(可以看作由其它容器实现的容器),可以通过下表总结一下它们和相应头文件的对应关系。数据结构描述实现头文件向量(vector)连续存储的元素列表(list)由节点组成的双向链表,每个结点包含着一个元素双队列(deque)连续存储的指向不同元素的指针所组成的数组集合(set)由节点组成的红黑树,每个节点都包含着一个元素,节点之间以某种作用于元素对的位次排列,没有两个不同的元素能够拥有相同的次序多重集合(multiset)允许存在两个次序相等的元素的集合栈(stack)后进先出的值的排列队列(queue)先进先出的值的排列优先队列(priority_queue)元素的次序是由作用于所存储的值对上的某种谓词决定的一种队列映射(map)由{键,值}对组成的集合,以某种作用于键对上的谓词排列多重映射(multimap)允许键对有相等的次序的映射为所有容器定义的操作
收起全文
精华内容
下载资源
问答
  • 同步类容器和并发类容器

    万次阅读 多人点赞 2019-07-31 19:22:20
    为什么会出现同步容器? 在Java的集合容器框架中,主要有四大类别:List、Set、Queue、Map。 注意Collection和Map是顶层接口,而List、Set、Queue接口则分别继承了Collection接口,分别代表数组、集合和队列这三大...

    一.为什么会出现同步容器?

    在Java的集合容器框架中,主要有四大类别:List、Set、Queue、Map。

    注意Collection和Map是顶层接口,而List、Set、Queue接口则分别继承了Collection接口,分别代表数组、集合和队列这三大类容器。

    像ArrayList、LinkedList都是实现了List接口,HashSet实现了Set接口,而Deque(双向队列,允许在队首、队尾进行入队和出队操作)继承了Queue接口,PriorityQueue实现了Queue接口。另外LinkedList(实际上是双向链表)同时也实现了Deque接口。

    但以上容器都是非线程安全的。如果有多个线程并发地访问这些容器时,就会出现问题。因此,在编写程序时,必须要求程序员手动地在访问到这些容器的地方进行同步处理,这样导致在使用这些容器的时候非常地不方便。所以,Java提供了同步容器供用户使用。

    二.Java中的同步类容器

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

      1)Vector、Stack、HashTable

      2)Collections类中提供的静态工厂方法创建的类

    Vector实现了List接口,Vector实际上就是一个数组,和ArrayList类似,但是Vector中的方法都是synchronized方法,即进行了同步措施;Stack也是一个同步容器,它的方法也用synchronized进行了同步,它实际上是继承于Vector类;HashTable实现了Map接口,它和HashMap很相似,但是HashTable进行了同步处理,而HashMap没有。

    Collections类是一个工具提供类,注意,它和Collection不同,Collection是一个顶层的接口。在Collections类中提供了大量的方法,比如对集合或者容器进行排序、查找等操作。最重要的是,在它里面提供了几个静态工厂方法来创建同步容器类,如下图所示:

    这些同步容器都是通过synchronized进行同步来实现线程安全的,那么很显然,这必然会影响到执行性能。

    而且虽然他们都是线程安全的,但这并不说明在任何情况下都可以线程安全,看你怎么用了,例如下面的这个例子:

    public class Test {
        static Vector<Integer> vector = new Vector<Integer>();
        public static void main(String[] args) throws InterruptedException {
            while(true) {
                for(int i=0;i<10;i++)
                    vector.add(i);
                Thread thread1 = new Thread(){
                    public void run() {
                        for(int i=0;i<vector.size();i++)
                            vector.remove(i);
                    };
                };
                Thread thread2 = new Thread(){
                    public void run() {
                        for(int i=0;i<vector.size();i++)
                            vector.get(i);
                    };
                };
                thread1.start();
                thread2.start();
                while(Thread.activeCount()>10)   {
                     
                }
            }
        }
    }

    运行结果是在执行过程中会出现数组下标越界的运行时异常。也许有朋友会问:Vector是线程安全的,为什么还会报这个错?很简单,对于Vector,虽然能保证每一个时刻只能有一个线程访问它,但是不排除这种可能,当某个线程在某个时刻执行这句时:

    for(int i=0;i<vector.size();i++)

         vector.get(i);

    假若此时vector的size方法返回的是10,i的值为9,在他要获取下标为9的元素时,有另外一个线程先执行了这句:

    for(int i=0;i<vector.size();i++)

         vector.remove(i);

    将下标为9的元素删除了,在删除过程中因为有锁,所以之前的那个线程无法执行vector.get(i);处于阻塞状态,等这个线程把下标为9的元素删除了之后获取到锁再执行。那么通过get方法访问下标为9的元素肯定就会出问题了。说明这是程序逻辑本身存在线程安全问题,因此为了保证线程安全,必须在方法调用端做额外的同步措施,如下面所示:

    public class Test {
        static Vector<Integer> vector = new Vector<Integer>();
        public static void main(String[] args) throws InterruptedException {
            while(true) {
                for(int i=0;i<10;i++)
                    vector.add(i);
                Thread thread1 = new Thread(){
                    public void run() {
                        synchronized (Test.class) {   //进行额外的同步
                            for(int i=0;i<vector.size();i++)
                                vector.remove(i);
                        }
                    };
                };
                Thread thread2 = new Thread(){
                    public void run() {
                        synchronized (Test.class) {
                            for(int i=0;i<vector.size();i++)
                                vector.get(i);
                        }
                    };
                };
                thread1.start();
                thread2.start();
                while(Thread.activeCount()>10)   {
                     
                }
            }
        }
    }

    三.Java中的并发类容器

    为了解决同步类容器的性能问题,在Java 1.5之后提供了并发容器,位于java.util.concurrent目录下,这个目录俗称并发包。

    3.1、ConcurrentMap

    ConcurrentMap接口下有两个重要的实现:ConcurrentHashMap、ConcurrentSkipListMap。ConcurrentHashMap把整个哈希表分成多个segment,每个segment一把锁,主要通过锁分段技术减小了锁的粒度,降低了冲突,从而提高了并发性。在实际的应用中,散列表一般是读多写少。ConcurrentHashMap 就针对读操作做了大量的优化,运用了很多并发技巧,如不可变对象和使用volatile保证内存可见性,这样,在大多数情况下读操作甚至无需加锁也能获得正确的值。ConcurrentHashMap的concurrencyLevel(默认值为16)表示并发级别,这个值用来确定Segment的个数,Segment的个数是大于等于concurrencyLevel的第一个2的n次方的数。比如,如果concurrencyLevel为12,13,14,15,16这些数,则Segment的数目为16(2的4次方)。理想情况下ConcurrentHashMap的真正的并发访问量能够达到concurrencyLevel,因为有concurrencyLevel个Segment,假如有concurrencyLevel个线程需要访问Map,并且需要访问的数据都恰好分别落在不同的Segment中,则这些线程能够无竞争地自由访问(因为他们不需要竞争同一把锁),达到同时访问的效果。这也是为什么这个参数起名为“并发级别”的原因。该值设置过高会照成空间的浪费,设置过低会降低并发性。这种对调优的把握是要通过对底层实现的深刻理解和不断的实践积累才能获取的。

    3.2、CopyOnWirte容器

    Cope-On-Write简称COW,是一种用于程序设计中的优化策略,称为写时复制,理解起来很简单,就是执行修改操作时进行底层数组复制,使得修改操作在新的数组上进行,不妨碍原数组的并发读操作,复制修改完成后把原数组引用指向新数组。这样做的好处是可以并发的读而不需要加锁,因为当前容器不会添加任何元素,所以也是一种读写分离的思想。但正是因为写时复制,所以不能保证数据的实时性,而只能保证最终一致性。

    在concurrent包下实现CopyOnWrite机制的容器有两种,CopyOnWriteArrayList和CopyOnWriteArraySet。

    CopyOnWriteArrayList中有一个Object数组array用来存放数据,对于set()、add()、remove()等修改数据的操作会加上重入锁ReentrantLock,等修改操作完成替换掉array的引用之后才释放锁,从而保证写操作的线程安全,而针对读操作没有任何锁。

    CopyOnWriteArraySet其实就是一个CopyOnWriteArrayList,不过就是在方法中避免重复数据而已,甚至这些避免重复数据的函数也是在CopyOnWriteArrayList中定义的,CopyOnWriteArraySet中只是包含一个CopyOnWriteArrayList的属性,然后在方法上做个包装,除了equals方法外,其他当前类中的所有函数都是调用的CopyOnWriteArrayList的方法,所以严格来讲可以使用一个CopyOnWriteArrayList作为具有Set特性的写时复制数组(不过就是没有继承AbstractSet)。

    根据CopyOnWirte容器的实现原理可知,CopyOnWirte容器保证读写分离,十分适合读多写少的场景,但不适合写多的场景。

    3.3、线程安全队列

    在并发编程中我们有时候需要使用线程安全的队列。如果我们要实现一个线程安全的队列有两种实现方式一种是使用阻塞算法,另一种是使用非阻塞算法。使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现,而非阻塞的实现方式则可以使用循环CAS的方式来实现。java.util.concurrent.atomic包相关类就是CAS的实现。

    ConcurrentLinkedQueue是一个适用于高并发场景下的非阻塞的队列,通过无锁的方式(采用CAS操作),实现了高并发状态下的高性能,通常ConcurrentLinkedQueue的性能优于BlockingQueue。ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素,该队列不允许NULL元素。

    阻塞队列当队列是空的时候,再想获取元素就会阻塞进入等待状态,所以非常适合生产者-消费者模式。阻塞队列BlockingQueue接口JDK提供了7种实现:

    • ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
    • LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
    • PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
    • DelayQueue:一个使用优先级队列实现的无界阻塞队列。
    • SynchronousQueue:一个不存储元素的阻塞队列。
    • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
    • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
    展开全文
  • Java集合容器面试题(2020最新版)

    万次阅读 多人点赞 2020-03-01 11:08:34
    文章目录集合容器概述什么是集合集合的特点集合和数组的区别使用集合框架的好处常用的集合类有哪些?List,Set,Map三者的区别?List、Set、Map 是否继承自 Collection 接口?List、Map、Set 三个接口存取元素时,各...

    Java面试总结(2021优化版)已发布在个人微信公众号【技术人成长之路】,优化版首先修正了读者反馈的部分答案存在的错误,同时根据最新面试总结,删除了低频问题,添加了一些常见面试题,对文章进行了精简优化,欢迎大家关注!😊😊

    【技术人成长之路】,助力技术人成长!更多精彩文章第一时间在公众号发布哦!

    文章目录

    Java面试总结汇总,整理了包括Java基础知识,集合容器,并发编程,JVM,常用开源框架Spring,MyBatis,数据库,中间件等,包含了作为一个Java工程师在面试中需要用到或者可能用到的绝大部分知识。欢迎大家阅读,本人见识有限,写的博客难免有错误或者疏忽的地方,还望各位大佬指点,在此表示感激不尽。文章持续更新中…

    序号内容链接地址
    1Java基础知识面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104390612
    2Java集合容器面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104588551
    3Java异常面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104390689
    4并发编程面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104863992
    5JVM面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104390752
    6Spring面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104397516
    7Spring MVC面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104397427
    8Spring Boot面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104397299
    9Spring Cloud面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104397367
    10MyBatis面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/101292950
    11Redis面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/103522351
    12MySQL数据库面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104778621
    13消息中间件MQ与RabbitMQ面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104588612
    14Dubbo面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104390006
    15Linux面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104588679
    16Tomcat面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104397665
    17ZooKeeper面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104397719
    18Netty面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104391081
    19架构设计&分布式&数据结构与算法面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/105870730

    集合容器概述

    什么是集合

    集合框架:用于存储数据的容器。

    集合框架是为表示和操作集合而规定的一种统一的标准的体系结构。
    任何集合框架都包含三大块内容:对外的接口、接口的实现和对集合运算的算法。

    接口:表示集合的抽象数据类型。接口允许我们操作集合时不必关注具体实现,从而达到“多态”。在面向对象编程语言中,接口通常用来形成规范。

    实现:集合接口的具体实现,是重用性很高的数据结构。

    算法:在一个实现了某个集合框架中的接口的对象身上完成某种有用的计算的方法,例如查找、排序等。这些算法通常是多态的,因为相同的方法可以在同一个接口被多个类实现时有不同的表现。事实上,算法是可复用的函数。
    它减少了程序设计的辛劳。

    集合框架通过提供有用的数据结构和算法使你能集中注意力于你的程序的重要部分上,而不是为了让程序能正常运转而将注意力于低层设计上。
    通过这些在无关API之间的简易的互用性,使你免除了为改编对象或转换代码以便联合这些API而去写大量的代码。 它提高了程序速度和质量。

    集合的特点

    集合的特点主要有如下两点:

    • 对象封装数据,对象多了也需要存储。集合用于存储对象。

    • 对象的个数确定可以使用数组,对象的个数不确定的可以用集合。因为集合是可变长度的。

    集合和数组的区别

    • 数组是固定长度的;集合可变长度的。

    • 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。

    • 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。

    数据结构:就是容器中存储数据的方式。

    对于集合容器,有很多种。因为每一个容器的自身特点不同,其实原理在于每个容器的内部数据结构不同。

    集合容器在不断向上抽取过程中,出现了集合体系。在使用一个体系的原则:参阅顶层内容。建立底层对象。

    使用集合框架的好处

    1. 容量自增长;
    2. 提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量;
    3. 允许不同 API 之间的互操作,API之间可以来回传递集合;
    4. 可以方便地扩展或改写集合,提高代码复用性和可操作性。
    5. 通过使用JDK自带的集合类,可以降低代码维护和学习新API成本。

    常用的集合类有哪些?

    Map接口和Collection接口是所有集合框架的父接口:

    1. Collection接口的子接口包括:Set接口和List接口
    2. Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
    3. Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
    4. List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

    List,Set,Map三者的区别?List、Set、Map 是否继承自 Collection 接口?List、Map、Set 三个接口存取元素时,各有什么特点?

    img

    Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是collection的子接口。

    Collection集合主要有List和Set两大接口

    • List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
    • Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。

    Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。

    Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap

    集合框架底层数据结构

    Collection

    1. List
    • Arraylist: Object数组
    • Vector: Object数组
    • LinkedList: 双向循环链表
    1. Set
    • HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
    • LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。
    • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)

    Map

    • HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
    • LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
    • HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
    • TreeMap: 红黑树(自平衡的排序二叉树)

    哪些集合类是线程安全的?

    • vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。
    • statck:堆栈类,先进后出。
    • hashtable:就比hashmap多了个线程安全。
    • enumeration:枚举,相当于迭代器。

    Java集合的快速失败机制 “fail-fast”?

    是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。

    例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

    原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

    解决办法:

    1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。

    2. 使用CopyOnWriteArrayList来替换ArrayList

    怎么确保一个集合不能被修改?

    可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。

    示例代码如下:

    List<String> list = new ArrayList<>();
    list. add("x");
    Collection<String> clist = Collections. unmodifiableCollection(list);
    clist. add("y"); // 运行时此行报错
    System. out. println(list. size());
    

    Collection接口

    List接口

    迭代器 Iterator 是什么?

    Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。

    Iterator 怎么使用?有什么特点?

    Iterator 使用代码如下:

    List<String> list = new ArrayList<>();
    Iterator<String> it = list. iterator();
    while(it. hasNext()){
      String obj = it. next();
      System. out. println(obj);
    }
    

    Iterator 的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。

    如何边遍历边移除 Collection 中的元素?

    边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,如下:

    Iterator<Integer> it = list.iterator();
    while(it.hasNext()){
       *// do something*
       it.remove();
    }
    

    一种最常见的错误代码如下:

    for(Integer i : list){
       list.remove(i)
    }
    

    运行以上错误代码会报 ConcurrentModificationException 异常。这是因为当使用 foreach(for(Integer i : list)) 语句时,会自动生成一个iterator 来遍历该 list,但同时该 list 正在被 Iterator.remove() 修改。Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。

    Iterator 和 ListIterator 有什么区别?

    • Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
    • Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
    • ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。

    遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么?

    遍历方式有以下几种:

    1. for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。

    2. 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。

    3. foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。

    最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。

    • 如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。
    • 如果没有实现该接口,表示不支持 Random Access,如LinkedList。

    推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历。

    说一下 ArrayList 的优缺点

    ArrayList的优点如下:

    • ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。
    • ArrayList 在顺序添加一个元素的时候非常方便。

    ArrayList 的缺点如下:

    • 删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。
    • 插入元素的时候,也需要做一次元素复制操作,缺点同上。

    ArrayList 比较适合顺序添加、随机访问的场景。

    如何实现数组和 List 之间的转换?

    • 数组转 List:使用 Arrays. asList(array) 进行转换。
    • List 转数组:使用 List 自带的 toArray() 方法。

    代码示例:

    // list to array
    List<String> list = new ArrayList<String>();
    list.add("123");
    list.add("456");
    list.toArray();
    
    // array to list
    String[] array = new String[]{"123","456"};
    Arrays.asList(array);
    

    ArrayList 和 LinkedList 的区别是什么?

    • 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
    • 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
    • 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
    • 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
    • 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;

    综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。

    补充:数据结构基础之双向链表

    双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。

    ArrayList 和 Vector 的区别是什么?

    这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合

    • 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。
    • 性能:ArrayList 在性能方面要优于 Vector。
    • 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。

    Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。

    Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。

    插入数据时,ArrayList、LinkedList、Vector谁速度较快?阐述 ArrayList、Vector、LinkedList 的存储性能和特性?

    ArrayList、LinkedList、Vector 底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢。

    Vector 中的方法由于加了 synchronized 修饰,因此 Vector 是线程安全容器,但性能上较ArrayList差

    LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需要记录当前项的前后项即可,所以 LinkedList 插入速度较快

    多线程场景下如何使用 ArrayList?

    ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样:

    List<String> synchronizedList = Collections.synchronizedList(list);
    synchronizedList.add("aaa");
    synchronizedList.add("bbb");
    
    for (int i = 0; i < synchronizedList.size(); i++) {
        System.out.println(synchronizedList.get(i));
    }
    

    为什么 ArrayList 的 elementData 加上 transient 修饰?

    ArrayList 中的数组定义如下:

    private transient Object[] elementData;
    

    再看一下 ArrayList 的定义:

    public class ArrayList<E> extends AbstractList<E>
         implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    

    可以看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化,重写了 writeObject 实现:

    private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
        *// Write out element count, and any hidden stuff*
            int expectedModCount = modCount;
        s.defaultWriteObject();
        *// Write out array length*
            s.writeInt(elementData.length);
        *// Write out all elements in the proper order.*
            for (int i=0; i<size; i++)
                s.writeObject(elementData[i]);
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
    }
    

    每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小。

    List 和 Set 的区别

    List , Set 都是继承自Collection 接口

    List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。

    Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。

    另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。

    Set和List对比

    Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
    List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变

    Set接口

    说一下 HashSet 的实现原理?

    HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为PRESENT,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。

    HashSet如何检查重复?HashSet是如何保证数据不可重复的?

    向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。
    HashSet 中的add ()方法会使用HashMap 的put()方法。

    HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。

    以下是HashSet 部分源码:

    private static final Object PRESENT = new Object();
    private transient HashMap<E,Object> map;
    
    public HashSet() {
        map = new HashMap<>();
    }
    
    public boolean add(E e) {
        // 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
    	return map.put(e, PRESENT)==null;
    }
    

    hashCode()与equals()的相关规定

    1. 如果两个对象相等,则hashcode一定也是相同的
    2. 两个对象相等,对两个equals方法返回true
    3. 两个对象有相同的hashcode值,它们也不一定是相等的
    4. 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
    5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。

    ==与equals的区别

    1. ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同
    2. ==是指对内存地址进行比较 equals()是对字符串的内容进行比较3.==指引用是否相同 equals()指的是值是否相同

    HashSet与HashMap的区别

    HashMapHashSet
    实现了Map接口实现Set接口
    存储键值对仅存储对象
    调用put()向map中添加元素调用add()方法向Set中添加元素
    HashMap使用键(Key)计算HashcodeHashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false
    HashMap相对于HashSet较快,因为它是使用唯一的键获取对象HashSet较HashMap来说比较慢

    Queue

    BlockingQueue是什么?

    Java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。

    在 Queue 中 poll()和 remove()有什么区别?

    • 相同点:都是返回第一个元素,并在队列中删除返回的对象。
    • 不同点:如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。

    代码示例:

    Queue<String> queue = new LinkedList<String>();
    queue. offer("string"); // add
    System. out. println(queue. poll());
    System. out. println(queue. remove());
    System. out. println(queue. size());
    

    Map接口

    说一下 HashMap 的实现原理?

    HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

    HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

    HashMap 基于 Hash 算法实现的

    1. 当我们往Hashmap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
    2. 存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
    3. 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
    4. 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。

    需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)

    HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现

    在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。

    JDK1.8之前

    JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

    jdk1.7中HashMap数据结构

    JDK1.8之后

    相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。

    jdk1.8中HashMap数据结构

    JDK1.7 VS JDK1.8 比较

    JDK1.8主要解决或优化了一下问题:

    1. resize 扩容优化
    2. 引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考
    3. 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。
    不同JDK 1.7JDK 1.8
    存储结构数组 + 链表数组 + 链表 + 红黑树
    初始化方式单独函数:inflateTable()直接集成到了扩容函数resize()
    hash值计算方式扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算
    存放数据的规则无冲突时,存放数组;冲突时,存放链表无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树
    插入数据方式头插法(先讲原位置的数据移到后1位,再插入数据到该位置)尾插法(直接插入到链表尾部/红黑树)
    扩容后存储位置的计算方式全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1))按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量)

    HashMap的put方法的具体流程?

    当我们put的时候,首先计算 keyhash值,这里调用了 hash方法,hash方法实际是让key.hashCode()key.hashCode()>>>16进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。按照函数注释,因为bucket数组大小是2的幂,计算下标index = (table.length - 1) & hash,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。

    putVal方法执行流程图

    putVal方法执行流程图

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
    //实现Map.put和相关方法
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 步骤①:tab为空则创建 
        // table未初始化或者长度为0,进行扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 步骤②:计算index,并对null做处理  
        // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        // 桶中已经存在元素
        else {
            Node<K,V> e; K k;
            // 步骤③:节点key存在,直接覆盖value 
            // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                    // 将第一个元素赋值给e,用e来记录
                    e = p;
            // 步骤④:判断该链为红黑树 
            // hash值不相等,即key不相等;为红黑树结点
            // 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node, e可能为null
            else if (p instanceof TreeNode)
                // 放入树中
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            // 步骤⑤:该链为链表 
            // 为链表结点
            else {
                // 在链表最末插入结点
                for (int binCount = 0; ; ++binCount) {
                    // 到达链表的尾部
                    
                    //判断该链表尾部指针是不是空的
                    if ((e = p.next) == null) {
                        // 在尾部插入新结点
                        p.next = newNode(hash, key, value, null);
                        //判断链表的长度是否达到转化红黑树的临界值,临界值为8
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //链表结构转树形结构
                            treeifyBin(tab, hash);
                        // 跳出循环
                        break;
                    }
                    // 判断链表中结点的key值与插入的元素的key值是否相等
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        // 相等,跳出循环
                        break;
                    // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                    p = e;
                }
            }
            //判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的value这个值
            if (e != null) { 
                // 记录e的value
                V oldValue = e.value;
                // onlyIfAbsent为false或者旧值为null
                if (!onlyIfAbsent || oldValue == null)
                    //用新值替换旧值
                    e.value = value;
                // 访问后回调
                afterNodeAccess(e);
                // 返回旧值
                return oldValue;
            }
        }
        // 结构性修改
        ++modCount;
        // 步骤⑥:超过最大容量就扩容 
        // 实际大小大于阈值则扩容
        if (++size > threshold)
            resize();
        // 插入后回调
        afterNodeInsertion(evict);
        return null;
    }
    

    ①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

    ②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;

    ③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

    ④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;

    ⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

    ⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

    HashMap的扩容操作是怎么实现的?

    ①.在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;

    ②.每次扩展的时候,都是扩展2倍;

    ③.扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。

    在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;//oldTab指向hash桶数组
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {//如果oldCap不为空的话,就是hash桶数组不为空
            if (oldCap >= MAXIMUM_CAPACITY) {//如果大于最大容量了,就赋值为整数最大的阀值
                threshold = Integer.MAX_VALUE;
                return oldTab;//返回
            }//如果当前hash桶数组的长度在扩容后仍然小于最大容量 并且oldCap大于默认值16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold 双倍扩容阀值threshold
        }
        // 旧的容量为0,但threshold大于零,代表有参构造有cap传入,threshold已经被初始化成最小2的n次幂
        // 直接将该值赋给新的容量
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        // 无参构造创建的map,给出默认容量和threshold 16, 16*0.75
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 新的threshold = 新的cap * 0.75
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        // 计算出新的数组长度后赋给当前成员变量table
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//新建hash桶数组
        table = newTab;//将新数组的值复制给旧的hash桶数组
        // 如果原先的数组没有初始化,那么resize的初始化工作到此结束,否则进入扩容元素重排逻辑,使其均匀的分散
        if (oldTab != null) {
            // 遍历新数组的所有桶下标
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    // 旧数组的桶下标赋给临时变量e,并且解除旧数组中的引用,否则就数组无法被GC回收
                    oldTab[j] = null;
                    // 如果e.next==null,代表桶中就一个元素,不存在链表或者红黑树
                    if (e.next == null)
                        // 用同样的hash映射算法把该元素加入新的数组
                        newTab[e.hash & (newCap - 1)] = e;
                    // 如果e是TreeNode并且e.next!=null,那么处理树中元素的重排
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    // e是链表的头并且e.next!=null,那么处理链表中元素重排
                    else { // preserve order
                        // loHead,loTail 代表扩容后不用变换下标,见注1
                        Node<K,V> loHead = null, loTail = null;
                        // hiHead,hiTail 代表扩容后变换下标,见注1
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        // 遍历链表
                        do {             
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    // 初始化head指向链表当前元素e,e不一定是链表的第一个元素,初始化后loHead
                                    // 代表下标保持不变的链表的头元素
                                    loHead = e;
                                else                                
                                    // loTail.next指向当前e
                                    loTail.next = e;
                                // loTail指向当前的元素e
                                // 初始化后,loTail和loHead指向相同的内存,所以当loTail.next指向下一个元素时,
                                // 底层数组中的元素的next引用也相应发生变化,造成lowHead.next.next.....
                                // 跟随loTail同步,使得lowHead可以链接到所有属于该链表的元素。
                                loTail = e;                           
                            }
                            else {
                                if (hiTail == null)
                                    // 初始化head指向链表当前元素e, 初始化后hiHead代表下标更改的链表头元素
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 遍历结束, 将tail指向null,并把链表头放入新数组的相应下标,形成新的映射。
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
    

    HashMap是怎么解决哈希冲突的?

    答:在解决这个问题之前,我们首先需要知道什么是哈希冲突,而在了解哈希冲突之前我们还要知道什么是哈希才行;

    什么是哈希?

    Hash,一般翻译为“散列”,也有直接音译为“哈希”的,这就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值);这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数

    所有散列函数都有如下一个基本特性**:根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同**。

    什么是哈希冲突?

    当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)

    HashMap的数据结构

    在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做链地址法的方式可以解决哈希冲突:

    img

    这样我们就可以将拥有相同哈希值的对象组织成一个链表放在hash值所对应的bucket下,但相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化

    hash()函数

    上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,在JDK 1.8中的hash()函数如下:

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或)
    }
    

    这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动)

    JDK1.8新增红黑树

    img

    通过上面的链地址法(使用散列表)扰动函数我们成功让我们的数据分布更平均,哈希碰撞减少,但是当我们的HashMap中存在大量数据时,加入我们某个bucket下对应的链表有n个元素,那么遍历时间复杂度就为O(n),为了针对这个问题,JDK1.8在HashMap中新增了红黑树的数据结构,进一步使得遍历复杂度降低至O(logn);

    总结

    简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:

    1. 使用链地址法(使用散列表)来链接拥有相同hash值的数据;
    2. 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
    3. 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;

    能否使用任何类作为 Map 的 key?

    可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点:

    • 如果类重写了 equals() 方法,也应该重写 hashCode() 方法。

    • 类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。

    • 如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。

    • 用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关的问题了。

    为什么HashMap中String、Integer这样的包装类适合作为K?

    答:String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率

    1. 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
    2. 内部已重写了equals()hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况;

    如果使用Object作为HashMap的Key,应该怎么办呢?

    答:重写hashCode()equals()方法

    1. 重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;
    2. 重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性

    HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?

    答:hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;

    那怎么解决呢?

    1. HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;
    2. 在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;

    HashMap 的长度为什么是2的幂次方

    为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。

    这个算法应该如何设计呢?

    我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。

    那为什么是两次扰动呢?

    答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的;

    HashMap 与 HashTable 有什么区别?

    1. 线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
    2. 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
    3. 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。
    4. **初始容量大小和每次扩充容量大小的不同 **: ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
    5. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
    6. 推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。

    如何决定使用 HashMap 还是 TreeMap?

    对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。

    HashMap 和 ConcurrentHashMap 的区别

    1. ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。)
    2. HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。

    ConcurrentHashMap 和 Hashtable 的区别?

    ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。

    • 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
    • 实现线程安全的方式(重要): ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

    两者的对比图

    HashTable:

    img

    JDK1.7的ConcurrentHashMap:

    img

    JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):

    img

    答:ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题。但是 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。

    ConcurrentHashMap 底层具体实现知道吗?实现原理是什么?

    JDK1.7

    首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。

    在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下:

    一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。

    img

    1. 该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色;
    2. Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。

    JDK1.8

    JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

    结构如下:

    img

    附加源码,有需要的可以看看

    插入元素过程(建议去看看源码):

    如果相应位置的Node还没有初始化,则调用CAS插入相应的数据;

    else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
            break;                   // no lock when adding to empty bin
    }
    

    如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如果该节点的hash不小于0,则遍历链表更新节点或插入新节点;

    if (fh >= 0) {
        binCount = 1;
        for (Node<K,V> e = f;; ++binCount) {
            K ek;
            if (e.hash == hash &&
                ((ek = e.key) == key ||
                 (ek != null && key.equals(ek)))) {
                oldVal = e.val;
                if (!onlyIfAbsent)
                    e.val = value;
                break;
            }
            Node<K,V> pred = e;
            if ((e = e.next) == null) {
                pred.next = new Node<K,V>(hash, key, value, null);
                break;
            }
        }
    }
    
    1. 如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值;
    2. 如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount;

    辅助工具类

    Array 和 ArrayList 有何区别?

    • Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。
    • Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
    • Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。

    对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。

    如何实现 Array 和 List 之间的转换?

    • Array 转 List: Arrays. asList(array) ;
    • List 转 Array:List 的 toArray() 方法。

    comparable 和 comparator的区别?

    • comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序
    • comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序

    一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort().

    Collection 和 Collections 有什么区别?

    • java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。
    • Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。

    TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素?

    TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进 行排 序。

    Collections 工具类的 sort 方法有两种重载的形式,

    第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较;

    第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java 中对函数式编程的支持)。

    展开全文
  • Docker技术( 容器虚拟化技术 )

    万次阅读 多人点赞 2019-10-18 19:59:21
    Docker虚拟化容器技术 第一章 Docker简介诞生背景Docker 介绍虚拟机技术容器虚拟化技术官方网址第二章 Docker安装前提条件安装DockerDocker底层原理Docker结构图工作原理Docker为什么比VM快第三章 Docker常用命令...

    第一章 Docker介绍

    诞生背景

    一款产品从开发到上线,从操作系统,到运行环境,再到应用配置。
    作为开发+运维之间的协作我们需要关心很多东西,这也是很多互联网公司都不得不面对的问题,
    特别是各种版本的迭代之后,不同版本环境的兼容,对运维人员都是考验 .
    Docker之所以发展如此迅速,也是因为它对此给出了一个标准化的解决方案。

    环境配置如此麻烦,换一台机器,就要重来一次,费力费时。
    很多人想到,能不能从根本上解决问题,软件可以带环境安装?
    也就是说,安装的时候,把原始环境一模一样地复制过来。
    开发人员利用 Docker 可以消除协作编码时“在我的机器上可正常工作”的问题。

    开发需要清楚的告诉运维部署团队,用的全部配置文件+所有软件环境。
    不过,即便如此,仍然常常发生部署失败的状况。
    Docker镜像的设计,使得Docker得以打破过去"程序即应用" 的观念。
    透过镜像(images)将作业系统核心除外,运作应用程式所需要的系统环境,
    由下而上打包,达到应用程式跨平台间的无缝接轨运作。

    在这里插入图片描述

    Docker 介绍

    Docker的主要目标是“Build,Ship and Run Any App,Anywhere”,
    也就是通过对应用组件的封装、分发、部署、运行等生命周期的管理,
    使用户的APP(可以是一个WEB应用或数据库应用等等)及其运行环境能够做到“一次封装,到处运行”。

    Linux 容器技术的出现就解决了这样一个问题,而 Docker 就是在它的基础上发展过来的。
    将应用运行在 Docker 容器上面,而 Docker 容器在任何操作系统上都是一致的,
    这就实现了跨平台、跨服务器只需要一次配置好环境,换到别的机子上就可以一键部署好, 大大简化了操作

    总结:
    Docker是解决运行环境和配置问题的软件容器 , 方便做持续集中并有助于整体发布的容器虚拟化技术

    在这里插入图片描述

    虚拟机技术与容器虚拟化技术

    虚拟机技术

    虚拟机(virtual machine)就是带环境安装的一种解决方案。
    可以在一种操作系统里面运行另一种操作系统,比如在Windows 系统里面运行Linux 系统。
    应用程序对此毫无感知,因为虚拟机看上去跟真实系统一模一样
    缺点 :1 资源占用多 2 冗余步骤多 3 启动慢

    在这里插入图片描述

    容器虚拟化技术

    Linux 容器(Linux Containers,缩写为 LXC)。
    Linux 容器不是模拟一个完整的操作系统,而是对进程进行隔离。
    有了容器,就可以将软件运行所需的所有资源打包到一个隔离的容器中。
    容器与虚拟机不同,不需要捆绑一整套操作系统,只需要软件工作所需的库资源和设置
    系统因此而变得高效轻量并保证部署在任何环境中的软件都能始终如一地运行。

    在这里插入图片描述
    二者区别

    1. 传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;
    2. 而容器内的应用进程直接运行于宿主机( 即:所在主机,下面统称宿主机 ) 的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。
    3. 每个容器之间互相隔离,每个容器有自己的文件系统 ,容器之间进程不会相互影响,能区分计算资源。

    官方网址

    官方网址 : https://www.docker.com
    Docker社区官方:https://hub.docker.com/

    第二章 Docker安装

    Docker支持以下的CentOS版本:CentOS 7 (64-bit) ,CentOS 6.5 (64-bit) 或更高的版本

    前提条件

    目前,CentOS 仅发行版本中的内核支持 Docker。
    Docker 运行在 CentOS 7 上,要求系统为64位、系统内核版本为 3.10 以上。
    Docker 运行在 CentOS-6.5 或更高的版本的 CentOS 上,要求系统为64位、系统内核版本为 2.6.32-431 或者更高版本。

    # 查看内核版本
    uname -r 
    

    安装Docker

    Docker安装教程(CentOS 7)本人阿里云服务器装成功
    Docker安装教程(CentOS 7)本人腾讯云服务器装成功
    Docker安装教程(CentOS 6)

    Docker管理命令

    # 重新载入后台启动进程
    systemctl daemon-reload
    
    # 启动docker容器 ,每次关闭虚拟机/运服务器时需要启动( 重要!!! )
    sudo service docker start
    
    # 查看Docker容器状态
    sudo service docker status (should see active (running))
    
    # 运行hello-world镜像/测试Docker是否可以使用
    sudo docker run hello-world
    

    Docker底层原理

    Docker结构图

    在这里插入图片描述
    在这里插入图片描述

    工作原理

    Docker是一个Client-Server结构的系统,Docker守护进程运行在主机上,
    然后通过Socket连接从客户端访问,守护进程从客户端接受命令并管理运行在主机上的容器。
    容器,是一个运行时环境,就是我们前面说到的集装箱。

    在这里插入图片描述

    Docker为什么比VM快

    (1) docker有着比虚拟机更少的抽象层。
    由亍docker不需要Hypervisor实现硬件资源虚拟化,
    运行在docker容器上的程序直接使用的都是实际物理机的硬件资源。
    因此在CPU、内存利用率上docker将会在效率上有明显优势。
    在这里插入图片描述

    (2) docker利用的是宿主机的内核,而不需要Guest OS。
    因此,当新建一个容器时,docker不需要和虚拟机一样重新加载一个操作系统内核。
    因而避免引寻、加载操作系统内核返个比较费时费资源的过程,
    当新建一个虚拟机时,虚拟机软件需要加载Guest OS,返个新建过程是分钟级别的。
    而docker由于直接利用宿主机的操作系统,则省略了返个过程,因此新建一个docker容器只需要几秒钟。
    通过下图着重了解二者的比较( 图很重要!!!)
    在这里插入图片描述

    第三章 Docker常用命令

    帮助命令

    # 查看docker版本信息
    docker version
    
    # 查看docker所有安装信息
    docker info
    
    # 查看docker帮助 ( 最为重要,学会利用帮助文档来学习 ,这是成为高手的必经之路!!! )
    docker --help
    

    镜像命令

    # 1.列出本地镜像(图1)
    docker images
    
    # option说明
     -a :列出所有镜像
     -q:只列出镜像id
     -digests :显示镜像的摘要信息
    --no-trunc :显示相信信息
    
    
    # 2.搜索某个镜像名字 (会在docker社区搜索相关的镜像, 而我们配置的阿里源只是帮我们同步了docker社区的镜像,图2)
    docker search 
    
    # option说明
    	- s显示收藏数不小于指定值的镜像
        --no-trunc :显示相信信息
        - automated :只列出automated类型的镜像
    
    # 3. 下载镜像(图3)
    docker pull 镜像的名字:[TAG](不写默认为 lasted)
    
    # 4. 删除镜像
    docker rmi  -f 镜像名/镜像id
    docker rmi -f $(docker images -qa )
       
    

    各个选项说明:
    REPOSITORY:表示镜像的仓库源
    TAG:镜像的标签
    IMAGE ID:镜像ID
    CREATED:镜像创建时间
    SIZE:镜像大小
    同一仓库源可以有多个 TAG,代表这个仓库源的不同个版本,我们使用 REPOSITORY:TAG 来定义不同的镜像。
    如果你不指定一个镜像的版本标签,例如你只使用 ubuntu,docker 将默认使用 ubuntu:latest 镜像

    图1
    在这里插入图片描述
    图2
    在这里插入图片描述

    图3
    在这里插入图片描述

    容器命令

    基本命令

    # 1. 新建并启动容器
    docker run [OPTIONS] IMAGE [COMMOND] [ARGS...]
    
    # OPTIONS 说明
    	--name="容器新名字": 为容器指定一个名称;
    	-d: 后台运行容器,并返回容器ID,也即启动守护式容器;
    	-i:以交互模式运行容器,通常与 -t 同时使用;
    	-t:为容器重新分配一个伪输入终端,通常与 -i 同时使用;
    	-P: 随机端口映射;
    	-p: 指定端口映射,有以下四种格式
    	      ip:hostPort:containerPort
    	      ip::containerPort
    	      hostPort:containerPort
    	      containerPort
    	      
    # eg: 使用镜像centos:latest以交互模式启动一个容器,在容器内执行/bin/bash命令。
    docker run -it centos /bin/bash 
    
     
    # 2. 查询当前正在运行的容器(类比虚拟中的查看进程)
    docker ps [OPTIONS] 
    
    # OPTIONS说明(常用):
    	-a :列出当前所有正在运行的容器+历史上运行过的
    	-l :显示最近创建的容器。
    	-n:显示最近n个创建的容器。
    	-q :静默模式,只显示容器编号。
    	--no-trunc :不截断输出。
    
    
    # 3. 退出容器
    exit  容器停止后退出
    ctrl+p+q  容器不停止退出
    
    # 4 .启动容器
    docker start 容器ID/容器名
    
    # 5. 重启容器 
    docker restart
    
    # 6. 停止容器
    docker stop 容器ID/容器名
    
    # 7. 强制停止容器
    docker kill 容器ID/容器名
    
    # 8. 删除已经停止的容器(如果没有停止,删除后悔停止)
    docker rm 容器ID
    删除多个容器(特别是第二种 ,先查询所有运行的进程, 然后通过管道传到后面的删除操作中)
    docker rm -f $(docker ps -a -q)
    docker ps -a -q | xargs docker rm
    

    重要命令

    # 1. 启动守护式容器(后台运行)
    docker -d 容器名
    
    # 使用镜像centos:latest以后台模式启动一个容器
    docker run -d centos
    
    问题:然后docker ps -a 进行查看, 会发现容器已经退出
    很重要的要说明的一点: Docker容器后台运行,就必须有一个前台进程.
    容器运行的命令如果不是那些一直挂起的命令(比如运行top,tail),就是会自动退出的。
    
    这个是docker的机制问题,比如你的web容器,我们以nginx为例,正常情况下,我们配置启动服务只需要启动响应的service即可。例如service nginx start
    但是,这样做,nginx为后台进程模式运行,就导致docker前台没有运行的应用,
    这样的容器后台启动后,会立即自杀因为他觉得他没事可做了.
    所以,最佳的解决方案是,将你要运行的程序以前台进程的形式运行
    
    # 2. 查看容器日志
    docker logs -f -t --tail 容器ID
    
    eg :(图1)
    docker run -d centos /bin/sh -c "while true;do echo hello testLogs;sleep 2;done"
    docker logs -tf --tail 10 02c81778b0e0
    
    -t 是加入时间戳
    -f 跟随最新的日志打印
    --tail 数字 显示最后多少条
    
    
    # 3. 查看容器内运行的进程(图2)
    docker top 容器ID
    
    # 4. 查看容器内部细节(图3)
    docker inspect 容器ID
    
    # 5.进入正在运行的容器并进行交互(图4)
    docker exec -it 容器ID bashShell
    
    # 6. 重新进入容器(图5)
    docker attach 容器ID bashShell(不写的话默认 /bin/bash下)
    
    # 比较5与6的区别
    attach 直接进入容器启动命令终端, 不会启动新的线程
    exec 是在容器中打开新的终端, 并且可以启动新的线程
    

    图1
    在这里插入图片描述

    图2
    在这里插入图片描述
    图3
    在这里插入图片描述
    图4
    在这里插入图片描述
    图5
    在这里插入图片描述

    Docker常用命令图解

    在这里插入图片描述

    第四章 Docker镜像

    镜像 / UnionFS(联合文件系统)

    Union文件系统(UnionFS)是一种分层、轻量级并且高性能的文件系统
    它支持对文件系统的修改作为一次提交来一层层的叠加,
    同时可以将不同目录挂载到同一个虚拟文件系统下。Union 文件系统是 Docker 镜像的基础。
    镜像可以通过分层来进行继承. 基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。

    特性
    一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,
    联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录

    在这里插入图片描述
    总结:
    镜像就像洋葱圈 ,就像莲花卷 ,一层套一层 ,对外只显示一个文件系统.
    而且, 这种分层镜像还可以复用

    特点

    Dcoker镜像都是只读的 , 当启动容器时, 一个新的可写层被加载到镜像的顶部
    这一层被称为"容器层", "容器层"执行的都称为"镜像层"

    Docker镜像加载原理:

    docker的镜像实际上由一层一层的文件系统组成,这种层级的文件构成文件系统UnionFS。
    在这里插入图片描述
    bootfs(boot file system) 主要包含bootloader和kernel,
    bootloader主要作用是引导加载kernel, Linux刚启动时会加载bootfs文件系统,Docker镜像的最底层是bootfs
    这一层与典型的Linux/Unix系统是一样的,包含boot加载器和内核。
    当boot加载完成之后整个内核就都在内存中了,此时内存的使用权已由bootfs转交给内核,此时系统也会卸载bootfs。

    rootfs (root file system) ,在bootfs之上。
    包含的就是典型 Linux 系统中的 /dev, /proc, /bin, /etc 等标准目录和文件。
    rootfs就是各种不同的操作系统发行版,比如Ubuntu,Centos等等。

    平时我们安装进虚拟机的CentOS都是好几个G,为什么docker这里才220M

    在这里插入图片描述
    对于一个精简的OS,rootfs可以很小,只需要包括最基本的命令、工具和程序库就可以了,
    因为底层直接用Host的kernel,自己只需要提供 rootfs 就行了。
    由此可见对于不同的linux发行版, bootfs基本是一致的, rootfs会有差别, 因此不同的发行版可以公用bootfs。

    Docker镜像Commit操作

    # 提交容器副本实质成为一个新的镜像
    docker commit -m="提交的描述信息" -a="作者"	容器ID 要创建的目标镜像名:[标签名]
    
    
    eg: 同时docker运行tomcat案例
    # 1.  创建交互式界面 ( 相当于前台启动)
    docker run -it -p 8888:8080 tomcat   # 端口映射将8888作为docker的入口,映射到tomcat镜像的8080(图1,图2)
    docker run -it -P  tomcat  #自动端口映射(通过 docker ps查看端口, 图3)
    #后台启动(不会占用当前页面)
    docker run -d -p 8888:8080 tomcat 
    
    
    # 2. 进入tomcat (如果前置启动,另开一个窗口 ,后置启动则直接进入)
    # 查看运行容器ID
    docker ps  
    # 进入根目录
    docker exec -it 容器ID /bin/bash
    /usr/local/tomcat# rm -rf webapps/docs/ 
    # 删除后访问tomcat ,可以看到访问主页下的文档出现404(图4)
    
    # 3. 提交容器副本实使之为一个新的镜像
    # 查看运行容器ID
    docker ps  
    # 提交容器
    docker commit -m="del tomcat docs" -a="timepaus" 容器ID tomcat:v1.2
    # 查看镜像(图5)
    docker images
    
    # 4.同时启动新镜像并和原来的对比
    可以看到我们自己提交的新镜像也没有文档()
    但是我们重新下载的tomcat是有的
    

    图1
    在这里插入图片描述
    图2
    在这里插入图片描述
    图3
    在这里插入图片描述
    图4

    在这里插入图片描述
    图5
    在这里插入图片描述

    第五章 Docker容器数据卷

    简介

    类似Redis中的rdb文件和aof文件
    用于容器的持久化和荣期间的继承与共享数据

    容器内添加数据卷

    1.直接命令添加

    # 通过-v命令直接添加 (需要在根目录下创建 containerData  )
    # 创建后修改容器内containerData 会同步到hostData ,反之亦是如此
    docker run -it -v /hostData:/containerData  镜像名/ID
    
    # 创建只读权限的文件 , 容器内只能读取这个文件, 但是不能修改. ro: readonly
    docker run -it -v /宿主机绝路路径目录:/容器内目录:ro 镜像名
    

    2.Docker添加

    DockerFile的简单理解
    在这里插入图片描述

    # 1. 宿主机根目录下创建mydocker文件夹并进入
    
    
    # 2. 在mydocker文件想创建DockerFile文件并编辑,内容如下(图1)
    
    # volume test
    FROM centos
    VOLUME ["/dataVolumeContainer1","/dataVolumeContainer2"]
    CMD echo "finished,--------success1"
    CMD /bin/bash
    
    说明:
    出于可移植和分享的考虑,用-v 主机目录:容器目录这种方法不能够直接在Dockerfile中实现。
    由于宿主机目录是依赖于特定宿主机的,并不能够保证在所有的宿主机上都存在这样的特定目录。
    
    
    3. 使用docker build命令创建一个镜像, 并挂载指定宿主机指定文件
    docker build -f /mydocker/DockerFile -t 定义的新镜像名称
    
    4. 查看镜像
    docker images
    
    5.运行新镜像
    docker run -it 定义的新镜像ID /bin/bash
    
    6.通过镜像运行后生成的容器ID查看 (图2), 对应的主机映射的数据卷地址
    docker ps
    docker inspect 容器ID
    
    7.测试添加卷和主机是否互通有无
    在容器中创建文件, 看看主机中是否创建对应的文件
    
    注意: 
    Docker挂载主机目录(第3步)Docker访问出现cannot open directory .: Permission denied
    解决办法:在挂载目录后多加一个--privileged=true参数即可
    

    图1
    在这里插入图片描述

    图2, 查看运行的容器id相关信息 docker inspect 容器ID
    在这里插入图片描述
    对比与总结
    在这里插入图片描述

    数据卷容器

    命名的容器挂载数据卷,其它容器通过挂载这个(父容器)实现数据共享,
    挂载数据卷的容器,称之为数据卷容器

    实现步骤

    # 1. 以上一步所建镜像为模板 ,首先启动一个父容器dc01 ,在创建容器的数据卷文夹中创建一个文件(图1)
    docker run -it --name 	dc01 zzyy/centos
    touch dc01_add.txt
    
    # 2. 创建子容器dc02,dc03继承自dc01 , 在创建容器的数据卷文夹中创建一个文件(图2)
    docker run -it --name dc02 --volumes-from dc01 zzyy/centos
    touch dc02_add.txt
    docker run -it --name dc03 --volumes-from dc01 zzyy/centos
    touch dc01=3_add.txt
    
    
    # 3. 回到dc01可以看到所有数据都被共享了(图3)
    # 4. 删除dc01, dc02修改后dc03是否可访问,可以 (图4)
    # 5. 删除dc02dc03是否可以访问, 可以(图5)
    # 6. 新建dc04继承dc03是否可以访问, 可以(图6)
    
    结论
    容器之间配置信息的传递, 数据卷的生命周期一直持续到没有容器使用它为止
    

    图1
    在这里插入图片描述
    图2
    在这里插入图片描述
    图3
    在这里插入图片描述
    图4
    在这里插入图片描述

    图5
    在这里插入图片描述
    图6
    在这里插入图片描述

    第六章 DockerFile解析

    介绍

    Dockerfile是用来构建Docker镜像文件 ,是有一系列命令和参数构成的脚本

    构建步骤

    1.编写Dockerfile文件
    2.docker build
    3.docker run

    格式

    以centos的文件为例

    FROM scratch
    ADD centos-8-container.tar.xz /
    
    LABEL org.label-schema.schema-version="1.0" \
        org.label-schema.name="CentOS Base Image" \
        org.label-schema.vendor="CentOS" \
        org.label-schema.license="GPLv2" \
        org.label-schema.build-date="20190927"
    
    CMD ["/bin/bash"]
    

    语法

    1. 每条保留字指令都必须为答谢字母且后面至少跟一个参数
    2. 指令从上到下, 顺序执行
    3. #表示注释
    4. 每条指令都会创建一个一个新的镜像层, 并提交

    Docker执行DockerFile的大致流程

    1. docker从基础镜像运行一个容器
    2. 执行一条指令并对容器做出修改
    3. 执行类似docker commit 的操作提交一个新的镜像层
    4. docker 再基于刚提交的镜像运行一个新容器
    5. 执行dockerfile 中的下一条执行, 直至所有指令都完成

    从应用软件的角度来看,Dockerfile、Docker镜像与Docker容器分别代表软件的三个不同阶段,

    • Dockerfile是软件的原材料
    • Docker镜像是软件的交付品
    • Docker容器则可以认为是软件的运行态。

    Dockerfile面向开发,Docker镜像成为交付标准,
    Docker容器则涉及部署与运维,三者缺一不可,合力充当Docker体系的基石。

    在这里插入图片描述

    1 Dockerfile,需要定义一个Dockerfile,Dockerfile定义了进程需要的一切东西。Dockerfile涉及的内容包括执行代码或者是文件、环境变量、依赖包、运行时环境、动态链接库、操作系统的发行版、服务进程和内核进程(当应用进程需要和系统服务和内核进程打交道,这时需要考虑如何设计namespace的权限控制)等等;
    2 Docker镜像,在用Dockerfile定义一个文件之后,docker build时会产生一个Docker镜像,当运行 Docker镜像时,会真正开始提供服务;
    3 Docker容器,容器是直接提供服务的

    DockerFile保留字指令

    查看tomcat的DockerFile文件 ,更好理解保留字
    在这里插入图片描述

    保留字整合案例

    自定义镜像

    我们安装的centos精简版是没有vim和ifconfig命令的支持的
    我们可以通过编写Dockerfile令其支持这些功能

    # 1.搜索centos镜像并下载
    docker search centos
    docker pull centos
    
    # 2.编写创建一个文件( /mydoker/dockerfile_centos ),编写Dockerfile ,内容如下
    --------------------------------------------------------------------
    FROM centos
    MAINTAINER timepause<qq_43371556@csdn.net>
    
    ENV MYPATH /usr/local
    WORKDIR $MYPATH
    
    RUN yum -y install vim
    RUN yum -y install net-tools
    
    EXPOSE 80
    
    CMD echo $MYPATH
    CMD echo "install vim and ifconfig commond plug-in components success"
    CMD /bin/bash
    -----------------------------------------------------------------------
    
    
    
    # 3. 编译运行centos( 注意最后的空格和点号,图1 )
    docker build -f /mydoker/dockerfile_centos -t mycentos:1.3 .
    
    # 4. 测试功能( 图2 )
    # 查看是否新增镜像
    docker ps
    # 运行新镜像 
    docker run -it mycentos:1.3
    # 在容器中使用vim 与if config命令
    
    

    图1
    在这里插入图片描述

    图2
    在这里插入图片描述

    自定义tomcat9

    # 1. 创建存放生成镜像文件的目录
    mkdir -p /myuse/mydocker/tomcat9
    
    # 2.创建相关文件(c.txt用于测试)
    touch c.txt 
    
    
    # 3.上传tomcat和jdk(一定要注意版本, 根据版本修改Dockerfile文件, 图1)
    
    #. 4.创建并编辑Dockerfile(需要提前下载centos)
    vim Dockerfile
    ---------------------Dockerfile-------------------------------
    FROM         centos
    MAINTAINER    chy<chy@qq.com>
    #把宿主机当前上下文的c.txt拷贝到容器/usr/local/路径下
    COPY c.txt /usr/local/cincontainer.txt
    #把java与tomcat添加到容器中
    ADD jdk-8u11-linux-x64.tar.gz /usr/local/
    ADD apache-tomcat-9.0.27.tar.gz /usr/local/
    #安装vim编辑器
    RUN yum -y install vim
    #设置工作访问时候的WORKDIR路径,登录落脚点
    ENV MYPATH /usr/local
    WORKDIR $MYPATH
    #配置java与tomcat环境变量
    ENV JAVA_HOME /usr/local/jdk1.8.0_11
    ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
    ENV CATALINA_HOME /usr/local/apache-tomcat-9.0.27
    ENV CATALINA_BASE /usr/local/apache-tomcat-9.0.27
    ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/lib:$CATALINA_HOME/bin
    #容器运行时监听的端口
    EXPOSE  8080
    #启动时运行tomcat
    # ENTRYPOINT ["/usr/local/apache-tomcat-9.0.8/bin/startup.sh" ]
    # CMD ["/usr/local/apache-tomcat-9.0.8/bin/catalina.sh","run"]
    CMD /usr/local/apache-tomcat-9.0.27/bin/startup.sh && tail -F /usr/local/apache-tomcat-9.0.27/bin/logs/catalina.out
    ---------------------Dockerfile-------------------------------
    
    
    
    
    # 5.构建镜像(在存放这些文件的目录下,图2)
    # 如果Dockerfile文件名称就是Dockerfile,可以省略参数 -f /Dockerfile所在位置的书写
    docker build -t mytomcat9 .
    
    # 6.查看镜像并运行镜像( 图3)
    docker images
    # 运行镜像 (后台启动, 成功后如图4)
    docker run -d -p 8080:8080 --name myt9 -v /myuse/mydocker/tomcat9/test:/usr/local/apache-tomcat-9.0.27/webapps/test -v /myuse/mydocker/tomcat9/tomcat9logs/:/usr/local/apache-tomcat-9.0.27/logs --privileged=true  mytomcat9
    
    # 注意这里配置了容器卷的映射,宿主机的test文件夹映射到tomcat的webapps目录下的test文件,且二者可以互通有无. 
    docker exec -it 容器ID /bin/bash #进入容器根目录 ,可以访问相关页面
    
    # 7. 访问成功后, 添加jsp文件与web.xml文件( a.jsp用于访问测试 ,web.xml用于令jsp页面可以编译)
    vim a.jsp
    mkidr WEB-INF
    vim /WEB-INF/web.xml
    ------------------------------------------a.jsp---------------------------
    <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
      <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Insert title here</title>
      </head>
      <body>
    
        -----------welcome------------
        <="i am in docker tomcat self "%>
        <br>
        <br>
        <% System.out.println("=============docker tomcat self");%>
      </body>
    </html>
    ------------------------------------------a.jsp---------------------------
    
    
    
    ------------------------------------------web.xml-----------------------------------
    <?xml version="1.0" encoding="UTF-8"?>
    
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                          http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
      version="4.0"
      metadata-complete="true">
    </web-app>
    ------------------------------------------web.xml-----------------------------------
    
    # 8. 重启服务, 访问a.jsp(图5)
    # 查看当前容器id
    docker ps (查看当前容器ID)
    docker ps -a (查看所有运行过的容器ID)
    # 重启服务
    docker restart 容器ID
    # 访问a.jsp页面
    

    图1
    在这里插入图片描述
    图2
    在这里插入图片描述

    图3
    在这里插入图片描述
    图4

    在这里插入图片描述
    图5

    在这里插入图片描述

    第七章 通过Docker安装软件

    MySQL

    # 1. 以安装mysql5.6为例(不知道什么原因5.7下不来)
    docker pull mysql:5.6
    
    # 2. 运行镜像,启动容器(端口3306,root用户密码:root,运行镜像:mysql5.6)
    docker run -p 3306:3306 --name mysql -v /datebase/mysql/conf:/etc/mysql/conf.d -v /datebase/mysql/logs:/logs -v /datebase/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=root -d mysql:5.6
    
    # 3. 进入msql内部测试登陆
    docker ps
    ## 进入mysql的当前目录下
    docker exec -it MySQL运行成功后的容器ID/name     /bin/bash
    ## 输入账号密码
    mysql -u 账号 -p(然后根据提示输入密码)
    
    
    # 4.连接数据库的图形化界面
    ## Mysql 解决连接客户端时出现1251 client does not support ...问题
    ALTER USER  'root'@'%' IDENTIFIED WITH mysql_native_password BY 'mysql密码';
    ALTER USER  'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'mysql密码';
    
    # 5. 备份数据库数据,之后我们可以通过直接读取这个sql文件恢复数据
    docker exec myql服务容器ID sh -c ' exec mysqldump --all-databases -uroot -p"root" ' > /datebase/all-databases.sql
    

    在这里插入图片描述

    Redis

    # 1.下载最新版redis镜像
    docker pull redis
    
    # 2.运行镜像(redis端口2333, -d后的redis不加参数默认为redis:latest)
    # 注意本地数据卷与容器内数据卷目录的对应
    docker run -p 2333:6379 -v /myuse/myredis/data:/data -v /myuse/myredis/conf/redis.conf:/usr/local/etc/redis/redis.conf  -d redis redis-server /usr/local/etc/redis/redis.conf --appendonly yes
    
    # 3.上传redis.conf到本地数据卷所在目录
    本地数据卷坐在目录 : /myuse/myredis/conf
    文件地址(太大了 ,这里放不下) : https://download.csdn.net/download/qq_43371556/11889084
    
    # 4. 运行redis的cil----shell命令行
    docker exec -it 运行着Rediis服务的容器ID redis-cli
    # 远程连接 docker redis
    docker exec -it redis_s redis-cli -h 192.168.1.100 -p 6379 -a your_password //如果有密码 使用 -a参数
    
    

    RabbitMQ

    # 1.下载镜像
    docker pull rabbitmq:3.7.7-management
    
    # 2.运行镜像
    docker run -d --name rabbitmq3.7.7 -p 5672:5672 -p 15672:15672 -v `pwd`/data:/var/lib/rabbitmq --hostname myRabbit -e RABBITMQ_DEFAULT_VHOST=/ -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin df80af9ca0c9
    
    -d 后台运行容器;
    --name 指定容器名;
    -p 指定服务运行的端口(5672:应用访问端口;15672:控制台Web端口号);
    -v 映射目录或文件;
    --hostname  主机名(RabbitMQ的一个重要注意事项是它根据所谓的 “节点名称” 存储数据,默认为主机名);
    -e 指定环境变量;(RABBITMQ_DEFAULT_VHOST:默认虚拟机名;RABBITMQ_DEFAULT_USER:默认的用户名;RABBITMQ_DEFAULT_PASS:默认用户名的密码)
    
    
    # 3.访问 (如下图)
    http://ip:15672
    账号 :admin
    密码: admin
    

    在这里插入图片描述

    MongoDB

    # 1.查看docker+MongoDB版本(图1)
    docker search
    
    # 2.下载最新的MongoDB
    docker pull mongo
    
    # 3.将镜像运行成容器
    docker run -itd --name mongo -p 27017:27017 mongo --auth
    ## 参数说明:
    -p 27017:27017 :映射容器服务的 27017 端口到宿主机的 27017 端口。外部可以直接通过 宿主机 ip:27017 访问到 mongo 的服务。
    --auth:需要密码才能访问容器服务。
    
    # 4.使用以下命令添加用户和设置密码,并且尝试连接。
    $ docker exec -it mongo mongo admin
    # 创建一个名为 admin,密码为 123456 的用户。
    >  db.createUser({ user:'admin',pwd:'123456',roles:[ { role:'userAdminAnyDatabase', db: 'admin'}]});
    # 尝试使用上面创建的用户信息进行连接。
    > db.auth('admin', '123456')
    

    图1
    在这里插入图片描述

    安装vsftpd+Nignx(图片服务器)

    环境搭建

    通过搭建Nginx和vfstpd 实现文件服务器

    # 1. 安装docker-compose . 实现对相关软件的配置
    curl -L https://get.daocloud.io/docker/compose/releases/download/1.25.4/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
    chmod +x /usr/local/bin/docker-compose
    
    # 2. 编写docker-compose.yml 配置文件
    ## 需要需要修改的地方的是: 
    ## /data/ftp:/home/vsftpd/ftpuser(20行), /data/ftp 是ftp中图片存放的地址
    ## /data/ftp:/home/images(11行) , /data/ftp 是Nginx来对应ftp中图片存放的地址
    ## FTP_USER,FTP_PASS 代表ftp用户的账号和密码, 需要在Linux中提前添加用户 
    ## 添加用户命令 a. useradd ftpuser,b. passwd ftpuser 然后输入自定义密码  
    ## ~/pic/nginx/conf/nginx.conf(9行), 注意! 需要提前准备Nginx的配置文件nginx.conf, ~ 代表home目录
    ## PASV_ADDRESS: 192.168.40.21, PASV_ADDRESS后面填写的是ftp服务器的ip
    ----------------docker-compose.yml---------------------
    version: '2'
    services:
      nginx-image:
        image: nginx:1.15.8
        ports:
          - "80:80"
        volumes:
          - ~/pic/nginx/html:/usr/share/nginx/html
          - ~/pic/nginx/conf/nginx.conf:/etc/nginx/nginx.conf
          - ~/pic/nginx/logs:/var/log/nginx
          - /data/ftp:/home/images
        restart: always
      vsftpd:
        image: fauria/vsftpd
        ports:
          - "20:20"
          - "21:21"
          - "21100-21110:21100-21110"
        volumes:
          - /data/ftp:/home/vsftpd/ftpuser
        environment:
          FTP_USER: ftpuser
          FTP_PASS: 1q2w3e4r
          PASV_ADDRESS: 192.168.40.21
          PASV_MIN_PORT: 21100
          PASV_MAX_PORT: 21110
          LOCAL_UMASK: 022
        restart: always
    -------------------------------------
    
    # 3. 分享上面需要nginx.conf文件放在下面了
    vim /home/pic/nginx/conf/nginx.conf
    
    # 4. docker-compose执行, 需要在docker-compose.yml所在的目录下执行
    docker-compose up -d
    
    # 5. docker ps(查看容器是否执行, 下图1)
    

    图1
    在这里插入图片描述

    上面第三步所需的nginx.conf配置文件

    # root   /data/ftp/, /data/ftp/代表的是我们上面配置的ftp图片存放的地址, 只需要修改这一处
    location / { root   /data/ftp/;i ..... }
    

    完整配置文件

    #user  nobody;
    worker_processes  1;
    
    #error_log  logs/error.log;
    #error_log  logs/error.log  notice;
    #error_log  logs/error.log  info;
    
    #pid        logs/nginx.pid;
    
    
    events {
        worker_connections  1024;
    }
    
    
    http {
        include       mime.types;
        default_type  application/octet-stream;
    
        #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
        #                  '$status $body_bytes_sent "$http_referer" '
        #                  '"$http_user_agent" "$http_x_forwarded_for"';
    
        #access_log  logs/access.log  main;
    
        sendfile        on;
        #tcp_nopush     on;
    
        #keepalive_timeout  0;
        keepalive_timeout  65;
    
        #gzip  on;
    
        server {
            listen       80;
            server_name  localhost;
    
            #charset koi8-r;
    
            #access_log  logs/host.access.log  main;
    
            location / {
                root   /data/ftp/; # /data/ftp/代表的是我们上面配置的ftp图片存放的地址, 只需要修改这一处
                index  index.html index.htm;
            }
    
            #error_page  404              /404.html;
    
            # redirect server error pages to the static page /50x.html
            #
            error_page   500 502 503 504  /50x.html;
            location = /50x.html {
                root   html;
            }
        }
    
    
    
    }
    
    

    代码测试

    1. 添加依赖
    <!--文件上传与下载-->
             <dependency>
                 <groupId>commons-net</groupId>
                 <artifactId>commons-net</artifactId>
                 <version>3.3</version>
             </dependency>
             <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-api</artifactId>
                <version>1.7.7</version>
                <scope>compile</scope>
            </dependency>
    
    1. 工具类代码
    package com.clife.common.utils;
    
    import org.apache.commons.net.ftp.FTP;
    import org.apache.commons.net.ftp.FTPClient;
    import org.apache.commons.net.ftp.FTPFile;
    import org.apache.commons.net.ftp.FTPReply;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Value;
    
    import java.io.*;
    
    /**
     * ftp上传下载工具类
     */
    public class FtpUtil {
    
    	private static   String hostname="192.168.40.21";
    	private static   int port=21;
    	private static   String ftpusername="ftpuser";
    	private static   String ftppassword="ftpuser";
    	private static   String basePath="/data/ftp";
    	private static   String filePath="";
    
    	//日志打印
    	private static Logger logger = LoggerFactory.getLogger(FtpUtil.class);
    
    	/**
    	 * 文件上传封装方法
    	 * @param sourceFileName 本地文件绝对地址,目录+文件名, eg:D:\\1.jpg
    	 * @param targetFileName 上传到ftp服务器/data/ftp/目录下的文件名
    	 * @return
    	 * @throws FileNotFoundException
    	 */
    	public static boolean ftpUploadFile(String sourceFileName, String targetFileName) throws FileNotFoundException {
    		FileInputStream in=new FileInputStream(new File(sourceFileName));
    		boolean b = uploadFile(hostname, port, ftpusername, ftppassword, basePath, filePath, targetFileName, in);
    		return b;
    	}
    
    	/**
    	 * Description: 向FTP服务器上传文件
    	 * @param host FTP服务器hostname
    	 * @param port FTP服务器端口
    	 * @param username FTP登录账号
    	 * @param password FTP登录密码
    	 * @param basePath FTP服务器基础目录
    	 * @param filePath FTP服务器文件存放路径。例如分日期存放:/2015/01/01。文件的路径为basePath+filePath
    	 * @param filename 上传到FTP服务器上的文件名
    	 * @param input 输入流
    	 * @return 成功返回true,否则返回false
    	 */
    public static boolean uploadFile(String host, int port, String username, String password, String basePath,
    			String filePath, String filename, InputStream input) {
    		boolean result = false;
    		FTPClient ftp = new FTPClient();
    		try {
    			int reply;
    			ftp.connect(host, port);// 连接FTP服务器
    			logger.info("连接ftp成功...");
    			// 如果采用默认端口,可以使用ftp.connect(host)的方式直接连接FTP服务器
    			ftp.login(username, password);// 登录
    			logger.info("ftp用户登陆成功!!!");
    			reply = ftp.getReplyCode();
    			if (!FTPReply.isPositiveCompletion(reply)) {
    				ftp.disconnect();
    				return result;
    			}
    			//切换到上传目录
    			if (!ftp.changeWorkingDirectory(basePath+filePath)) {
    				//如果目录不存在创建目录
    				String[] dirs = filePath.split("/");
    				String tempPath = basePath;
    				for (String dir : dirs) {
    					if (null == dir || "".equals(dir)) {continue;}
    					tempPath += "/" + dir;
    					if (!ftp.changeWorkingDirectory(tempPath)) {
    						if (!ftp.makeDirectory(tempPath)) {
    							return result;
    						} else {
    							ftp.enterRemotePassiveMode();
    							ftp.changeWorkingDirectory(tempPath);
    						}
    					}
    				}
    			}
    			//设置上传文件的类型为二进制类型
    			ftp.setFileType(FTP.BINARY_FILE_TYPE);
    			//上传文件
    			if (!ftp.storeFile(filename, input)) {
    				logger.info("文件 {} 上传失败!",filename);
    				return result;
    			}
    			input.close();
    			ftp.logout();
    			result = true;
    			logger.info("文件 {} 上传成功!",filename);
    		} catch (IOException e) {
    			e.printStackTrace();
    		} finally {
    			if (ftp.isConnected()) {
    				try {
    					ftp.disconnect();
    				} catch (IOException ioe) {
    				}
    			}
    		}
    		return result;
    	}
    
    
    
    	public static void main(String[] args) {
    		//FileInputStream in=new FileInputStream(new File("D:\\1.jpg"));
    		//boolean flag = uploadFile("192.168.40.21", 21, "ftpuser", "ftpuser", "/data/ftp","", "123.png", in);
    		boolean flag = false;
    		try {
    			flag = ftpUploadFile("D:\\11.jpg", "6666.jpg");
    			System.out.println(flag);
    		} catch (FileNotFoundException e) {
    			e.printStackTrace();
    		}
    
    
    	}
    }
    
    
    1. 图片访问
      http://192.168.40.21/6666.jpg

    第八章 发布镜像到阿里云

    步骤

    # 1.登陆阿里云, 进入容器镜像服务(可以通过下方地址注册登录跳转到容器镜像服务)
    https://cr.console.aliyun.com/cn-hangzhou/instances/repositories
    
    # 2. 创建镜像仓库(图1)
    # 3. 在docker中登陆 ,根据图2,图3中的操作指南进行操作
    
    # 5. 将正在运行的容器创建成一个镜像(图4)
    docker commit 
    
    OPTIONS说明:
    -a :提交的镜像作者;
    -m :提交时的说明文字;
    
    # 4.将镜像推送到阿里云的镜像仓库(最好是根据阿里云上的操作指南)
    docker tag [ImageId] registry.cn-hangzhou.aliyuncs.com/timepause/mydocker:[镜像版本号]
    sudo docker push registry.cn-hangzhou.aliyuncs.com/timepause/mydocker:[镜像版本号]
    
    # 5. 搜索镜像 ,下载镜像(图5,图6)
    docker pull 复制的镜像地址
    

    图1
    在这里插入图片描述
    图2
    在这里插入图片描述
    图3

    在这里插入图片描述

    图4
    在这里插入图片描述
    图5
    在这里插入图片描述
    图6
    在这里插入图片描述


    点击本博客相关学习视频地址
    图片服务器参考博客

    展开全文
  • Spring系列第24篇:父子容器详解

    万次阅读 多人点赞 2020-04-18 16:30:00
    又一次被面试官带到坑里面了。面试官:springmvc用过么?我:用过啊,经常用呢面试官:springmvc中为什么需要用父子容器?我:嗯。。。没听明白你说的什么。面试官:就是contr...

    又一次被面试官带到坑里面了。

    面试官:springmvc用过么?

    我:用过啊,经常用呢

    面试官:springmvc中为什么需要用父子容器?

    我:嗯。。。没听明白你说的什么。

    面试官:就是controller层交给一个spring容器加载,其他的service和dao层交给另外一个spring容器加载,web.xml中有这块配置,这两个容器组成了父子容器的关系。

    我:哦,原来是这块啊,我想起来了,我看大家都这么用,所以我也这么用

    面试官:有没有考虑过为什么?

    我:我在网上看大家都这么用,所以我也这么用了,具体也不知道为什么,不过用起来还挺顺手的

    面试官:如果只用一个容器可以么,所有的配置都交给一个spring容器加载?

    我:应该不行吧!

    面试官:确定不行么?

    我:让我想一会。。。。。我感觉是可以的,也可以正常运行。

    面试官:那我们又回到了开头的问题,为什么要用父子容器呢?

    我:我叫你哥好么,别这么玩我了,被你绕晕了?

    面试官:好吧,你回去试试看吧,下次再来告诉我,出门右转,不送!

    我:脸色变绿了,灰头土脸的走了。

    回去之后,我好好研究了一番,下次准备再去给面试官一点颜色看看。

    主要的问题

    1. 什么是父子容器?

    2. 为什么需要用父子容器?

    3. 父子容器如何使用?

    下面我们就来探讨探讨。

    我们先来看一个案例

    系统中有2个模块:module1和module2,两个模块是独立开发的,module2会使用到module1中的一些类,module1会将自己打包为jar提供给module2使用,我们来看一下这2个模块的代码。

    模块1

    放在module1包中,有3个类

    Service1

    package com.javacode2018.lesson002.demo17.module1;
    
    import org.springframework.stereotype.Component;
    
    @Component
    public class Service1 {
        public String m1() {
            return "我是module1中的Servce1中的m1方法";
        }
    }
    

    Service2

    package com.javacode2018.lesson002.demo17.module1;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    @Component
    public class Service2 {
    
        @Autowired
        private com.javacode2018.lesson002.demo17.module1.Service1 service1; //@1
    
        public String m1() { //@2
            return this.service1.m1();
        }
    
    }
    

    上面2个类,都标注了@Compontent注解,会被spring注册到容器中。

    @1:Service2中需要用到Service1,标注了@Autowired注解,会通过spring容器注入进来

    @2:Service2中有个m1方法,内部会调用service的m1方法。

    来个spring配置类:Module1Config

    package com.javacode2018.lesson002.demo17.module1;
    
    import org.springframework.context.annotation.ComponentScan;
    
    @ComponentScan
    public class Module1Config {
    }
    

    上面使用了@CompontentScan注解,会自动扫描当前类所在的包中的所有类,将标注有@Compontent注解的类注册到spring容器,即Service1和Service2会被注册到spring容器。

    再来看模块2

    放在module2包中,也是有3个类,和模块1中的有点类似。

    Service1

    模块2中也定义了一个Service1,内部提供了一个m2方法,如下:

    package com.javacode2018.lesson002.demo17.module2;
    
    import org.springframework.stereotype.Component;
    
    @Component
    public class Service1 {
        public String m2() {
            return "我是module2中的Servce1中的m2方法";
        }
    }
    

    Service3

    package com.javacode2018.lesson002.demo17.module2;
    
    import com.javacode2018.lesson002.demo17.module1.Service2;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    @Component
    public class Service3 {
        //使用模块2中的Service1
        @Autowired
        private com.javacode2018.lesson002.demo17.module2.Service1 service1; //@1
        //使用模块1中的Service2
        @Autowired
        private com.javacode2018.lesson002.demo17.module1.Service2 service2; //@2
    
        public String m1() {
            return this.service2.m1();
        }
    
        public String m2() {
            return this.service1.m2();
        }
    
    }
    

    @1:使用module2中的Service1

    @2:使用module1中的Service2

    先来思考一个问题

    上面的这些类使用spring来操作会不会有问题?会有什么问题?

    这个问题还是比较简单的,大部分人都可以看出来,会报错,因为两个模块中都有Service1,被注册到spring容器的时候,bean名称会冲突,导致注册失败。

    来个测试类,看一下效果

    package com.javacode2018.lesson002.demo17;
    
    import com.javacode2018.lesson001.demo21.Config;
    import com.javacode2018.lesson002.demo17.module1.Module1Config;
    import com.javacode2018.lesson002.demo17.module2.Module2Config;
    import org.junit.Test;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    
    public class ParentFactoryTest {
    
        @Test
        public void test1() {
            //定义容器
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
            //注册bean
            context.register(Module1Config.class, Module2Config.class); //@1
            //启动容器
            context.refresh();
        }
    }
    

    @1:将Module1Config、Module2Config注册到容器,spring内部会自动解析这两个类上面的注解,即:@CompontentScan注解,然后会进行包扫描,将标注了@Compontent的类注册到spring容器。

    运行test1输出

    下面是部分输出:

    Caused by: org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'service1' for bean class [com.javacode2018.lesson002.demo17.module2.Service1] conflicts with existing, non-compatible bean definition of same name and class [com.javacode2018.lesson002.demo17.module1.Service1]
    

    service1这个bean的名称冲突了。

    那么我们如何解决?

    对module1中的Service1进行修改?这个估计是行不通的,module1是别人以jar的方式提供给我们的,源码我们是无法修改的。

    而module2是我们自己的开发的,里面的东西我们可以随意调整,那么我们可以去修改一下module2中的Service1,可以修改一下类名,或者修改一下这个bean的名称,此时是可以解决问题的。

    不过大家有没有想过一个问题:如果我们的模块中有很多类都出现了这种问题,此时我们一个个去重构,还是比较痛苦的,并且代码重构之后,还涉及到重新测试的问题,工作量也是蛮大的,这些都是风险。

    而spring中的父子容器就可以很好的解决上面这种问题。

    什么是父子容器

    创建spring容器的时候,可以给当前容器指定一个父容器。

    BeanFactory的方式

    //创建父容器parentFactory
    DefaultListableBeanFactory parentFactory = new DefaultListableBeanFactory();
    //创建一个子容器childFactory
    DefaultListableBeanFactory childFactory = new DefaultListableBeanFactory();
    //调用setParentBeanFactory指定父容器
    childFactory.setParentBeanFactory(parentFactory);
    

    ApplicationContext的方式

    //创建父容器
    AnnotationConfigApplicationContext parentContext = new AnnotationConfigApplicationContext();
    //启动父容器
    parentContext.refresh();
    
    //创建子容器
    AnnotationConfigApplicationContext childContext = new AnnotationConfigApplicationContext();
    //给子容器设置父容器
    childContext.setParent(parentContext);
    //启动子容器
    childContext.refresh();
    

    上面代码还是比较简单的,大家都可以看懂。

    我们需要了解父子容器的特点,这些是比较关键的,如下。

    父子容器特点

    1. 父容器和子容器是相互隔离的,他们内部可以存在名称相同的bean

    2. 子容器可以访问父容器中的bean,而父容器不能访问子容器中的bean

    3. 调用子容器的getBean方法获取bean的时候,会沿着当前容器开始向上面的容器进行查找,直到找到对应的bean为止

    4. 子容器中可以通过任何注入方式注入父容器中的bean,而父容器中是无法注入子容器中的bean,原因是第2点

    使用父子容器解决开头的问题

    关键代码

    @Test
    public void test2() {
        //创建父容器
        AnnotationConfigApplicationContext parentContext = new AnnotationConfigApplicationContext();
        //向父容器中注册Module1Config配置类
        parentContext.register(Module1Config.class);
        //启动父容器
        parentContext.refresh();
    
        //创建子容器
        AnnotationConfigApplicationContext childContext = new AnnotationConfigApplicationContext();
        //向子容器中注册Module2Config配置类
        childContext.register(Module2Config.class);
        //给子容器设置父容器
        childContext.setParent(parentContext);
        //启动子容器
        childContext.refresh();
    
        //从子容器中获取Service3
        Service3 service3 = childContext.getBean(Service3.class);
        System.out.println(service3.m1());
        System.out.println(service3.m2());
    }
    

    运行输出

    我是module1中的Servce1中的m1方法
    我是module2中的Servce1中的m2方法
    

    这次正常了。

    父子容器使用注意点

    我们使用容器的过程中,经常会使用到的一些方法,这些方法通常会在下面的两个接口中

    org.springframework.beans.factory.BeanFactory
    org.springframework.beans.factory.ListableBeanFactory
    

    这两个接口中有很多方法,这里就不列出来了,大家可以去看一下源码,这里要说的是使用父子容器的时候,有些需要注意的地方。

    BeanFactory接口,是spring容器的顶层接口,这个接口中的方法是支持容器嵌套结构查找的,比如我们常用的getBean方法,就是这个接口中定义的,调用getBean方法的时候,会从沿着当前容器向上查找,直到找到满足条件的bean为止。

    而ListableBeanFactory这个接口中的方法是不支持容器嵌套结构查找的,比如下面这个方法

    String[] getBeanNamesForType(@Nullable Class<?> type)
    

    获取指定类型的所有bean名称,调用这个方法的时候只会返回当前容器中符合条件的bean,而不会去递归查找其父容器中的bean。

    来看一下案例代码,感受一下:

    @Test
    public void test3() {
        //创建父容器parentFactory
        DefaultListableBeanFactory parentFactory = new DefaultListableBeanFactory();
        //向父容器parentFactory注册一个bean[userName->"路人甲Java"]
        parentFactory.registerBeanDefinition("userName",
                BeanDefinitionBuilder.
                        genericBeanDefinition(String.class).
                        addConstructorArgValue("路人甲Java").
                        getBeanDefinition());
    
        //创建一个子容器childFactory
        DefaultListableBeanFactory childFactory = new DefaultListableBeanFactory();
        //调用setParentBeanFactory指定父容器
        childFactory.setParentBeanFactory(parentFactory);
        //向子容器parentFactory注册一个bean[address->"上海"]
        childFactory.registerBeanDefinition("address",
                BeanDefinitionBuilder.
                        genericBeanDefinition(String.class).
                        addConstructorArgValue("上海").
                        getBeanDefinition());
    
        System.out.println("获取bean【userName】:" + childFactory.getBean("userName"));//@1
    
        System.out.println(Arrays.asList(childFactory.getBeanNamesForType(String.class))); //@2
    }
    

    上面定义了2个容器

    父容器:parentFactory,内部定义了一个String类型的bean:userName->路人甲Java

    子容器:childFactory,内部也定义了一个String类型的bean:address->上海

    @1:调用子容器的getBean方法,获取名称为userName的bean,userName这个bean是在父容器中定义的,而getBean方法是BeanFactory接口中定义的,支持容器层次查找,所以getBean是可以找到userName这个bean的

    @2:调用子容器的getBeanNamesForType方法,获取所有String类型的bean名称,而getBeanNamesForType方法是ListableBeanFactory接口中定义的,这个接口中方法不支持层次查找,只会在当前容器中查找,所以这个方法只会返回子容器的address

    我们来运行一下看看效果:

    获取bean【userName】:路人甲Java
    [address]
    

    结果和分析的一致。

    那么问题来了:有没有方式解决ListableBeanFactory接口不支持层次查找的问题?

    spring中有个工具类就是解决这个问题的,如下:

    org.springframework.beans.factory.BeanFactoryUtils
    

    这个类中提供了很多静态方法,有很多支持层次查找的方法,源码你们可以去细看一下,名称中包含有Ancestors的都是支持层次查找的。

    在test2方法中加入下面的代码:

    //层次查找所有符合类型的bean名称
    String[] beanNamesForTypeIncludingAncestors = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(childFactory, String.class);
    System.out.println(Arrays.asList(beanNamesForTypeIncludingAncestors));
    
    Map<String, String> beansOfTypeIncludingAncestors = BeanFactoryUtils.beansOfTypeIncludingAncestors(childFactory, String.class);
    System.out.println(Arrays.asList(beansOfTypeIncludingAncestors));
    

    运行输出

    [address, userName]
    [{address=上海, userName=路人甲Java}]
    

    查找过程是按照层次查找所有满足条件的bean。

    回头看一下springmvc父子容器的问题

    问题1:springmvc中只使用一个容器是否可以?

    只使用一个容器是可以正常运行的。

    问题2:那么springmvc中为什么需要用到父子容器?

    通常我们使用springmvc的时候,采用3层结构,controller层,service层,dao层;父容器中会包含dao层和service层,而子容器中包含的只有controller层;这2个容器组成了父子容器的关系,controller层通常会注入service层的bean。

    采用父子容器可以避免有些人在service层去注入controller层的bean,导致整个依赖层次是比较混乱的。

    父容器和子容器的需求也是不一样的,比如父容器中需要有事务的支持,会注入一些支持事务的扩展组件,而子容器中controller完全用不到这些,对这些并不关心,子容器中需要注入一下springmvc相关的bean,而这些bean父容器中同样是不会用到的,也是不关心一些东西,将这些相互不关心的东西隔开,可以有效的避免一些不必要的错误,而父子容器加载的速度也会快一些。

    总结

    1. 本文需掌握父子容器的用法,了解父子容器的特点:子容器可以访问父容器中bean,父容器无法访问子容器中的bean

    2. BeanFactory接口支持层次查找

    3. ListableBeanFactory接口不支持层次查找

    4. BeanFactoryUtils工具类中提供了一些非常实用的方法,比如支持bean层次查找的方法等等

    案例源码

    https://gitee.com/javacode2018/spring-series
    

    路人甲java所有案例代码以后都会放到这个上面,大家watch一下,可以持续关注动态。

    Spring系列

    1. Spring系列第1篇:为何要学spring?

    2. Spring系列第2篇:控制反转(IoC)与依赖注入(DI)

    3. Spring系列第3篇:Spring容器基本使用及原理

    4. Spring系列第4篇:xml中bean定义详解(-)

    5. Spring系列第5篇:创建bean实例这些方式你们都知道?

    6. Spring系列第6篇:玩转bean scope,避免跳坑里!

    7. Spring系列第7篇:依赖注入之手动注入

    8. Spring系列第8篇:自动注入(autowire)详解,高手在于坚持

    9. Spring系列第9篇:depend-on到底是干什么的?

    10. Spring系列第10篇:primary可以解决什么问题?

    11. Spring系列第11篇:bean中的autowire-candidate又是干什么的?

    12. Spring系列第12篇:lazy-init:bean延迟初始化

    13. Spring系列第13篇:使用继承简化bean配置(abstract & parent)

    14. Spring系列第14篇:lookup-method和replaced-method比较陌生,怎么玩的?

    15. Spring系列第15篇:代理详解(Java动态代理&cglib代理)?

    16. Spring系列第16篇:深入理解java注解及spring对注解的增强(预备知识)

    17. Spring系列第17篇:@Configration和@Bean注解详解(bean批量注册)

    18. Spring系列第18篇:@ComponentScan、@ComponentScans详解(bean批量注册)

    19. Spring系列第18篇:@import详解(bean批量注册)

    20. Spring系列第20篇:@Conditional通过条件来控制bean的注册

    21. Spring系列第21篇:注解实现依赖注入(@Autowired、@Resource、@Primary、@Qulifier)

    22. Spring系列第22篇:@Scope、@DependsOn、@ImportResource、@Lazy 详解

    23. Spring系列第23篇:Bean生命周期详解

    更多好文章

    1. Java高并发系列(共34篇)

    2. MySql高手系列(共27篇)

    3. Maven高手系列(共10篇)

    4. Mybatis系列(共12篇)

    5. 聊聊db和缓存一致性常见的实现方式

    6. 接口幂等性这么重要,它是什么?怎么实现?

    7. 泛型,有点难度,会让很多人懵逼,那是因为你没有看这篇文章!

    感谢大家的阅读,也欢迎您把这篇文章分享给更多的朋友一起阅读!谢谢!

    路人甲java

    ▲长按图片识别二维码关注

    路人甲Java:工作10年的前阿里P7分享Java、算法、数据库方面的技术干货!坚信用技术改变命运,让家人过上更体面的生活!

    展开全文
  • Docker总结(配合阿里云容器镜像服务)

    万次阅读 多人点赞 2019-04-09 16:34:40
    Docker是个很好的工具,刚开始用觉得... Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。它是目前最流行的 Linux 容器解决方案。 Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个...
  • C++STL容器总结

    万次阅读 多人点赞 2019-02-27 16:34:46
    持续更新中!!! 各大容器的特点: ...1.可以用下标访问的容器有(既可以插入也可以赋值):vector、deque、map; 特别要注意一下,vector和deque如果没有预先指定大小,是不能用下标法插入元素的...
  • IOC容器单例与多例

    万次阅读 2019-03-24 12:19:31
    IOC默认使用单例模式创建Bean,默认在spring容器启动时会自动创建对象。 @Lazy 懒加载:容器启动不创建对象,而在第一次使用Bean时创建对象,并初始化。 @Configuration public class MainConfig2 { @Lazy @...
  • 容器操作 运行容器 查看容器 停止容器 启动容器 删除容器 端口映射 容器日志 本文导读 本文承接《Docker 镜像操作 常用命令》 软件镜像(如 weChat.exe)----> 运行镜像----> 产生一个容器(正在...
  • 云端容器概念

    万次阅读 2021-03-02 15:23:59
    容器编排工具:解决应用部署在Docker时,存在的扩容、缩容、故障自愈等容器资源管理问题 当前最为热门的容器编排技术: 编排技术 优势 劣势 K8S 容器的高可用性、集群的精细管理、复杂的网络场景 学习和运维...
  • Docker删除镜像和容器

    万次阅读 多人点赞 2018-06-23 18:28:58
    删除docker中的容器可以使用如下命令: docker rm 使用如下命令可以查看当前正在运行的容器 docker ps 对于已退出的容器,可以使用如下命令进行查看: docker ps -a 例如: 查看当前正在运行的容器: ...
  • 列出退出状态的容器id docker container ls -f 'status=exited' -q 删除所有退出状态的容器 docker rm $(docker container ls -f 'status=exited' -q)
  • servlet容器,web容器,spring容器,springmvc容器的区别

    千次阅读 多人点赞 2019-05-03 21:42:40
    web容器中有servlet容器,spring项目部署后存在spring容器和springmvc容器。其中spring控制service层和dao层的bean对象。springmvc容器控制controller层bean对象。servlet容器控制servlet对象。项目启动是,首先 ...
  • Docker 容器技术 — 容器网络

    千次阅读 多人点赞 2020-10-02 10:39:47
    文章目录目录容器网络容器网络类型bridge 模式host 模式Container 模式none 模式容器端口映射容器跨主机通信 容器网络 容器网络类型 Docker 提供几种类型的网络,它决定容器之间、容器与外界之前的通信方式。 查看...
  • 重命名 docker 容器

    万次阅读 2018-04-16 21:51:26
    见:http://doc.okbase.net/ylchou/archive/186545.htmldocker rename old容器名 new容器名[root@ylchou ~]# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS ...
  • 如何批量删除Docker中已经停止的容器

    万次阅读 多人点赞 2018-03-09 16:28:44
    方法一:#显示所有的容器,过滤出Exited状态的容器,取出这些容器的ID,sudo docker ps -a|grep Exited|awk '{print $1}'#查询所有的容器,过滤出Exited状态的容器,列出容器ID,删除这些容器sudo docker rm `docker...
  • java 容器都有哪些?

    万次阅读 多人点赞 2019-07-27 18:36:57
    容器可以说是Java Core中比较重要的一部分了。 数组,String,java.util下的集合容器 ============================================================================== 数组长度限制为 Integer.Integer.MAX_...
  • Docker 容器技术 — 容器存储

    千次阅读 多人点赞 2020-10-02 10:40:46
    文章目录目录容器存储数据卷管理挂载时创建卷创建卷后挂载数据容器管理 容器存储 数据卷管理 核心选项: -v 宿主机目录:指定挂载到容器内的目录。 映射多个宿主机目录,只需要多写几个 -v 即可。 挂载时创建卷 ...
  • Docker 退出容器不停止容器运行

    万次阅读 2019-08-16 17:14:31
    Docker 退出容器不停止容器运行 通常我们使用Ctrl+C退出正在执行的操作 在docker容器中使用Ctrl+D即可退出容器,但是这样会让容器停止运行。 如果想退出容器但又不想让容器停止,使用Ctrl+P+Q即可 ...
  • 顺序容器和关联容器图示: 1.顺序容器和关联容器的区别: ①顺序容器只有实值val。 ②关联容器的一个元素包含两个部分:键值对(key-value) 即&lt;k值(键值)|实值&gt;。 ③顺序容器不涉及排序,关联容器内部...
  • Web容器

    万次阅读 2016-09-25 10:59:22
    1.2容器 2.Web容器的作用 主要参考文章: http://www.cnblogs.com/yezhaohui/archive/2013/05/21/3091931.html http://www.cnphp6.com/archives/51094 好记性不如烂笔头,从网上搜罗了一...
  • 本文会介绍如何部署使用显卡的docker容器、如何使用pycharm连接docker容器运行机器学习代码。 版本 Pycharm: 2020.1.3 docker:19.03.12 python: 3.6.13 demo算法: BackgroundMattingV2 部署 下面我会按照.
  • C++ map容器和multimap容器(STL map容器

    万次阅读 多人点赞 2018-11-08 22:30:05
    1. 关联容器和map容器概述 2. map容器 2.1 map的创建以及初始化列表 2.2 map容器的一般常用属性(方法) 2.3 插入数据 2.4 数据的访问和遍历 2.5 数据的删除 2.6 map中关键词的排序 3. multimap容器 3.1 ...
  • C++ vector容器的swap方法(容器互换)

    万次阅读 多人点赞 2019-11-21 23:01:53
    swap方法可以交换两容器的内容。 int main() { vector<int> v1; v1.push_back(10); v1.push_back(20); vector<int> v2; v2.push_back(30); //printVector是自己编写用于遍历输出vector容器的...
  • Spring容器的启动流程

    万次阅读 多人点赞 2021-02-08 19:04:42
    Spring的启动流程可以归纳为三个步骤: 1、初始化Spring容器,注册内置的BeanPostProcessor的BeanDefinition到容器中; 2、将配置类的BeanDefinition注册到容器中;...3、调用refresh()方法刷新容器
  • --凑字数结束,开始正题:spring-boot项目需要部署在外部容器中的时候,spring-boot导出的war包无法再外部容器(tomcat)中运行或运行报错,本章就是详细讲解如何解决这个问题 1、pom.xml一览 <project xmlns="ht
  • 传输文件到docker容器

    万次阅读 2019-12-01 16:41:51
    传输文件到docker容器 首先需要知道docker容器的container_id,可以使用docker ps命令来查看你要操作的docker容器的container_id Docker容器向宿主机传送文件 格式: docker cp container_id:<docker容器内的...
  • 容器:管理对象的地方。 spring,springMVC和web容器的区别? Tomcat(web容器)是管理servlet对象的地方,而spring和springMVC是管理...其次,spring容器和springMVC容器是父子容器的关系,spring容器是父容器,...
  • docker退出容器的方式(注此时处于运行的容器内部): 启动容器 1.退出容器并结束容器运行 exit 2.退出容器但是容器仍在执行 ctrl + p + q 会回到宿主机桌面 但是容器还在执行 3.如何回到已经退出的...
  • Servlet容器和Tomcat容器区别

    千次阅读 多人点赞 2019-07-01 15:02:34
    一提到容器,很多新手就会忘文生畏,认为这是很难很高深的东西。其实一项技术存在与发展,自然有其广度与深度的地方。但是对于开发人员而言,我们并不是要对容器研究要有多深,我们的目的有一个基本的了解,然后会...
  • 请问tomcat容器与spring容器是父子关系吗,tomcat容器包含spring容器

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,979,899
精华内容 791,959
关键字:

容器