首页 文章 精选 留言 我的

精选列表

搜索[面试],共4912篇文章
优秀的个人博客,低调大师

设计模式常见面试知识点总结(Java版)

设计模式 这篇总结主要是基于我设计模式系列的文章而形成的的。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢 更多详细内容可以到我的cdsn博客上查看: https://blog.csdn.net/a724888 最后,如果想要更好地完成这部分内容的学习,建议大家还是去看一下原文。 创建型模式 创建型模式 创建型模式的作用就是创建对象,说到创建一个对象,最熟悉的就是 new 一个对象,然后 set 相关属性。但是,在很多场景下,我们需要给客户端提供更加友好的创建对象的方式,尤其是那种我们定义了类,但是需要提供给其他开发者用的时候。 单例 单例模式保证全局的单例类只有一个实例,这样的话使用的时候直接获取即可,比如数据库的一个连接,Spring里的bean,都可以是单例的。 单例模式一般有5种写法。 第一种是饿汉模式,先把单例进行实例化,获取的时候通过静态方法直接获取即可。缺点是类加载后就完成了类的实例化,浪费部分空间。 第二种是饱汉模式,先把单例置为null,然后通过静态方法获取单例时再进行实例化,但是可能有多线程同时进行实例化,会出现并发问题。 第三种是逐步改进的方法,一开始可以用synchronized关键字进行同步,但是开销太大,而后改成使用volatile修饰单例,然后通过一次检查判断单例是否已初始化,如果未初始化就使用synchronized代码块,再次检查单例防止在这期间被初始化,而后才真正进行初始化。 第四种是使用静态内部类来实现,静态内部类只在被使用的时候才进行初始化,所以在内部类中进行单例的实例化,只有用到的时候才会运行实例化代码。然后外部类再通过静态方法返回静态内部类的单例即可。 第五种是枚举类,枚举类的底层实现其实也是内部类。枚举类确保每个类对象在全局是唯一的。所以保证它是单例,这个方法是最简单的。 工厂模式 简单工厂一般是用一个工厂创建多个类的实例。 工厂模式一般是指一个工厂服务一个接口,为这个接口的实现类进行实例化 抽象工厂模式是指一个工厂服务于一个产品族,一个产品族可能包含多个接口,接口又会包含多个实现类,通过一个工厂就可以把这些绑定在一起,非常方便。 原型模式 一般通过一个实例进行克隆从而获得更多同一原型的实例。使用实例的clone方法即可完成。 建造者模式 建造者模式中有一个概念叫做链式调用,链式调用为一个类的实例化提供便利,一般提供系列的方法进行实例化,实际上就是将set方法改造一下,将原本返回为空的set方法改为返回this实例,从而实现链式调用。 建造者模式在此基础上加入了builder方法,提供给外部进行调用,同样使用链式调用来完成参数注入。 结构型模式 结构型模式 前面创建型模式介绍了创建对象的一些设计模式。 这节介绍的结构型模式旨在通过改变代码结构来达到解耦的目的,使得我们的代码容易维护和扩展。 桥接模式 有点复杂。建议参考原文 适配器模式 适配器模式用于将两个不同的类进行适配。 适配器模式和代理模式的异同 比较这两种模式,其实是比较对象适配器模式和代理模式,在代码结构上, 它们很相似,都需要一个具体的实现类的实例。 但是它们的目的不一样,代理模式做的是增强原方法的活; 适配器做的是适配的活,为的是提供“把鸡包装成鸭,然后当做鸭来使用”, 而鸡和鸭它们之间原本没有继承关系。 适配器模式可以分为类适配器,对象适配器等。 类适配器通过继承父类就可以把自己适配成父类了。 而对象适配器则需要把对象传入另一个对象的构造方法中,以便进行包装。 享元模式 享元模式的核心在于享元工厂类, 享元工厂类的作用在于提供一个用于存储享元对象的享元池, 用户需要对象时,首先从享元池中获取, 如果享元池中不存在,则创建一个新的享元对象返回给用户, 在享元池中保存该新增对象。 代理模式 我们发现没有,代理模式说白了就是做 “方法包装” 或做 “方法增强”。 在面向切面编程中,算了还是不要吹捧这个名词了,在 AOP 中, 其实就是动态代理的过程。比如 Spring 中, 我们自己不定义代理类,但是 Spring 会帮我们动态来定义代理, 然后把我们定义在 @Before、@After、@Around 中的代码逻辑动态添加到代理中。 外观模式 外观模式一般封装具体的实现细节,为用户提供一个更加简单的接口。 通过一个方法调用就可以获取需要的内容。 组合模式 组合模式用于表示具有层次结构的数据,使得我们对单个对象和组合对象的访问具有一致性。 直接看一个例子吧,每个员工都有姓名、部门、薪水这些属性, 同时还有下属员工集合(虽然可能集合为空), 而下属员工和自己的结构是一样的, 也有姓名、部门这些属性, 同时也有他们的下属员工集合。 class Employee { private String name; private String dept; private int salary; private List<Employee> subordinates; // 下属 } 装饰者模式 装饰者 装饰者模式把每个增强类都继承最高级父类。然后需要功能增强时把类实例传入增强类即可,然后增强类在使用时就可以增强原有类的功能了。 和代理模式不同的是,装饰者模式每个装饰类都继承父类,并且可以进行多级封装。 行为型模式 行为型模式 行为型模式关注的是各个类之间的相互作用,将职责划分清楚,使得我们的代码更加地清晰。 策略模式 策略模式一般把一个策略作为一个类,并且在需要指定策略的时候传入实例,于是我们可以在需要使用算法的地方传入指定算法。 命令模式 命令模式一般分为命令发起者,命令以及命令接受者三个角色。 命令发起者在使用时需要注入命令实例。然后执行命令调用。 命令调用实际上会调用命令接收者的方法进行实际调用。 比如遥控器按钮相当于一条命令,点击按钮时命令运行,自动调用电视机提供的方法即可。 模板方法模式 模板方法一般指提供了一个方法模板,并且其中有部分实现类和部分抽象类,并且规定了执行顺序。 实现类是模板提供好的方法。而抽象类则需要用户自行实现。 模板方法规定了一个模板中方法的执行顺序,非常适合一些开发框架,于是模板方法也广泛运用在开源框架中。 观察者模式和事件监听机制 观察者模式一般用于订阅者和消息发布者之间的数据订阅。 一般分为观察者和主题,观察者订阅主题,把实例注册到主题维护的观察者列表上。 而主题更新数据时自动把数据推给观察者或者通知观察者数据已经更新。 但是由于这样的方式消息推送耦合关系比较紧。并且很难在不打开数据的情况下知道数据类型是什么。 知道后来为了使数据格式更加灵活,使用了事件和事件监听器的模式,事件包装的事件类型和事件数据,从主题和观察者中解耦。 主题当事件发生时,触发该事件的所有监听器,把该事件通过监听器列表发给每个监听器,监听得到事件以后,首先根据自己支持处理的事件类型中找到对应的事件处理器,再用处理器处理对应事件。 责任链模式 责任链通常需要先建立一个单向链表,然后调用方只需要调用头部节点就可以了,后面会自动流转下去。 比如流程审批就是一个很好的例子,只要终端用户提交申请,根据申请的内容信息,自动建立一条责任链,然后就可以开始流转了。 微信公众号 个人公众号:黄小斜 黄小斜是跨考软件工程的 985 硕士,自学 Java 两年,拿到了 BAT 等近十家大厂 offer,从技术小白成长为阿里工程师。 作者专注于 JAVA 后端技术栈,热衷于分享程序员干货、学习经验、求职心得和程序人生,目前黄小斜的CSDN博客有百万+访问量,知乎粉丝2W+,全网已有10W+读者。 黄小斜是一个斜杠青年,坚持学习和写作,相信终身学习的力量,希望和更多的程序员交朋友,一起进步和成长! 原创电子书:关注公众号【黄小斜】后回复【原创电子书】即可领取我原创的电子书《菜鸟程序员修炼手册:从技术小白到阿里巴巴Java工程师》 程序员3T技术学习资源: 一些程序员学习技术的资源大礼包,关注公众号后,后台回复关键字 “资料” 即可免费无套路获取。 考研复习资料: 计算机考研大礼包,都是我自己考研复习时用的一些复习资料,包括公共课和专业的复习视频,这里也推荐给大家,关注公众号后,后台回复关键字 “考研” 即可免费获取。 技术公众号:Java技术江湖 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号【Java技术江湖】一位阿里 Java 工程师的技术小站,作者黄小斜,专注 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点Docker、ELK,同时也分享技术干货和学习经验,致力于Java全栈开发! Java工程师必备学习资源: 一些Java工程师常用学习资源,关注公众号后,后台回复关键字 “Java” 即可免费无套路获取。

优秀的个人博客,低调大师

HashMap面试必问的6个点,你知道几个?

一、HashMap的实现原理? 此题可以组成如下连环炮来问 你看过HashMap源码嘛,知道原理嘛?为什么用数组+链表?hash冲突你还知道哪些解决办法?我用LinkedList代替数组结构可以么?既然是可以的,为什么HashMap不用LinkedList,而选用数组?1.你看过HashMap源码嘛,知道原理嘛? 针对这个问题,嗯,当然是必须看过HashMap源码。至于原理,下面那张图很清楚了: HashMap采用Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体。 只是在JDK1.8中,链表长度大于8的时候,链表会转成红黑树! 2.为什么用数组+链表? 数组是用来确定桶的位置,利用元素的key的hash值对数组长度取模得到. 链表是用来解决hash冲突问题,当出现hash值一样的情形,就在数组上的对应位置形成一条链表。ps:这里的hash值并不是指hashcode,而是将hashcode高低十六位异或过的。至于为什么要这么做,继续往下看。 3.hash冲突你还知道哪些解决办法? 比较出名的有四种(1)开放定址法(2)链地址法(3)再哈希法(4)公共溢出区域法 ps:大家有兴趣拓展的,自己去搜一下就懂了,这个就不拓展了! 4.我用LinkedList代替数组结构可以么? 这里我稍微说明一下,此题的意思是,源码中是这样的 Entry[] table = new Entry[capacity]; ps:Entry就是一个链表节点。 那我用下面这样表示 List table = new LinkedList(); 是否可行? 答案很明显,必须是可以的。 5.既然是可以的,为什么HashMap不用LinkedList,而选用数组? 因为用数组效率最高! 在HashMap中,定位桶的位置是利用元素的key的哈希值对数组长度取模得到。此时,我们已得到桶的位置。显然数组的查找效率比LinkedList大。 那ArrayList,底层也是数组,查找也快啊,为啥不用ArrayList? (烟哥写到这里的时候,不禁觉得自己真有想法,自己把自己问死了,还好我灵机一动想出了答案) 因为采用基本数组结构,扩容机制可以自己定义,HashMap中数组扩容刚好是2的次幂,在做取模运算的效率高。 而ArrayList的扩容机制是1.5倍扩容,那ArrayList为什么是1.5倍扩容这就不在本文说明了。 欢迎关注我的公中浩【程序员追风】,文章都会在里面更新,整理的资料也都会放在里面。 二、HashMap在什么条件下扩容? 此题可以组成如下连环炮来问 HashMap在什么条件下扩容?为什么扩容是2的n次幂?为什么为什么要先高16位异或低16位再取模运算?1.HashMap在什么条件下扩容? 如果bucket满了(超过load factor*current capacity),就要resize。 load factor为0.75,为了最大程度避免哈希冲突 current capacity为当前数组大小。 2.为什么扩容是2的次幂? HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现就在把数据存到哪个链表中的算法;这个算法实际就是取模,hash%length。 但是,大家都知道这种运算不如位移运算快。 因此,源码中做了优化hash&(length-1)。 也就是说hash%length==hash&(length-1) 那为什么是2的n次方呢? 因为2的n次方实际就是1后面n个0,2的n次方-1,实际就是n个1。 例如长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞。 而长度为5的时候,3&(5-1)=0 2&(5-1)=0,都在0上,出现碰撞了。 所以,保证容积是2的n次方,是为了保证在做(length-1)的时候,每一位都能&1 ,也就是和1111……1111111进行与运算。 3.为什么为什么要先高16位异或低16位再取模运算? 我先晒一下,jdk1.8里的hash方法。1.7的比较复杂,咱就不看了。 hashmap这么做,只是为了降低hash冲突的几率。 打个比方,当我们的length为16的时候,哈希码(字符串“abcabcabcabcabc”的key对应的哈希码)对(16-1)与操作,对于多个key生成的hashCode,只要哈希码的后4位为0,不论不论高位怎么变化,最终的结果均为0。 如下图所示 而加上高16位异或低16位的“扰动函数”后,结果如下 可以看到: 扰动函数优化前:1954974080 % 16 = 1954974080 & (16 - 1) = 0 扰动函数优化后:1955003654 % 16 = 1955003654 & (16 - 1) = 6 很显然,减少了碰撞的几率。 三、讲讲hashmap的get/put的过程? 此题可以组成如下连环炮来问 知道hashmap中put元素的过程是什么样么?知道hashmap中get元素的过程是什么样么?你还知道哪些hash算法?说说String中hashcode的实现?(此题很多大厂问过)1.知道hashmap中put元素的过程是什么样么? 对key的hashCode()做hash运算,计算index; 如果没碰撞直接放到bucket里; 如果碰撞了,以链表的形式存在buckets后; 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树(JDK1.8中的改动); 如果节点已经存在就替换old value(保证key的唯一性) 如果bucket满了(超过load factor*current capacity),就要resize。 2.知道hashmap中get元素的过程是什么样么? 对key的hashCode()做hash运算,计算index; 如果在bucket里的第一个节点里直接命中,则直接返回; 如果有冲突,则通过key.equals(k)去查找对应的Entry; 若为树,则在树中通过key.equals(k)查找,O(logn);若为链表,则在链表中通过key.equals(k)查找,O(n)。3.你还知道哪些hash算法? 先说一下hash算法干嘛的,Hash函数是指把一个大范围映射到一个小范围。把大范围映射到一个小范围的目的往往是为了节省空间,使得数据容易保存。 比较出名的有MurmurHash、MD4、MD5等等 4.说说String中hashcode的实现?(此题频率很高) public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }String类中的hashCode计算方法还是比较简单的,就是以31为权,每一位为字符的ASCII值进行运算,用自然溢出来等效取模。 哈希计算公式可以计为s[0]31^(n-1) + s[1]31^(n-2) + … + s[n-1] 那为什么以31为质数呢? 主要是因为31是一个奇质数,所以31i=32i-i=(i<<5)-i,这种位移与减法结合的计算相比一般的运算快很多。 四、为什么hashmap的在链表元素数量超过8时改为红黑树? 此题可以组成如下连环炮来问 知道jdk1.8中hashmap改了啥么?为什么在解决hash冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树?我不用红黑树,用二叉查找树可以么?那为什么阀值是8呢?当链表转为红黑树后,什么时候退化为链表? 1.知道jdk1.8中hashmap改了啥么? 由数组+链表的结构改为数组+链表+红黑树。优化了高位运算的hash算法:h^(h>>>16)扩容后,元素要么是在原位置,要么是在原位置再移动2次幂的位置,且链表顺序不变。最后一条是重点,因为最后一条的变动,hashmap在1.8中,不会在出现死循环问题。 2.为什么在解决hash冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树? 因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。 当元素小于8个当时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于8个的时候,此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。 因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。 3.我不用红黑树,用二叉查找树可以么? 可以。但是二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。 4.那为什么阀值是8呢? 不知道,等jdk作者来回答。 这道题,网上能找到的答案都是扯淡。 我随便贴一个牛客网的答案,如下图所示 看出bug没?交点是6.64?交点分明是4,好么。 log4=2,4/2=2。 jdk作者选择8,一定经过了严格的运算,觉得在长度为8的时候,与其保证链表结构的查找开销,不如转换为红黑树,改为维持其平衡开销。 5.当链表转为红黑树后,什么时候退化为链表? 为6的时候退转为链表。中间有个差值7可以防止链表和树之间频繁的转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。 五、HashMap的并发问题? 此题可以组成如下连环炮来问 HashMap在并发编程环境下有什么问题啊?在jdk1.8中还有这些问题么?你一般怎么解决这些问题的?HashMap在并发编程环境下有什么问题啊? (1)多线程扩容,引起的死循环问题(2)多线程put的时候可能导致元素丢失(3)put非null元素后get出来的却是null在jdk1.8中还有这些问题么? 在jdk1.8中,死循环问题已经解决。其他两个问题还是存在。 你一般怎么解决这些问题的? 比如ConcurrentHashmap,Hashtable等线程安全等集合类。 六、你一般用什么作为HashMap的key? 此题可以组成如下连环炮来问 健可以为Null值么?你一般用什么作为HashMap的key?我用可变类当HashMap的key有什么问题?如果让你实现一个自定义的class作为HashMap的key该如何实现?1.健可以为Null值么? 必须可以,key为null的时候,hash算法最后的值以0来计算,也就是放在数组的第一个位置。 2.你一般用什么作为HashMap的key? 一般用Integer、String这种不可变类当HashMap当key,而且String最为常用。 (1)因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。(2)因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的,这些类已经很规范的覆写了hashCode()以及equals()方法。 3.我用可变类当HashMap的key有什么问题? hashcode可能发生改变,导致put进去的值,无法get出,如下所示 HashMap, Object> changeMap = new HashMap<>();List list = new ArrayList<>();list.add("hello");Object objectValue = new Object();changeMap.put(list, objectValue);System.out.println(changeMap.get(list));list.add("hello world");//hashcode发生了改变System.out.println(changeMap.get(list));输出值如下 java.lang.Object@74a14482null 4.如果让你实现一个自定义的class作为HashMap的key该如何实现? 此题考察两个知识点 重写hashcode和equals方法注意什么?如何设计一个不变类针对问题一,记住下面四个原则即可 (1)两个对象相等,hashcode一定相等 (2)两个对象不等,hashcode不一定不等 (3)hashcode相等,两个对象不一定相等 (4)hashcode不等,两个对象一定不等 针对问题二,记住如何写一个不可变类 (1)类添加final修饰符,保证类不被继承。 如果类可以被继承会破坏类的不可变性机制,只要继承类覆盖父类的方法并且继承类可以改变成员变量值,那么一旦子类以父类的形式出现时,不能保证当前类是否可变。 (2)保证所有成员变量必须私有,并且加上final修饰 通过这种方式保证成员变量不可改变。但只做到这一步还不够,因为如果是对象成员变量有可能再外部改变其值。所以第4点弥补这个不足。 (3)不提供改变成员变量的方法,包括setter 避免通过其他接口改变成员变量的值,破坏不可变特性。 (4)通过构造器初始化所有成员,进行深拷贝(deep copy) 如果构造器传入的对象直接赋值给成员变量,还是可以通过对传入对象的修改进而导致改变内部变量的值。例如: public final class ImmutableDemo { private final int[] myArray; public ImmutableDemo(int[] array) { this.myArray = array; // wrong } }这种方式不能保证不可变性,myArray和array指向同一块内存地址,用户可以在ImmutableDemo之外通过修改array对象的值来改变myArray内部的值。 为了保证内部的值不被修改,可以采用深度copy来创建一个新内存保存传入的值。正确做法: public final class MyImmutableDemo { private final int[] myArray; public MyImmutableDemo(int[] array) { this.myArray = array.clone(); } }(5)在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝 这种做法也是防止对象外泄,防止通过getter获得内部可变成员对象后对成员变量直接操作,导致成员变量发生改变。 最后欢迎大家一起交流,喜欢文章记得点个赞哟,感谢支持!

优秀的个人博客,低调大师

27道高频Spring面试题,你能答对几个?

1、什么是Spring框架,Spring框架有哪些主要模块Spring框架是一个为Java应用程序开发提供综合、广泛的基础性支持的Java平台。 Spring帮助开发者解决了开发中基础性的问题,使得开发人员可以专注于应用程序的开发。 Spring框架本身也是按照设计模式精心打造的,这使得我们可以在开发环境中安心地集成Spring框架,不必担心Spring是如何在后台工作的。2、使用Spring框架能带来哪些好处下面列举了一些使用Spring框架带来的主要好处。(1)Dependency Injection(DI)使得构造器和JavaBean properties文件中的依赖关系一目了然。 (2)与EJB容器相比较,IoC容器更加趋向于轻量级。这样一来使用IoC容器在有限的内存和CPU资源的情况下进行应用程序的开发和发布就变得十分有利。 (3)Spring并没有闭门造车,Spring利用了已有的技术,比如ORM框架、logging框架、J2EE、Quartz和JDK Timer,以及其他视图技术。 (4)Spring框架是按照模块的形式来组织的。由包和类的编号就可以看出其所属的模块,开发者只需选用需要的模块即可。 (5)要测试一个用Spring开发的应用程序十分简单,因为测试相关的环境代码都已经囊括在框架中了。更加简单的是,利用JavaBean形式的POJO类,可以很方便地利用依赖注入来写入测试数据。 (6)Spring的Web框架也是一个精心设计的Web MVC框架,为开发者在Web框架的选择上提供了一个除主流框架(比如Struts)和过度设计的、不流行Web框架以外的选择。 (7)Spring提供了一个便捷的事务管理接口,适用于小型的本地事务处理(比如在单DB的环境下)和复杂的共同事务处理(比如利用JTA的复杂DB环境)。3、什么是控制反转(IoC),什么是依赖注入(1)控制反转是应用于软件工程领域的,在运行时被装配器对象用来绑定耦合对象的一种编程技巧,对象之间的耦合关系在编译时通常是未知的。在传统的编程方式中,业务逻辑的流程是由应用程序中早已被设定好关联关系的对象来决定的。在使用控制反转的情况下,业务逻辑的流程是由对象关系图来决定的,该对象关系图由装配器负责实例化,这种实现方式还可以将对象之间的关联关系的定义抽象化。绑定的过程是通过“依赖注入”实现的。 (2)控制反转是一种以给予应用程序中目标组件更多控制为目的设计范式,并在实际工作中起到了有效的作用。 (3)依赖注入是在编译阶段尚未知所需的功能是来自哪个的类的情况下,将其他对象所依赖的功能对象实例化的模式。这就需要一种机制来激活相应的组件以提供特定的功能,所以依赖注入是控制反转的基础。否则如果在组件不受框架控制的情况下,框架又怎么知道要创建哪个组件呢?4、在Java中依赖注入有哪些方式(1)构造器注入。 (2)Setter方法注入。 (3)接口注入。5、BeanFactory和ApplicationContext有什么区别BeanFactory可以理解为含有Bean集合的工厂类。BeanFactory 包含了Bean的定义,以便在接收到客户端请求时将对应的Bean实例化。 BeanFactory还能在实例化对象时生成协作类之间的关系。此举将Bean自身从Bean客户端的配置中解放出来。BeanFactory还包含Bean生命周期的控制,调用客户端的初始化方法(Initialization Method)和销毁方法(Destruction Method)。 从表面上看,ApplicationContext如同BeanFactory一样具有Bean定义、Bean关联关系的设置及根据请求分发Bean的功能。但ApplicationContext在此基础上还提供了其他功能。 (1)提供了支持国际化的文本消息。 (2)统一的资源文件读取方式。 (3)已在监听器中注册的Bean的事件。 以下是三种较常见的 ApplicationContext 实现方式。 (1)ClassPathXmlApplicationContext:从ClassPath的XML配置文件中读取上下文,并生成上下文定义。应用程序上下文从程序环境变量中取得。 ApplicationContext context = new ClassPathXmlApplicationContext(“application.xml”); (2)FileSystemXmlApplicationContext :由文件系统中的XML配置文件读取上下文。 ApplicationContext context = new FileSystemXmlApplicationContext(“application.xml”); (3)XmlWebApplicationContext:由Web应用的XML文件读取上下文。6、Spring提供几种配置方式来设置元数据Spring提供以下三种配置方式来设置元数据:(1)基于XML的配置。 (2)基于注解的配置。 (3)基于Java的配置。7、如何使用XML配置方式配置Spring在Spring框架中,依赖和服务需要专门的配置文件实现,一般用XML格式的配置文件。这些配置文件的格式采用公共的模板,由一系列的Bean定义和专门的应用配置选项组成。 Spring XML配置的主要目的是使所有的Spring组件都可以用XML文件的形式来进行配置。这意味着不会出现其他的Spring配置类型(比如声明配置方式或基于Java Class的配置方式)。 Spring的XML配置方式是使用被Spring命名空间所支持的一系列的XML标签来实现的。Spring主要的命名空间有context、beans、jdbc、tx、aop、mvc和aso。例如: <beans> <!-- JSON Support --> <bean name="viewResolver" class="org.springframework.web.servlet.view.BeanNameViewResolver"/> <bean name="jsonTemplate" class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/> <bean id="restTemplate" class="org.springframework.web.client.RestTemplate"/> </beans> 下面这个web.xml仅配置了DispatcherServlet,最简单的配置便能满足应用程序配置运行时组件的需求。 <web-app> <display-name>Archetype Created Web Application</display-name> <servlet> <servlet-name>spring</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>spring</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app> 8、Spring提供哪些配置形式Spring对Java配置的支持是由@Configuration注解和@Bean注解来实现的。由@Bean注解的方法将会实例化、配置和初始化一个新对象,这个对象将由Spring的IoC容器来管理。@Bean声明所起到的作用与元素类似。被@Configuration所注解的类则表示这个类的主要目的是作为Bean定义的资源。被@Configuration声明的类可以通过在同一个类内部调用@bean方法来设置嵌入Bean的依赖关系。最简单的@Configuration 声明类请参考下面的代码: @Configuration publicclass AppConfig{ @Bean public MyService myService() { return new MyServiceImpl(); } } 与上面的@Beans配置文件相同的XML配置文件如下: <beans> <bean id="myService" class="com.gupaoedu.services.MyServiceImpl"/> </beans> 上述配置方式的实例化方式如下: publicstaticvoid main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); MyService myService = ctx.getBean(MyService.class); myService.doStuff(); } 要使用组件扫描,仅需用@Configuration进行注解即可: @Configuration @ComponentScan(basePackages = "com.gupaoedu") publicclass AppConfig { } 在上面的例子中,com.gupaoedu包首先会被扫描到,然后在容器内查找被@Component 声明的类,找到后将这些类按照Spring Bean定义进行注册。如果你要在Web应用开发中选用上述配置方式,需要用AnnotationConfigWebApplicationContext类来读取配置文件,可以用来配置Spring的Servlet监听器ContrextLoaderListener或者Spring MVC的DispatcherServlet。例如: <web-app> <context-param> <param-name>contextClass</param-name> <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value> </context-param> <context-param> <param-name>contextConfigLocation</param-name> <param-value>com.gupaoedu.AppConfig</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextClass</param-name> <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value> </init-param> <init-param> <param-name>contextConfigLocation</param-name> <param-value>com.gupaoedu.web.MVCConfig</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/web/*</url-pattern> </servlet-mapping> </web-app> 9、怎样用注解的方式配置SpringSpring在2.5版本以后开始支持用注解的方式配置依赖注入。可以用注解的方式来替代XML方式的Bean描述,可以将Bean描述转移到组件类的内部,只需要在相关类上、方法上或者字段声明上使用注解即可。注解注入将会被容器在XML注入之前处理,所以后者会覆盖前者对于同一个属性的处理结果。注解装配在Spring中是默认关闭的,需要在Spring文件中进行配置才能使用基于注解的装配模式。如果你想要在应用程序中使用注解的方式,请参考如下配置: <beans> <context:annotation-config/> </beans> 配置完成以后,就可以用注解的方式在Spring中向属性、方法和构造方法中自动装配变量。下面是几种比较重要的注解类型。(1)@Required:该注解应用于设值方法。 (2)@Autowired:该注解应用于设值方法、非设值方法、构造方法和变量。 (3)@Qualifier:该注解和@Autowired注解搭配使用,用于消除特定Bean自动装配的歧义。 (4)JSR-250 Annotations:Spring支持基于JSR-250 注解的注解,即@Resource、@PostConstruct和@PreDestroy。10、请解释Spring Bean的生命周期Spring Bean的生命周期简单易懂。在一个Bean实例被初始化时,需要执行一系列初始化操作以使其达到可用的状态。同样,当一个Bean不再被调用时需要进行相关的析构操作,并从Bean容器中移除。 Spring Bean Factory 负责管理在Spring容器中被创建的Bean的生命周期。Bean的生命周期由两组回调方法组成。(1)初始化之后调用的回调方法。 (2)销毁之前调用的回调方法。Spring提供了以下4种方式来管理Bean的生命周期事件:(1)InitializingBean和DisposableBean回调接口。 (2)针对特殊行为的其他Aware接口。 (3)Bean配置文件中的customInit()方法和customDestroy()方法。 (4)@PostConstruct和@PreDestroy注解方式。使用customInit()和 customDestroy()方法管理Bean生命周期的代码样例如下: <beans> <bean id="demoBean" class="com.gupaoedu.task.DemoBean" init-Method="customInit" destroy-Method="customDestroy"></bean> </beans> 11、Spring Bean作用域的区别是什么Spring容器中的Bean可以分为5个作用域。所有作用域的名称都是自说明的,但是为了避免混淆,还是让我们来解释一下。(1)singleton:这种Bean作用域是默认的,这种作用域确保不管接收到多少个请求,每个容器中只有一个Bean实例,单例模式由Bean Factory自身来维护。 (2)prototype:prototype作用域与singleton作用域相反,为每一个Bean请求提供一个实例。 (3)request:在请求Bean作用域内为每一个来自客户端的网络请求创建一个实例,在请求完成以后,Bean会失效并被垃圾回收器回收。 (4)Session:与request作用域类似,确保每个Session中有一个Bean实例,在Session过期后,Bean会随之失效。(5)global-session:global-session和Portlet应用相关。当应用部署在Portlet容器中时,它包含很多Portlet。如果想让所有的Portlet共用全局存储变量,那么这个全局存储变量需要存储在global-session中。全局作用域与Servlet中的Session作用域效果相同。12、什么是Spring Inner Bean在Spring中,无论何时,当Bean仅被调用了一个属性时,一个明智的做法是将这个Bean声明为内部Bean。内部Bean可以用setter注入“属性”和用构造方法注入“构造参数”的方式来实现。 比如,在应用程序中一个Customer类引用了一个Person类,我们要创建一个Person类的实例,然后在Customer内部使用。 public class Customer { private Person person; } public class Person { private String name; private String address; private int age; } 内部Bean的声明方式如下: <bean id="CustomerBean" class="com.gupaoedu.common.Customer"> <property name="person"> <bean class="com.gupaoedu.common.Person"> <property name="name" value="lokesh"/> <property name="address" value="India"/> <property name="age" value="34"/> </bean> </property> </bean> 13、Spring中的单例Bean是线程安全的吗Spring并没有对单例Bean进行任何多线程的封装处理。关于单例Bean的线程安全和并发问题需要开发者自行解决。但实际上,大部分Spring Bean并没有可变的状态(比如Serview类和DAO类),所以在某种程度上,Spring的单例Bean是线程安全的。如果你的Bean有多种状态(比如View Model对象),就需要自行保证线程安全。最容易的解决办法就是将多态Bean的作用域由“singleton”变更为“prototype”。14、请举例说明如何在Spring中注入一个Java集合Spring提供了以下4种集合类的配置元素:(1)标签用来装配可重复的list值。(2)标签用来装配没有重复的set值。(3)标签用来注入键和值,可以为任何类型的键值对。(4)标签支持注入键和值都是字符串类型的键值对。下面看一个具体的例子: <beans> <bean id="javaCollection" class="com.gupaoedu.JavaCollection"> <property name="customList"> <list> <value>INDIA</value> <value>Pakistan</value> <value>USA</value> <value>UK</value> </list> </property> <property name="customSet"> <set> <value>INDIA</value> <value>Pakistan</value> <value>USA</value> <value>UK</value> </set> </property> <property name="customMap"> <map> <entry key="1" value="INDIA"/> <entry key="2" value="Pakistan"/> <entry key="3" value="USA"/> <entry key="4" value="UK"/> </map> </property> <property name="customProperies"> <props> <prop key="admin">admin@gupaoedu.com</prop> <prop key="support">support@gupaoedu.com</prop> </props> </property> </bean> </beans> 15、如何向Spring Bean中注入java.util.Properties第一种方法是使用如下代码所示的标签: <bean id="adminUser" class="com.gupaoedu.common.Customer"> <property name="emails"> <props> <prop key="admin">admin@gupaoedu.com</prop> <prop key="support">support@gupaoedu.com</prop> </props> </property> </bean> 也可用“util:”命名空间从Properties文件中创建一个Properties Bean,然后利用setter方法注入Bean的引用。16、请解释Spring Bean的自动装配在Spring框架中,在配置文件中设定Bean的依赖关系是一个很好的机制,Spring容器还可以自动装配合作关系Bean之间的关联关系。这意味着Spring可以通过向BeanFactory中注入的方式自动搞定Bean之间的依赖关系。自动装配可以设置在每个Bean上,也可以设置在特定的Bean上。下面的XML配置文件表明了如何根据名称将一个Bean设置为自动装配模式: <bean id="employeeDAO" class="com.gupaoedu.EmployeeDAOImpl" autowire="byName" /> 除了Bean配置文件中提供的自动装配模式,还可以使用@Autowired注解来自动装配指定的Bean。在使用@Autowired注解之前需要按照如下的配置方式在Spring配置文件中进行配置: <context:annotation-config /> 也可以通过在配置文件中配置AutowiredAnnotationBeanPostProcessor 达到相同的效果: <bean class ="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor"/> 配置好以后就可以使用@Autowired来标注了: @Autowired public EmployeeDAOImpl(EmployeeManager manager){ this.manager=manager; } 17、自动装配有哪些局限性自动装配有如下局限性。• 重写:你仍然需要使用< property>设置指明依赖,这意味着总要重写自动装配。 • 原生数据类型:你不能自动装配简单的属性,如原生类型、字符串和类。 • 模糊特性:自动装配总是没有自定义装配精确,因此如果可能尽量使用自定义装配。18、请解释各种自动装配模式的区别在Spring中共有5种自动装配模式,让我们逐一分析。(1)no:这是Spring的默认设置,在该设置下自动装配是关闭的,开发者需要自行在Bean定义中用标签明确地设置依赖关系。 (2)byName:该模式可以根据Bean名称设置依赖关系。当向一个Bean中自动装配一个属性时,容器将根据Bean的名称自动在配置文件中查询一个匹配的Bean。如果找到就装配这个属性,如果没找到就报错。 (3)byType:该模式可以根据Bean类型设置依赖关系。当向一个Bean中自动装配一个属性时,容器将根据Bean的类型自动在配置文件中查询一个匹配的Bean。如果找到就装配这个属性,如果没找到就报错。 (4)constructor:和byType模式类似,但是仅适用于有与构造器相同参数类型的Bean,如果在容器中没有找到与构造器参数类型一致的Bean,那么将会抛出异常。 (5)autodetect:该模式自动探测使用constructor自动装配或者byType自动装配。首先会尝试找合适的带参数的构造器,如果找到就是用构造器自动装配,如果在Bean内部没有找到相应的构造器或者构造器是无参构造器,容器就会自动选择byType模式。19、请举例解释@Required注解在产品级别的应用中,IoC容器可能声明了数十万个Bean,Bean与Bean之间有着复杂的依赖关系。设值注解方法的短板之一就是验证所有的属性是否被注解是一项十分困难的操作。可以通过设置“dependency-check”来解决这个问题。在应用程序的生命周期中,你可能不大愿意花时间验证所有Bean的属性是否按照上下文文件正确配置,或者你宁可验证某个Bean的特定属性是否被正确设置。即使用“dependency-check”属性也不能很好地解决这个问题,在这种情况下需要使用@Required 注解。可用如下的方式来标明Bean的设值方法: public class EmployeeFactoryBean extends AbstractFactoryBean<Object> { private String designation; public String getDesignation() { return designation; } @Required public void setDesignation(String designation) { this.designation = designation; } } RequiredAnnotationBeanPostProcessor是Spring中的后置处理器,用来验证被@Required 注解的Bean属性是否被正确设置了。在使用RequiredAnnotationBeanPostProcesso验证Bean属性之前,要在IoC容器中对其进行注册: <bean class="org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor" /> 但是如果没有属性被用@Required注解过,后置处理器会抛出一个BeanInitializationException异常。20、请举例说明@Qualifier注解@Qualifier注解意味着可以在被标注Bean的字段上自动装配。@Qualifier注解可以用来取消Spring不能取消的Bean应用。21、构造方法注入和设值注入有什么区别请注意以下明显的区别:(1)设值注入支持大部分依赖注入,如果我们仅需要注入int、string和long型的变量,不要用设值方法注入。对于基本类型,如果没有注入,可以为基本类型设置默认值。构造方法注入不支持大部分依赖注入,因为在调用构造方法时必须传入正确的构造参数,否则会报错。 (2)设值注入不会重写构造方法的值。如果我们对同一个变量同时使用了构造方法注入和设值注入,那么构造方法将不能覆盖设值注入的值。很明显,因为构造方法只在对象被创建时被调用。 (3)在使用设值注入时还不能保证某种依赖是否已经被注入,也就是说,这时对象的依赖关系有可能是不完整的。而在另一种情况下,构造器注入则不允许生成依赖关系不完整的对象。 (4)在设值注入时如果对象A和对象B互相依赖,在创建对象A时Spring会抛出ObjectCurrentlyInCreationException异常,因为在对象B被创建之前对象A是不能被创建的,反之亦然。Spring用设值注入解决了循环依赖问题,因为对象的设值方法是在对象被创建之前被调用的。22、Spring中有哪些不同类型的事件Spring的ApplicationContext 提供了支持事件和代码中监听器的功能。我们可以创建Bean来监听在ApplicationContext 中发布的事件。对于ApplicationEvent类和在ApplicationContext接口中处理的事件,如果一个Bean实现了ApplicationListener接口,当一个ApplicationEvent 被发布以后,Bean会自动被通知。 public class AllApplicationEventListener implements ApplicationListener<ApplicationEvent> { @Override public void onApplicationEvent(ApplicationEvent applicationEvent) { //process event } } Spring 提供了以下5种标准的事件。(1)上下文更新事件(ContextRefreshedEvent):该事件会在ApplicationContext被初始化或者更新时发布。也可以在调用ConfigurableApplicationContext 接口中的refresh()方法时被触发。 (2)上下文开始事件(ContextStartedEvent):当容器调用ConfigurableApplicationContext的Start()方法开始或重新开始容器时触发该事件。 (3)上下文停止事件(ContextStoppedEvent):当容器调用ConfigurableApplicationContext的Stop()方法停止容器时触发该事件。 (4)上下文关闭事件(ContextClosedEvent):当ApplicationContext被关闭时触发该事件。容器被关闭时,其管理的所有单例Bean都被销毁。 (5)请求处理事件(RequestHandledEvent):在Web应用中,当一个HTTP请求(Request)结束时触发该事件。除了上面介绍的事件,还可以通过扩展ApplicationEvent类来自定义事件: public class CustomApplicationEvent extends ApplicationEvent { public CustomApplicationEvent(Object source, final String msg) { super(source); System.out.println("Created a Custom event"); } } 为了监听这个事件,还需要创建一个监听器: public class CustomEventListener implements ApplicationListener<CustomApplicationEvent> { @Override public void onApplicationEvent(CustomApplicationEvent applicationEvent) { } } 之后通过ApplicationContext接口的publishEvent()方法来发布自定义事件: CustomApplicationEvent customEvent=new CustomApplicationEvent(applicationContext, "Test message"); applicationContext.publishEvent(customEvent 23、FileSystemResource和ClassPathResource有什么区别在FileSystemResource 中需要给出spring-config.xml文件在项目中的相对路径或者绝对路径。在ClassPathResource中Spring会在ClassPath中自动搜寻配置文件,所以要把ClassPathResource 文件放在ClassPath下。如果将spring-config.xml保存在了src目录下,只需给出配置文件的名称即可,因为src是默认的路径。简而言之,ClassPathResource在环境变量中读取配置文件,FileSystemResource在配置文件中读取配置文件。24、Spring中用到了哪些设计模式Spring中使用了大量的设计模式,下面列举了一些比较有代表性的设计模式。(1)代理模式:在AOP和remoting中被用得比较多。 (2)单例模式:在Spring配置文件中定义的Bean默认为单例模式。 (3)模板模式:用来解决代码重复问题,比如RestTemplate、JmsTemplate、JpaTemplate。 (4)委派模式:Spring提供了DispatcherServlet来对请求进行分发。 (5)工厂模式:BeanFactory用来创建对象的实例,贯穿于BeanFactory和ApplicationContext接口。 (6)代理模式:代理模式AOP思想的底层实现技术,Spring中采用JDK Proxy和CGLib类库。25、在Spring中如何更有效地使用JDBC使用Spring JDBC可以使得资源管理及错误处理的代价减小。开发人员只需通过statements和queries语句从数据库中存取数据。Spring通过模板类能更有效地使用JDBC,也就是所谓的JdbcTemplate。26、请解释Spring中的IoC容器Spring中的org.springframework.beans包和org.springframework.context包构成了Spring IoC容器的基础。BeanFactory接口提供了一个先进的配置机制,使得任何类型的对象的配置都成为可能。ApplicationContex接口对BeanFactory(是一个子接口)进行了扩展,在BeanFactory的基础上添加了其他功能,比如与Spring的AOP更容易集成,也提供了处理Message Resource的机制(用于国际化),以及事件传播及应用层的特别配置,比如针对Web应用的WebApplicationContext。27、在Spring中可以注入null或空字符串吗完全可以。 喜欢文章记得点个赞,感谢支持!

优秀的个人博客,低调大师

Java线程安全面试题,你真的了解吗?

多个线程不管以何种方式访问某个类,并且在主调代码中不需要进行同步,都能表现正确的行为。 线程安全有以下几种实现方式: 不可变不可变(Immutable)的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个线程之中处于不一致的状态。多线程环境下,应当尽量使对象成为不可变,来满足线程安全。 不可变的类型: final 关键字修饰的基本数据类型String枚举类型Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的。对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。 public class ImmutableExample { public static void main(String[] args) { Map<String, Integer> map = new HashMap<>(); Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map); unmodifiableMap.put("a", 1); } }Exception in thread "main" java.lang.UnsupportedOperationException at java.util.Collections$UnmodifiableMap.put(Collections.java:1457) at ImmutableExample.main(ImmutableExample.java:9) Collections.unmodifiableXXX() 先对原始的集合进行拷贝,需要对集合进行修改的方法都直接抛出异常。 public V put(K key, V value) { throw new UnsupportedOperationException(); }互斥同步synchronized 和 ReentrantLock。 非阻塞同步互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。 互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。 CAS随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。 乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。 AtomicIntegerJ.U.C 包里面的整数原子类 AtomicInteger 的方法调用了 Unsafe 类的 CAS 操作。 以下代码使用了 AtomicInteger 执行了自增的操作。 private AtomicInteger cnt = new AtomicInteger(); public void add() { cnt.incrementAndGet(); }以下代码是 incrementAndGet() 的源码,它调用了 Unsafe 的 getAndAddInt() 。 public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; }以下代码是 getAndAddInt() 源码,var1 指示对象内存地址,var2 指示该字段相对对象内存地址的偏移,var4 指示操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值,通过调用 compareAndSwapInt() 来进行 CAS 比较,如果该字段内存地址中的值等于 var5,那么就更新内存地址为 var1+var2 的变量为 var5+var4。 可以看到 getAndAddInt() 在一个循环中进行,发生冲突的做法是不断的进行重试。 public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; } ABA如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。 J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。 无同步方案要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。 栈封闭多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。 public class StackClosedExample { public void add100() { int cnt = 0; for (int i = 0; i < 100; i++) { cnt++; } System.out.println(cnt); } }public static void main(String[] args) { StackClosedExample example = new StackClosedExample(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> example.add100()); executorService.execute(() -> example.add100()); executorService.shutdown(); }100100 线程本地存储(Thread Local Storage)如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。 符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。 可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。 对于以下代码,thread1 中设置 threadLocal 为 1,而 thread2 设置 threadLocal 为 2。过了一段时间之后,thread1 读取 threadLocal 依然是 1,不受 thread2 的影响。 public class ThreadLocalExample { public static void main(String[] args) { ThreadLocal threadLocal = new ThreadLocal(); Thread thread1 = new Thread(() -> { threadLocal.set(1); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(threadLocal.get()); threadLocal.remove(); }); Thread thread2 = new Thread(() -> { threadLocal.set(2); threadLocal.remove(); }); thread1.start(); thread2.start(); } }1为了理解 ThreadLocal,先看以下代码: public class ThreadLocalExample1 { public static void main(String[] args) { ThreadLocal threadLocal1 = new ThreadLocal(); ThreadLocal threadLocal2 = new ThreadLocal(); Thread thread1 = new Thread(() -> { threadLocal1.set(1); threadLocal2.set(1); }); Thread thread2 = new Thread(() -> { threadLocal1.set(2); threadLocal2.set(2); }); thread1.start(); thread2.start(); } }它所对应的底层结构图为: 每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象。 /* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals = null; 当调用一个 ThreadLocal 的 set(T value) 方法时,先得到当前线程的 ThreadLocalMap 对象,然后将 ThreadLocal->value 键值对插入到该 Map 中。 public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }get() 方法类似。 public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。 在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,应该尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。 可重入代码(Reentrant Code)这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。 可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。

优秀的个人博客,低调大师

死磕 java原子类之终结篇(面试题)

概览 原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何线程上下文切换。 原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分,将整个操作视作一个整体是原子性的核心特征。 在java中提供了很多原子类,笔者在此主要把这些原子类分成四大类。 原子更新基本类型或引用类型 如果是基本类型,则替换其值,如果是引用,则替换其引用地址,这些类主要有: (1)AtomicBoolean 原子更新布尔类型,内部使用int类型的value存储1和0表示true和false,底层也是对int类型的原子操作。 (2)AtomicInteger 原子更新int类型。 (3)AtomicLong 原子更新long类型。 (4)AtomicReference 原子更新引用类型,通过泛型指定要操作的类。 (5)AtomicMarkableReference 原子更新引用类型,内部使用Pair承载引用对象及是否被更新过的标记,避免了ABA问题。 (6)AtomicStampedReference 原子更新引用类型,内部使用Pair承载引用对象及更新的邮戳,避免了ABA问题。 这几个类的操作基本类似,底层都是调用Unsafe的compareAndSwapXxx()来实现,基本用法如下: private static void testAtomicReference() { AtomicInteger atomicInteger = new AtomicInteger(1); atomicInteger.incrementAndGet(); atomicInteger.getAndIncrement(); atomicInteger.compareAndSet(3, 666); System.out.println(atomicInteger.get()); AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1); atomicStampedReference.compareAndSet(1, 2, 1, 3); atomicStampedReference.compareAndSet(2, 666, 3, 5); System.out.println(atomicStampedReference.getReference()); System.out.println(atomicStampedReference.getStamp()); } 原子更新数组中的元素 原子更新数组中的元素,可以更新数组中指定索引位置的元素,这些类主要有: (1)AtomicIntegerArray 原子更新int数组中的元素。 (2)AtomicLongArray 原子更新long数组中的元素。 (3)AtomicReferenceArray 原子更新Object数组中的元素。 这几个类的操作基本类似,更新元素时都要指定在数组中的索引位置,基本用法如下: private static void testAtomicReferenceArray() { AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10); atomicIntegerArray.getAndIncrement(0); atomicIntegerArray.getAndAdd(1, 666); atomicIntegerArray.incrementAndGet(2); atomicIntegerArray.addAndGet(3, 666); atomicIntegerArray.compareAndSet(4, 0, 666); System.out.println(atomicIntegerArray.get(0)); System.out.println(atomicIntegerArray.get(1)); System.out.println(atomicIntegerArray.get(2)); System.out.println(atomicIntegerArray.get(3)); System.out.println(atomicIntegerArray.get(4)); System.out.println(atomicIntegerArray.get(5)); } 原子更新对象中的字段 原子更新对象中的字段,可以更新对象中指定字段名称的字段,这些类主要有: (1)AtomicIntegerFieldUpdater 原子更新对象中的int类型字段。 (2)AtomicLongFieldUpdater 原子更新对象中的long类型字段。 (3)AtomicReferenceFieldUpdater 原子更新对象中的引用类型字段。 这几个类的操作基本类似,都需要传入要更新的字段名称,基本用法如下: private static void testAtomicReferenceField() { AtomicReferenceFieldUpdater<User, String> updateName = AtomicReferenceFieldUpdater.newUpdater(User.class, String.class,"name"); AtomicIntegerFieldUpdater<User> updateAge = AtomicIntegerFieldUpdater.newUpdater(User.class, "age"); User user = new User("tong ge", 21); updateName.compareAndSet(user, "tong ge", "read source code"); updateAge.compareAndSet(user, 21, 25); updateAge.incrementAndGet(user); System.out.println(user); } private static class User { volatile String name; volatile int age; public User(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "name: " + name + ", age: " + age; } } 高性能原子类 高性能原子类,是java8中增加的原子类,它们使用分段的思想,把不同的线程hash到不同的段上去更新,最后再把这些段的值相加得到最终的值,这些类主要有: (1)Striped64 下面四个类的父类。 (2)LongAccumulator long类型的聚合器,需要传入一个long类型的二元操作,可以用来计算各种聚合操作,包括加乘等。 (3)LongAdder long类型的累加器,LongAccumulator的特例,只能用来计算加法,且从0开始计算。 (4)DoubleAccumulator double类型的聚合器,需要传入一个double类型的二元操作,可以用来计算各种聚合操作,包括加乘等。 (5)DoubleAdder double类型的累加器,DoubleAccumulator的特例,只能用来计算加法,且从0开始计算。 这几个类的操作基本类似,其中DoubleAccumulator和DoubleAdder底层其实也是用long来实现的,基本用法如下: private static void testNewAtomic() { LongAdder longAdder = new LongAdder(); longAdder.increment(); longAdder.add(666); System.out.println(longAdder.sum()); LongAccumulator longAccumulator = new LongAccumulator((left, right)->left + right * 2, 666); longAccumulator.accumulate(1); longAccumulator.accumulate(3); longAccumulator.accumulate(-4); System.out.println(longAccumulator.get()); } 问题 关于原子类的问题,笔者整理了大概有以下这些: (1)Unsafe是什么? (3)Unsafe为什么是不安全的? (4)Unsafe的实例怎么获取? (5)Unsafe的CAS操作? (6)Unsafe的阻塞/唤醒操作? (7)Unsafe实例化一个类? (8)实例化类的六种方式? (9)原子操作是什么? (10)原子操作与数据库ACID中A的关系? (11)AtomicInteger怎么实现原子操作的? (12)AtomicInteger主要解决了什么问题? (13)AtomicInteger有哪些缺点? (14)ABA是什么? (15)ABA的危害? (16)ABA的解决方法? (17)AtomicStampedReference是怎么解决ABA的? (18)实际工作中遇到过ABA问题吗? (19)CPU的缓存架构是怎样的? (20)CPU的缓存行是什么? (21)内存屏障又是什么? (22)伪共享是什么原因导致的? (23)怎么避免伪共享? (24)消除伪共享在java中的应用? (25)LongAdder的实现方式? (26)LongAdder是怎么消除伪共享的? (27)LongAdder与AtomicLong的性能对比? (28)LongAdder中的cells数组是无限扩容的吗? 关于原子类的问题差不多就这么多,都能回答上来吗?点击下面的链接可以直接到相应的章节查看: 死磕 java魔法类之Unsafe解析 死磕 java原子类之AtomicInteger源码分析 死磕 java原子类之AtomicStampedReference源码分析 杂谈 什么是伪共享(false sharing)? 死磕 java原子类之LongAdder源码分析 彩蛋 原子类系列源码分析到此就结束了,虽然分析的类比较少,但是牵涉的内容非常多,特别是操作系统底层的知识,比如CPU指令、CPU缓存架构、内存屏障等。 下一章,我们将进入“同步系列”,同步最常见的就是各种锁了,这里会着重分析java中的各种锁、各种同步器以及分布式锁相关的内容。 欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

资源下载

更多资源
优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。