说到多线程的原子性、可见性和有序性。这是多线程确保线程安全的三个标准。首先。咱说说。原子性。原子性其实很好理解。原子就是最小的单元,他就是可执行的最小的单元。在程序执行的时候,最小的一个可执行单元就是一个原子。一段原子性的代码执行的时候。不会被打断。这一段代码的执行,要么不执行,要么全部执行完毕。这段代码也许只有一行代码,也许是多行代码。一行代码很多也不是原子性的,因为这个原子性并非是我们Java程序代码的原子性,而是CPU执行 阶段的原子性。并不是说代码少他就是原子性。也不是说代码多的就不是原子性。一行代码很多都不是原子性的,多行代码加上锁,也可以是原子性的。比如。定义一个整型int i = 10。这一行代码它就是原子性的,这里是给整形变量i赋值,这个就是原子性的,它不可能被打断。如果定义的一个长整型的 long j=10,这就不是原子性的,为什么呢?因为这个long类型啊,他是64位的,64位的长整型。在CPU中执行赋值操作的时候,它是分两步,分别64位的高位和低位进行赋值,这是分两步来完成的。类似的double也是这样子,double也是64位的,会分两步分别给高32位和低32位赋值,就是两步操作,不是原子性的。那么咱们来看另一个语句。int i=10; i++ ; 这个i=10我们刚刚说过了,是原子性的,那这个 i++是不是原子型的呢?直接说答案:i++不是原子型的。别看这么简单的一个计算,写程序就一行,但到了CPU级别就需要3条指令,1 获取i的值, 2 执行i+1 的操作,3 把i+1的结果赋值给i 。所以这个i++不是原子性的。这么简单的语句都不是原子性的,那么是不是对于多行代码就肯定不是原子性了呢?其实也不是,Java可以利用锁来保证多行语句是原子性的。从CPU的角度来看,就是插入一个Lock指令。当一段代码被Lock指令锁住后,一个线程执行这段代码的时候,其他线程就无法执行,只能等着这个线程执行完毕才能获得执行权。这就是保证这段代码的原子性。具体在Java代码里面写的时候就用synchronized 和lock对象,来实现一段代码,一个方法,一个对象的锁。这是对原子性的解释。那么可见性是什么呢?在Java的内存模型中,每一个子线程会拥有一个单独的工作内存,主线程有一个主内存。主内存中存放的是共享变量,虽说是共享变量,看起来是被主线程和子线程共享的,那么子线程就可以读写操作该共享变量了吧?其实不行的,子线程是不能直接读写操作主内存中的共享变量的。子线程会在工作内存中创建该共享变量的副本,然后读写操作该副本。这里再强调一下,子线程不能直接读写主内存的共享变量,只能读写工作内存中的共享变量的副本,读写完毕后再将副本的值回写到主内存中。这时候,我们就发现一个问题了,假设主线程中有共享变量 x = 10 , 有两个子线程 A和B,要操作共享变量x,就会分别在各自的工作内存中加载一份x的副本。这时候咱们看到了,原来只有一个x,现在变成了3份:主内存中的x和两个副本x。如果子线程A对副本x做了+1 操作,线程A中的x就变成 11 了,但线程B看到的x还是10,这就出现了可见性的问题。通过上面的描述,总结一下什么是线程的可见性问题:就是多线程情况下,一个线程修改了变量的值,另一个线程看不到这个变化。那怎么解决这个问题呢?思路是这样的,对于线程A来说,只要修改了x副本的值,马上把这个值回写到主内存中;对于线程B来说,只要想读x的值,就再次从主线程加载x,这样副本的值永远都与主内存中的值是一样的。这就可以解决可见性问题了。在具体实现上Java内存模型是通过内存屏障来实现的。那这里又引出一个内存屏障的概念,啥是内存屏障呢?内存屏障其实就是一个指令,将这个指令插入到其他指令之间,会执行某些特殊的操作。具体到解决可见性是两个内存屏障指令:load屏障和store屏障。 load和store这俩词儿之前讲内存模型的8个指令的时候见过,load指令是工作内存从主内存加载共享变量生成副本的指令,store指令是将工作内存的共享变量副本回写到主内存的指令。那么load屏障就呼之欲出了,这是在指令A前插入load屏障,那么指令A用到的变量副本将失效,必须从主内存加载对应的共享变量的值并替换当前副本;对应的store屏障插入到指令B之后,那么指令B修改的副本的值,马上会被回写到主内存。这个效果应该可以想象了吧?通过load屏障和store屏障,可以实现修改了副本马上回写到主内存,读副本之前先从主内存加载一下,这样就确保了,只要副本值修改了马上就更新到主内存里面,然后另一个线程读副本的时候直接装载主内存中的新值,这样就解决了多线程下变量的可见性。接下来讲解一下有序性。什么是有序性呢?这里的序是指什么的顺序呢?这里说的序是指CPU中指令执行的顺序,就是一条条的汇编指令的先后顺序。我们Java程序员写的是Java代码,写出来一行行的Java代码最后编译转为汇编在CPU中执行,这时候就是一条条的汇编指令了。一般程序员的理解是这样的,写了两行代码,行A在行B的前面,应该是先执行A再执行B,但其实CPU在指令执行的时候做了优化,如果指令A和指令B调换顺序后不影响执行结果,那么CPU会进行指令重排,重排后就是先执行指令B再执行指令A。这个原则叫做as if serial ,就是好像是连续的一样,也就是重排序优化后就好像没有重排序,而是正常连续执行的那样。比如:int i=10;int j=20;这样的两条指令,先执行哪条其实是无所谓的,这时候CPU就会执行指令重排。对于单线程程序来说,CPU指令重排绝对不会有问题,但对于多线程指令重排就有可能产生数据安全问题。打个比喻:刘能和赵四去买冰糕,这个指令顺序是,推门进冷饮店,掏钱,拿冰糕。如果就刘能自己去买冰糕的话,先掏钱再拿冰糕还是先拿冰糕再掏钱,没什么关系,冷饮店老板照顾的过来。现在刘能和赵四俩人一起去,结果赵四掏钱了,还没拿冰糕,刘能先执行了拿冰糕,这下赵四不干了,我掏的钱,我掏的钱。老板说,没办法,人太多忙不过来,先后顺序调整一下,这样快,你别叫唤了。那赵四能干嘛?赵四大叫:不行啊,出现多线程的有序性问题了。那咋解决这个问题呢?还是下内存屏障。有序内存屏障是在两条指令之间插入的屏障,插入后,前后两条指令就不能重排序了。处理有序性的屏障有4个。LoadLoad屏障:下在load和load之间,这个屏障指令之前的load指令和之后的load相关的指令不能重排序;LoadStore屏障:下在load和store之间,这个屏障指令之前的load指令和之后的store指令不能重排序;StoreLoad屏障:下在store和load之间,这个屏障指令之前的store指令和之后的load指令不能重排序;StoreStore屏障:下载store和store之间,这个屏障指令之前的store指令和之后的store指令不能重排序。说白了,这个有序性的内存屏障,就是禁止CPU对前后的指令进行重排序的,有了这个屏障,多线程下CPU也不能做重排序优化了,就解决了有序性的问题了。关于内存屏障的细节,我们今儿就不聊了,以后我们再细说。这个多线程的原子性、可见性和有序性是啥意思,你明白了吧?
1 Java中有4中引用,强引用、软引用、弱引用、虚引用。强引用:普通new出来一个对象 ,都是强引用。软引用:SoftReference类,当内存不足的时候,会被回收。弱引用:WeakReference类,内存充足也会被垃圾回收。虚引用:最脆弱的引用,记录一个对象已经被回收了。2 当某个对象只被弱引用的时候,Java的垃圾回收机制就会回收该对象。3 我们知道一个对象定义出来,在堆内存中为其开辟空间,在栈中存储该对象的引用。比如定义一个Car a1 = new Car() ; 这时候在堆中开辟了一块空间存储Car的数据,而a1则存储在栈中,引用堆中的Car。4 如果我们设置 a1 = null,那么堆中的Car空间就不被栈引用了,Java的垃圾回收就会回收这块内存。5 如果我们设置 a1 = new Car() ; a2 = a1 ; a1 = null ; 这时候虽然 a1 不引用堆中的Car,但a2 还引用着堆中的Car,那么堆中的Car空间是不会被回收的。6 这时候就可以使用弱引用了。 a1 = new Car() ; a2 = new WeakReference(a1) ; a1 = null。这时候 a1 不再引用堆中的Car空间,a2 是一个弱引用,虽然引用着堆中的Car空间,由于堆中的Car空间只被弱引用,因此Java的垃圾回收就会回收该空间。
1 准备Spring的上下文环境,也就是ApplicationContext2 扫描XML文件,或者是注解,得到一系列的BeanDefinitaion3 BeanFactoryPostProcessor,Bean工厂的后置处理器, 要对BeanDefinition做一些处理,替换一些属性的值,比如MyBatis的Bean,就会做这个处理。4 开始实例化Bean,即new出来Java对象,当然,他不是真的new,而是通过反射实例化Bean5 开始初始化Bean,对Bean的属性赋值,即根据Bean的依赖关键,进行Bean的依赖注入6 对Bean的后置处理,调用BeanPostProcessor,进行AOP相关的操作,这时候就会对Bean进行切面增强。7 将Bean放入Bean容器,即一个HashMap中,这时候开发者就可以使用这个Bean了。8 Bean的销毁,当Spring的AppliationContext要关闭的时候,会调用DiposibleBean的destory方法,或者Bean的destory方法,进行Bean销毁。
1 AOP就是面向切面编程,通过切面来达到对目标类的增强的目的,关于面向切面就不做太多的解释了。2 AOP底层是动态代理。3 AOP针对实现了接口的类,基于原生JDK的动态代理实现的; 针对没有实现接口的类,基于CGLib来实现的。4 CGLIb的动态代理,通过实现一个子类来实现的。5 JDK的动态代理有一个限制,只能针对实现了接口的类进行动态代理,这是为什么呢?因为JDK的动态代理实现的代理类不是继承目标类,而是继承自Proxy类,而JAVA不允许双重代理,因此就要求目标类必须实现了接口的类。
1 各种ApplicationContext ,核心实现的是 BeanFactory ,例如 XmlWebApplicationContext AnnotationConfigApplicationContext ,2 BeanDefination3 创建一个 DefaultListableBeanFactory, 基于BeanDefination 创建Bean ,只处理单例的 , 先实例化,再初始化, DI ,4 所有的Bean 都放到一个 Map 里面,这就是Bean的容器。
1 IOC 是控制反转, Inversion of Control。2 控制反转不是一种语法,不是类或方法,它是一种设计模式3 当A类依赖于B类,如果不使用控制反转,A类要主动创建B类的对象,也就是new B类,这时候控制权在A类的手中,这就是主动控制; 使用控制反转,A类只要声明自己需要B类,而控制权交给Spring容器,Spring容器在A需要B类的时候为其注入B类即可,这时候控制权在Spring容器手中,即控制反转了。4 好处是什么呢?控制反转后A类与B类的耦合性就降低了,程序更加灵活。5 比如,一家公司为了卫生和清洁,需要有人打扫卫生,这家公司可以自己聘用一个保洁,就是将管理保洁的权利控制在自己手中,自己new一个保洁,那么这个保洁请假、调休、年终福利、婚丧嫁娶都要操心,太麻烦了,公司和这个保洁紧紧地绑在 一起,给公司带了了很大的负担。公司决定采用控制反转模式,将保洁管理的权限交给物业来做,自己需要保洁的时候,由物业来分配一个保洁即可,公司和保洁直接就就没有什么关系了,松耦合了,这时候物业公司就是Spring容器,公司将保洁的控制权反转给了物业公司,当公司需要保洁的时候,物业公司将一个保洁注入到公司即可。这就是控制反转模式。
1.反射的设计初衷是为了程序在运行的过程中可以动态的创建对象,什么意思呢,也就是说在编码阶段我们不进行对象的创建,让程序运行起来之后按照需求通过反射技术动态创建所需要的对象.2.那么这个过程虽然不难,但理解起来确实不太容易理解,我们用一个场景来解析一下你就明白了3.咱们java在操作数据库的时候会使用到JDBC,那么在原生的连接数据库进行查询操作后,会获得一个resultset集合,在这个集合中有当前查询的数据表的字段名和字段名对应的值.4.但是这个resultset集合中的数据的在使用起来时是极其不方便的.怎么样才能操作方便呢,我们操作实体类对象才是最方便的,那么我们需要将resultset集合中的每一行数据转化成实体类对应的对象,如果是一张数据表的查询那么手写一下就可以了,如果是很多张数据表的查询就需要一种通用的转化方式,这时就必须要使用反射技术来实现了.6.现在市面上使用的持久化框架全部都使用了反射技术的.7.说句不夸张的话,如果没有反射那么现在市面上所有的主流框架(spring,springMVC,hibernate,mybatis等...)都将会失去理论基础.
1.浮动你肯定清楚是咋回事,设置属性float向左或者是向右浮动2.浮动所带来的问题是,无法撑开父元素的高度。 非浮动情况下,子元素会把父元素的高度撑开,这个好理解。当子元素是浮动的时候,子元素浮起来脱离了父元素的包裹了,自然父元素内部就空了,所以父元素的高度就塌陷了。由于有这个大问题,所以必须清除浮动,目的是让子元素即便浮动了依旧能撑开父元素的高度。3.有两种方式:一个是使用CSS的clear属性,还有一种是利用BFC机制。4.先说第一种方法,在浮动元素之后,再增加一个块儿元素,然后对该块元素设置clear属性,从而达到清除浮动的目的。还有一种是在父级标签上设置overflow属性,触发BFC的机制来实现清除浮动。BFC是啥呢,简单来说,BFC就是一个独立的渲染区域。这个区域会裹住浮动的子元素,从而脱标的子元素也可以撑开父元素,使父元素又高
ArrayList和LinkedList的区别LinkedList和ArrayList的区别主要来自于他们俩的数据结构不同ArrayList是基于数组实现的,LinkedList是基于双链表实现的ArrayList是基于数组的数据结构,数组在内存中存储数据的空间是连续的,可以通过下角标直接访问数组中的元素,因此在随机访问集合元素时有较好的性能。ArrayList获取数据的时间复杂度是O(1),但是要插入、删除数据的开销很大,因为要将插入位置之后的所有元素进行后移操作。例如买票时,有人插队,则插队的位置后面的所有人都要后移,消耗就比较大。相比于ArrayList,LinkedList的随机访问元素时性能较差,因为双向链表在内存中存储数据不是连续的;但在插入,删除操作是很快的,那么这个链表呢,就好比幼儿园小朋友手拉手围成一个大圈儿做游戏,这时候来了一个新的小朋友要加入,只需要任意2个小朋友松开手,这个新的小朋友加入即可,其他小朋友不用动。所以性能相对ArrayList要好很多因为LinkedList它是链表操作,所以在插入或删除的时间复杂度仅为O(1) 。使用场景如果应用程序对数据有较多的随机访问,首选肯定是ArrayList集合;如果应用程序有更多的插入或者删除操作,较少的数据读取,LinkedList对象要优于ArrayList对象;不过ArrayList的插入,删除操作也不一定比LinkedList慢,如果在集合靠近末尾的地方插入,那么ArrayList只需要移动较少的数据,而LinkedList则需要一直查找到列表尾部,反而耗费较多时间,这时ArrayList就比LinkedList效率要高。
1 Java中操作字符串的有3个类:String StringBuilder StringBuffer2 String的本质是字符数组3 String内部定义了一个字符数组存储字符串的每个字符,并且改字符串被final修饰,所以,决定了String的内容是不可修改的,一旦修改其实是重新开辟了内存,创建了新的字符数组。4少量的字符串编辑和修改,用String没问题。频繁的字符串编辑修改,String的性能太低了。5 StringBuilder是为了频繁操作字符串而生的。它默认开辟一块更大的字符数组,比如一个字符串只有8个字符,StringBuilder开辟一块儿32个字符的空间存储改字符串,这样如果在该字符串后再追加8个字符,就不需要开辟新空间了,从而提高了性能。7 StringBuilder和StringBuffer的功能和方法是一模一样的,区别就是StringBuilder是线程不安全的,StringBuffer是线程安全的。8 StringBuffer在每个方法上使用Synchronized关键字进行修饰,方法在多线程中是同步的,从而实现线程安全。你好你好9总结:String性能低,操作简单,只能在不频繁操作字符串的时候使用;StringBuilder的性能最好;StringBuilder和StringBuffer所有功能和方法都一样;StringBuilder是线程不安全的,StringBuffer是线程安全的;StringBuffer因为线程安全,所以性能不如StringBuilder好
1 ==是比较运算符,Equals是方法2 ==在判断基本数据类型的时候,就是判断数值是否相等,比如int 10和int 20的时候就是判断10和20这两个数是否相等3 ==在判断引用数据类型的时候,也就是对象是否相等的时候,判断的是内存地址是否相等,也就是这两个对象是否就是同一个对象。4总结:==判断基本数据类型的值,判断引用数据类型的内存地址。5 Equals是定义在Object类的方法,不同类有不同的实现。6用Integer和String这两个阐述一下,从而理解Equals方法的设计思想。7 Integer的Equals方法通过三步完成比较:地址是否相同; 类型是否相同;数值是否相同;8 String的Equals方法通过四步完成比较:地址是否相同;类型是否相同;长度是否相同;每个字符是否相同;9总结:Equals方法最终目的是比较内容相同。
题目本期要说的是《2020年Java面试208题》的第16题,面试官提了这样一个问题:请说说File类有哪些常用方法? 知识点剖析首先说File类是干什么的。File类是用来操作文件和目录的,这里注意,不仅仅是文件,也包括目录的操作File都提供了很多方法。比如创建、删除、重命名,都是可以的。但并不提供文件内容的操作,如果需要操作文件内容必须基于IO来实现。 File还有两个常量很有用,PathSeparator和speparator,第一个常量是多个路径之间的分隔符,windows下是分号,第二个常量是路径中多层目录之间的分隔符,windows下是向右的斜杠。这俩常量可以保证程序在不同操作系统下可以自由的切换。 演示面试好了,关于File类,只强调这两点,我现在来做演示面试: 你好,面试官。 File类是用来操作文件和目录的,主要功能有创建、删除、判断状态、获取文件信息、还有文件和目录遍历。创建文件的方法有:createNewFile: 创建一个空文件,然后可以使用IO流向其中写内容。如果已经存在文件的话,则返回false,不会覆盖;如果不存在文件的话,则创建文件并返回true。mkdir:新增文件夹,这个是新增单级文件夹。mkdirs:新增多级文件夹,一般这个方法使用的更多一些。renameTo:对文件重命名,如果是不同路径的话,就是文件剪切操作。 删除相关的方法:delete:删除一个文件或一个空文件夹,如果不是空文件夹的话是不允许删除的。deleteOnExit:这是当java程序退出的时候才删除。用来删除临时文件,比如我们createNewFile创建了一个临时文件,在最后程序运行结束后,肯定是要删除的,那么删除操作就比较麻烦了,因为那个时间点不好控制,但执行这个方法后,在java程序结束后就会自动删除这个临时文件。 判断相关的方法:exists:判断文件或文件夹是否存在,这个使用非常频繁。isFile:判断是否是一个文件。isDirectory:判断是否是一个文件夹。IsHidden:判断是否是隐藏的isAbsolute:判断File的路径是否是绝对路径。 获取文件相关信息的方法:getName:获取文件或文件夹的名称,并不包括路径getAbsolutePath:获取文件的绝对路径,这个并不管文件是否存在,只是根据文件的路径描述转换为绝对路径形式。对应的还有getAbsoluteFile,这个就不赘述了。length:获取文件的大小,就是字节数,返回long,长整型。文件不存在就返回为0L。getParent:获取父路径。lastModified:获取文件最后修改时间。 文件和目录的遍历:listRoots:列出根目录,在windows下就是所有的盘符。list:列出文件夹下的所有文件和目录,返回值是子目录和文件的名称字符串数组,这个不会做层级遍历,只会列出直接下级的子目录和文件。类似的还有listFiles,就不再赘述了。这个list方法还可以增加一个过滤文件名的参数,有了这个参数后只会返回符合条件的目录和文件名。 ========================================================= 以上就是我的演示面试,不知道是否让你满意。 下期再见 我们下期再见。
题目本期要说的是《2020年Java面试208题》的第15题,面试官提了这样一个问题:请说说BIO NIO AIO的区别? 知识点剖析 * IO是什么用中国话说,IO =输入/输出英文全写,IO = in/out从表现上来看,IO =读写文件,微信发消息,网上看电影,键盘信号,信息显示在显示器上,这些都是基于输入输出的。从技术核心来说,IO =一个个的字节从一个地方进入内存,或者从内存传到另一个地方。IO的中心是内存,是针对内存的方向说的in(入),针对内存的方向说的out(出)IO基本来说做计算机的人都能理解这个是啥东西,哪里会用到这个。但做软件开发的就需要从原理、模型、性能方面来理解,具体到最后还得会写程序,这个就比较复杂了。我这里不说怎么写程序,只说模型和原理,希望帮助你理解IO和三代IO是怎么回事,原理理解了,到具体写代码的时候就很简单了。 * BIO的模型和原理BIO的英文全称是Block IO,就是阻塞式IO这是最原始的模型,这个模型的特点就是同步+阻塞,结果就是高并发下性能很差。 举个聊天服务器的例子: 聊天服务器端,有一个线程充当看门老头儿的角色,等着有新用户创建聊天连接请求,他啥也不干,就等着有新用户来了做接待工作。 新用户来了,这个门房老头线程把这个请求连接创建好,然后交个另一个处理聊天业务的线程为新用户服务,这时候门房老头继续等着新用户上门。 接下来说负责这个用户服务的线程,只能负责一个连接请求。也就是有一个用户要聊天就有一个业务线程创建出来了。如果有100个用户进来了,就得创建100个业务线程。 这种模型下,有一个只负责接待的线程,就是那个门房老头,他是阻塞的,也是同步的。每个处理聊天业务的线程也是阻塞的,也是同步的。 * BIO的优点这种模型的结构简单,一看就明白,也好理解。 * BIO存在的问题问题是一旦聊天的用户多了,线程就会蹭蹭的增加,线程的效率太低了,这个模型结构处理高并发的业务就会出问题。 * NIO的模型和原理在JDK1.5就推出了一个新的IO叫NIO,有人称其为New IO,我更愿意称其为NonBlock IO。目的就是解决BIO的高并发性能问题。如何实现的呢?这个就要从设计模式来解释了。NIO采用了Reactor的设计模式,这个Reactor中最牛的地方是提供了一个Selector和一个Dispacher,Selector是负责监听流事件的,Dispatcher负责将事件消息内容分发给具体的方法来处理。这俩兄弟配合到一起,扮演了收发室老头的工作。收发室老头儿负责接收信件(Selector),并把信件送到各科室(Dispatcher)。这种模式下带来的最大的改善,不需要每个通信连接由一个单独的线程来处理了,在消息内容分发后,调用的业务处理程序可以都在一个线程中执行,然后减少了线程创建和线程切换的花销,从而大大提升了程序性能。那么我们可能把BIO改造一下,也实现高并发不?其实也不是不可能,比如Http请求,每次请求有大量的连接,一个图片是一个连接、一个css也是一个连接,一个js文件也是一个连接,说明一下,我们不考虑http2.0对连接复用的机制,只基于简化的http模型来说明这个问题。但其实我们可以发现,http的每次请求在服务端处理的时间都很短,这样我们可以采用线程池的概念,启动若干个线程放到线程池,每个http请求都是一个任务,创建一个任务然后看看线程池里面有没有闲的线程,如果有的话就直接复用闲置的线程,没有空闲线程的话可以把任务放入队列,也可以创建新线程,这些是线程池的不同策略。从而实现线程的复用。因为BIO核心的问题并不是单任务性能差,而是高并发会出现线程创建和线程切换的花销。当然,依旧是NIO更牛,因为NIO是从操作系统层实现的,我们自己写优化的话,一定不如NIO的性能好。 * NIO存在的问题NIO这么牛了,是不是就是终极解决方案了?其实也不是。NIO也存在问题,因为NIO的方式是在一个线程中处理多个业务请求,从而降低多线程创建和切换造成的时间和资源的消耗,但我们知道线程中的程序只能有一个执行,不可能两个方法都执行,别说俩方法了,两行代码也得一行执行完了另一行执行。那么如果NIO的一个消息处理时间比较长,就会造成麻烦了,其他的消息都干不了活了。NIO模型存在这个问题,是不是一听感觉麻烦大了?其实也不是,因为NIO适合的场景是每个消息处理时间都不长的情景,这种情景下性能极高。曾经有过在windows下写的电信行业的业务处理模块,每秒30万条的消息,几百兆大小,跑起来CPU也就20%左右的使用率。所以NIO是没办法处理每个消息的处理时间太长的业务。这可怎么办呢?多线程解决嘛,也可以将NIO的业务处理放到多个任务里面,然后放到线程池来处理,这样一个线程处理时间长,不会影响另一个线程。如此一来,世界美丽,回头看看,似乎跟BIO的优化方案差不多,这多线程池似乎是一切高并发IO问题的终极解决办法啊。这就引出了AIO。 * AIO的模型和原理AIO的模型核心就是在NIO的基础上增加了一个线程池,每个消息的处理交给一个线程来处理,这个线程池谁维护的?操作系统维护的,那是计算机里面的终极大BOSS,一定比我们自己写的线程池方案要优秀的多。当然AIO不仅仅是这些优化,在数据流接收和分发机制方面都做了优化,不过,这些都不是核心最优秀的特性,我们这里不做过多评估。AIO的设计模型是基于Proactor,这个又涉及到设计模式,这个设计模式是啥意思呢?Proactor模式的思想是有消息了,消息通道帮我接收好,全部收好后通知我,我取了消息后通知业务代码来处理消息。这里我们看看,Proactor在中间需要处理的工作很轻量,收消息他不操心,消息都收好了他才得到通知去取消息,他取了消息后并不做复杂的处理,只是通知业务代码去处理消息,在这个过程中,Proactor处理的工作量很轻。事实上,从代码层分析AIO也比BIO和NIO简单。Proactor像是一个甩手掌柜,负责指挥干活,但自己啥事儿不干。因此总结一下就是AIO一切都是非阻塞的事件来驱动的,然后所有的业务处理都是异步的,因为业务都是在线程池中执行的。并且,所有这些都是在操作系统级别处理的,性能及其优秀。 *三者的使用场景的比较BIO适合处理低并发的IO通信。NIO适合处理高并发,但每个并发执行时间很短的IO通信。AIO适合处理高并发,且每个并发执行时间都较长的IO通信。 BIO是同步阻塞的NIO是同步非阻塞的AIO是异步非阻塞的 演示面试基本概念说完了,我就开始做演示面试: 你好,面试官。BIO全称是Block IO,是同步阻塞的,这个模型结构最简单,最容易理解。NIO是NonBlock IO,是同步非阻塞的,NIO的模型是Reactor的。AIO是异步非阻塞的,AIO的模型是Proactor的。 NIO中引入了通道、缓冲区和Selector,通道对应BIO的流,缓冲区对应BIO中的自定义byte数组,Selector作为事件选择器,将消息分发给不同的处理代码去处理。NIO可以将多连接的任务放在一个线程下完成。 AIO中继续使用NIO的通道、缓冲区的概念,在消息分发的方式上采用了事件和回调的方式实现,从而达到极其高效的消息响应效率,和极低的资源消耗。AIO基于线程池的思路实现了任务的异步,这些都是基于操作系统实现的。 NIO和AIO在底层都是基于操作系统的select/poll/epoll,在windows下基于ICOP,性能非常优秀。 ========================================================= 以上就是我的演示面试,不知道是否让你满意。 下期再见 我们下期再见。
上期纠正今天查了一下资料,发现上期说的东西有点儿不太对,这里纠正一下。上期说到接口不允许有方法的实现,只允许有抽象方法,这个在以前是对的,但是在JDK1.8的新特性中,接口增加了默认方法和静态方法,尽管设计的很奇怪,但是新版的JDK接口确实是允许实现方法。 题目本期要说的是《2020年Java面试208题》的第14题,面试官提了这样一个问题:Java的IO流有哪几种? 知识点剖析IO流,即输入输出流,是比较明确的知识点,也没有啥难理解的。我先简单说一点,然后再做演示面试。 1输入输出流是干什么的?输入输出流是做数据读写的,比如读写文件、读键盘信号、读鼠标信号、输出信息到显示器、网络上传下载、浏览网页、微信聊天发信息、抖音播放小视频,这些都要用到输入输出流。 2输入到哪里?从哪里输出?我们用读写文件来举例子,文件在硬盘上,对于文件的输入输出流形象的理解就有两个动作:读出文件内容、写入文件内容。如果对输入输出流理解不深的话,就会错误的认为读出文件用输出流,写入文件用输入流,这是错误的,正确的定义是读出文件用的是输入流,即in,写入文件用的是输出流,即out。 这要从输入输出流的架构来理解:输入输出针对的并不是文件或者网络,而是针对内存,无论任何形式的输入输出流,都是相对于内存而言的,可以看看下图的架构,这个架构就描述了输入输出流都是以内存来定义“进”和“出”的方向的。 这一点一定要记住,这是使用输入输出流的时候比较重要的概念。 3输入输出的是什么通俗一点说,输入输出的是数据,这个好像说的对,又好像是废话。再深入一点分析,计算机里面的数据都是啥?想了想,计算机里面所有的东西都保存为0和1,那么应该传输的是0和1。但是这么说又不太精确,在计算机语言中并不直接操作0和1,最小的可操作单位是一个字节,即8位的0和1。那么我们可以得出结论,输入输出的是一个个的字节。举例子:读取文件,从文件中读到内容,无论是图片、还是文字、或者是电影MP4,歌曲MP3,我们如果考虑文件格式的话,那就太复杂了,成百上千的文件格式,实在没法统一格式管理。但如果我们深入底层去考虑,无论啥格式的,在计算机底层都是一个个的byte组成的字节数组。这样就简单了,我们把文件的二进制内容看做是一个大的字节数组就可以了,那么读取文件的过程就变成了读取一个字节数组到内存的过程了。这个读取字节数组的过程很好理解,为啥又叫流了呢?弄得人很难理解。这是因为文件很可能比较大,我们没法一次把文件解析为一个单独的字节数组读到内存里,只能一次读一小部分写入内存,然后再读到下一小部分写入内存中,这样一小点一小点儿的读取,就好像一个水管中的水流,一个大池子的水,一点点儿的流入到另一个池子中。因此我们将输入输出命名为输入输出流。 3 Java中字节流和字符流的概念上面说的是把文件解析为字节数组做传输,这是所有语言中的输入输出流都支持的方式,毕竟计算机中所有的数据和文件最小单位都被保存为字节,所以用字节流可以支持所有的输入输出需要。不过Java为了方便读写字符内容,比如我们在聊天的时候,传输聊天内容很多时候都是字符串,这时候如果用字节流做传输就得有一个字节转字符--字符转字节的过程,因为字节是8位的,字符是16位的,再加上不同的字符编码规范的设计,会让这个转换过程更加复杂。为了方便的处理字符串内容的传输,Java就提供了字符流来支持。 演示面试基本概念说完了,我就开始做演示面试: 你好,面试官。Java中的输入输出流主要有两种:字节流和字符流。字节流主要是传输字节内容的,字符流主要是传输字符内容的。字节流的最小传输单位是字节,每个字节是8位的。字符流的最小传输单位是字符,每个字符是16位的。字符流和字节流在类名上最大的区别是,字节输入输出流的类名普遍都以Stream结尾,输入字符流的类名普遍都以Reader结尾,输出字符流的类名普遍都以Writer结尾。 ========================================================= 以上就是我的演示面试,不知道是否让你满意。 自我点评因为输入输出流的概念实在是比较明确的,没有啥可扩展的知识点,所以我演示答题的时候就回答的比较干净利索脆。在实际答题的时候,如果看面试官意犹未尽,我建议可以把输入输出流的模型描述一下。好了,本期就到这里。 下期再见 我们下期再见。