面向对象特征有哪些?
继承,封装,多态
接口与抽象类区别?
一个类可以继承多个接口,接口里都是抽象方法,一个类只能继承一个抽象类,抽象类里可以有具体方法
java基本数据类型有哪些?
byte,char,short,int,long,float,double,boolean
基本类型与封装类型有什么区别?
基本类型存储在栈上,包装类型存储在堆上,参数传递
java循环有几种方式?
for,do while,while
集合接口有几种?
Collection,Map
Array List低层数据结构是什么?
数组
Arraylist扩容倍数多少?
1.5倍
Arraylist插入是怎么实现的低层?
数组拷贝
Arraylist特点?
查询快,插入慢
Arraylist是线程安全的吗?
不是
Hashmap低层数据结构?
数组加加链表或者数组加红黑树,存储Node数据结构,Node是一个包含key,value的数据结构
Hashmap如何添加数据?
首先计算出key值的hash值,然后hash值高位地位进行异或运算,得到新的hash值,使用hash值与hash数组长度进行与运算得到node的桶位置,如果当前桶位置没有节点,则直接将该新的节点放入当前桶,如果已经存在节点,比较当前桶的key值与新进来的key是否相等,如果相等,则更新当然节点,如果不相等则将当前节点的next指针指向新节点
Hash Map扩容?
如果Hashmap大小达到阀值,就需要扩容,扩容方式为将原来的大小扩大到原来的2倍
HashMap 初始容量为什么是16?
十进制16的二进制位00010000,15=16-1;15的二进制为00001111.当我们调用hashmap.get(key)方法的时候,为了加快查询速度,存的逻辑与查询逻辑要一致,并且为了更快的找到,在存的时候key 的hash值的高位与低位进行异或运算得到新的hash值,之所以要进行高位与低位异或运算是为了高位与低位都能用上,降低碰撞的几率。假设我们的key的hash值为1,那么1的二进制就是0000 0001,0000 0001 高位与低位异或运算后的值为0000 0001;(异或运算就是相同为零,相异为1)因为1的高位都是0,所以异或之后还是0000 0001. 0000 0001&00001111 等于0000 0001,所以hash值为1的key都会放在index为1的数组位置。如果数组index 为1的位置已经有一个node 节点了,说明之前已经有数据在这个桶位置,那么这时候需要比较原来节点key值与新put进来的key值是否相等。主要通过key的equals方法比较。如果equals返回true说明是同一个key,如果是相同key那么我们只需要更新当前旧节点,如果不是相同的,那么需要旧节点的next指针指向新的节点,形成链表。之所以是16,因为16-1的二进制是全1,做&运算与mod运算结果是一样的,但是&运算是位运算,运算效率会更高。所以扩容时都是2的n次方才能达到更好的&运算。
为什么HashMap扩容是2倍?
因为一个数乘以2只需要向左移一位即可;移一位数组长度-1能保证各个位都是1;如16-1 = 15,15二进制位0000 1111;32 = 16*2 = 0001 0000 <<1 = 0010 0000,(最左边的0 被移走了,右边补一个0)
HashMap是线程安全的吗?
不是,如果HashMap处于多线程共享的情况下,在HashMap进行扩容时,因为需要重新将数据存放到各个桶下,有可能会出现环结构,导致get对象时出现死环。所以1.8将头插法改成了尾插法。但是还是无法保证线程安全,因为多线程put数据时不是原子性操作,有可能数据会被覆盖最后导致程序的不正确。
如何保证线程安全?
如果一个变量是多个线程共享操作,为了让同一时间只能有一个程序能更改这个共享变量,那么需要在对这个共享变量进行操作是加上锁。可以使用sychronize关键字或者Lock的实现类进行加锁。
sychronize内置锁底层是怎么实现的?
java每个对象都有对象头,对象头会有存有monitor这么一个数据结构,用来标志当前的对象是被那个线程锁定的。如果一个线程a打算获取对象object这个对象的锁,那么首先线程a去判断object这个对象是否的monitor中的值是不是为0,如果是0,说明没有其他线程占有它,如果不是0,说明已经有其他线程占有个object的锁了,那么当前线程需要挂起等待占有这把锁的对象释放掉这把锁才能重新竞争。
Lock底层是怎么实现的?
委托内部的AQS子类实现锁,AQS的两个子类,公平锁,非公平锁。公平锁的就是谁先申请锁,谁就优先获得锁,要做到这个,底层是需要使用队列去实现,将申请锁的线程放入队列,等到锁被释放,从队列中出一个线程,将锁分配给它即可。非公平锁的意思就是无论什么时候所有竞争锁的线程大家获取锁的几率都是一样,无论先后。Lock还可以设置一个超时时间,需要手动释放锁。
什么是死锁?
1、A线程获得了1资源,B线程获得2资源,A线程要继续往下执行,必须获取到2资源,B线程要继续执行必须获取到1资源,但是A线程已经占有了1资源并且不会释放,B线程占有的2,并且不释放。那么这时候A线程B线程互相等待,一直等下去这就是死锁。解决死锁问题,其中一种方式就是A线程B线程获取1跟2资源顺序要一致。这样如果A线程获取的1资源,因为A线程已经占有了1资源,B线程要想获取1资源只能等A线程完成了释放1资源后才能运行。
分布式锁实现方式有哪些?
redis ,zookeeper
redis 是单线程的,使用setnx命令,该命令如果指定key存在则获取锁失败,如果不存在说明没有人去占有这把锁,说明获取锁成功。举个例子,我使用setnx("sku00001:stock");当我扣库存时我想对sku00001的货品扣减库存,首先需要获取这个sku00001这个货品的锁,保证当前只有一个节点操作sku00001这个货品库存。如果获取锁成功,那么就可以扣库存,否则,需要等一会再过来获取这把锁。分布式锁上锁的时候可以设置超时时间,这个跟Lock子类一样。业务中会出现锁超时时间到了,但是业务还没有做完。这时候的解决方法就是开启一个线程定时去查看当前的业务如果没有完成,并且当前锁准备过期,那需要延长这个锁的过期时间。
zookeeper实现分布式锁原理差不多,利用zookeeper这个中间件的特性作为分布式锁。zookeeper客户可以新建像目录结构的path,每个path都是唯一的,并且如果存在,再创建就会创建失败,path可以设置超时时间。当一个客户想获取锁时,比如sku00001,那么就新建一个path是sku00001的path。如果其他客户想创建,发现已经存在了,那么就获取锁失败。释放锁将该path删除就好,其他客户就可以再次竞争这把锁。
请谈一下Spring MVC的工作原理是怎样的?
SpringMVC的工作原理图:
SpringMVC流程
1、 用户发送请求至前端控制器DispatcherServlet。
2、 DispatcherServlet收到请求调用HandlerMapping处理器映射器。
3、 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
4、 DispatcherServlet调用HandlerAdapter处理器适配器。
5、 HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。
6、 Controller执行完成返回ModelAndView。
7、 HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
8、 DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
9、 ViewReslover解析后返回具体View。
10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
11、 DispatcherServlet响应用户。
组件说明:
以下组件通常使用框架提供实现:
DispatcherServlet:作为前端控制器,整个流程控制的中心,控制其它组件执行,统一调度,降低组件之间的耦合性,提高每个组件的扩展性。
HandlerMapping:通过扩展处理器映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。
HandlAdapter:通过扩展处理器适配器,支持更多类型的处理器。
ViewResolver:通过扩展视图解析器,支持更多类型的视图解析,例如:jsp、freemarker、pdf、excel等。
组件:
1、前端控制器DispatcherServlet(不需要工程师开发),由框架提供
作用:接收请求,响应结果,相当于转发器,中央处理器。有了dispatcherServlet减少了其它组件之间的耦合度。
用户请求到达前端控制器,它就相当于mvc模式中的c,dispatcherServlet是整个流程控制的中心,由它调用其它组件处理用户的请求,dispatcherServlet的存在降低了组件之间的耦合性。
2、处理器映射器HandlerMapping(不需要工程师开发),由框架提供
作用:根据请求的url查找Handler
HandlerMapping负责根据用户请求找到Handler即处理器,springmvc提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。
3、处理器适配器HandlerAdapter
作用:按照特定规则(HandlerAdapter要求的规则)去执行Handler
通过HandlerAdapter对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行。
4、处理器Handler(需要工程师开发)
注意:编写Handler时按照HandlerAdapter的要求去做,这样适配器才可以去正确执行Handler
Handler 是继DispatcherServlet前端控制器的后端控制器,在DispatcherServlet的控制下Handler对具体的用户请求进行处理。
由于Handler涉及到具体的用户业务请求,所以一般情况需要工程师根据业务需求开发Handler。
5、视图解析器View resolver(不需要工程师开发),由框架提供
作用:进行视图解析,根据逻辑视图名解析成真正的视图(view)
View Resolver负责将处理结果生成View视图,View Resolver首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成View视图对象,最后对View进行渲染将处理结果通过页面展示给用户。 springmvc框架提供了很多的View视图类型,包括:jstlView、freemarkerView、pdfView等。
一般情况下需要通过页面标签或页面模版技术将模型数据通过页面展示给用户,需要由工程师根据业务需求开发具体的页面。
6、视图View(需要工程师开发jsp...)
View是一个接口,实现类支持不同的View类型(jsp、freemarker、pdf...)
核心架构的具体流程步骤如下:
1、首先用户发送请求——>DispatcherServlet,前端控制器收到请求后自己不进行处理,而是委托给其他的解析器进行处理,作为统一访问点,进行全局的流程控制;
2、DispatcherServlet——>HandlerMapping, HandlerMapping 将会把请求映射为HandlerExecutionChain 对象(包含一个Handler 处理器(页面控制器)对象、多个HandlerInterceptor 拦截器)对象,通过这种策略模式,很容易添加新的映射策略;
3、DispatcherServlet——>HandlerAdapter,HandlerAdapter 将会把处理器包装为适配器,从而支持多种类型的处理器,即适配器设计模式的应用,从而很容易支持很多类型的处理器;
4、HandlerAdapter——>处理器功能处理方法的调用,HandlerAdapter 将会根据适配的结果调用真正的处理器的功能处理方法,完成功能处理;并返回一个ModelAndView 对象(包含模型数据、逻辑视图名);
5、ModelAndView的逻辑视图名——> ViewResolver, ViewResolver 将把逻辑视图名解析为具体的View,通过这种策略模式,很容易更换其他视图技术;
6、View——>渲染,View会根据传进来的Model模型数据进行渲染,此处的Model实际是一个Map数据结构,因此很容易支持其他视图技术;
7、返回控制权给DispatcherServlet,由DispatcherServlet返回响应给用户,到此一个流程结束。
下边两个组件通常情况下需要开发:
Handler:处理器,即后端控制器用controller表示。
View:视图,即展示给用户的界面,视图中通常需要标签语言展示模型数据。
在将SpringMVC之前我们先来看一下什么是MVC模式
MVC:MVC是一种设计模式
MVC的原理图:
分析:
M-Model 模型(完成业务逻辑:有javaBean构成,service+dao+entity)
V-View 视图(做界面的展示 jsp,html……)
C-Controller 控制器(接收请求—>调用模型—>根据结果派发页面)
springMVC是什么:
springMVC是一个MVC的开源框架,springMVC=struts2+spring,springMVC就相当于是Struts2加上sring的整合,但是这里有一个疑惑就是,springMVC和spring是什么样的关系呢?这个在百度百科上有一个很好的解释:意思是说,springMVC是spring的一个后续产品,其实就是spring在原有基础上,又提供了web应用的MVC模块,可以简单的把springMVC理解为是spring的一个模块(类似AOP,IOC这样的模块),网络上经常会说springMVC和spring无缝集成,其实springMVC就是spring的一个子模块,所以根本不需要同spring进行整合。
SpringMVC的原理图:
看到这个图大家可能会有很多的疑惑,现在我们来看一下这个图的步骤:(可以对比MVC的原理图进行理解)
第一步:用户发起请求到前端控制器(DispatcherServlet)
第二步:前端控制器请求处理器映射器(HandlerMappering)去查找处理器(Handle):通过xml配置或者注解进行查找
第三步:找到以后处理器映射器(HandlerMappering)像前端控制器返回执行链(HandlerExecutionChain)
第四步:前端控制器(DispatcherServlet)调用处理器适配器(HandlerAdapter)去执行处理器(Handler)
第五步:处理器适配器去执行Handler
第六步:Handler执行完给处理器适配器返回ModelAndView
第七步:处理器适配器向前端控制器返回ModelAndView
第八步:前端控制器请求视图解析器(ViewResolver)去进行视图解析
第九步:视图解析器像前端控制器返回View
第十步:前端控制器对视图进行渲染
第十一步:前端控制器向用户响应结果
看到这些步骤我相信大家很感觉非常的乱,这是正常的,但是这里主要是要大家理解springMVC中的几个组件:
前端控制器(DispatcherServlet):接收请求,响应结果,相当于电脑的CPU。
处理器映射器(HandlerMapping):根据URL去查找处理器
处理器(Handler):(需要程序员去写代码处理逻辑的)
处理器适配器(HandlerAdapter):会把处理器包装成适配器,这样就可以支持多种类型的处理器,类比笔记本的适配器(适配器模式的应用)
视图解析器(ViewResovler):进行视图解析,多返回的字符串,进行处理,可以解析成对应的页面
请问aop的应用场景有哪些?
AOP使用场景
AOP用来封装横切关注点,具体可以在下面的场景中使用
Authentication 权限
Caching 缓存
Context passing 内容传递
Error handling 错误处理
Lazy loading 懒加载
Debugging 调试
logging, tracing, profiling and monitoring 记录跟踪 优化 校准
Performance optimization 性能优化
Persistence 持久化
Resource pooling 资源池
Synchronization 同步
Transactions 事务
Spring 如何给静态变量注入
Spring无法直接给静态变量注入值,因为静态变量不属于对象,只属于类,也就是说在类被加载字节码的时候变量已经初始化了,也就是给该变量分配内存了,导致spring忽略静态变量。所以如下这种写法就是错误的,这样是无法注入的,在使用该变量的时候会导致空指针错误:
1
2 @Autowired
private static IOptionService optionService;
Spring 依赖注入是依赖set方法,静态变量不属于对象,只属于类。解决方法就是加上非静态的set方法,如下:
private static IOptionService optionService;
@Autowired
public void setOptionService(IOptionService optionService) {
Commons.optionService = optionService;
}
这样就能在工具类中使用optionService了。
Java面试题
Java 基础
并发
1、
java中的线程分为两种:守护线程(Daemon)和用户线程(User)。
任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bool on);true则把该线程设置为守护线程,反之则为用户线程。Thread.setDaemon()必须在Thread.start()之前调用,否则运行时会抛出异常。
两者的区别:
唯一的区别是判断虚拟机(JVM)何时离开,Daemon是为其他线程提供服务,如果全部的User Thread已经撤离,Daemon 没有可服务的线程,JVM撤离。也可以理解为守护线程是JVM自动创建的线程(但不一定),用户线程是程序创建的线程;比如JVM的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产生垃圾,守护线程自然就没事可干了,当垃圾回收线程是Java虚拟机上仅剩的线程时,Java虚拟机会自动离开。
扩展:Thread Dump打印出来的线程信息,含有daemon字样的线程即为守护进程,可能会有:服务守护进程、编译守护进程、windows下的监听Ctrl+break的守护进程、Finalizer守护进程、引用处理守护进程、GC守护进程。
2、线程与进程的区别?
进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元。
一个程序至少有一个进程,一个进程至少有一个线程。
3、什么是多线程中的上下文切换?
多线程会共同使用一组计算机上的CPU,而线程数大于给程序分配的CPU数量时,为了让各个线程都有执行的机会,就需要轮转使用CPU。不同的线程切换使用CPU发生的切换数据等就是上下文切换。
4、死锁与活锁的区别,死锁与饥饿的区别?
死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
产生死锁的必要条件:
- 互斥条件:所谓互斥就是进程在某一时间内独占资源。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。
活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。
Java中导致饥饿的原因:
- 高优先级线程吞噬所有的低优先级线程的CPU时间。
- 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
- 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait方法),因为其他线程总是被持续地获得唤醒。
5、Java中用到的线程调度算法是什么?
采用时间片轮转的方式。可以设置线程的优先级,会映射到下层的系统上面的优先级上,如非特别需要,尽量不要用,防止线程饥饿。
6、什么是线程组,为什么在Java中不推荐使用?
ThreadGroup类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。
为什么不推荐使用?因为使用有很多的安全隐患吧,没有具体追究,如果需要使用,推荐使用线程池。
7、为什么使用Executor框架?
每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的。
调用 new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。
接使用new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。
8、在Java中Executor和Executors的区别?
Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
Executor 接口对象能执行我们的线程任务。
ExecutorService接口继承了Executor接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
使用ThreadPoolExecutor 可以创建自定义线程池。
Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使用get()方法获取计算的结果。
9、如何在Windows和Linux上查找哪个线程使用的CPU时间最长?
参考:
http://daiguahub.com/2016/07/...
10、什么是原子操作?在Java Concurrency API中有哪些原子类(atomic classes)?
原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。
处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。
在Java中可以通过锁和循环CAS的方式来实现原子操作。 CAS操作——Compare & Set,或是 Compare & Swap,现在几乎所有的CPU指令都支持CAS的原子操作。
原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。
int++并不是一个原子操作,所以当一个线程读取它的值并加1时,另外一个线程有可能会读到之前的值,这就会引发错误。
为了解决这个问题,必须保证增加操作是原子的,在JDK1.5之前我们可以使用同步技术来做到这一点。到JDK1.5,java.util.concurrent.atomic包提供了int和long类型的原子包装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。
java.util.concurrent这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。
原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
解决ABA问题的原子类:AtomicMarkableReference(通过引入一个boolean来反映中间有没有变过),AtomicStampedReference(通过引入一个int来累加来反映中间有没有变过)
11、Java Concurrency API中的Lock接口(Lock interface)是什么?对比同步它有什么优势?
Lock接口比同步方法和同步块提供了更具扩展性的锁操作。
他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。
它的优势有:
可以使锁更公平
可以使线程在等待锁的时候响应中断
可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
可以在不同的范围,以不同的顺序获取和释放锁
整体上来说Lock是synchronized的扩展版,Lock提供了无条件的、可轮询的(tryLock方法)、定时的(tryLock带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition方法)锁操作。另外Lock的实现类基本都支持非公平锁(默认)和公平锁,synchronized只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。
12、什么是Executors框架?
Executor框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。
无限制的创建线程会引起应用程序内存溢出。所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用Executors框架可以非常方便的创建一个线程池。
13、什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。
这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
JDK7提供了7个阻塞队列。分别是:
ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
Java 5之前实现同步存取时,可以使用普通的一个集合,然后在使用线程的协作和线程同步可以实现生产者,消费者模式,主要的技术就是用好,wait ,notify,notifyAll,sychronized这些关键字。而在java 5之后,可以使用阻塞队列来实现,此方式大大简少了代码量,使得多线程编程更加容易,安全方面也有保障。
BlockingQueue接口是Queue的子接口,它的主要用途并不是作为容器,而是作为线程同步的的工具,因此他具有一个很明显的特性,当生产者线程试图向BlockingQueue放入元素时,如果队列已满,则线程被阻塞,当消费者线程试图从中取出一个元素时,如果队列为空,则该线程会被阻塞,正是因为它所具有这个特性,所以在程序中多个线程交替向BlockingQueue中放入元素,取出元素,它可以很好的控制线程之间的通信。
阻塞队列使用最经典的场景就是socket客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。
14、什么是Callable和Future?
Callable接口类似于Runnable,从名字就可以看出来了,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值。
可以认为是带有回调的Runnable。
Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable用于产生结果,Future用于获取结果。
15、什么是FutureTask?使用ExecutorService启动任务。
在Java并发程序中FutureTask表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞。一个FutureTask对象可以对调用了Callable和Runnable的对象进行包装,由于FutureTask也是调用了Runnable接口所以它可以提交给Executor来执行。
16、什么是并发容器的实现?
何为同步容器:可以简单地理解为通过synchronized来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如Vector,Hashtable,以及Collections.synchronizedSet,synchronizedList等方法返回的容器。
可以通过查看Vector,Hashtable等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字synchronized。
并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在ConcurrentHashMap中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问map,并且执行读操作的线程和写操作的线程也可以并发的访问map,同时允许一定数量的写操作线程并发地修改map,所以它可以在并发环境下实现更高的吞吐量。
17、多线程同步和互斥有几种实现方法,都是什么?
线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。
线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。
用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。
18、什么是竞争条件?你怎样发现和解决竞争?
当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则我们认为这发生了竞争条件(race condition)。
19、你将如何使用thread dump?你将如何分析Thread dump?
新建状态(New)
用new语句创建的线程处于新建状态,此时它和其他Java对象一样,仅仅在堆区中被分配了内存。
就绪状态(Runnable)
当一个线程对象创建后,其他线程调用它的start()方法,该线程就进入就绪状态,Java虚拟机会为它创建方法调用栈和程序计数器。处于这个状态的线程位于可运行池中,等待获得CPU的使用权。
运行状态(Running)
处于这个状态的线程占用CPU,执行程序代码。只有处于就绪状态的线程才有机会转到运行状态。
阻塞状态(Blocked)
阻塞状态是指线程因为某些原因放弃CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配CPU。直到线程重新进入就绪状态,它才有机会转到运行状态。
阻塞状态可分为以下3种:
位于对象等待池中的阻塞状态(Blocked in object’s wait pool):当线程处于运行状态时,如果执行了某个对象的wait()方法,Java虚拟机就会把线程放到这个对象的等待池中,这涉及到“线程通信”的内容。
位于对象锁池中的阻塞状态(Blocked in object’s lock pool):当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,Java虚拟机就会把这个线程放到这个对象的锁池中,这涉及到“线程同步”的内容。
其他阻塞状态(Otherwise Blocked):当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了I/O请求时,就会进入这个状态。
死亡状态(Dead)
当线程退出run()方法时,就进入死亡状态,该线程结束生命周期。
我们运行之前的那个死锁代码SimpleDeadLock.java,然后尝试输出信息(/这是注释,作者自己加的/):
/ 时间,jvm信息 /
2017-11-01 17:36:28
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.144-b01 mixed mode):
/* 线程名称:DestroyJavaVM
编号:#13
优先级:5
系统优先级:0
jvm内部线程id:0x0000000001c88800
对应系统线程id(NativeThread ID):0x1c18
线程状态: waiting on condition [0x0000000000000000] (等待某个条件)
线程详细状态:java.lang.Thread.State: RUNNABLE 及之后所有*/
"DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x0000000001c88800 nid=0x1c18 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Thread-1" #12 prio=5 os_prio=0 tid=0x0000000018d49000 nid=0x17b8 waiting for monitor entry [0x0000000019d7f000]
/* 线程状态:阻塞(在对象同步上)
代码位置:at com.leo.interview.SimpleDeadLock$B.run(SimpleDeadLock.java:56)
等待锁:0x00000000d629b4d8
已经获得锁:0x00000000d629b4e8*/
java.lang.Thread.State: BLOCKED (on object monitor)
at com.leo.interview.SimpleDeadLock$B.run(SimpleDeadLock.java:56)
- waiting to lock <0x00000000d629b4d8> (a java.lang.Object)
- locked <0x00000000d629b4e8> (a java.lang.Object)
"Thread-0" #11 prio=5 os_prio=0 tid=0x0000000018d44000 nid=0x1ebc waiting for monitor entry [0x000000001907f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.leo.interview.SimpleDeadLock$A.run(SimpleDeadLock.java:34)
- waiting to lock <0x00000000d629b4e8> (a java.lang.Object)
- locked <0x00000000d629b4d8> (a java.lang.Object)
"Service Thread" #10 daemon prio=9 os_prio=0 tid=0x0000000018ca5000 nid=0x1264 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C1 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x0000000018c46000 nid=0xb8c waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x0000000018be4800 nid=0x1db4 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x0000000018be3800 nid=0x810 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x0000000018bcc800 nid=0x1c24 runnable [0x00000000193ce000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
- locked <0x00000000d632b928> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
- locked <0x00000000d632b928> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)
"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x0000000017781800 nid=0x524 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000000001778f800 nid=0x1b08 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000001776a800 nid=0xdac in Object.wait() [0x0000000018b6f000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000d6108ec8> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
- locked <0x00000000d6108ec8> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)
"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000000017723800 nid=0x1670 in Object.wait() [0x00000000189ef000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000d6106b68> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x00000000d6106b68> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
"VM Thread" os_prio=2 tid=0x000000001771b800 nid=0x604 runnable
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x0000000001c9d800 nid=0x9f0 runnable
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x0000000001c9f000 nid=0x154c runnable
"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x0000000001ca0800 nid=0xcd0 runnable
"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x0000000001ca2000 nid=0x1e58 runnable
"VM Periodic Task Thread" os_prio=2 tid=0x0000000018c5a000 nid=0x1b58 waiting on condition
JNI global references: 33
/ 此处可以看待死锁的相关信息! /
Found one Java-level deadlock:
"Thread-1":
waiting to lock monitor 0x0000000017729fc8 (object 0x00000000d629b4d8, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x0000000017727738 (object 0x00000000d629b4e8, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
"Thread-1":
at com.leo.interview.SimpleDeadLock$B.run(SimpleDeadLock.java:56)
- waiting to lock <0x00000000d629b4d8> (a java.lang.Object)
- locked <0x00000000d629b4e8> (a java.lang.Object)
"Thread-0":
at com.leo.interview.SimpleDeadLock$A.run(SimpleDeadLock.java:34)
- waiting to lock <0x00000000d629b4e8> (a java.lang.Object)
- locked <0x00000000d629b4d8> (a java.lang.Object)
Found 1 deadlock.
/ 内存使用状况,详情得看JVM方面的书 /
Heap
PSYoungGen total 37888K, used 4590K [0x00000000d6100000, 0x00000000d8b00000, 0x0000000100000000)
eden space 32768K, 14% used [0x00000000d6100000,0x00000000d657b968,0x00000000d8100000)
from space 5120K, 0% used [0x00000000d8600000,0x00000000d8600000,0x00000000d8b00000)
to space 5120K, 0% used [0x00000000d8100000,0x00000000d8100000,0x00000000d8600000)
ParOldGen total 86016K, used 0K [0x0000000082200000, 0x0000000087600000, 0x00000000d6100000)
object space 86016K, 0% used [0x0000000082200000,0x0000000082200000,0x0000000087600000)
Metaspace used 3474K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 382K, capacity 388K, committed 512K, reserved 1048576K
20、为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?
当你调用start()方法时你将创建新的线程,并且执行在run()方法里的代码。
但是如果你直接调用run()方法,它不会创建新的线程也不会执行调用线程的代码,只会把run方法当作普通方法去执行。
21、Java中你怎样唤醒一个阻塞的线程?
在Java发展史上曾经使用suspend()、resume()方法对于线程进行阻塞唤醒,但随之出现很多问题,比较典型的还是死锁问题。
解决方案可以使用以对象为目标的阻塞,即利用Object类的wait()和notify()方法实现线程阻塞。
首先,wait、notify方法是针对对象的,调用任意对象的wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取改对象的锁,直到获取成功才能往下执行;其次,wait、notify方法必须在synchronized块或方法中被调用,并且要保证同步块或方法的锁对象与调用wait、notify方法的对象是同一个,如此一来在调用wait之前当前线程就已经成功获取某对象的锁,执行wait阻塞后当前线程就将之前获取的对象锁释放。
22、在Java中CycliBarriar和CountdownLatch有什么区别?
CyclicBarrier可以重复使用,而CountdownLatch不能重复使用。
Java的concurrent包里面的CountDownLatch其实可以把它看作一个计数器,只不过这个计数器的操作是原子操作,同时只能有一个线程去操作这个计数器,也就是同时只能有一个线程去减这个计数器里面的值。
你可以向CountDownLatch对象设置一个初始的数字作为计数值,任何调用这个对象上的await()方法都会阻塞,直到这个计数器的计数值被其他的线程减为0为止。
所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。
CountDownLatch的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续往下执行的任务调用一个CountDownLatch对象的await()方法,其他的任务执行完自己的任务后调用同一个CountDownLatch对象上的countDown()方法,这个调用await()方法的任务将一直阻塞等待,直到这个CountDownLatch对象的计数值减到0为止。
CyclicBarrier一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。
23、什么是不可变对象,它对写并发应用有什么帮助?
不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。
不可变对象的类即为不可变类(Immutable Class)。Java平台类库中包含许多不可变类,如String、基本类型的包装类、BigInteger和BigDecimal等。
不可变对象天生是线程安全的。它们的常量(域)是在构造函数中创建的。既然它们的状态无法修改,这些常量永远不会变。
不可变对象永远是线程安全的。
只有满足如下状态,一个对象才是不可变的;
它的状态不能在创建后再被修改;
所有域都是final类型;并且,
它被正确创建(创建期间没有发生this引用的逸出)。
24、什么是多线程中的上下文切换?
在上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。在程序中,上下文切换过程中的“页码”信息是保存在进程控制块(PCB)中的。PCB还经常被称作“切换桢”(switchframe)。“页码”信息会一直保存到CPU的内存中,直到他们被再次使用。
上下文切换是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。
25、Java中用到的线程调度算法是什么?
计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令.所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务.在运行池中,会有多个处于就绪状态的线程在等待CPU,JAVA虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配CPU的使用权.
有两种调度模型:分时调度模型和抢占式调度模型。
分时调度模型是指让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的CPU的时间片这个也比较好理解。
java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。
26、什么是线程组,为什么在Java中不推荐使用?
线程组和线程池是两个不同的概念,他们的作用完全不同,前者是为了方便线程的管理,后者是为了管理线程的生命周期,复用线程,减少创建销毁线程的开销。
27、为什么使用Executor框架比使用应用创建和管理线程好?
为什么要使用Executor线程池框架
1、每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的。
2、调用 new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。
3、直接使用new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。
使用Executor线程池框架的优点
1、能复用已存在并空闲的线程从而减少线程对象的创建从而减少了消亡线程的开销。
2、可有效控制最大并发线程数,提高系统资源使用率,同时避免过多资源竞争。
3、框架中已经有定时、定期、单线程、并发数控制等功能。
综上所述使用线程池框架Executor能更好的管理线程、提供系统资源使用率。
28、java中有几种方法可以实现一个线程?
继承 Thread 类
实现 Runnable 接口
实现 Callable 接口,需要实现的是 call() 方法
29、如何停止一个正在运行的线程?
使用共享变量的方式
在这种方式中,之所以引入共享变量,是因为该变量可以被多个执行相同任务的线程用来作为是否中断的信号,通知中断线程的执行。
使用interrupt方法终止线程
如果一个线程由于等待某些事件的发生而被阻塞,又该怎样停止该线程呢?这种情况经常会发生,比如当一个线程由于需要等候键盘输入而被阻塞,或者调用Thread.join()方法,或者Thread.sleep()方法,在网络中调用ServerSocket.accept()方法,或者调用了DatagramSocket.receive()方法时,都有可能导致线程阻塞,使线程处于处于不可运行状态时,即使主程序中将该线程的共享变量设置为true,但该线程此时根本无法检查循环标志,当然也就无法立即中断。这里我们给出的建议是,不要使用stop()方法,而是使用Thread提供的interrupt()方法,因为该方法虽然不会中断一个正在运行的线程,但是它可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态,退出堵塞代码。
30、notify()和notifyAll()有什么区别?
当一个线程进入wait之后,就必须等其他线程notify/notifyall,使用notifyall,可以唤醒所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤醒一个。
如果没把握,建议notifyAll,防止notigy因为信号丢失而造成程序异常。
31、什么是Daemon线程?它有什么意义?
所谓后台(daemon)线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这个线程并不属于程序中不可或缺的部分。因此,当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,
只要有任何非后台线程还在运行,程序就不会终止。必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。注意:后台进程在不执行finally子句的情况下就会终止其run()方法。
比如:JVM的垃圾回收线程就是Daemon线程,Finalizer也是守护线程。
32、java如何实现多线程之间的通讯和协作?
中断 和 共享变量
33、什么是可重入锁(ReentrantLock)?
举例来说明锁的可重入性
public class UnReentrant{
Lock lock = new Lock();
public void outer(){
lock.lock();
inner();
lock.unlock();
}
public void inner(){
lock.lock();
//do something
lock.unlock();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
outer中调用了inner,outer先锁住了lock,这样inner就不能再获取lock。其实调用outer的线程已经获取了lock锁,但是不能在inner中重复利用已经获取的锁资源,这种锁即称之为 不可重入可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码块。
synchronized、ReentrantLock都是可重入的锁,可重入锁相对来说简化了并发编程的开发。
34、当一个线程进入某个对象的一个synchronized的实例方法后,其它线程是否可进入此对象的其它方法?
如果其他方法没有synchronized的话,其他线程是可以进入的。
所以要开放一个线程安全的对象时,得保证每个方法都是线程安全的。
35、乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
乐观锁的实现方式:
1、使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。
2、java中的Compare and Swap即CAS ,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。
CAS缺点:
- ABA问题:
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但可能存在潜藏的问题。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。
2、循环时间长开销大:
对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
3、只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
36、SynchronizedMap和ConcurrentHashMap有什么区别?
SynchronizedMap一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为map。
ConcurrentHashMap使用分段锁来保证在多线程下的性能。ConcurrentHashMap中则是一次锁住一个桶。ConcurrentHashMap默认将hash表分为16个桶,诸如get,put,remove等常用操作只锁当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。
另外ConcurrentHashMap使用了一种不同的迭代方式。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据 ,iterator完成后再将头指针替换为新的数据 ,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。
37、CopyOnWriteArrayList可以用于什么应用场景?
CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModificationException。在CopyOnWriteArrayList中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。
1、由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc;
2、不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;
CopyOnWriteArrayList透露的思想
1、读写分离,读和写分开
2、最终一致性
3、使用另外开辟空间的思路,来解决并发冲突
38、什么叫线程安全?servlet是线程安全吗?
线程安全是编程中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
Servlet不是线程安全的,servlet是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。
Struts2的action是多实例多线程的,是线程安全的,每个请求过来都会new一个新的action分配给这个请求,请求完成后销毁。
SpringMVC的Controller是线程安全的吗?不是的,和Servlet类似的处理流程。
Struts2好处是不用考虑线程安全问题;Servlet和SpringMVC需要考虑线程安全问题,但是性能可以提升不用处理太多的gc,可以使用ThreadLocal来处理多线程的问题。
39、volatile有什么用?能否用一句话说明下volatile的应用场景?
volatile保证内存可见性和禁止指令重排。
volatile用于多线程环境下的单次操作(单次读或者单次写)。
40、为什么代码会重排序?
在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:
在单线程环境下不能改变程序运行的结果;
存在数据依赖关系的不允许重排序
需要注意的是:重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义。
41、在java中wait和sleep方法的不同?
最大的不同是在等待时wait会释放锁,而sleep一直持有锁。Wait通常被用于线程间交互,sleep通常被用于暂停执行。
直接了解的深入一点吧:
在Java中线程的状态一共被分成6种:
初始态:NEW
创建一个Thread对象,但还未调用start()启动线程时,线程处于初始态。
运行态:RUNNABLE
在Java中,运行态包括就绪态 和 运行态。
就绪态 该状态下的线程已经获得执行所需的所有资源,只要CPU分配执行权就能运行。所有就绪态的线程存放在就绪队列中。
运行态 获得CPU执行权,正在执行的线程。由于一个CPU同一时刻只能执行一条线程,因此每个CPU每个时刻只有一条运行态的线程。
阻塞态
当一条正在执行的线程请求某一资源失败时,就会进入阻塞态。而在Java中,阻塞态专指请求锁失败时进入的状态。由一个阻塞队列存放所有阻塞态的线程。处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列,等待执行。PS:锁、IO、Socket等都资源。
等待态
当前线程中调用wait、join、park函数时,当前线程就会进入等待态。也有一个等待队列存放所有等待态的线程。线程处于等待态表示它需要等待其他线程的指示才能继续运行。进入等待态的线程会释放CPU执行权,并释放资源(如:锁)
超时等待态
当运行中的线程调用sleep(time)、wait、join、parkNanos、parkUntil时,就会进入该状态;它和等待态一样,并不是因为请求不到资源,而是主动进入,并且进入后需要其他线程唤醒;进入该状态后释放CPU执行权 和 占有的资源。与等待态的区别:到了超时时间后自动进入阻塞队列,开始竞争锁。
终止态
线程执行结束后的状态。
注意:
wait()方法会释放CPU执行权 和 占有的锁。
sleep(long)方法仅释放CPU使用权,锁仍然占用;线程被放入超时等待队列,与yield相比,它会使线程较长时间得不到运行。
yield()方法仅释放CPU执行权,锁仍然占用,线程会被放入就绪队列,会在短时间内再次执行。
wait和notify必须配套使用,即必须使用同一把锁调用;
wait和notify必须放在一个同步块中调用wait和notify的对象必须是他们所处同步块的锁对象。
42、用Java实现阻塞队列
参考java中的阻塞队列的内容吧,直接实现有点烦:
http://www.infoq.com/cn/artic...
43、一个线程运行时发生异常会怎样?
如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。
44、如何在两个线程间共享数据?
在两个线程间共享变量即可实现共享。
一般来说,共享变量要求变量本身是线程安全的,然后在线程内使用的时候,如果有对共享变量的复合操作,那么也得保证复合操作的线程安全性。
45、Java中notify 和 notifyAll有什么区别?
notify() 方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。而notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。
46、为什么wait, notify 和 notifyAll这些方法不在thread类里面?
一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。
47、什么是ThreadLocal变量?
ThreadLocal是Java里一种特殊的变量。每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。它是为创建代价高昂的对象获取线程安全的好方法,比如你可以用ThreadLocal让SimpleDateFormat变成线程安全的,因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。首先,通过复用减少了代价高昂的对象的创建个数。其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。
48、Java中interrupted 和 isInterrupted方法的区别?
interrupt
interrupt方法用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。
注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。
interrupted
查询当前线程的中断状态,并且清除原状态。如果一个线程被中断了,第一次调用interrupted则返回true,第二次和后面的就返回false了。
isInterrupted
仅仅是查询当前线程的中断状态
49、为什么wait和notify方法要在同步块中调用?
Java API强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。还有一个原因是为了避免wait和notify之间产生竞态条件。
50、为什么你应该在循环中检查等待条件?
处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。
51、Java中的同步集合与并发集合有什么区别?
同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在Java1.5之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。
52、什么是线程池? 为什么要使用它?
创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。从JDK1.5开始,Java API提供了Executor框架让你可以创建不同的线程池。
53、怎么检测一个线程是否拥有锁?
在java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。
54、你如何在Java中获取线程堆栈?
kill -3 [java pid]
不会在当前终端输出,它会输出到代码执行的或指定的地方去。比如,kill -3 tomcat pid, 输出堆栈到log目录下。
Jstack [java pid]
这个比较简单,在当前终端显示,也可以重定向到指定文件中。
-JvisualVM:Thread Dump
不做说明,打开JvisualVM后,都是界面操作,过程还是很简单的。
55、JVM中哪个参数是用来控制线程的栈堆栈小的?
-Xss 每个线程的栈大小
56、Thread类中的yield方法有什么作用?
使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。
当前线程到了就绪状态,那么接下来哪个线程会从就绪状态变成执行状态呢?可能是当前线程,也可能是其他线程,看系统的分配了。
57、Java中ConcurrentHashMap的并发度是什么?
ConcurrentHashMap把实际map划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是ConcurrentHashMap类构造函数的一个可选参数,默认值为16,这样在多线程情况下就能避免争用。
在JDK8后,它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。同时加入了更多的辅助变量来提高并发度,具体内容还是查看源码吧。
58、Java中Semaphore是什么?
Java中的Semaphore是一种新的同步类,它是一个计数信号。从概念上讲,从概念上讲,信号量维护了一个许可集合。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore只对可用许可的号码进行计数,并采取相应的行动。信号量常常用于多线程的代码中,比如数据库连接池。
59、Java线程池中submit() 和 execute()方法有什么区别?
两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中。
而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。
60、什么是阻塞式方法?
阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。
61、Java中的ReadWriteLock是什么?
读写锁是用来提升并发程序性能的锁分离技术的成果。
62、volatile 变量和 atomic 变量有什么不同?
Volatile变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用volatile修饰count变量那么 count++ 操作就不是原子性的。
而AtomicInteger类提供的atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。
63、可以直接调用Thread类的run ()方法么?
当然可以。但是如果我们调用了Thread的run()方法,它的行为就会和普通的方法一样,会在当前线程中执行。为了在新的线程中执行我们的代码,必须使用Thread.start()方法。
64、如何让正在运行的线程暂停一段时间?
我们可以使用Thread类的Sleep()方法让线程暂停一段时间。需要注意的是,这并不会让线程终止,一旦从休眠中唤醒线程,线程的状态将会被改变为Runnable,并且根据线程调度,它将得到执行。
65、你对线程优先级的理解是什么?
每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个int变量(从1-10),1代表最低优先级,10代表最高优先级。
java的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级。
66、什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing )?
线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。
同上一个问题,线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。
时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。
67、你如何确保main()方法所在的线程是Java 程序最后结束的线程?
我们可以使用Thread类的join()方法来确保所有程序创建的线程在main()方法退出前结束。
68、线程之间是如何通信的?
当线程间是可以共享资源时,线程间通信是协调它们的重要的手段。Object类中wait()notify()notifyAll()方法可以用于线程间通信关于资源的锁的状态。
69、为什么线程通信的方法wait(), notify()和notifyAll()被定义在Object 类里?
Java的每个对象中都有一个锁(monitor,也可以成为监视器) 并且wait(),notify()等方法用于等待对象的锁或者通知其他线程对象的监视器可用。在Java的线程中并没有可供任何对象使用的锁和同步器。这就是为什么这些方法是Object类的一部分,这样Java的每一个类都有用于线程间通信的基本方法。
70、为什么wait(), notify()和notifyAll ()必须在同步方法或者同步块中被调用?
当一个线程需要调用对象的wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的notify()方法。同样的,当一个线程需要调用对象的notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。
71、为什么Thread类的sleep()和yield ()方法是静态的?
Thread类的sleep()和yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。
72、如何确保线程安全?
在Java中可以有很多方法来保证线程安全——同步,使用原子类(atomic concurrent classes),实现并发锁,使用volatile关键字,使用不变类和线程安全类。
73、同步方法和同步块,哪个是更好的选择?
同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。
同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁。
74、如何创建守护线程?
使用Thread类的setDaemon(true)方法可以将线程设置为守护线程,需要注意的是,需要在调用start()方法前调用这个方法,否则会抛出IllegalThreadStateException异常。
75、什么是Java Timer 类?如何创建一个有特定时间间隔的任务?
java.util.Timer是一个工具类,可以用于安排一个线程在未来的某个特定时间执行。Timer类可以用安排一次性任务或者周期任务。
java.util.TimerTask是一个实现了Runnable接口的抽象类,我们需要去继承这个类来创建我们自己的定时任务并使用Timer去安排它的执行。
目前有开源的Qurtz可以用来创建定时任务。
多线程
java中有几种方法可以实现一个线程?
继承Thread类;实现Runnable接口;实现Callable接口通过FutureTask包装器来创建Thread线程;使用ExecutorService、Callable、Future实现有返回结果的多线程(也就是使用了ExecutorService来管理前面的三种方式)。详情参见: https://radiancel.github.io/2...
如何停止一个正在运行的线程?
1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。 2、使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。 3、使用interrupt方法中断线程。
参考: https://www.cnblogs.com/greta...
notify()和notifyAll()有什么区别?
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
参考: 再谈notify和notifyAll的区别和相同
sleep()和 wait()有什么区别?
对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。
当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。
参考: https://www.cnblogs.com/hongt...
什么是Daemon线程?它有什么意义?
Java语言自己可以创建两种进程“用户线程”和“守护线程”
用户线程:就是我们平时创建的普通线程.
守护线程:主要是用来服务用户线程.
Daemon就是守护线程,他的意义是:
只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。
Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。
参考: https://www.cnblogs.com/Chris...
java如何实现多线程之间的通讯和协作?
参考这篇: http://www.cnblogs.com/hapjin...
锁
什么是可重入锁(ReentrantLock)?
线程可以进入任何一个它已经拥有的锁所同步着的代码块。
代码设计如下:
参考链接: https://www.cnblogs.com/dj383...
当一个线程进入某个对象的一个synchronized的实例方法后,其它线程是否可进入此对象的其它方法?
如果其他方法前加了synchronized关键字,就不能,如果没加synchronized,则能够进去。
如果这个方法内部调用了wait(),则可以进入其他加synchronized的方法。
如果其他方法加了synchronized关键字,并且没有调用wai方法,则不能。
ynchronized和java.util.concurrent.locks.Lock的异同?
主要相同点:Lock能完成Synchronized所实现的所有功能。
主要不同点:Lock有比Synchronized更精确的线程予以和更好的性能。Synchronized会自动释放锁,但是Lock一定要求程序员手工释放,并且必须在finally从句中释放。
乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
乐观锁是假设每次操作都不会冲突,若是遇到冲突失败就重试直到成功;悲观锁是让其他线程都等待,等锁释放完了再竞争锁。
乐观锁实现方式:cas,volatile
悲观锁实现方式:synchronized,Lock
并发框架
SynchronizedMap和ConcurrentHashMap有什么区别?
SynchronizedMap()和Hashtable一样,实现上在调用map所有方法时,都对整个map进行同步。而ConcurrentHashMap的实现却更加精细,它对map中的所有桶加了锁。所以,只要有一个线程访问map,其他线程就无法进入map,而如果一个线程在访问ConcurrentHashMap某个桶时,其他线程,仍然可以对map执行某些操作。
所以,ConcurrentHashMap在性能以及安全性方面,明显比Collections.synchronizedMap()更加有优势。同时,同步操作精确控制到桶,这样,即使在遍历map时,如果其他线程试图对map进行数据修改,也不会抛出ConcurrentModificationException。
参考: https://www.cnblogs.com/shamo...
CopyOnWriteArrayList可以用于什么应用场景?
CopyOnWriteArrayList的特性是针对读操作,不做处理,和普通的ArrayList性能一样。而在写操作时,会先拷贝一份,实现新旧版本的分离,然后在拷贝的版本上进行修改操作,修改完后,将其更新至就版本中。
那么他的使用场景就是:一个需要在多线程中操作,并且频繁遍历。其解决了由于长时间锁定整个数组导致的性能问题,解决方案即写时拷贝。
另外需要注意的是CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
参考: https://www.cnblogs.com/ducha... https://www.cnblogs.com/yw-technology/p/7476106.html
线程安全
什么叫线程安全?servlet是线程安全吗?
线程安全就是说多线程访问同一代码,不会产生不确定的结果。
在多线程环境中,当各线程不共享数据的时候,即都是私有(private)成员,那么一定是线程安全的。但这种情况并不多见,在多数情况下需要共享数据,这时就需要进行适当的同步控制了。
线程安全一般都涉及到synchronized, 就是一段代码同时只能有一个线程来操作 不然中间过程可能会产生不可预制的结果。
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
Servlet不是线程安全的,详见:漫画 | Servlet属于线程安全的吗?
同步有几种实现方法?
1.同步方法
即有synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
2.同步代码块
即有synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
3.使用特殊域变量(volatile)实现线程同步
a.volatile关键字为域变量的访问提供了一种免锁机制,
b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,
c.因此每次使用该域就要重新计算,而不是使用寄存器中的值
d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
4.使用重入锁实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
5.使用局部变量实现线程同步。
参考: https://www.cnblogs.com/jians...
volatile有什么用?能否用一句话说明下volatile的应用场景?
作用是:作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值,即不是从寄存器里取备份值,而是去该地址内存存储的值。
一句话说明volatile的应用场景:
对变量的写操作不依赖于当前值且该变量没有包含在具有其他变量的不变式中。
请说明下java的内存模型。
Java内存模型的逻辑视图
为了保证并发编程中可以满足原子性、可见性及有序性。有一个重要的概念,那就是内存模型。
为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。
通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。
它解决了 CPU 多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。
内存模型解决并发问题主要采用两种方式:
限制处理器优化使用内存屏障关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节。
Java内存模型定义了以下八种操作来完成:
lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
也就是read和load之间,store和write之间是可以插入其他指令的,如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b, load a。
Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:
不允许read和load、store和write操作之一单独出现不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。参考: https://www.cnblogs.com/nexiy... http://ifeve.com/java-memory-model-6/
为什么代码会重排序?
直接参考: https://www.cnblogs.com/toov5... https://blog.csdn.net/qq_32646795/article/details/78221064
并发容器和框架
如何让一段程序并发的执行,并最终汇总结果?
使用CyclicBarrier 在多个关口处将多个线程执行结果汇总, CountDownLatch 在各线程执行完毕后向总线程汇报结果。
CountDownLatch : 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。
CyclicBarrier : N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。
这样应该就清楚一点了,对于CountDownLatch来说,重点是那个“一个线程”, 是它在等待,而另外那N的线程在把“某个事情”做完之后可以继续等待,可以终止。而对于CyclicBarrier来说,重点是那N个线程,他们之间任何一个没有完成,所有的线程都必须等待。
从api上理解就是CountdownLatch有主要配合使用两个方法countDown()和await(),countDown()是做事的线程用的方法,await()是等待事情完成的线程用个方法,这两种线程是可以分开的(下面例子:CountdownLatchTest2),当然也可以是同一组线程;CyclicBarrier只有一个方法await(),指的是做事线程必须大家同时等待,必须是同一组线程的工作。
CyclicBarrier例子:
如何合理的配置java线程池?如CPU密集型的任务,基本线程池应该配置多大?IO密集型的任务,基本线程池应该配置多大?用有界队列好还是无界队列好?任务非常多的时候,使用什么阻塞队列能获取最好的吞吐量?
虽然Exectors可以生成一些很常用的线程池,但毕竟在什么情况下使用还是开发者最清楚的。在某些自己很清楚的使用场景下,java线程池还是推荐自己配置的。下面是java线程池的配置类的参数,我们逐一分析一下:
corePoolSize - 池中所保存的线程数,包括空闲线程。maximumPoolSize - 池中允许的最大线程数。keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。unit - keepAliveTime 参数的时间单位。workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute 方法提交的 Runnable 任务。用BlocingQueue的实现类都可以。threadFactory - 执行程序创建新线程时使用的工厂。自定义线程工厂可以做一些额外的操作,比如统计生产的线程数等。handler - 饱和策略,即超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。策略有:Abort终止并抛出异常,Discard悄悄抛弃任务,Discard-Oldest抛弃最老的任务策略,Caller-Runs将任务退回给调用者策略。至于线程池应当配置多大的问题,一般有如下的经验设置:
如果是CPU密集型应用,则线程池大小设置为N+1。如果是IO密集型应用,则线程池大小设置为2N+1。用有界队列好还是无界队列好?这种问题的答案肯定是视情况而定:
有界队列有助于避免资源耗尽的情况发生。但他带来了新的问题:当队列填满后,新的任务怎么办?所以有界队列适用于执行比较耗资源的任务,同时要设计好相应的饱和策略。无界队列和有界队列刚好相反,在资源无限的情况下可以一直接收新任务。适用于小任务,请求和处理速度相对持平的状况。其实还有一种同步移交的队列 SynchronousQueue ,这种队列不存储任务信息,直接将任务提交给线程池。可以理解为容量只有1的有界队列,在特殊场景下有特殊作用,同样得设计好相应的饱和策略。参考:https://blog.csdn.net/qq_3403...
如何使用阻塞队列实现一个生产者和消费者模型?请写代码。
下面这是一个完整的生产者消费者代码例子,对比传统的wait、nofity代码,它更易于理解。
ProducerConsumerPattern.java如下:
生产者,Producer.java如下:
消费者,Consumer.java如下所示:
参考: https://www.cnblogs.com/expia...
多读少写的场景应该使用哪个并发容器,为什么使用它?比如你做了一个搜索引擎,搜索引擎每次搜索前需要判断搜索关键词是否在黑名单里,黑名单每天更新一次。
Java中的锁
如何实现乐观锁(CAS)?如何避免ABA问题?
CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查+数据更新的原理是一样的。
这里再强调一下,乐观锁是一种思想。CAS是这种思想的一种实现方式。
ABA问题:
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
解决方法:通过版本号(version)的方式来解决,每次比较要比较数据的值和版本号两项内容即可。
读写锁可以用于什么应用场景?
在多线程的环境下,对同一份数据进行读写,会涉及到线程安全的问题。比如在一个线程读取数据的时候,另外一个线程在写数据,而导致前后数据的不一致性;一个线程在写数据的时候,另一个线程也在写,同样也会导致线程前后看到的数据的不一致性。
这时候可以在读写方法中加入互斥锁,任何时候只能允许一个线程的一个读或写操作,而不允许其他线程的读或写操作,这样是可以解决这样以上的问题,但是效率却大打折扣了。因为在真实的业务场景中,一份数据,读取数据的操作次数通常高于写入数据的操作,而线程与线程间的读读操作是不涉及到线程安全的问题,没有必要加入互斥锁,只要在读-写,写-写期间上锁就行了。
对于以上这种情况,读写锁是最好的解决方案!其中它的实现类:ReentrantReadWriteLock--顾名思义是可重入的读写锁,允许多个读线程获得ReadLock,但只允许一个写线程获得WriteLock
读写锁的机制:
"读-读" 不互斥
"读-写" 互斥
"写-写" 互斥
参考:https://www.cnblogs.com/liang...
什么时候应该使用可重入锁?
可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁。
参考:http://ifeve.com/javalocksee4/
什么场景下可以使用volatile替换synchronized?
状态标志:把简单地volatile变量作为状态标志,来达成线程之间通讯的目的,省去了用synchronized还要wait,notify或者interrupt的编码麻烦。
替换重量级锁:如果某个变量仅是单次读或者单次写操作,没有复合操作(i++,先检查后判断之类的)就可以用volatile替换synchronized。
并发工具
如何实现一个流控程序,用于控制请求的调用次数?
一、概念
什么是线程
一个线程要执行任务,必须得有线程一个进程(程序)的所有任务都在线程中执行的一个线程执行任务是串行的,也就是说一个线程,同一时间内,只能执行一个任务
多线程原理
同一时间,CPU只能处理1条线程,只有一条线程在工作(执行)多线程并发(同时)执行,其实质是CPU快速的在多线程之间调度(切换)
如果线程过多,会怎样?
CPU在N多条线程中调度,会消耗大量的cpu资源每条线程被调度执行的频率越低(线程的执行效率低)
多线程的优点
能适当提高程序的执行效率能适当提高资源的利用率(CPU 内存利用率等)
多线程的缺点
创建线程是有开销的,iOS下主要成本包括:内核数据结构(大约1KB)、栈空间(子线程512KB、主线程1MB,也可以使用-setStackSize:设置,但必须是4K的倍数,而且最小是16K),创建线程大约需要90毫秒的创建时间如果开启大量的线程,会降低程序的性能程序越多CPU的线程上的开销就越大程序设计更加复杂:线程之间的通讯,多线程的数据共享
主线程的主要作用
显示和刷新UI界面处理UI事件(比如点击事件,滚动事件,拖拽事件等)
主线程的使用注意
别将比较耗时的操作放在主线程中,会导致UI界面的卡顿将耗时操作放在子线程(后台线程,非主线程)
二、多线程的4种方案
三、常见多线程面试题:
下面这些是我在不同时间不同地点喜欢问的Java线程问题。我没有提供答案,但只要可能我会给你线索,有些时候这些线索足够回答问题。
1.现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?
这个线程问题通常会在第一轮或电话面试阶段被问到,目的是检测你对”join”方法是否熟悉。这个多线程问题比较简单,可以用join方法实现。
2.在Java中Lock接口比synchronized块的优势是什么?你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它?
lock接口在多线程和并发编程中最大的优势是它们为读和写分别提供了锁,它能满足你写像ConcurrentHashMap这样的高性能数据结构和有条件的阻塞。Java线程面试的问题越来越会根据面试者的回答来提问。我强烈建议在你去参加多线程的面试之前认真读一下Locks,因为当前其大量用于构建电子交易终统的客户端缓存和交易连接空间。
3.在java中wait和sleep方法的不同?
通常会在电话面试中经常被问到的Java线程面试问题。最大的不同是在等待时wait会释放锁,而sleep一直持有锁。Wait通常被用于线程间交互,sleep通常被用于暂停执行。
4.用Java写代码来解决生产者——消费者问题。
与上面的问题很类似,但这个问题更经典,有些时候面试都会问下面的问题。在Java中怎么解决生产者——消费者问题,当然有很多解决方法,我已经分享了一种用阻塞队列实现的方法。有些时候他们甚至会问怎么实现哲学家进餐问题。
5.你将如何使用thread dump?你将如何分析Thread dump?
在UNIX中你可以使用kill -3,然后thread dump将会打印日志,在windows中你可以使用”CTRL+Break”。非常简单和专业的线程面试问题,但是如果他问你怎样分析它,就会很棘手。
6.**用Java编程一个会导致死锁的程序,你将怎么解决?
这是我最喜欢的Java线程面试问题,因为即使死锁问题在写多线程并发程序时非常普遍,但是很多侯选者并不能写deadlock free code(无死锁代码?),他们很挣扎。只要告诉他们,你有N个资源和N个线程,并且你需要所有的资源来完成一个操作。为了简单这里的n可以替换为2,越大的数据会使问题看起来更复杂。通过避免Java中的死锁来得到关于死锁的更多信息。
7.你在多线程环境中遇到的共同的问题是什么?你是怎么解决它的?
多线程和并发程序中常遇到的有Memory-interface、竞争条件、死锁、活锁和饥饿。问题是没有止境的,如果你弄错了,将很难发现和调试。这是大多数基于面试的,而不是基于实际应用的Java线程问题。
8.为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?
这是一个非常经典的java多线程面试问题。这也是我刚开始写线程程序时候的困惑。现在这个问题通常在电话面试或者是在初中级Java面试的第一轮被问到。这个问题的回答应该是这样的,当你调用start()方法时你将创建新的线程,并且执行在run()方法里的代码。但是如果你直接调用run()方法,它不会创建新的线程也不会执行调用线程的代码。
9.Java中的volatile关键是什么作用?怎样使用它?在Java中它跟synchronized方法有什么不同?
自从Java 5和Java内存模型改变以后,基于volatile关键字的线程问题越来越流行。应该准备好回答关于volatile变量怎样在并发环境中确保可见性、顺序性和一致性。
10.**什么是不可变对象,它对写并发应用有什么帮助?
另一个多线程经典面试问题,并不直接跟线程有关,但间接帮助很多。这个java面试问题可以变的非常棘手,如果他要求你写一个不可变对象,或者问你为什么String是不可变的。
四、多线程并发的学习思路:
Java内存模型
第一部分 走进Java
一、走进Java
1、概述
java广泛应用于嵌入式系统、移动终端、企业服务器、大型机等各种场合,摆脱了硬件平台的束缚,实现了“一次编写,到处运行”的理想
2、java技术体系结构
按照功能来划分
1.包括以下几个组成部分:Java程序设计语言,各种硬件平台的java虚拟机,Java API类库,来自商业机构和开源社区的第三方Java类库,Class文件格式
2.Java程序设计语言,java虚拟机,Java API类库统称为JDK,是用于支持java程序开发的最小环境
3.Java API类库中的Java SE API子集和Java虚拟机统称为JRE,是支持java程序运行的基本环境
按照技术所服务的领域划分分为4个平台
1.Java Card:支持java小程序运行在java小内存设备(如智能卡)上的平台
2.Java ME:支持Java程序运行在移动设备上的平台
3.Java SE:支持面向桌面级应用的平台
4.Java EE:支持使用多层架构的企业级应用的平台
第二部分 自动内存管理机制
二、内存区域和内存溢出异常
1、运行时数据区
程序计数器
记录的是正在执行的虚拟机字节码指令的地址,可以看成是当前线程所执行的字节码的行号指示器,每个线程都有一个独立的程序计数器,各条线程的程序计数器互不影响,独立存储,这类内存区域成为“线程私有的内存”。
此内存区域是唯一在虚拟机规范中没有OutOfMemoryError的情况的区域
Java虚拟机栈
同程序计数器一样,也是线程私有的。每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每一个方法从调用直至执行完成的过程,都对应着一个栈帧在虚拟机栈中入栈和出栈的过程。
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
如果请求的栈深度超过虚拟机锁允许的深度,将抛出StackOverFlowError异常。如果拓展无法申请到足够的内存,将抛出OutOfMemoryError异常。
本地方法栈
为虚拟机使用的native方法服务,和虚拟机栈一样,本地方法栈也会抛出StackOverFlowError和OutOfMemoryError异常。
Java堆
Java堆是所有线程共享的一块内存区域,用来存放对象实例,几乎所有的对象实例都在这里分配。
Java堆是垃圾回收的主要区域,采用分代收集算法。
Java堆分为新生代和老年代,新生代在细致一点分为Eden,From Survivor,To Survivor空间。
如果堆中无法完成对象实例的内存分配,且堆也无法扩展时,将抛出OutOfMemoryError异常。
方法区
是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,HotSpot虚拟机的设计团队把GC分代收集扩展至方法区,或者说使用永久代来代替方法区。
在目前已经发布的JDK1.7的HotSpot中,已经把原本放在永久代的字符串常量池移出了。当方法区无法满足内存的分配需求时,将抛出OutOfMemoryError异常。
运行时常量池
是方法区的一部分,Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放。
运行时常量池相对于Class文件常量池,具有动态性,运行期间也可以将新的常量放入常量池,比如String类的intern()方法。
当运行时常量池无法申请到更多的内存时,将会抛出OutOfMemoryError异常。
直接内存
并不是运行时区域的一部分,JDK 1.4加入的NIO 它可以使用Native函数库直接分配堆外内存,然后通过Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
2、HotSpot虚拟机对象探秘
对象的创建
当遇到new指令时,先判断这个类是否被加载、解析、初始化过,如果没有,先执行相应类的加载过程。
类加载检查通过后,为新生对象分配内存,如果Java堆内存是规整连续的,采用“指针碰撞”的分配方式,如果是不连续规整的,采用“空闲列表”分配方式。内存是否规整取决于垃圾收集器是否带有压缩整理功能。
Serial,ParNew等带有Compact过程的收集器,采用的分配算法是“指针碰撞”。而CMS这种基于Mark-Sweep算法的收集器,通常采用“空闲列表”分配方式。
创建对象涉及到分配内存和指针指向两个操作,不是原子性的,不是线程安全的。针对这个问题,有两个解决办法:1是采用CAS加上失败重试来保证操作的原子性。2是采用TLAB(Thread Local Allocation Buffer)策略,在Java堆中预先为每一个线程分配一小块内存,称为TLAB(Thread Local Allocation Buffer),哪个线程要分配内存就在各自的TLAB上进行内存的分配,只有TLAB用完进行新的TLAB的分配时才需要同步锁定,虚拟机是否使用TLAB,可以通过 -XX:+/- UseTLAB
内存分配完成后,需要对对象头进行设置,包括这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。
最后执行init方法,把对象按照程序员的意愿进行初始化。这样一个真正可用的对象才算完全生产出来。
对象的内存布局
分为三块区域,对象头(Header)、实例数据(Instance Data)、对齐补充(Padding)。
对象头,存储对象自身的运行时数据,如哈希码、对象的GC分代年龄、锁状态标志、偏向线程ID、偏向时间戳,这部分数据的长度在32位和64位虚拟机中分别为32bit和64bit。
另一个部分是类型指针,虚拟机通过这个对象来确定这个对象是哪个类的实例。
对象的访问定位
Java程序需要通过栈上的reference数据来操作堆中的具体对象,具体实现有两种方式:使用句柄和直接指针两种。
使用句柄:Java堆中划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄中包括了对象的实例数据和类型数据各自的地址信息。最大好处是当对象修改时,reference本身不需要修改,因为reference中存储的是稳定的句柄地址
直接指针:reference中存储的直接就是堆中的对象地址,堆对象的布局中需要考虑如何放置访问类型数据的相关信息。最大好处是速度更快,节省了一次指针定位的开销,HotSpot就采用的直接指针方式。
3、OutOfMemoryError异常
堆溢出
不断创建对象,保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,达到最大堆的容量限制后就会产生内存溢出异常。
-Xms20m 堆的最小值;-Xmx20m 堆的最大值;-XX:+HeapDumpOnOutOfMemoryError 内存溢出异常时Dump出当前的内存堆转储快照以便日后分析
虚拟机栈和本地方法栈溢出
-Xss 栈容量
方法区和运行常量池溢出
多次调用String.intern()方法可以产生内存溢出异常。JDK 1.6之间,可以通过 -XX:PermSize 和 -XX:MaxPermSize 限制永久代大小,从而达到限制方法区大小的目的
本地直接内存溢出
可以通过 -XX:MaxDirectMemorySize 指定。如果不指定,则默认和Java堆最大值(-Xmx 指定)一样
三、垃圾收集器和内存分配策略
1、对象已死吗?如何确定对象是否还“活着”
引用计数器方法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加1,当引用失效时,计数器就减1。
优点是判定简单,效率也很高。缺点是无法解决相互循环引用的问题
可达性分析方法
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为“引用链”,当一个对象到GC Roots没有任何引用链相连时,说明这个对象是可回收的。
Java语言中,可作为GC Roots的对象包括以下几种:虚拟机栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI引用的对象。
再谈引用
JDK1.2 之后把引用分为了四种:强引用、软引用、弱引用、虚引用
强引用:只要强引用还存在,就不会被垃圾回收器回收。类似 Object o=new Object()
软引用:指一些有用但并非必须的对象,在系统将要发生内存溢出的时候,会将这部分对象回收。SoftReference 类来实现软引用
弱引用:被弱引用关联的对象只能生存到下一次垃圾回收。WeakReference 类来实现弱引用
虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间造车影响,也无法通过虚引用取得对象的引用。一个对象设置虚引用的唯一目的是在被垃圾回收的时候收到一个系统通知
对象被回收的过程
当对象进行可达性分析没有与GC Roots相连的引用链,将会被第一次标记,并根据是否需要执行finalize()方法进行一次筛选,对象没有重写finalize()或者虚拟机已经调用过finalize(),都被视为不需要执行
如果对象有必要执行finalize,会被放入到F-Queue队列中,并在稍后由虚拟机自动创建的低优先级的Finalizer线程去触发它,并不保证等待此方法执行结束。
如果对象在finalize()方法执行中,重新和GC Roots产生了引用链,则可以逃脱此次被回收的命运,但finalize()方法只能运行一次,所以并不能通过此方法逃脱下一次被回收
笔者不建议使用这个方法,建议大家完全忘掉这个方法的存在。
回收方法区
主要包括废弃常量和无用类的回收。判断类无用:类的实例都被回收,类的ClassLoader被回收,类的Java.Lang.Class对象没有在任何地方引用。满足这三个条件,类才可以被回收(卸载)
HotSpot虚拟机通过 -Xnoclassgc 参数进行控制是否启用类卸载功能。在大量使用反射、动态代理、CGLib等框架,需要虚拟机具备类卸载功能,避免方法区发生内存溢出
2、垃圾回收算法
标记-清除
先标记出所有要回收的对象,在标记完成后统一进行对象的回收。有两个不足:
1 是效率问题,标记和清除的效率都不高。
2 是空间问题,会产生大量不连续的内存碎片,碎片太多会都导致大对象无法找到足够的内存,从提前触发垃圾回收。
复制算法
新生代分为一个Eden,两个Survival空间,默认比例是8:1。回收时,将Eden和一个Survival的存活对象全部放入到另一个Survival空间中,最后清理掉刚刚的Eden和Survival空间
当Survival空间不够时,由老年代进行内存分配担保
标记-整理
根据老年代对象的特点,先标记存活对象,将存活对象移动到一端,然后直接清理掉端边界以外的对象
分代收集
新生代采用复制算法,老年代采用标记-删除,或者标记-整理算法。
3、HotSpot算法实现
枚举根节点实现
可达性分析时会进行GC停顿,停顿所有的Java线程。
HotSpot进行的是准确式GC,当系统停顿下来后,虚拟机有办法得知哪些地方存在着对象引用,HotSpot中使用一组称为OopMap的数据结构来达到这个目的
安全点
HotSpot没有为每个指令都生成OopMap,只在特定的位置记录这些信息,这些位置称为安全点。安全点的选定不能太少,也不能太频繁,安全点的选定以“是否让程序长时间执行”为标准
采用主动式中断的方式让所有线程都跑到最近的安全点上停顿下来。设置一个标志,各个程序执行的时候轮询这个标志,发现中断标志为真时自己就中断挂起
安全区域
解决没有分配Cpu时间的暂时不执行的程序停顿。
4、垃圾收集器
如果两个收集器之间有连线,说明可以搭配使用。没有最好的收集器,也没有万能的收集器,只有对应具体应用最合适的收集器。
Serial 收集器
新生代收集器,单线程回收。优点在于,简单而高效,对于运行在Client模式下的虚拟机来说是一个很好的选择(比如用户的桌面应用)
参数 -XX:UseSerialGC,打开此开关后,使用Serial+Serial Old的收集器组合进行内存回收
ParNew收集器
新生代收集器,Serial的多线程版本,除了Serial收集器之外,只有它能与CMS收集器配合工作。
-XX:+UseConcMarkSweepGC 选项后默认的新生代收集器,也可以使用 -XX:+UseParNewGC 选项来强制指定它
ParNew收集器在单CPU的环境中,效果不如Serial好,随着CPU的增加,对于GC时系统资源的利用还是很有效的。
默认开启的收集线程数和CPU数相等,可以使用 -XX:ParallelGCThreads 指定
Parallel Scavenge收集器
新生代收集器,并行收集器,复制算法,和其他收集器不同,关注点的是吞吐量(垃圾回收时间占总时间的比例)。提供了两个参数用于控制吞吐量。
-XX:MaxGCPauseMillis,最大垃圾收集停顿时间,减少GC的停顿时间是以牺牲吞吐量和新生代空间来换取的,不是设置的越小越好
-XX:GCTimeRatio,设置吞吐量大小,值是大于0小于100的范围,相当于吞吐量的倒数,比如设置成99,吞吐量就为1/(1+99)=1%。
-XX:UseAdaptiveSizePolicy ,这是一个开关参数,打开之后,就不需要设置新生代大小(-Xmn)、Eden和Survival的比例(-XX:SurvivalRatio)、 晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数,收集器会自动调节这些参数。
Serial Old 收集器
单线程收集器,老年代,主要意义是在Client模式下的虚拟机使用。在Server端,用于在JDK1.5以及之前版本和Parallel Scavenge配合使用,或者作为CMS的后备预案。
Palallel Old 收集器
是Parallel Scavenge的老年代版本。在注重吞吐量的场合,都可以优先考虑Parallel Scavenge 和Palallel Old 配合使用
CMS 收集器
Concurrent Mark Sweep,是一种以获取最短回收停顿时间为目标的收集器,尤其重视服务的响应速度。基于标记-清除算法实现。
分为四个步骤进行垃圾回收:初始标记,并发标记,重新标记,并发清除。只有初始标记和重新标记需要停顿。
初始标记只是标记一下GC Roots能直接关联到的对象,速度很快。并发标记就是进行GC Roots的Tracing。
重新标记为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会比初始标记阶段稍长,远比并发时间短。
耗时最长的并发标记和并发清除过程中,处理器可以与用户线程一起工作。
它并不是完美的,有如下三个比较明显的缺点:
1、垃圾回收时会占用一部分线程,导致系统变慢,总吞吐量会降低。
2、无法处理浮动垃圾,需要预留足够的内存空间给用户线程使用,可以通过 -XX:CMSInitiatingOccupancyFraction 参数控制触发垃圾回收的阈值。
如果预留的内存无法满足程序需要,就会出现“Concurrent Mode Failure”失败,这时将启动应急预案,启用Serial Old 进行垃圾回收,停顿时间会变长
所以-XX:CMSInitiatingOccupancyFraction 参数的值设置的太高,会导致频繁“Concurrent Mode Failure”失败,性能反而降低。
3、标记-清理,容易产生内存碎片。-XX:+UseCMSCompactAtFullColletion 开启碎片整理功能,默认开启,-XX:CMSFullGCsBeforeCompaction,控制多少次不压缩的FullGC之后来一次带压缩的
G1 收集器
包括新生代和老年代的垃圾回收。和其他收集器相比的优点:并行和并发,分代收集,标记-整理,可预测的停顿。垃圾回收分为以下几个步骤:
初始标记:标记GC Roots能够直接关联到的对象,这阶段需要停顿线程,时间很短
并发标记:进行可达性分析,这阶段耗时较长,可与用户程序并发执行
最终标记:修正发生变化的记录,需要停顿线程,但是可并行执行
筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来执行回收计划
5、内存分配和回收策略
对象优先在Eden分配,当新生区没有足够的内存是,通过分配担保机制提前转移到老年代中去
大对象直接进入老年代。大对象是指需要大量连续内存空间的对象,虚拟机提供了参数 -XX:PretenureSizeThreshold(只对Serial,PerNew两个回收器起效),令大于这个值得对象直接在老年代分配,避免了Eden和两个Survival之间发生大量的内存复制。
长期存活的对象将进入老年代。虚拟机给每个对象定义了对象年龄计数器(Age),如果对象在Eden出生,经过第一次Minor GC后依然存活,并且能被Survival容纳的话,将被移动到Survival,对象年龄设为1。对象在Survival中每熬过一次Major GC,年龄就增加1,达到一定程度(默认是15),就会被晋升到老年代。对象晋升老年代的阈值,可以通过参数-XX:MaxTenuringThreShold 指定
动态对象年龄判断。如果在Survival空间中相同年龄所有对象的大小综合超过了Survival空间的一半,年龄大于等于这个年龄的对象都会被晋升到老年代。无需等待年龄超过MaxTenuringThreShold指定的年龄
空间分配担保。只要老年代的连续空间大于新生代对象总和或者历次晋升的平均大小,就进行Major GC,否则进行Full GC。
四、虚拟机性能监控与故障处理工具
1、jps
命令用法: jps [options] [hostid]
功能描述: jps是用于查看有权访问的hotspot虚拟机的进程. 当未指定hostid时,默认查看本机jvm进程
常用参数:-lmvV
详细说明:JAVA JPS 命令详解
2、jstat。监视JVM内存工具。
语法结构:
Usage: jstat -help|-options
jstat -
(2)使用用户线程实现。不需要切换回内核态,也可以支持规模更大的线程数量。部分高性能数据库的多线程就是使用用户线程实现的。缺点是没有系统内核的支援,所有问题需要自己考虑,程序实现比较复杂
(3)内核线程和用户线程结合
(4)JVM,对于Sun JDK来说,在Windows和LInux系统下,都是使用的一对一的线程模型实现的。
Java线程调度
协同式线程调度。线程的执行时间由自己控制,线程执行完毕,会主动通知系统
java使用的是抢占式调度。每个线程有系统分配执行时间,线程的切换也有系统来决定,线程的执行时间是可控的。线程可以设置优先级,来争取更多的执行时间。Java一共设置了10个优先级,操作系统的优先级数量可能和java定义的不一致,另外操作系统还可以更改线程的优先级,所以Java中优先级高的线程并不一定被优先执行。
Java线程状态转换
十三、线程安全与锁优化
高效并发是从jdk1.5 到jdk1.6的一个重要改进,HotSpot虚拟机开发团队耗费了大量的精力去实现锁优化技术
自旋锁与自适应自旋。同步互斥对性能最大的影响就是线程挂起、恢复需要从用户态切换到内核态,切换的过程会造成系统消耗。往往锁定的代码段执行时间非常短,为了这个短的时间去挂起和恢复是不值得的。所以提出了自旋锁的概念,当线程申请获取一个其他线程占用的锁时,这个线程不会立即挂起,而是通过一定次数的循环自旋,这个过程不会释放cpu的控制权,自适应自旋就是根据上一次自旋的结果来决定这一次自旋的次数
锁消除。虚拟机即时编译器在运行时会把检测到不可能发生共享数据竞争的锁消除
锁粗化。一系列的操作都是对同一个对象的加锁和解锁,虚拟机检测到这种情况会将锁的范围扩大(粗化)
轻量级锁
偏向锁。如果程序中大多数的锁总是被多个线程访问,那偏向锁模式就是多余的。可以使用参数 -XX:-UseBiasedLocking来禁止偏向锁
运行时数据集区域
一、JDK1.8 JVM运行时数据区域概览
这里介绍的是JDK1.8 JVM运行时内存数据区域划分。1.8同1.7比,最大的差别就是:元数据区取代了永久代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。
二、各区域介绍
三、
- 程序计数器
每个线程一块,指向当前线程正在执行的字节码代码的行号。如果当前线程执行的是native方法,则其值为null。
- Java虚拟机栈
线程私有,每个线程对应一个Java虚拟机栈,其生命周期与线程同进同退。每个Java方法在被调用的时候都会创建一个栈帧,并入栈。一旦完成调用,则出栈。所有的的栈帧都出栈后,线程也就完成了使命。
- 本地方法栈
功能与Java虚拟机栈十分相同。区别在于,本地方法栈为虚拟机使用到的native方法服务。不多说。
- 堆
堆是JVM内存占用最大,管理最复杂的一个区域。其唯一的用途就是存放对象实例:几乎所有的对象实例及数组都在对上进行分配。1.7后,字符串常量池从永久代中剥离出来,存放在堆中。堆有自己进一步的内存分块划分,按照GC分代收集角度的划分请参见上图。
4.1 堆空间内存分配(默认情况下)
老年代 : 三分之二的堆空间
年轻代 : 三分之一的堆空间
eden区: 8/10 的年轻代空间
survivor0 : 1/10 的年轻代空间
survivor1 : 1/10 的年轻代空间
命令行上执行如下命令,查看所有默认的jvm参数
java -XX:+PrintFlagsFinal -version
1
输出
输出有大几百行,这里只取其中的两个有关联的参数
[Global flags]
uintx InitialSurvivorRatio = 8 {product}
uintx NewRatio = 2 {product}
... ...
java version "1.8.0_91"
Java(TM) SE Runtime Environment (build 1.8.0_91-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.91-b14, mixed mode)
参数解释
参数 作用
-XX:InitialSurvivorRatio 新生代Eden/Survivor空间的初始比例
-XX:Newratio Old区 和 Yong区 的内存比例
一道推算题
默认参数下,如果仅给出eden区40M,求堆空间总大小
根据比例可以推算出,两个survivor区各5M,年轻代50M。老年代是年轻代的两倍,即100M。那么堆总大小就是150M。
4.2 字符串常量池
JDK1.7 就开始“去永久代”的工作了。 1.7把字符串常量池从永久代中剥离出来,存放在堆空间中。
a. jvm参数配置
-XX:MaxPermSize=10m
-XX:PermSize=10m
-Xms100m
-Xmx100m
-XX:-UseGCOverheadLimit
1
2
3
4
5
b. 测试代码
public class StringOomMock {
public static void main(String[] args) {
try {
List list = new ArrayList();
for (int i = 0; ; i++) {
System.out.println(i);
list.add(String.valueOf("String" + i++).intern());
}
} catch (java.lang.Exception e) {
e.printStackTrace();
}
}
}
c. jdk1.6 下的运行结果
jdk1.6 环境下是永久代OOM
153658
153660
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at com.jd.im.StringOomMock.main(StringOomMock.java:17)
d. jdk1.7 下的运行结果
jdk1.7 下是堆OOM,并且伴随着频繁的FullGC, CPU一直高位运行
2252792
2252794
2252796
2252798
java.lang.instrument ASSERTION FAILED : "!errorOutstanding" with message can't create name string at ../../../src/share/instrument/JPLISAgent.c line: 807
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.nio.CharBuffer.wrap(CharBuffer.java:369)
at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:265)
at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)
at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207)
at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129)
at java.io.PrintStream.write(PrintStream.java:526)
at java.io.PrintStream.print(PrintStream.java:597)
at java.io.PrintStream.println(PrintStream.java:736)
at com.jd.im.StringOomMock.main(StringOomMock.java:16)
e. jdk1.8 下的运行结果
jdk1.8的运行结果同1.7的一样,都是堆空间OOM。
2236898
2236900
2236902
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.lang.Integer.toString(Integer.java:403)
at java.lang.String.valueOf(String.java:3099)
at java.io.PrintStream.print(PrintStream.java:597)
at java.io.PrintStream.println(PrintStream.java:736)
at com.jd.im.StringOomMock.main(StringOomMock.java:16)
- 元数据区
元数据区取代了1.7版本及以前的永久代。元数据区和永久代本质上都是方法区的实现。方法区存放虚拟机加载的类信息,静态变量,常量等数据。
元数据区OOM测试:
a. jvm参数配置
-XX:MetaspaceSize=8m
-XX:MaxMetaspaceSize=50m
1
2
b. 测试代码
借助cglib框架生成新类。
public class MetaSpaceOomMock {
public static void main(String[] args) {
ClassLoadingMXBean loadingBean = ManagementFactory.getClassLoadingMXBean();
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MetaSpaceOomMock.class);
enhancer.setCallbackTypes(new Class[]{Dispatcher.class, MethodInterceptor.class});
enhancer.setCallbackFilter(new CallbackFilter() {
@Override
public int accept(Method method) {
return 1;
}
@Override
public boolean equals(Object obj) {
return super.equals(obj);
}
});
Class clazz = enhancer.createClass();
System.out.println(clazz.getName());
//显示数量信息(共加载过的类型数目,当前还有效的类型数目,已经被卸载的类型数目)
System.out.println("total: " + loadingBean.getTotalLoadedClassCount());
System.out.println("active: " + loadingBean.getLoadedClassCount());
System.out.println("unloaded: " + loadingBean.getUnloadedClassCount());
}
}
}
如果是1.7的jdk,那么报OOM的将是PermGen区域。
- 直接内存
jdk1.4引入了NIO,它可以使用Native函数库直接分配堆外内存。
Java运行时数据区域划分
Java
JVM
内存
堆
栈
1、 概述
对于Java程序员来说,在虚拟机自动内存管理机制下,不容易出现内存泄漏和内存溢出现象。但如果不了解虚拟机是如何使用内存的,一旦出现了内存泄漏和溢出方面的问题,那么排错就无从下手了。
2、 运行时数据区域
Java虚拟机在执行Java程序的过程中会将它所管理的内存划分为若干个不同的数据区域,如下图所示。
2.1、 程序计数器
程序计数器(Program Counter Register):是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。(虚拟机的概念模型中,字节码解释器依靠程序计数器的值来选择下一条需要执行的字节码指令)
为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响独立储存。由上图也可知程序计数器为线程隔离的数据区。
若线程执行Java方法: 程序计数器记录的是正在执行的虚拟机字节码指令的地址。
若执行Native方法: 计数器值为空
2.2、 Java虚拟机栈
与程序计数器一样Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。
Java虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)。每个方法从调用直至执行完成的过程中,就对应一个栈帧从Java虚拟机栈中入栈到出栈的过程。
栈帧(Stack Frame):用于储存局部变量表,操作数栈,动态链接,方法出口等信息。
局部变量表:局部变量表中存放了编译器可知的各种基本数据类型(boolean,byte,int...etc)、对象引用和 returnAddress 类型。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
在Java虚拟机规范中对Java虚拟机栈区域规定了二种异常:
若线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
如果当前Java虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
2.3、 本地方法栈
本地方法栈(Native Method Stack)与Java虚拟机栈作用相似。Java虚拟机栈为Java方法(也就是字节码)服务。本地方法栈则为虚拟机使用到的Native方法服务。
在虚拟机规范中对本地方法栈中方法使用的语言、使用方式、数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。例如:Sun HotSpot虚拟机直接就把虚拟机栈与本地方法栈合二为一。
抛出的异常同Java虚拟机栈一样。
2.4、 Java堆
一般来说Java堆是Java虚拟机所管理的内存中最大的一块。Java堆(Java Heap)是被所有线程共享的一块内存区域,在虚拟机启动时创建。
Java虚拟机规范对Java堆的描述是:
所有的对象及数组都要在堆上分配内存(随着技术的发展这一点也不那么“绝对”了)。
Java堆可以处在物理上不连续的内存空间中,只要逻辑上是连续的即可。
当前主流的虚拟机对Java堆都是按照可扩展来实现的,如果在堆中没有内存可供实例完成分配,并且堆也无法在扩展时,将会抛出OutOfMemoryError异常。
2.5、 方法区
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,用于储存已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
Java虚拟机规范对堆的描述为:
方法区是堆的一个逻辑部分,有一个别名Non-Heap(非堆),目的应该是与Java堆区分开来。
方法区和堆一样不需要连续的内存和可扩展外,还可以不实现垃圾收集。
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
2.6、 运行时常量池
运行时常量池(Runtime Constant Poll)是方法区的一部分。
Class文件中包含了类的版本、字段、方法、接口等描述信息外还包含常量池(Costant Pool Table)——用来存放在编译期间生成的各种字面量和符号引用。这部分内容将在类加载后再进入方法区的运行时常量池中存放。
Java虚拟机对Class文件的每一部分(包括常量池)的格式都有严格的规定,必须符合规范才能被虚拟机认可、装载和执行。但对于运行时常量池,Java虚拟机规范没有做任何细节要求,不同的虚拟机可以有不同的实现,但一般来说,除了保存Class文件中的符号引用外,还会把翻译出来的直接引用也保存在运行时常量池中。
运行时常量池相对于Class文件常量池来说具有一个重要特征——动态性,并非只有预置入Class文件常量池的内容才能进入运行时常量池,运行期间也可以将新的常量放入运行时常量池中。(这种特性运用比如String类的intern()方法)
运行时常量池是方法区的一部分,所以受到方法区内存的限制,当常量池无法再申请到内存时将抛出OutOfMemoryError异常。
Class文件结构
我们都知道java实现跨平台靠的是虚拟机技术,将源文件编译成与操作系统无关的,只有虚拟机能识别并执行的字节码文件,由各个操作系统上的jvm来负责执行,屏蔽了底层具体的操作系统。这里我们就来认识一下这个只有jvm才认识的字节码文件的真实样子。
为了节省空间,类文件中没有任何分隔符,各个数据项都是一个挨着一个紧凑排列的,所以其中无论是顺序还是数量等都是严格规定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。下面我们先看一下类文件的整体结构:
Class文件结构
其中常量、接口、字段、方法和属性在其中按各自的结构紧密排列,个数由其前面的数量字段决定。同时类文件中最小单位为1个字节,超过一个字节的数据以大端方式存储。
下面依次介绍其中的每个部分:
魔数
魔数是用来确定文件的类型是否是class文件,因为只靠文件扩展名来确定文件类型并不可靠。
这个魔数占文件的开始4个字节,为CA FE BA BE。(注意:这里的字面代表的是十六进制数,而不是ASCII码)
版本号
接下来的4个字节为class文件版本号,其中前两个字节表示的是次版本号,后两个字节表示的是主版本号(从45开始)。
虚拟机可以向下兼容运行class文件,但不能运行高于其版本的class文件。
常量池
由于常量池中的常量数量是不确定的,所以在常量池的入口需要有两个字节用来代表常量池容量计数值(常量池索引从1开始)。
一共有14种常量类型,有着各自对应的结构,但开始的一个字节同样都是表示标志位,用来区分不同的类型。
下面为14种常量的具体类型和对应的标志位:
每种类型的结构如下(其中u1表示1个字节,u2表示2个字节,其他同理):
读取常量池的时候首先读取标志位,判断常量类型,就可以知道对应的结构,获取对应的信息了。
访问标志
常量池之后的两个字节代表访问标志,即这个class是类还是接口,是否为public等的信息。不同的含义有不同的标志值(没有用到的标志位一律为0。),具体信息如下:
类索引
类索引占两个字节,分别指向常量池中的CONSTANT_Class_info类型的常量,这个类型的常量结构见常量池中的图表,其中包含一个指向全限定名常量项的索引。
父类索引
因为java只允许单继承,所以只有一个父类,具体内容同上-类索引。
接口索引
接口索引开始两个字节用来表示接口的数量,之后的每两个字节表示一个接口索引,用法同类索引与父类索引。
字段
字段用于描述接口或者类中声明的变量,包括类级变量以及实例变量,但不包括局部变量。
字段域的开始两个字节表示字段数量,之后为紧密排列的字段结构体数据,其结构如下:
其中的字段和方法的描述符,对于字段来说用来描述字段的数据类型;而对于方法来说,描述的就是方法的参数列表(包括数量、类型以及顺序)和返回值,这个描述顺序也是固定的,必须是参数列表在前,返回值在后,参数列表必须放在一组小括号内。同时为了节省空间,各种数据类型都使用规定的一个字母来表示,具体如下:
对象使用L加上对象的全限定名来表示,而数组则是在每一个维度前添加一个"["来描述。
属性表在之后进行介绍。
方法
class文件中对方法的描述与以前对字段的描述几乎采用了完全一致的方式,唯一的区别就是访问类型不完全一致。
属性
java7中预定义了21项属性,具体内容限于篇幅不再列出。
对于每个属性的结构,没有特别严格的要求,并且可以自定义属性信息,jvm运行时会忽略不认识的属性。
符合规范的属性表基本结构如下:
其中前两个字节为指向常量池中的CONSTANT_Utf8_info类型的属性名称,之后4个字节表示属性值所占用的位数,最后就是具体属性了。
其中有一个比较重要的名称为「Code」的属性为方法的代码,即字节码指令。
Code属性表结构如下:
以上只列出了一些Class文件最基本的结构,如有错误欢迎指正。
容器集合
一、集合与数组
数组(可以存储基本数据类型)是用来存现对象的一种容器,但是数组的长度固定,不适合在对象数量未知的情况下使用。
集合(只能存储对象,对象类型可以不一样)的长度可变,可在多数情况下使用。
二、层次关系
如图所示:图中,实线边框的是实现类,折线边框的是抽象类,而点线边框的是接口
Collection接口是集合类的根接口,Java中没有提供这个接口的直接的实现类。但是却让其被继承产生了两个接口,就是Set和List。Set中不能包含重复的元素。List是一个有序的集合,可以包含重复的元素,提供了按索引访问的方式。
Map是Java.util包中的另一个接口,它和Collection接口没有关系,是相互独立的,但是都属于集合类的一部分。Map包含了key-value对。Map不能包含重复的key,但是可以包含相同的value。
Iterator,所有的集合类,都实现了Iterator接口,这是一个用于遍历集合中元素的接口,主要包含以下三种方法:
1.hasNext()是否还有下一个元素。
2.next()返回下一个元素。
3.remove()删除当前元素。
三、几种重要的接口和类简介
1、List(有序、可重复)
List里存放的对象是有序的,同时也是可以重复的,List关注的是索引,拥有一系列和索引相关的方法,查询速度快。因为往list集合里插入或删除数据时,会伴随着后面数据的移动,所有插入删除数据速度慢。
2、Set(无序、不能重复)
Set里存放的对象是无序,不能重复的,集合中的对象不按特定的方式排序,只是简单地把对象加入集合中。
3、Map(键值对、键唯一、值不唯一)
Map集合中存储的是键值对,键不能重复,值可以重复。根据键得到值,对map集合遍历时先得到键的set集合,对set集合进行遍历,得到相应的值。
对比如下:
是否有序
是否允许元素重复
Collection
List
是
是
Set
AbstractSet
否
否
HashSet
TreeSet
是(用二叉排序树)
Map
AbstractMap
否
使用key-value来映射和存储数据,key必须唯一,value可以重复
HashMap
TreeMap
是(用二叉排序树)
四、遍历
在类集中提供了以下四种的常见输出方式:
1)Iterator:迭代输出,是使用最多的输出方式。
2)ListIterator:是Iterator的子接口,专门用于输出List中的内容。
3)foreach输出:JDK1.5之后提供的新功能,可以输出数组或集合。
4)for循环
代码示例如下:
for的形式:for(int i=0;i foreach的形式: for(int i:arr){...} iterator的形式: 五、ArrayList和LinkedList ArrayList和LinkedList在用法上没有区别,但是在功能上还是有区别的。LinkedList经常用在增删操作较多而查询操作很少的情况下,ArrayList则相反。 六、Map集合 实现类:HashMap、Hashtable、LinkedHashMap和TreeMap HashMap HashMap是最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度,遍历时,取得数据的顺序是完全随机的。因为键对象不可以重复,所以HashMap最多只允许一条记录的键为Null,允许多条记录的值为Null,是非同步的 Hashtable Hashtable与HashMap类似,是HashMap的线程安全版,它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了Hashtale在写入时会比较慢,它继承自Dictionary类,不同的是它不允许记录的键或者值为null,同时效率较低。 ConcurrentHashMap 线程安全,并且锁分离。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hash table,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。 LinkedHashMap LinkedHashMap保存了记录的插入顺序,在用Iteraor遍历LinkedHashMap时,先得到的记录肯定是先插入的,在遍历的时候会比HashMap慢,有HashMap的全部特性。 TreeMap TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序(自然顺序),也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。不允许key值为空,非同步的; map的遍历 第一种:KeySet() 第二种:entrySet() 七、主要实现类区别小结 Vector和ArrayList arraylist和linkedlist HashMap与TreeMap HashTable与HashMap 3、hashtable的key和value都不允许为null。 Java集合框架为Java编程语言的基础,也是Java面试中很重要的一个知识点。这里,我列出了一些关于Java集合的重要问题和答案。 1.Java集合框架是什么?说出一些集合框架的优点? 每种编程语言中都有集合,最初的Java版本包含几种集合类:Vector、Stack、HashTable和Array。随着集合的广泛使用,Java1.2提出了囊括所有集合接口、实现和算法的集合框架。在保证线程安全的情况下使用泛型和并发集合类,Java已经经历了很久。它还包括在Java并发包中,阻塞接口以及它们的实现。集合框架的部分优点如下: (1)使用核心集合类降低开发成本,而非实现我们自己的集合类。 (2)随着使用经过严格测试的集合框架类,代码质量会得到提高。 (3)通过使用JDK附带的集合类,可以降低代码维护成本。 (4)复用性和可操作性。 2.集合框架中的泛型有什么优点? Java1.5引入了泛型,所有的集合接口和实现都大量地使用它。泛型允许我们为集合提供一个可以容纳的对象类型,因此,如果你添加其它类型的任何元素,它会在编译时报错。这避免了在运行时出现ClassCastException,因为你将会在编译时得到报错信息。泛型也使得代码整洁,我们不需要使用显式转换和instanceOf操作符。它也给运行时带来好处,因为不会产生类型检查的字节码指令。 3.Java集合框架的基础接口有哪些? Collection为集合层级的根接口。一个集合代表一组对象,这些对象即为它的元素。Java平台不提供这个接口任何直接的实现。 Set是一个不能包含重复元素的集合。这个接口对数学集合抽象进行建模,被用来代表集合,就如一副牌。 List是一个有序集合,可以包含重复元素。你可以通过它的索引来访问任何元素。List更像长度动态变换的数组。 Map是一个将key映射到value的对象.一个Map不能包含重复的key:每个key最多只能映射一个value。 一些其它的接口有Queue、Dequeue、SortedSet、SortedMap和ListIterator。 4.为何Collection不从Cloneable和Serializable接口继承? Collection接口指定一组对象,对象即为它的元素。如何维护这些元素由Collection的具体实现决定。例如,一些如List的Collection实现允许重复的元素,而其它的如Set就不允许。很多Collection实现有一个公有的clone方法。然而,把它放到集合的所有实现中也是没有意义的。这是因为Collection是一个抽象表现。重要的是实现。 当与具体实现打交道的时候,克隆或序列化的语义和含义才发挥作用。所以,具体实现应该决定如何对它进行克隆或序列化,或它是否可以被克隆或序列化。 在所有的实现中授权克隆和序列化,最终导致更少的灵活性和更多的限制。特定的实现应该决定它是否可以被克隆和序列化。 5.为何Map接口不继承Collection接口? 尽管Map接口和它的实现也是集合框架的一部分,但Map不是集合,集合也不是Map。因此,Map继承Collection毫无意义,反之亦然。 如果Map继承Collection接口,那么元素去哪儿?Map包含key-value对,它提供抽取key或value列表集合的方法,但是它不适合“一组对象”规范。 6.Iterator是什么? Iterator接口提供遍历任何Collection的接口。我们可以从一个Collection中使用迭代器方法来获取迭代器实例。迭代器取代了Java集合框架中的Enumeration。迭代器允许调用者在迭代过程中移除元素。 7.Enumeration和Iterator接口的区别? Enumeration的速度是Iterator的两倍,也使用更少的内存。Enumeration是非常基础的,也满足了基础的需要。但是,与Enumeration相比,Iterator更加安全,因为当一个集合正在被遍历的时候,它会阻止其它线程去修改集合。 迭代器取代了Java集合框架中的Enumeration。迭代器允许调用者从集合中移除元素,而Enumeration不能做到。为了使它的功能更加清晰,迭代器方法名已经经过改善。 8.为何没有像Iterator.add()这样的方法,向集合中添加元素? 语义不明,已知的是,Iterator的协议不能确保迭代的次序。然而要注意,ListIterator没有提供一个add操作,它要确保迭代的顺序。 9.为何迭代器没有一个方法可以直接获取下一个元素,而不需要移动游标? 它可以在当前Iterator的顶层实现,但是它用得很少,如果将它加到接口中,每个继承都要去实现它,这没有意义。 10.Iterater和ListIterator之间有什么区别? (1)我们可以使用Iterator来遍历Set和List集合,而ListIterator只能遍历List。 (2)Iterator只可以向前遍历,而LIstIterator可以双向遍历。 (3)ListIterator从Iterator接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。 11.遍历一个List有哪些不同的方式? List 12.通过迭代器fail-fast属性,你明白了什么? 每次我们尝试获取下一个元素的时候,Iterator fail-fast属性检查当前集合结构里的任何改动。如果发现任何改动,它抛出ConcurrentModificationException。Collection中所有Iterator的实现都是按fail-fast来设计的(ConcurrentHashMap和CopyOnWriteArrayList这类并发集合类除外)。 13.fail-fast与fail-safe有什么区别? Iterator的fail-fast属性与当前的集合共同起作用,因此它不会受到集合中任何改动的影响。Java.util包中的所有集合类都被设计为fail-fast的,而java.util.concurrent中的集合类都为fail-safe的。Fail-fast迭代器抛出ConcurrentModificationException,而fail-safe迭代器从不抛出ConcurrentModificationException。 14.在迭代一个集合的时候,如何避免ConcurrentModificationException? 在遍历一个集合的时候,我们可以使用并发集合类来避免ConcurrentModificationException,比如使用CopyOnWriteArrayList,而不是ArrayList。 15.为何Iterator接口没有具体的实现? Iterator接口定义了遍历集合的方法,但它的实现则是集合实现类的责任。每个能够返回用于遍历的Iterator的集合类都有它自己的Iterator实现内部类。 这就允许集合类去选择迭代器是fail-fast还是fail-safe的。比如,ArrayList迭代器是fail-fast的,而CopyOnWriteArrayList迭代器是fail-safe的。 16.UnsupportedOperationException是什么? UnsupportedOperationException是用于表明操作不支持的异常。在JDK类中已被大量运用,在集合框架java.util.Collections.UnmodifiableCollection将会在所有add和remove操作中抛出这个异常。 17.在Java中,HashMap是如何工作的? HashMap在Map.Entry静态内部类实现中存储key-value对。HashMap使用哈希算法,在put和get方法中,它使用hashCode()和equals()方法。当我们通过传递key-value对调用put方法的时候,HashMap使用Key hashCode()和哈希算法来找出存储key-value对的索引。Entry存储在LinkedList中,所以如果存在entry,它使用equals()方法来检查传递的key是否已经存在,如果存在,它会覆盖value,如果不存在,它会创建一个新的entry然后保存。当我们通过传递key调用get方法时,它再次使用hashCode()来找到数组中的索引,然后使用equals()方法找出正确的Entry,然后返回它的值。下面的图片解释了详细内容。 其它关于HashMap比较重要的问题是容量、负荷系数和阀值调整。HashMap默认的初始容量是32,负荷系数是0.75。阀值是为负荷系数乘以容量,无论何时我们尝试添加一个entry,如果map的大小比阀值大的时候,HashMap会对map的内容进行重新哈希,且使用更大的容量。容量总是2的幂,所以如果你知道你需要存储大量的key-value对,比如缓存从数据库里面拉取的数据,使用正确的容量和负荷系数对HashMap进行初始化是个不错的做法。 18.hashCode()和equals()方法有何重要性? HashMap使用Key对象的hashCode()和equals()方法去决定key-value对的索引。当我们试着从HashMap中获取值的时候,这些方法也会被用到。如果这些方法没有被正确地实现,在这种情况下,两个不同Key也许会产生相同的hashCode()和equals()输出,HashMap将会认为它们是相同的,然后覆盖它们,而非把它们存储到不同的地方。同样的,所有不允许存储重复数据的集合类都使用hashCode()和equals()去查找重复,所以正确实现它们非常重要。equals()和hashCode()的实现应该遵循以下规则: (1)如果o1.equals(o2),那么o1.hashCode() == o2.hashCode()总是为true的。 (2)如果o1.hashCode() == o2.hashCode(),并不意味着o1.equals(o2)会为true。 19.我们能否使用任何类作为Map的key? 我们可以使用任何类作为Map的key,然而在使用它们之前,需要考虑以下几点: (1)如果类重写了equals()方法,它也应该重写hashCode()方法。 (2)类的所有实例需要遵循与equals()和hashCode()相关的规则。请参考之前提到的这些规则。 (3)如果一个类没有使用equals(),你不应该在hashCode()中使用它。 (4)用户自定义key类的最佳实践是使之为不可变的,这样,hashCode()值可以被缓存起来,拥有更好的性能。不可变的类也可以确保hashCode()和equals()在未来不会改变,这样就会解决与可变相关的问题了。 比如,我有一个类MyKey,在HashMap中使用它。 //传递给MyKey的name参数被用于equals()和hashCode()中 20.Map接口提供了哪些不同的集合视图? Map接口提供三个集合视图: (1)Set keyset():返回map中包含的所有key的一个Set视图。集合是受map支持的,map的变化会在集合中反映出来,反之亦然。当一个迭代器正在遍历一个集合时,若map被修改了(除迭代器自身的移除操作以外),迭代器的结果会变为未定义。集合支持通过Iterator的Remove、Set.remove、removeAll、retainAll和clear操作进行元素移除,从map中移除对应的映射。它不支持add和addAll操作。 (2)Collection values():返回一个map中包含的所有value的一个Collection视图。这个collection受map支持的,map的变化会在collection中反映出来,反之亦然。当一个迭代器正在遍历一个collection时,若map被修改了(除迭代器自身的移除操作以外),迭代器的结果会变为未定义。集合支持通过Iterator的Remove、Set.remove、removeAll、retainAll和clear操作进行元素移除,从map中移除对应的映射。它不支持add和addAll操作。 (3)Set 21.HashMap和HashTable有何不同? (1)HashMap允许key和value为null,而HashTable不允许。 (2)HashTable是同步的,而HashMap不是。所以HashMap适合单线程环境,HashTable适合多线程环境。 (3)在Java1.4中引入了LinkedHashMap,HashMap的一个子类,假如你想要遍历顺序,你很容易从HashMap转向LinkedHashMap,但是HashTable不是这样的,它的顺序是不可预知的。 (4)HashMap提供对key的Set进行遍历,因此它是fail-fast的,但HashTable提供对key的Enumeration进行遍历,它不支持fail-fast。 (5)HashTable被认为是个遗留的类,如果你寻求在迭代的时候修改Map,你应该使用CocurrentHashMap。 22.如何决定选用HashMap还是TreeMap? 对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。 23.ArrayList和Vector有何异同点? ArrayList和Vector在很多时候都很类似。 (1)两者都是基于索引的,内部由一个数组支持。 (2)两者维护插入的顺序,我们可以根据插入顺序来获取元素。 (3)ArrayList和Vector的迭代器实现都是fail-fast的。 (4)ArrayList和Vector两者允许null值,也可以使用索引值对元素进行随机访问。 以下是ArrayList和Vector的不同点。 (1)Vector是同步的,而ArrayList不是。然而,如果你寻求在迭代的时候对列表进行改变,你应该使用CopyOnWriteArrayList。 (2)ArrayList比Vector快,它因为有同步,不会过载。 (3)ArrayList更加通用,因为我们可以使用Collections工具类轻易地获取同步列表和只读列表。 24.Array和ArrayList有何区别?什么时候更适合用Array? Array可以容纳基本类型和对象,而ArrayList只能容纳对象。 Array是指定大小的,而ArrayList大小是固定的。 Array没有提供ArrayList那么多功能,比如addAll、removeAll和iterator等。尽管ArrayList明显是更好的选择,但也有些时候Array比较好用。 (1)如果列表的大小已经指定,大部分情况下是存储和遍历它们。 (2)对于遍历基本数据类型,尽管Collections使用自动装箱来减轻编码任务,在指定大小的基本类型的列表上工作也会变得很慢。 (3)如果你要使用多维数组,使用[][]比List 25.ArrayList和LinkedList有何区别? ArrayList和LinkedList两者都实现了List接口,但是它们之间有些不同。 (1)ArrayList是由Array所支持的基于一个索引的数据结构,所以它提供对元素的随机访问,复杂度为O(1),但LinkedList存储一系列的节点数据,每个节点都与前一个和下一个节点相连接。所以,尽管有使用索引获取元素的方法,内部实现是从起始点开始遍历,遍历到索引的节点然后返回元素,时间复杂度为O(n),比ArrayList要慢。 (2)与ArrayList相比,在LinkedList中插入、添加和删除一个元素会更快,因为在一个元素被插入到中间的时候,不会涉及改变数组的大小,或更新索引。 (3)LinkedList比ArrayList消耗更多的内存,因为LinkedList中的每个节点存储了前后节点的引用。 26.哪些集合类提供对元素的随机访问? ArrayList、HashMap、TreeMap和HashTable类提供对元素的随机访问。 27.EnumSet是什么? java.util.EnumSet是使用枚举类型的集合实现。当集合创建时,枚举集合中的所有元素必须来自单个指定的枚举类型,可以是显示的或隐示的。EnumSet是不同步的,不允许值为null的元素。它也提供了一些有用的方法,比如copyOf(Collection c)、of(E first,E…rest)和complementOf(EnumSet s)。 28.哪些集合类是线程安全的? Vector、HashTable、Properties和Stack是同步类,所以它们是线程安全的,可以在多线程环境下使用。Java1.5并发API包括一些集合类,允许迭代时修改,因为它们都工作在集合的克隆上,所以它们在多线程环境中是安全的。 29.并发集合类是什么? Java1.5并发包(java.util.concurrent)包含线程安全集合类,允许在迭代时修改集合。迭代器被设计为fail-fast的,会抛出ConcurrentModificationException。一部分类为:CopyOnWriteArrayList、 ConcurrentHashMap、CopyOnWriteArraySet。 30.BlockingQueue是什么? Java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。 31.队列和栈是什么,列出它们的区别? 栈和队列两者都被用来预存储数据。java.util.Queue是一个接口,它的实现类在Java并发包中。队列允许先进先出(FIFO)检索元素,但并非总是这样。Deque接口允许从两端检索元素。 栈与队列很相似,但它允许对元素进行后进先出(LIFO)进行检索。 Stack是一个扩展自Vector的类,而Queue是一个接口。 32.Collections类是什么? Java.util.Collections是一个工具类仅包含静态方法,它们操作或返回集合。它包含操作集合的多态算法,返回一个由指定集合支持的新集合和其它一些内容。这个类包含集合框架算法的方法,比如折半搜索、排序、混编和逆序等。 33.Comparable和Comparator接口是什么? 如果我们想使用Array或Collection的排序方法时,需要在自定义类里实现Java提供Comparable接口。Comparable接口有compareTo(T OBJ)方法,它被排序方法所使用。我们应该重写这个方法,如果“this”对象比传递的对象参数更小、相等或更大时,它返回一个负整数、0或正整数。但是,在大多数实际情况下,我们想根据不同参数进行排序。比如,作为一个CEO,我想对雇员基于薪资进行排序,一个HR想基于年龄对他们进行排序。这就是我们需要使用Comparator接口的情景,因为Comparable.compareTo(Object o)方法实现只能基于一个字段进行排序,我们不能根据对象排序的需要选择字段。Comparator接口的compare(Object o1, Object o2)方法的实现需要传递两个对象参数,若第一个参数比第二个小,返回负整数;若第一个等于第二个,返回0;若第一个比第二个大,返回正整数。 34.Comparable和Comparator接口有何区别? Comparable和Comparator接口被用来对对象集合或者数组进行排序。Comparable接口被用来提供对象的自然排序,我们可以使用它来提供基于单个逻辑的排序。 Comparator接口被用来提供不同的排序算法,我们可以选择需要使用的Comparator来对给定的对象集合进行排序。 35.我们如何对一组对象进行排序? 如果我们需要对一个对象数组进行排序,我们可以使用Arrays.sort()方法。如果我们需要排序一个对象列表,我们可以使用Collection.sort()方法。两个类都有用于自然排序(使用Comparable)或基于标准的排序(使用Comparator)的重载方法sort()。Collections内部使用数组排序方法,所有它们两者都有相同的性能,只是Collections需要花时间将列表转换为数组。 36.当一个集合被作为参数传递给一个函数时,如何才可以确保函数不能修改它? 在作为参数传递之前,我们可以使用Collections.unmodifiableCollection(Collection c)方法创建一个只读集合,这将确保改变集合的任何操作都会抛出UnsupportedOperationException。 37.我们如何从给定集合那里创建一个synchronized的集合? 我们可以使用Collections.synchronizedCollection(Collection c)根据指定集合来获取一个synchronized(线程安全的)集合。 38.集合框架里实现的通用算法有哪些? Java集合框架提供常用的算法实现,比如排序和搜索。Collections类包含这些方法实现。大部分算法是操作List的,但一部分对所有类型的集合都是可用的。部分算法有排序、搜索、混编、最大最小值。 39.大写的O是什么?举几个例子? 大写的O描述的是,就数据结构中的一系列元素而言,一个算法的性能。Collection类就是实际的数据结构,我们通常基于时间、内存和性能,使用大写的O来选择集合实现。比如:例子1:ArrayList的get(index i)是一个常量时间操作,它不依赖list中元素的数量。所以它的性能是O(1)。例子2:一个对于数组或列表的线性搜索的性能是O(n),因为我们需要遍历所有的元素来查找需要的元素。 40.与Java集合框架相关的有哪些最好的实践? (1)根据需要选择正确的集合类型。比如,如果指定了大小,我们会选用Array而非ArrayList。如果我们想根据插入顺序遍历一个Map,我们需要使用TreeMap。如果我们不想重复,我们应该使用Set。 (2)一些集合类允许指定初始容量,所以如果我们能够估计到存储元素的数量,我们可以使用它,就避免了重新哈希或大小调整。 (3)基于接口编程,而非基于实现编程,它允许我们后来轻易地改变实现。 (4)总是使用类型安全的泛型,避免在运行时出现ClassCastException。 (5)使用JDK提供的不可变类作为Map的key,可以避免自己实现hashCode()和equals()。 (6)尽可能使用Collections工具类,或者获取只读、同步或空的集合,而非编写自己的实现。它将会提供代码重用性,它有着更好的稳定性和可维护性。 JVM 双亲委派好处 加载 在准备阶段value1和value2都等于0; package classloader; }public class PathClassLoader extends ClassLoader { } public class SSClass } public class SuperClass extends SSClass }public class SubClass extends SuperClass }public class NotInitialization } 加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。以下陈述的内容都已HotSpot为基准。 public class Test } 那么去掉报错的那句,改成下面: public class Test } 输出结果是什么呢?当然是1啦~在准备阶段我们知道i=0,然后类初始化阶段按照顺序执行,首先执行static块中的i=0,接着执行static赋值操作i=1,最后在main方法中获取i的值为1。 package jvm.classload; } static } 运行结果: 3. } 5. 2. }public class NotInitialization } 4. 运行时数据区域 堆 Java虚拟机栈 本地方法栈 方法区 垃圾回收器 IO . } } Bit最小的二进制单位 ,是计算机的操作部分 取值0或者1 说下常用的io流 21 写一段代码读取一个序列化的对象一般使用哪种Stream? 22 io流怎样读取文件的? 23 说说你对io流的理解 24 JAVA的IO流和readLine方法 25 用什么把对象动态的写入磁盘中,写入要实现什么接口。 27 用io流中的技术,指定一个文件夹的目录,获取此目录下的所有子文件夹路径 28 请问你在什么情况下会在你得java代码中使用可序列化? 如何实现java序列化? 字符编码和字符集的概念 如果仅仅是抽象的字符集,其实是顾名思义的,但是我们常说的字符集,其实是指编码字符集(coded character set),比如: Unicode、ASCII、GB2312、GBK等等。什么是编码字符集呢?编码字符集是指,这个字符集里的每一个字符,都对应到唯一的一个代码值,这些代码值叫做代码点(code point),可以看做是这个字符在编码字符集里的序号,字符在给定的编码方式下的二进制比特序列称为代码单元(code unit)。在Unicode字符集中,字母A对应的数值是十六进制下的0041,书写时前面加U+,所以Unicode里A的代码点是U+0041。 常见的编码字符集有: Unicode:也叫统一字符集,它包含了几乎世界上所有的已经发现且需要使用的字符(如中文、日文、英文、德文等)。 Java的字符编码和字符集 Java如何解决这个问题的呢? Java的char类型使用UTF-16编码描述一个代码单元。在这种表现形式下,增补字符用一对代码单元编码,即2个char,其中,第一个值取值自uD800-uDBFF(高代理项范围),第二个值取值自uDC00-uDFFF(低代理项范围)。Unicode规定,U+D800到U+DFFF的值不对应于任何字符,为代理区。因此,UTF-16利用保留下来的0xD800-0xDFFF区段的码位来对增补字符进行编码。具体的UTF-16编码格式,可见这篇文章:https://www.cnblogs.com/drago...。 所以,char值表示BMP代码点,包括代理项代码点和UTF-16编码的代码单元。而int值可以表示所有的Unicode代码点,包括增补代码点。int的21个低位表示Unicode代码点,且11个高位必须为0。 Exception public static void main(String[] args) { } } 反射 2)什么是反射机制? 4)哪里用到反射机制? } } } catch (Exception e) { } } } catch (Exception e) { } }long end = System.currentTimeMillis(); }long end = System.currentTimeMillis(); } } } public static void main(String[] args) { } * } * } } save方法中就需要考虑到有注解的情况,修改代码,增加获取注解中值的逻辑: } 更完整的反射实现的ORM可以参考我的框架:https://github.com/yinjihuan/... } 泛型 } } } } } // T inferred to be Object String[] sa = new String[100]; // T inferred to be String // T inferred to be Object Integer[] ia = new Integer[100]; // T inferred to be Number // T inferred to be Number // T inferred to be Number // T inferred to be Object // compile-time error } } } } public void drawAll(List extends Shape> shapes) { } Spring相关 ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml"); Composing XML-based Configuration Metadata The following bean definition specifies that the bean be created by calling a factory method. The definition does not specify the type (class) of the returned object, only the class containing the factory method. In this example, the createInstance() method must be a static method. The following example shows how to specify a factory method: The following example shows a class that would work with the preceding bean definition: } The value of a map key or value, or a set value, can also be any of the following elements: } 1.9.1. @Required } 注解 SpringApplication loads properties from application.properties files in the following locations and adds them to the Spring Environment: The preceding example would be transformed into these properties: To bind to properties like that by using Spring Boot’s Binder utilities (which is what @ConfigurationProperties does), you need to have a property in the target bean of type java.util.List (or Set) and you either need to provide a setter or initialize it with a mutable value. For example, the following example binds to the properties shown previously: } @Validated } 数据库相关 主 键: id name |id score | 以下均在查询分析器中执行 2.左连接:left join 或 left outer join 1lee190 注释:包含table1的所有子句,根据指定条件返回table2相应的字段,不符合的以null显示 3.右连接:right join 或 right outer join 1lee190 注释:包含table2的所有子句,根据指定条件返回table1相应的字段,不符合的以null显示 4.完整外部联接:full join 或 full outer join 1lee190 注释:返回左右连接的和(见上左、右连接) 二、内连接 2.内连接:join 或 inner join 3.sql 语句 1lee190 注释:只返回符合条件的table1和table2的列 4.等价(与下列执行效果相同) 三、交叉连接(完全) 1.概念:没有 WHERE 子句的交叉联接将产生联接所涉及的表的笛卡尔积。第一个表的行数乘以第二个表的行数等于笛卡尔积结果集的大小。(table1和table2交叉连接产生3*3=9条记录) 2.交叉连接:cross join (不带条件where...) 3.sql语句 1lee190 注释:返回3*3=9条记录,即笛卡尔积 4.等价(与下列执行效果相同) 在任何一个关系数据库中,第一范式(1NF)是对关系模式的基本要求,不满足第一范式(1NF)的数据库就不是关系数据库。 第二范式(2NF)是在第一范式(1NF)的基础上建立起来的,即满足第二范式(2NF)必须先满足第一范式(1NF)。第二范式(2NF)要求数据库表中的每个实例或行必须可以被惟一地区分。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。这个惟一属性列被称为主关键字或主键、主码。 满足第三范式(3NF)必须先满足第二范式(2NF)。简而言之,第三范式(3NF)要求一个数据库表中不包含已在其它表中已包含的非主关键字信息。例如,存在一个部门信息表,其中每个部门有部门编号(dept_id)、部门名称、部门简介等信息。那么在员工信息表中列出部门编号后就不能再将部门名称、部门简介等与部门有关的信息再加入员工信息表中。如果不存在部门信息表,则根据第三范式(3NF)也应该构建它,否则就会有大量的数据冗余。简而言之,第三范式就是属性不依赖于其它非主属性。(我的理解是消除冗余) 建表优化 l 第三范式(3NF):首先是 2NF,另外非主键列必须直接依赖于主键,不能存在传递依赖。 B树与B-树: 需要数据库表1.学生表 1.关系型数据库通过外键关联来建立表与表之间的关系,---------常见的有:SQLite、Oracle、mysql MyISAM:默认的MySQL插件式存储引擎,它是在Web、数据仓储和其他应用环境下最常使用的存储引擎之一。 什么是范式:简言之就是,数据库设计对数据的存储性能,还有开发人员对数据的操作都有莫大的关系。所以建立科学的,规范的的数据库是需要满足一些 规范的来优化数据数据存储方式。在关系型数据库中这些规范就可以称为范式。 事务就是一段sql 语句的批处理,但是这个批处理是一个原子 ,不可分割,要么都执行,要么回滚(rollback)都不执行。 一对多:学生与班级---一个学生只能属于一个班级,一个班级可以有多个学生 创建一个商城表---包含(id,商品名,每一个商品对应数量) 触发器:触发器是一个特殊的存储过程,它是MySQL在insert、update、delete的时候自动执行的代码块。 索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),更通俗的说,数据库索引好比是一本书前面的目录,能加快数据库的查询速度 mysql在使用组合索引查询的时候需要遵循“最左前缀”规则 1.主键是能确定一条记录的唯一标识 聚合函数: 1.如果条件中有or,即使其中有条件带索引也不会使用(这也是为什么尽量少用or的原因) 1 执行 SHOW VARIABLES LIKE “%slow%”,获知 mysql 是否开启慢查询 slow_query_log 慢查询开启状态 OFF 未开启 ON 为开启 slow_query_log_file 慢查询日志存放的位置(这个目录需要MySQL的运行帐号的可写权限,一般设置为MySQL的数据存放目录) 1.导出整个数据库 总体思路从以下几个方面: char:定长,char的存取数度相对快 执行计划explain命令是查看查询优化器如何决定执行查询的主要方法。 select * from tb where name = ‘Oldboy’ -------------查找到tb表中所有name = ‘Oldboy’的数据 索引 分布式相关 分布式事务 数据库分库分表; 下单:减少库存同时更新订单状态。库存和订单不在不同一个数据库,因此涉及分布式事务。 3.1 两阶段提交协议 . . 1.1 轮询(Round Robin) . . . . . . . 2.1 DNS 解析 . . . . . 例如一个应用有手机 APP 端和 Web 端,如果在两个客户端同时进行一项操作时,那么就会导致这项操作重复进行。 2.1 数据库分布式锁 . 原理 原理 使用分布式缓存方案比如 Memcached、Redis,但是要求 Memcached 或 Redis 必须是集群。 . . 原理 原理 . . 使用分布式事务。 使用汇总表。 使用全局唯一 ID:GUID; 1.多系统之间怎么实现通信的?A系统—》B系统的服务 还有一种便是MQ,使用前,首先搭建一个rabbitMQ的服务器,MQ和HttpClient不同的地方在于HttpClient是同步调用,而MQ可以解耦的异步调用的,正是因为这个原因,MQ才很好的解决了同步的响应速度慢的问题。在这里我们使用的是rabbiMq,同类的产品还有例如ActiveMQ,Kafka等。 什么时候使用异步,什么时候同步? (比如我们的缓存系统) 2.Solr集群的搭建 3.请你谈谈对MQ的理解?以及你们在项目中是怎么用的? 消息队列 静态页面—库存—实时性较差 4.请你谈谈对Redis的认识? 5.redis空间不够,怎么保证经常访问的数据? 6.redis应用场景场景: 7.请你谈谈对Spring的认识? Spring是一个开放源代码的设计层面框架,解决业务逻辑层和其他各层的松耦合问题 AOP是面向切面的编程,将程序中的交叉业务逻辑,封装成一个切面,然后注入目标对象;是一种编程思想,将系统中非核心的业务提取出来单独处理 8.请你谈谈单点登录的实现方案?你们怎么包括cookie的安全性?跨域取cookie的问题,你们怎么解决的? 9.请你谈谈购物车的实现方案?当商品信息发生变更,购物车中的商品信息是否可以同步到变化? 10.如何应对高并发问题? 11.Zookeeper应用场景 /provider:存放服务地址 3.反向代理,负载均衡 13.RabbitMq应用场景 14.谈谈你对ThreadLocal的理解,以及他的作用 15.Erueka和ZooKeeper的区别 16.池化技术 网络编程相关 InputStream input = ... ; // get the InputStream from the client socket String nameLine = reader.readLine(); 而一个NIO的实现会有所不同,下面是一个简单的例子: ByteBuffer buffer = ByteBuffer.allocate(48); } 四、总结 Java NIO: 单线程管理多个连接 Java IO: 一个典型的IO服务器设计- 一个连接通过一个线程处理. 以下是本文的目录大纲: 一.什么是同步?什么是异步? 如果还不理解,可以先看下面这2段代码: void fun2() { void function(){ 接着看下面这段代码: void fun2() { void function(){ new Thread(){ 事实上,同步和异步是一个非常广的概念,它们的重点在于多个任务和事件发生时,一个事件的发生或执行是否会导致整个流程的暂时等待。我觉得可以将同步和异步与Java中的synchronized关键字联系起来进行类比。当多个线程同时访问一个变量时,每个线程访问该变量就是一个事件,对于同步来说,就是这些线程必须逐个地来访问该变量,一个线程在访问该变量的过程中,其他线程必须等待;而对于异步来说,就是多个线程不必逐个地访问该变量,可以同时进行访问。 因此,个人觉得同步和异步可以表现在很多方面,但是记住其关键在于多个任务和事件发生时,一个事件的发生或执行是否会导致整个流程的暂时等待。一般来说,可以通过多线程的方式来实现异步,但是千万记住不要将多线程和异步画上等号,异步只是宏观上的一个模式,采用多线程来实现异步只是一种手段,并且通过多进程的方式也可以实现异步。 二.什么是阻塞?什么是非阻塞? 阻塞就是:当某个事件或者任务在执行过程中,它发出一个请求操作,但是由于该请求操作需要的条件不满足,那么就会一直在那等待,直至条件满足; 非阻塞就是:当某个事件或者任务在执行过程中,它发出一个请求操作,如果该请求操作需要的条件不满足,会立即返回一个标志信息告知条件不满足,不会一直在那等待。 这就是阻塞和非阻塞的区别。也就是说阻塞和非阻塞的区别关键在于当发出请求一个操作时,如果条件不满足,是会一直等待还是返回一个标志信息。 举个简单的例子: 假如我要读取一个文件中的内容,如果此时文件中没有内容可读,对于同步来说就是会一直在那等待,直至文件中有内容可读;而对于非阻塞来说,就会直接返回一个标志信息告知文件中暂时无内容可读。 在网上有一些朋友将同步和异步分别与阻塞和非阻塞画上等号,事实上,它们是两组完全不同的概念。注意,理解这两组概念的区别对于后面IO模型的理解非常重要。 同步和异步着重点在于多个任务的执行过程中,一个任务的执行是否会导致整个流程的暂时等待;而阻塞和非阻塞着重点在于发出一个请求操作时,如果进行操作的条件不满足是否会返会一个标志信息告知条件不满足。理解阻塞和非阻塞可以同线程阻塞类比地理解,当一个线程进行一个请求操作时,如果条件不满足,则会被阻塞,即在那等待条件满足。 三.什么是阻塞IO?什么是非阻塞IO? 通常来说,IO操作包括:对硬盘的读写、对socket的读写以及外设的读写。 当用户线程发起一个IO请求操作(本文以读请求操作为例),内核会去查看要读取的数据是否就绪,对于阻塞IO来说,如果数据没有就绪,则会一直在那等待,直到数据就绪;对于非阻塞IO来说,如果数据没有就绪,则会返回一个标志信息告知用户线程当前要读的数据没有就绪。当数据就绪之后,便将数据拷贝到用户线程,这样才完成了一个完整的IO读请求操作,也就是说一个完整的IO读请求操作包括两个阶段: Java中传统的IO都是阻塞IO,比如通过socket来读数据,调用read()方法之后,如果数据没有就绪,当前线程就会一直阻塞在read方法调用那里,直到有数据才返回;而如果是非阻塞IO的话,当数据没有就绪,read()方法应该返回一个标志信息,告知当前线程数据没有就绪,而不是一直在那里等待。 四.什么是同步IO?什么是异步IO? 从字面的意思可以看出:同步IO即 如果一个线程请求进行IO操作,在IO操作完成之前,该线程会被阻塞;而异步IO为 如果一个线程请求进行IO操作,IO操作不会导致请求线程被阻塞。 事实上,同步IO和异步IO模型是针对用户线程和内核的交互来说的: 对于同步IO:当用户发出IO请求操作之后,如果数据没有就绪,需要通过用户线程或者内核不断地去轮询数据是否就绪,当数据就绪时,再将数据从内核拷贝到用户线程; 而异步IO:只有IO请求操作的发出是由用户线程来进行的,IO操作的两个阶段都是由内核自动完成,然后发送通知告知用户线程IO操作已经完成。也就是说在异步IO中,不会对用户线程产生任何阻塞。 这是同步IO和异步IO关键区别所在,同步IO和异步IO的关键区别反映在数据拷贝阶段是由用户线程完成还是内核完成。所以说异步IO必须要有操作系统的底层支持。 注意同步IO和异步IO与阻塞IO和非阻塞IO是不同的两组概念。 阻塞IO和非阻塞IO是反映在当用户请求IO操作时,如果数据没有就绪,是用户线程一直等待数据就绪,还是会收到一个标志信息这一点上面的。也就是说,阻塞IO和非阻塞IO是反映在IO操作的第一个阶段,在查看数据是否就绪时是如何处理的。 五.五种IO模型 下面就分别来介绍一下这5种IO模型的异同。 1.阻塞IO模型 当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。 典型的阻塞IO模型的例子为: 2.非阻塞IO模型 所以事实上,在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。 典型的非阻塞IO模型一般如下: 3.多路复用IO模型 在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。 在Java NIO中,是通过selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。 也许有朋友会说,我可以采用 多线程+ 阻塞IO 达到类似的效果,但是由于在多线程 + 阻塞IO 中,每个socket对应一个线程,这样会造成很大的资源占用,并且尤其是对于长连接来说,线程的资源一直不会释放,如果后面陆续有很多连接的话,就会造成性能上的瓶颈。 而多路复用IO模式,通过一个线程就可以管理多个socket,只有当socket真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用IO比较适合连接数比较多的情况。 另外多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断地询问socket状态时通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多。 不过要注意的是,多路复用IO模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用IO模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。 4.信号驱动IO模型 5.异步IO模型 也就说在异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用IO函数进行具体的读写。这点是和信号驱动模型有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用IO函数进行实际的读写操作;而在异步IO模型中,收到信号表示IO操作已经完成,不需要再在用户线程中调用iO函数进行实际的读写操作。 注意,异步IO是需要操作系统的底层支持,在Java 7中,提供了Asynchronous IO。 前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞。 六.两种高性能IO设计模式 对于多线程模式,也就说来了client,服务器就会新建一个线程来处理该client的读写事件,如下图所示: 这种模式虽然处理起来简单方便,但是由于服务器为每个client的连接都采用一个线程去处理,使得资源占用非常大。因此,当连接数量达到上限时,再有用户请求连接,直接会导致资源瓶颈,严重的可能会直接导致服务器崩溃。 因此,为了解决这种一个线程对应一个客户端模式带来的问题,提出了采用线程池的方式,也就说创建一个固定大小的线程池,来一个客户端,就从线程池取一个空闲线程来处理,当客户端处理完读写操作之后,就交出对线程的占用。因此这样就避免为每一个客户端都要创建线程带来的资源浪费,使得线程可以重用。 但是线程池也有它的弊端,如果连接大多是长连接,因此可能会导致在一段时间内,线程池中的线程都被占用,那么当再有用户请求连接时,由于没有可用的空闲线程来处理,就会导致客户端连接失败,从而影响用户体验。因此,线程池比较适合大量的短连接应用。 因此便出现了下面的两种高性能IO设计模式:Reactor和Proactor。 在Reactor模式中,会先对每个client注册感兴趣的事件,然后有一个线程专门去轮询每个client是否有事件发生,当有事件发生时,便顺序处理每个事件,当所有事件处理完之后,便再转去继续轮询,如下图所示: 从这里可以看出,上面的五种IO模型中的多路复用IO就是采用Reactor模式。注意,上面的图中展示的 是顺序处理每个事件,当然为了提高事件处理速度,可以通过多线程或者线程池的方式来处理事件。 在Proactor模式中,当检测到有事件发生时,会新起一个异步操作,然后交由内核线程去处理,当内核线程完成IO操作之后,发送一个通知告知操作已完成,可以得知,异步IO模型采用的就是Proactor模式。
Iterator it = arr.iterator();
while(it.hasNext()){ object o =it.next(); ...}
将Map中所有的键存入到set集合中。因为set具备迭代器。所有可以迭代方式取出所有的键,再根据get方法。获取每一个键对应的值。 keySet():迭代后只能通过get()取key 。
取到的结果会乱序,是因为取得数据行主键的时候,使用了HashMap.keySet()方法,而这个方法返回的Set结果,里面的数据是乱序排放的。
典型用法如下:
Map map = new HashMap();
map.put("key1","lisi1");
map.put("key2","lisi2");
map.put("key3","lisi3");
map.put("key4","lisi4");
//先获取map集合的所有键的set集合,keyset()
Iterator it = map.keySet().iterator();
//获取迭代器
while(it.hasNext()){
Object key = it.next();
System.out.println(map.get(key));
}
Set
典型用法如下:
Map map = new HashMap();
map.put("key1","lisi1");
map.put("key2","lisi2");
map.put("key3","lisi3");
map.put("key4","lisi4");
//将map集合中的映射关系取出,存入到set集合
Iterator it = map.entrySet().iterator();
while(it.hasNext()){
Entry e =(Entry) it.next();
System.out.println("键"+e.getKey () + "的值为" + e.getValue());
}
推荐使用第二种方式,即entrySet()方法,效率较高。
对于keySet其实是遍历了2次,一次是转为iterator,一次就是从HashMap中取出key所对于的value。而entryset只是遍历了第一次,它把key和value都放到了entry中,所以快了。两种遍历的遍历时间相差还是很明显的。
1,vector是线程同步的,所以它也是线程安全的,而arraylist是线程异步的,是不安全的。如果不考虑到线程的安全因素,一般用arraylist效率比较高。
2,如果集合中的元素的数目大于目前集合数组的长度时,vector增长率为目前数组长度的100%,而arraylist增长率为目前数组长度的50%。如果在集合中使用数据量比较大的数据,用vector有一定的优势。
3,如果查找一个指定位置的数据,vector和arraylist使用的时间是相同的,如果频繁的访问数据,这个时候使用vector和arraylist都可以。而如果移动一个指定位置会导致后面的元素都发生移动,这个时候就应该考虑到使用linklist,因为它移动一个指定位置的数据时其它元素不移动。
ArrayList 和Vector是采用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,都允许直接序号索引元素,但是插入数据要涉及到数组元素移动等内存操作,所以索引数据快,插入数据慢,Vector由于使用了synchronized方法(线程安全)所以性能上比ArrayList要差,LinkedList使用双向链表实现存储,按序号索引数据需要进行向前或向后遍历,但是插入数据时只需要记录本项的前后项即可,所以插入数度较快。
1.ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
2.对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
3.对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。 这一点要看实际情况的。若只对单条数据插入或删除,ArrayList的速度反而优于LinkedList。但若是批量随机的插入删除数据,LinkedList的速度大大优于ArrayList. 因为ArrayList每插入一条数据,要移动插入点及之后的所有数据。
1、 HashMap通过hashcode对其内容进行快速查找,而TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的)。
2、在Map 中插入、删除和定位元素,HashMap是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。使用HashMap要求添加的键类明确定义了hashCode()和 equals()的实现。
两个map中的元素一样,但顺序不一样,导致hashCode()不一样。
同样做测试:
在HashMap中,同样的值的map,顺序不同,equals时,false;
而在treeMap中,同样的值的map,顺序不同,equals时,true,说明,treeMap在equals()时是整理了顺序了的。
1、同步性:Hashtable是线程安全的,也就是说是同步的,而HashMap是线程序不安全的,不是同步的。
2、HashMap允许存在一个为null的key,多个为null的value 。
//使用for-each循环
for(String obj : strList){
System.out.println(obj);
}
//using iterator
Iterator
while(it.hasNext()){
String obj = it.next();
System.out.println(obj);
}
使用迭代器更加线程安全,因为它可以确保,在当前遍历的集合元素被更改的时候,它会抛出ConcurrentModificationException。
MyKey key = new MyKey('Pankaj'); //assume hashCode=1234
myHashMap.put(key, 'Value');
// 以下的代码会改变key的hashCode()和equals()值
key.setName('Amit'); //assume new hashCode=7890
//下面会返回null,因为HashMap会尝试查找存储同样索引的key,而key已被改变了,匹配失败,返回null
myHashMap.get(new MyKey('Pankaj'));
那就是为何String和Integer被作为HashMap的key大量使用。>更容易。
类加载器
什么是类加载器
负责读取 Java 字节代码,并转换成java.lang.Class类的一个实例;
类加载器与类的”相同“判断
类加载器除了用于加载类外,还可用于确定类在Java虚拟机中的唯一性。
即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。
通俗一点来讲,要判断两个类是否“相同”,前提是这两个类必须被同一个类加载器加载,否则这个两个类不“相同”。
这里指的“相同”,包括类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法、instanceof关键字等判断出来的结果。
类加载器种类
启动类加载器,Bootstrap ClassLoader,加载JACA_HOMElib,或者被-Xbootclasspath参数限定的类
扩展类加载器,Extension ClassLoader,加载libext,或者被java.ext.dirs系统变量指定的类
应用程序类加载器,Application ClassLoader,加载ClassPath中的类库
自定义类加载器,通过继承ClassLoader实现,一般是加载我们的自定义类
双亲委派模型
类加载器 Java 类如同其它的 Java 类一样,也是要由类加载器来加载的;除了启动类加载器,每个类都有其父类加载器(父子关系由组合(不是继承)来实现);
所谓双亲委派是指每次收到类加载请求时,先将请求委派给父类加载器完成(所有加载请求最终会委派到顶层的Bootstrap ClassLoader加载器中),如果父类加载器无法完成这个加载(该加载器的搜索范围中没有找到对应的类),子类尝试自己加载。
避免同一个类被多次加载;
每个加载器只能加载自己范围内的类;
类加载过程
类加载分为三个步骤:加载,连接,初始化;
如下图 , 是一个类从加载到使用及卸载的全部生命周期,图片来自参考资料;
根据一个类的全限定名(如cn.edu.hdu.test.HelloWorld.class)来读取此类的二进制字节流到JVM内部;
将字节流所代表的静态存储结构转换为方法区的运行时数据结构(hotspot选择将Class对象存储在方法区中,Java虚拟机规范并没有明确要求一定要存储在方法区或堆区中)
转换为一个与目标类型对应的java.lang.Class对象;
连接
验证
验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证;
准备
为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量将不再此操作范围内);
解析
将常量池中所有的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法)。这个阶段可以在初始化之后再执行。
初始化
在连接的准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员自己写的逻辑去初始化类变量和其他资源,举个例子如下:public static int value1 = 5;
public static int value2 = 6;
static{
value2 = 66;
}
在初始化阶段value1和value2分别等于5和66;
所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是
编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量;
如果超类还没有被初始化,那么优先对超类初始化,但在
JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。(所以可以利用静态内部类实现线程安全的单例模式)
如果一个类没有声明任何的类变量,也没有静态代码块,那么可以没有类
何时触发初始化
1.为一个类型创建一个新的对象实例时(比如new、反射、序列化)
2.调用一个类型的静态方法时(即在字节码中执行invokestatic指令)
3.调用一个类型或接口的静态字段,或者对这些静态字段执行赋值操作时(即在字节码中,执行getstatic或者putstatic指令),不过用final修饰的静态字段除外,它被初始化为一个编译时常量表达式
4.调用JavaAPI中的反射方法时(比如调用java.lang.Class中的方法,或者java.lang.reflect包中其他类的方法)
5.初始化一个类的派生类时(Java虚拟机规范明确要求初始化一个类时,它的超类必须提前完成初始化操作,接口例外)
6.JVM启动包含main方法的启动类时。
自定义类加载器
要创建用户自己的类加载器,只需要继承java.lang.ClassLoader类,然后覆盖它的findClass(String name)方法即可,即指明如何获取类的字节码流。
如果要符合双亲委派规范,则重写findClass方法(用户自定义类加载逻辑);要破坏的话,重写loadClass方法(双亲委派的具体逻辑实现)。
例子:
import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.io.InputStream;
class TestClassLoad {@Override
public String toString() {
return "类加载成功。";
}
private String classPath;
public PathClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
byte[] classData = getData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getData(String className) {
String path = classPath + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
try {
InputStream is = new FileInputStream(path);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int num = 0;
while ((num = is.read(buffer)) != -1) {
stream.write(buffer, 0, num);
}
return stream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static void main(String args[]) throws ClassNotFoundException,
InstantiationException, IllegalAccessException {
ClassLoader pcl = new PathClassLoader("D:\\ProgramFiles\\eclipseNew\\workspace\\cp-lib\\bin");
Class c = pcl.loadClass("classloader.TestClassLoad");//注意要包括包名
System.out.println(c.newInstance());//打印类加载成功. }
JAVA热部署实现
首先谈一下何为热部署(hotswap),热部署是在不重启 Java 虚拟机的前提下,能自动侦测到 class 文件的变化,更新运行时 class 的行为。Java 类是通过 Java 虚拟机加载的,某个类的 class 文件在被 classloader 加载后,会生成对应的 Class 对象,之后就可以创建该类的实例。默认的虚拟机行为只会在启动时加载类,如果后期有一个类需要更新的话,单纯替换编译的 class 文件,Java 虚拟机是不会更新正在运行的 class。如果要实现热部署,最根本的方式是修改虚拟机的源代码,改变 classloader 的加载行为,使虚拟机能监听 class 文件的更新,重新加载 class 文件,这样的行为破坏性很大,为后续的 JVM 升级埋下了一个大坑。
另一种友好的方法是创建自己的 classloader 来加载需要监听的 class,这样就能控制类加载的时机,从而实现热部署。
热部署步骤:
1、销毁自定义classloader(被该加载器加载的class也会自动卸载);
2、更新class
3、使用新的ClassLoader去加载class
JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):
- 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。
- 加载该类的ClassLoader已经被GC。
- 该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法
延伸出来问题进行分析:
看到这个题目,很多人会觉得我写我的java代码,至于类,JVM爱怎么加载就怎么加载,博主有很长一段时间也是这么认为的。随着编程经验的日积月累,越来越感觉到了解虚拟机相关要领的重要性。闲话不多说,老规矩,先来一段代码吊吊胃口。
{static
{
System.out.println("SSClass");
}
{static
{
System.out.println("SuperClass init!");
}
public static int value = 123;
public SuperClass()
{
System.out.println("init SuperClass");
}
{static
{
System.out.println("SubClass init");
}
static int a;
public SubClass()
{
System.out.println("init SubClass");
}
{public static void main(String[] args)
{
System.out.println(SubClass.value);
}
运行结果:
SSClass
SuperClass init!
123
答案答对了嚒?
也许有人会疑问:为什么没有输出SubClass init。ok~解释一下:对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
上面就牵涉到了虚拟机类加载机制。如果有兴趣,可以继续看下去。
类加载过程
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为连接(Linking)。如图所示。
加载
在加载阶段(可以参考java.lang.ClassLoader的loadClass()方法),虚拟机需要完成以下3件事情:
1.通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等);
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;
加载阶段和连接阶段(Linking)的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。
验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致会完成4个阶段的检验动作:
1.文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以魔术0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
2.元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
3.字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
4.符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:
public static int value=123;
那变量value在准备阶段过后的初始值为0而不是123.因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。
至于“特殊情况”是指:public static final int value=123,即当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为123而非0.
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
初始化
类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的java程序代码。在准备极端,变量已经付过一次系统要求的初始值,而在初始化阶段,则根据程序猿通过程序制定的主管计划去初始化类变量和其他资源,或者说:初始化阶段是执行类构造器
{static
{
i=0;
System.out.println(i);//这句编译器会报错:Cannot reference a field before it is defined(非法向前应用) }
static int i=1;
{static
{
i=0;// System.out.println(i); }
static int i=1;
public static void main(String args[])
{
System.out.println(i);
}
()方法与实例构造器
由于父类的
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成
虚拟机会保证一个类的
public class DealLoopTest
{static class DeadLoopClass
{
static
{
if(true)
{
System.out.println(Thread.currentThread()+"init DeadLoopClass");
while(true)
{
}
}
}
}
public static void main(String[] args)
{
Runnable script = new Runnable(){
public void run()
{
System.out.println(Thread.currentThread()+" start");
DeadLoopClass dlc = new DeadLoopClass();
System.out.println(Thread.currentThread()+" run over");
}
};
Thread thread1 = new Thread(script);
Thread thread2 = new Thread(script);
thread1.start();
thread2.start();
}
运行结果:(即一条线程在死循环以模拟长时间操作,另一条线程在阻塞等待)
Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-0,5,main]init DeadLoopClass
需要注意的是,其他线程虽然会被阻塞,但如果执行()方法的那条线程退出()方法后,其他线程唤醒之后不会再次进入()方法。同一个类加载器下,一个类型只会初始化一次。
将上面代码中的静态块替换如下:
{System.out.println(Thread.currentThread() + "init DeadLoopClass");
try
{
TimeUnit.SECONDS.sleep(10);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-1,5,main]init DeadLoopClass (之后sleep 10s)
Thread[Thread-1,5,main] run over
Thread[Thread-0,5,main] run over
虚拟机规范严格规定了有且只有5中情况(jdk1.7)必须对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
1.遇到new,getstatic,putstatic,invokestatic这失调字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
5.当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。
开篇已经举了一个范例:通过子类引用付了的静态字段,不会导致子类初始化。
这里再举两个例子。
1.通过数组定义来引用类,不会触发此类的初始化:(SuperClass类已在本文开篇定义)
2.
public class NotInitialization
{public static void main(String[] args)
{
SuperClass[] sca = new SuperClass[10];
}
4.
6.
运行结果:(无)
1.常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化:
public class ConstClass
{static
{
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";
{public static void main(String[] args)
{
System.out.println(ConstClass.HELLOWORLD);
}
3.
5.
运行结果:
hello world
参数
运行时常量池
程序计数器
引用计数法
根搜索算法GC root 引用链相连
GC root 对象有哪些?
1、虚拟机栈中的引用的对象
2、方法区中静态属性引用的对象
3、方法区中常量引用的对象
4、本地方法栈中引用的对象
垃圾回收算法有哪些?
标记清除算法
复制算法
标记整理算法
分代收集算法
垃圾收集器有哪些?
1、Serial收集器
单线程收集器,进行垃圾收集时,必须停止其他工作线程,stop the world
2、Parnew收集器
多线程收集器,进行垃圾回收时,多线程并行进行垃圾回收,回收时,其他工作线程停止。
3、parallel savage收集器
多线程收集器,采用复制算法进行收集
4、serial old 收集器
单线程,标记-整理算法
5、parallel old
多线程,标记整理算法
6、cms收集器
目标:获取最短的回收停顿时间为目标
标记-清除算法
多线程,不会导致用户线程停顿,对cpu个数有要求
步骤:1初始标记,2、并发标记3、重新标记4、并发清除
优缺点:并发手机,低停顿,对cpu资源非常敏感,无法处理浮动垃圾。会产生大量的空间碎片。
G1收集器
标记整理法
虚拟机性能监控与故障处理分析工具
Jps
Jstat
Jinfo
Jmap
Jstack
VisualVM
什么是IO流?
它是一种数据的流从源头流到目的地。比如文件拷贝,输入流和输出流都包括了。输入流从文件中读取数据存储到进程(process)中,输出流从进程中读取数据然后写入到目标文件。
.
.
字节流和字符流的区别。
字节流在JDK1.0中就被引进了,用于操作包含ASCII字符的文件。JAVA也支持其他的字符如Unicode,为了读取包含Unicode字符的文件,JAVA语言设计者在JDK1.1中引入了字符流。ASCII作为Unicode的子集,对于英语字符的文件,可以可以使用字节流也可以使用字符流。
.
.
Java中流类的超类主要由那些?
.
java.io.InputStream
java.io.OutputStream
java.io.Reader
java.io.Writer
.FileInputStream和FileOutputStream是什么?
这是在拷贝文件操作的时候,经常用到的两个类。在处理小文件的时候,它们性能表现还不错,在大文件的时候,最好使用BufferedInputStream (或 BufferedReader) 和 BufferedOutputStream (或 BufferedWriter)
案例:
public class InputAndOutputBuffering
{
public static void main(String args[]) throws IOException
{FileInputStream fistream = new FileInputStream("pqr.txt"); BufferedInputStream bistream = new BufferedInputStream(fistream);
FileOutputStream fostream = new FileOutputStream("xyz.txt");
BufferedOutputStream bostream = new BufferedOutputStream(fostream);
int temp;
while( ( temp = bistream.read() ) != -1 )
{
bostream.write(temp);
System.out.print((char) temp);
}
bostream.close(); fostream.close();
bistream.close(); fistream.close();
}
.
字节流和字符流,你更喜欢使用拿一个?
个人来说,更喜欢使用字符流,因为他们更新一些。许多在字符流中存在的特性,字节流中不存在。比如使用BufferedReader而不是BufferedInputStreams或DataInputStream,使用newLine()方法来读取下一行,但是在字节流中我们需要做额外的操作。
.
.
System.out.println()是什么?
println是PrintStream的一个方法。out是一个静态PrintStream类型的成员变量,System是一个java.lang包中的类,用于和底层的操作系统进行交互。
.
.
什么是Filter流?
Filter Stream是一种IO流主要作用是用来对存在的流增加一些额外的功能,像给目标文件增加源文件中不存在的行数,或者增加拷贝的性能。
.
.有哪些可用的Filter流?
在java.io包中主要由4个可用的filter Stream。两个字节filter stream,两个字符filter stream. 分别是FilterInputStream, FilterOutputStream, FilterReader and FilterWriter.这些类是抽象类,不能被实例化的。
有些Filter流的子类。
LineNumberInputStream 给目标文件增加行号
- DataInputStream 有些特殊的方法如readInt(), readDouble()和readLine() 等可以读取一个 int, double和一个string一次性的,
BufferedInputStream 增加性能
PushbackInputStream 推送要求的字节到系统中
.SequenceInputStream的作用?
在拷贝多个文件到一个目标文件的时候是非常有用的。可用使用很少的代码实现
案例:
public class TwoFiles {public static void main(String args[]) throws IOException
{
FileInputStream fistream1 = new FileInputStream("/Users/aihe/Desktop/Songshu/code/java8source/src/main/resources/A.txt"); // first source file
FileInputStream fistream2 = new FileInputStream("/Users/aihe/Desktop/Songshu/code/java8source/src/main/resources/B.txt"); //second source file
SequenceInputStream sistream = new SequenceInputStream(fistream1, fistream2);
FileOutputStream fostream = new FileOutputStream("C.txt"); // destination file
int temp;
while( ( temp = sistream.read() ) != -1)
{
System.out.print( (char) temp ); // to print at DOS prompt
fostream.write(temp); // to write to file
}
fostream.close();
sistream.close();
fistream1.close();
fistream2.close();
}
.
说说PrintStream和PrintWriter
他们两个的功能相同,但是属于不同的分类。字节流和字符流。他们都有println()方法。
.
.
在文件拷贝的时候,那一种流可用提升更多的性能?
在字节流的时候,使用BufferedInputStream和BufferedOutputStream。
在字符流的时候,使用BufferedReader 和 BufferedWriter
.
.
说说管道流(Piped Stream)
有四种管道流, PipedInputStream, PipedOutputStream, PipedReader 和 PipedWriter.在多个线程或进程中传递数据的时候管道流非常有用。
.
.
说说File类
它不属于 IO流,也不是用于文件操作的,它主要用于知道一个文件的属性,读写权限,大小等信息。
.
.说说RandomAccessFile?
它在java.io包中是一个特殊的类,既不是输入流也不是输出流,它两者都可以做到。他是Object的直接子类。通常来说,一个流只有一个功能,要么读,要么写。但是RandomAccessFile既可以读文件,也可以写文件。 DataInputStream 和 DataOutStream有的方法,在RandomAccessFile中都存在。
Byte是计算机操作数据的最小单位由8位bit组成 取值(-128-127)
Char是用户的可读写的最小单位,在java里面由16位bit组成 取值(0-65535)
Bit 是最小单位 计算机 只能认识 0或者1
8个字节 是给计算机看的
字符 是看到的东西 一个字符=二个字节
2.什么是流,按照传输的单位,分成哪两种流,并且他们的父类叫什么流是指数据的传输
答案
字节流,字符流
字节流:InputStream OutputStream
字符流:Reader Writer
3.流按照传输的方向可以分为哪两种,分别举例说明
答案
输入输出相对于程序
输入流InputStream
,输出流OutputStream
4.按照实现功能分为哪两种,分别举例说明
答案
节点流,处理流
节点流:OutputStream
处理流: OutputStreamWriter
5.BufferedReader属于哪种流,它主要是用来做什么的,它里面有那些经典的方法
答案
属于处理流中的缓冲流,可以将读取的内容存在内存里面,有readLine()方法
6.什么是节点流,什么是处理流,它们各有什么用处,处理流的创建有什么特征
答案
节点流 直接与数据源相连,用于输入或者输出
处理流:在节点流的基础上对之进行加工,进行一些功能的扩展
处理流的构造器必须要 传入节点流的子类
7.如果我要对字节流进行大量的从硬盘读取,要用那个流,为什么
答案
BufferedInputStream 使用缓冲流能够减少对硬盘的损伤
8.如果我要打印出不同类型的数据到数据源,那么最适合的流是那个流,为什么
答案
Printwriter 可以打印各种数据类型
9.怎么样把我们控制台的输出改成输出到一个文件里面,这个技术叫什么
答案
SetOut(printWriter,printStream)重定向
11.怎么样把输出字节流转换成输出字符流,说出它的步骤
答案
使用 转换处理流OutputStreamWriter 可以将字符流转为字节流
New OutputStreamWriter(new FileOutputStream(File file));
12.把包括基本类型在内的数据和字符串按顺序输出到数据源,或者按照顺序从数据源读入,一般用哪两个流
答案
DataInputStream DataOutputStream
13.把一个对象写入数据源或者从一个数据源读出来,用哪两个流
答案
ObjectInputStream ObjectOutputStream
14.什么叫对象序列化,什么是反序列化,实现对象序列化需要做哪些工作
答案
对象序列化,将对象以二进制的形式保存在硬盘上
反序列化;将二进制的文件转化为对象读取
实现serializable接口
不想让字段放在硬盘上就加transient
15.如果在对象序列化的时候不想给一个字段的数据保存在硬盘上面,采用那个关键字?
答案
transient关键字
16.在实现序列化接口是时候一般要生成一个serialVersionUID字段,它叫做什么,一般有什么用
答案
是版本号,要保持版本号的一致 来进行序列化
为了防止序列化出错
17.InputStream里的read()返回的是什么,read(byte[] data)是什么意思,返回的是什么值
答案
返回的是所读取的字节的int型(范围0-255)
read(byte [ ] data)将读取的字节储存在这个数组
返回的就是传入数组参数个数
Read 字节读取字节 字符读取字符
18.OutputStream里面的write()是什么意思,write(byte b[], int off, int len)这个方法里面的三个参数分别是什么意思
答案
write将指定字节传入数据源
Byte b[ ]是byte数组
b[off]是传入的第一个字符
b[off+len-1]是传入的最后的一个字符
len是实际长度
19.流一般需要不需要关闭,如果关闭的话在用什么方法,一般要在那个代码块里面关闭比较好,处理流是怎么关闭的,如果有多个流互相调用传入是怎么关闭的?
答案
流一旦打开就必须关闭,使用close方法
放入finally语句块中(finally 语句一定会执行)
调用的处理流就关闭处理流
多个流互相调用只关闭最外层的流
20.Java中的所有的流可以分为几大类,它们的名字是什么,各代表什么
答案
分为 字节输入流 InputStream
字节输出流 OutputStream
字符输入流 Reader
字符输出流 Writer
所有流都是这四个流的子类
Icon
InputStream,OutputStream,
FileInputStream,FileOutputStream,
BufferedInputStream,BufferedOutputStream
Reader,Writer
BufferedReader,BufferedWriter
Icon
A、InputStream B、FileReader C、DataInputStream D、ObjectStream
Icon
使用File对象获取文件路径,通过字符流Reader加入文件,使用字符缓存流BufferedReader处理Reader,再定义一个字符串,循环遍历出文件。代码如下:
File file = new File("d:/spring.txt");
try {
Reader reader = new FileReader(file);
BufferedReader buffered = new BufferedReader(reader);
String data = null;
while((data = buffered.readLine())!=null){
System.out.println(data);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
Icon
Io流主要是用来处理输入输出问题,常用的io流有InputStream,OutputStream,Reader,Writer等
Icon
Java的io流用来处理输入输出问题,readLine是BufferedReader里的一个方法,用来读取一行。
Icon
ObjectInputStream,需要实现Serializable接口
26 FileInputStream 创建详情,就是怎样的创建不报错,它列出了几种形式!
Icon
FileInputStream是InputStream的子类,通过接口定义,子类实现创建FileInputStream,
Icon
把一个对象写入数据源或者从一个数据源读出来,使用可序列化,需要实现Serializable接口
字符编码
字符编码和字符集是两个基础性的概念,很多开发人员对其都并不陌生,但是很少有人能将其讲得很准确。当应用出现乱码时,如何分析和定位原因,很多人仍是一头雾水。这篇文章,将从字符编码和字符集的相关概念开始讲解,然后结合Java进行实例分析。
字符集(character set)是一个系统支持的所有抽象字符的集合。字符(character)就是各种文字和符号,包括国家文字、标点符号、图形符号、数字等。
ASCII:早期的计算机系统只能处理英文,所以ASCII也就成为了计算机的缺省字符集,包含了英文所需要的所有字符。
GB2312:中文字符集,包含ASCII字符集。ASCII部分用单字节表示,剩余部分用双字节表示。
GBK:GB2312的扩展,完整包含了GB2312的所有内容。
GB18030:GBK字符集的超集,常叫大汉字字符集,也叫CJK(Chinese,Japanese,Korea)字符集,包含了中、日、韩三国语言中的所有字符。
字符编码(character encoding),是编码字符集的字符和实际的存储值之间的转换关系。常见的编码方式有:UTF-8(Unicode字符集的编码方式)、UTF-16(Unicode字符集的编码方式)、UTF-32(Unicode字符集的编码方式)、ASCII(ASCII字符集的编码方式)等。
Java中char类型是16位无符号基本数据类型,用来存储Unicode字符。字符数据类型的范围为0到65535,可以存储65536个不同的Unicode字符,这在起初Unicode字符集不是很大的时候,是没问题的。然而随着Unicode字符集的增长,已经超过65536个了,根据Unicode标准,现在Unicode代码点的合法范围是U+0000到U+10FFFF,U+0000到U+FFFF称为Basic Multilingual Plane(BMP),代码点大于U+FFFF的字符称为增补字符。
1、error和exception有什么区别
error表示系统级的错误,是java运行环境内部错误或者硬件问题,不能指望程序来处理这样的问题,除了退出运行外别无选择,它是Java虚拟机抛出的。
exception 表示程序需要捕捉、需要处理的异常,是由与程序设计的不完善而出现的问题,程序必须处理的问题
2、运行时异常和一般异常有何不同
Java提供了两类主要的异常:runtimeException和checkedException
一般异常(checkedException)主要是指IO异常、SQL异常等。对于这种异常,JVM要求我们必须对其进行cathc处理,所以,面对这种异常,不管我们是否愿
意,都是要写一大堆的catch块去处理可能出现的异常。
运行时异常(runtimeException)我们一般不处理,当出现这类异常的时候程序会由虚拟机接管。比如,我们从来没有去处理过NullPointerException,而且
这个异常还是最常见的异常之一。
出现运行时异常的时候,程序会将异常一直向上抛,一直抛到遇到处理代码,如果没有catch块进行处理,到了最上层,如果是多线程就有Thread.run()抛出,如
果不是多线程那么就由main.run()抛出。抛出之后,如果是线程,那么该线程也就终止了,如果是主程序,那么该程序也就终止了。
其实运行时异常的也是继承自Exception,也可以用catch块对其处理,只是我们一般不处理罢了,也就是说,如果不对运行时异常进行catch处理,那么结果不
是线程退出就是主程序终止。
如果不想终止,那么我们就必须捕获所有可能出现的运行时异常。如果程序中出现了异常数据,但是它不影响下面的程序执行,那么我们就该在catch块里面将异
常数据舍弃,然后记录日志。如果,它影响到了下面的程序运行,那么还是程序退出比较好些。
3、Java中异常处理机制的原理
Java通过面向对象的方式对异常进行处理,Java把异常按照不同的类型进行分类,并提供了良好的接口。在Java中,每个异常都是一个对象,它都是Throwable
或其子类的实例。当一个方法出现异常后就会抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并对异常进行处理。Java的
异常处理是通过5个关键词来实现的:try catch throw throws finally。
一般情况下是用try来执行一段程序,如果出现异常,系统会抛出(throws),我们可以通过它的类型来捕捉它,或最后由缺省处理器来处理它(finally)。
try:用来指定一块预防所有异常的程序
catch:紧跟在try后面,用来捕获异常
throw:用来明确的抛出一个异常
throws:用来标明一个成员函数可能抛出的各种异常
finally:确保一段代码无论发生什么异常都会被执行的一段代码。
4、你平时在项目中是怎样对异常进行处理的。
(1)尽量避免出现runtimeException 。例如对于可能出现空指针的代码,带使用对象之前一定要判断一下该对象是否为空,必要的时候对runtimeException
也进行try catch处理。
(2)进行try catch处理的时候要在catch代码块中对异常信息进行记录,通过调用异常类的相关方法获取到异常的相关信息,返回到web端,不仅要给用户良好
的用户体验,也要能帮助程序员良好的定位异常出现的位置及原因。例如,以前做的一个项目,程序遇到异常页面会显示一个图片告诉用户哪些操作导致程序出现
了什么异常,同时图片上有一个按钮用来点击展示异常的详细信息给程序员看的。
5、final、finally、finalize的区别
(1)、final用于声明变量、方法和类的,分别表示变量值不可变,方法不可覆盖,类不可以继承
(2)、finally是异常处理中的一个关键字,表示finally{}里面的代码一定要执行
(3)、finalize是Object类的一个方法,在垃圾回收的时候会调用被回收对象的此方法。
6、try()里面有一个return语句,那么后面的finally{}里面的code会不会被执行,什么时候执行,是在return前还是return后?
自己写了个代码测试了一下:
int i=getInt();
System.out.println(i);
private static int getInt() {// TODO Auto-generated method stub
try {
return 0;
} catch (Exception e) {
// TODO Auto-generated catch block e.printStackTrace();
}finally{
return 1;
}
显示输出结果为1,记住就行了,不想去钻这个问题的牛角尖,也没有什么大用处。
1) Java中什么是Exception?
这个问题经常在第一次问有关异常的时候或者是面试菜鸟的时候问。我从来没见过面高级或者资深工程师的
时候有人问这玩意,但是对于菜鸟,是很愿意问这个的。简单来说,异常是Java传达给你的系统和程序错误的方
式。在java中,异常功能是通过实现比如Throwable,Exception,RuntimeException之类的类,然后还有一
些处理异常时候的关键字,比如throw,throws,try,catch,finally之类的。 所有的异常都是通过Throwable
衍生出来的。Throwable把错误进一步划分为 java.lang.Exception 和 java.lang.Error. java.lang.Error 用
来处理系统错误,例如java.lang.StackOverFlowError 或者 Java.lang.OutOfMemoryError 之类的。然后
Exception用来处理程序错误,请求的资源不可用等等。
2) Java中的检查型异常和非检查型异常有什么区别?
这又是一个非常流行的Java异常面试题,会出现在各种层次的Java面试中。检查型异常和非检查型异常的
主要区别在于其处理方式。检查型异常需要使用try, catch和finally关键字在编译期进行处理,否则会出现编译
器会报错。对于非检查型异常则不需要这样做。Java中所有继承自java.lang.Exception类的异常都是检查型
异常,所有继承自RuntimeException的异常都被称为非检查型异常。你也可以查看下一篇文章来了解
更多关于检查型异常和非检查型异常之间的区别。
3) Java中的NullPointerException和ArrayIndexOutOfBoundException之间有什么相同之处?
在Java异常面试中这并不是一个很流行的问题,但会出现在不同层次的初学者面试中,用来测试应聘者对检查
型异常和非检查型异常的概念是否熟悉。顺便说一下,该题的答案是,这两个异常都是非检查型异常,都继承自RuntimeException。该问题可能会引出另一个问题,即Java和C的数组有什么不同之处,因为C里面的数组是没有
大小限制的,绝对不会抛出ArrayIndexOutOfBoundException。
4)在Java异常处理的过程中,你遵循的那些最好的实践是什么?
这个问题在面试技术经理是非常常见的一个问题。因为异常处理在项目设计中是非常关键的,所以精通异常处
理是十分必要的。异常处理有很多最佳实践,下面列举集中,它们提高你代码的健壮性和灵活性:
1) 调用方法的时候返回布尔值来代替返回null,这样可以 NullPointerException。由于空指针是java异常里最恶
心的异常。
2) catch块里别不写代码。空catch块是异常处理里的错误事件,因为它只是捕获了异常,却没有任何处理或者
提示。通常你起码要打印出异常信息,当然你最好根据需求对异常信息进行处理。
3)能抛受控异常(checked Exception)就尽量不抛受非控异常(checked Exception)。通过去掉重复的异常处
理代码,可以提高代码的可读性。
4) 绝对不要让你的数据库相关异常显示到客户端。由于绝大多数数据库和SQLException异常都是受控异常,在Java中,
你应该在DAO层把异常信息处理,然后返回处理过的能让用户看懂并根据异常提示信息改正操作的异常信息。
5) 在Java中,一定要在数据库连接,数据库查询,流处理后,在finally块中调用close()方法。
5) 既然我们可以用RuntimeException来处理错误,那么你认为为什么Java中还存在检查型异常?
这是一个有争议的问题,在回答该问题时你应当小心。虽然他们肯定愿意听到你的观点,但其实他们最感兴
趣的还是有说服力的理由。我认为其中一个理由是,存在检查型异常是一个设计上的决定,受到了诸如C++等比
Java更早的编程语言设计经验的影响。绝大多数检查型异常位于java.io包内,这是合乎情理的,因为在你请求了
不存在的系统资源的时候,一段强壮的程序必须能够优雅的处理这种情况。通过把IOException声明为检查型异
常,Java 确保了你能够优雅的对异常进行处理。另一个可能的理由是,可以使用catch或finally来确保数量受限
的系统资源(比如文件描述符)在你使用后尽早得到释放。
6) throw 和 throws这两个关键字在java中有什么不同?
一个java初学者应该掌握的面试问题。 throw 和 throws乍看起来是很相似的尤其是在你还是一个java初学者的时
候。尽管他们看起来相似,都是在处理异常时候使用到的。但在代码里的使用方法和用到的地方是不同的。throws
总是出现在一个函数头中,用来标明该成员函数可能抛出的各种异常, 你也可以申明未检查的异常,但这不是编译
器强制的。如果方法抛出了异常那么调用这个方法的时候就需要将这个异常处理。另一个关键字 throw 是用来
抛出任意异常的,按照语法你可以抛出任意 Throwable (i.e. Throwable 或任何Throwable的衍生类) , throw
可以中断程序运行,因此可以用来代替return . 最常见的例子是用 throw 在一个空方法中需要return的地方抛出 UnSupportedOperationException 代码如下 :
1 private static voidshow() {
2 throw new UnsupportedOperationException(“Not yet implemented”);
3 }
7) 什么是“异常链”?
“异常链”是Java中非常流行的异常处理概念,是指在进行一个异常处理时抛出了另外一个异常,由此产生
了一个异常链条。该技术大多用于将“ 受检查异常” ( checked exception)封装成为“非受检查异常”
(unchecked exception)或者RuntimeException。顺便说一下,如果因为因为异常你决定抛出一个新的异常,
你一定要包含原有的异常,这样,处理程序才可以通过getCause()和initCause()方法来访问异常最终的根源。
8) 你曾经自定义实现过异常吗?怎么写的?
很显然,我们绝大多数都写过自定义或者业务异常,像AccountNotFoundException。在面试过程中询问
这个Java异常问题的主要原因是去发现你如何使用这个特性的。这可以更准确和精致的去处理异常,当然这也跟
你选择checked 还是unchecked exception息息相关。通过为每一个特定的情况创建一个特定的异常,你就为
调用者更好的处理异常提供了更好的选择。相比通用异常(general exception),我更倾向更为精确的异常。大
量的创建自定义异常会增加项目class的个数,因此,在自定义异常和通用异常之间维持一个平衡是成功的关键。
9) JDK7中对异常处理做了什么改变?
这是最近新出的Java异常处理的面试题。JDK7中对错误(Error)和异常(Exception)处理主要新增加了2个特性,
一是在一个catch块中可以出来多个异常,就像原来用多个catch块一样。另一个是自动化资源管理(ARM), 也称为
try-with-resource块。这2个特性都可以在处理异常时减少代码量,同时提高代码的可读性。对于这些特性了解,
不仅帮助开发者写出更好的异常处理的代码,也让你在面试中显的更突出。我推荐大家读一下Java 7攻略,这样
可以更深入的了解这2个非常有用的特性。
10) 你遇到过 OutOfMemoryError 错误嘛?你是怎么搞定的?
这个面试题会在面试高级程序员的时候用,面试官想知道你是怎么处理这个危险的OutOfMemoryError错误的。
必须承认的是,不管你做什么项目,你都会碰到这个问题。所以你要是说没遇到过,面试官肯定不会买账。要是
你对这个问题不熟悉,甚至就是没碰到过,而你又有3、4年的Java经验了,那么准备好处理这个问题吧。在回答
这个问题的同时,你也可以借机向面试秀一下你处理内存泄露、调优和调试方面的牛逼技能。我发现掌握这些技
术的人都能给面试官留下深刻的印象。
11) 如果执行finally代码块之前方法返回了结果,或者JVM退出了,finally块中的代码还会执行吗?
这个问题也可以换个方式问:“如果在try或者finally的代码块中调用了System.exit(),结果会是怎样”。
了解finally块是怎么执行的,即使是try里面已经使用了return返回结果的情况,对了解Java的异常处理都非常
有价值。只有在try里面是有System.exit(0)来退出JVM的情况下finally块中的代码才不会执行。
12)Java中final,finalize,finally关键字的区别
这是一个经典的Java面试题了。我的一个朋友为Morgan Stanley招电信方面的核心Java开发人员的时候就
问过这个问题。final和finally是Java的关键字,而finalize则是方法。final关键字在创建不可变的类的时候
非常有用,只是声明这个类是final的。而finalize()方法则是垃圾回收器在回收一个对象前调用,但也Java规
范里面没有保证这个方法一定会被调用。finally关键字是唯一一个和这篇文章讨论到的异常处理相关的关键字。
在你的产品代码中,在关闭连接和资源文件的是时候都必须要用到finally块。
13)下面的代码都有哪些错误:
01 public static void start() throws IOException, RuntimeException{
02
03 throw new RuntimeException(“Not able to Start”);
04 }
05
06 public static void main(String args[]) {
07 try {
08 start();
09 } catch (Exception ex) {
10 ex.printStackTrace();
11 } catch (RuntimeException re) {
12 re.printStackTrace();
13 }
14 }
这段代码会在捕捉异常代码块的RuntimeException类型变量“re”里抛出编译异常错误。因为Exception是RuntimeException的超类,在start方法中所有的RuntimeException会被第一个捕捉异常块捕捉,这样就无法到
达第二个捕捉块,这就是抛出“exception java.lang.RuntimeException has already been caught”的编译错误原因。
14)下面的Java代码都有哪些错误:
01 public classSuperClass {
02 public void start() throws IOException{
03 throw new IOException(“Not able to open file”);
04 }
05 }
06
07 public class SubClass extendsSuperClass{
08 public void start() throws Exception{
09 throw new Exception(“Not able to start”);
10 }
11 }
这段代码编译器将对子类覆盖start方法产生不满。因为每个Java中方法的覆盖是有规则的,一个覆盖的方法
不能抛出的异常比原方法继承关系高。因为这里的start方法在超类中抛出了IOException,所有在子类中的start
方法只能抛出要么是IOExcepition或是其子类,但不能是其超类,如Exception。
15)下面的Java异常代码有什么错误:
01 public static void start(){
02 System.out.println(“Java Exception interivew question Answers for Programmers”);
03 }
04
05 public static void main(String args[]) {
06 try{
07 start();
08 }catch(IOException ioe){
09 ioe.printStackTrace();
10 }
11 }
1)Java反射机制的作用
简单说,反射机制值得是程序在运行时能够获取自身的信息。在java中,只要给定类的名字,那么就可以通过反射机制来获得类的所有信息。
3)java反射机制提供了什么功能?
1.各种框架用的最多的就是反射
2.加载驱动
3.读取配置文件
5)运用反射的优缺点
优点:
反射提高了程序的灵活性和扩展性,降低耦合性,提高自适应能力。它允许程序创建和控制任何类的对象,无需提前硬编码目标类
缺点:
(1)性能问题:使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此反射机制主要应用在对灵活性和扩展性要求很高的系统框架上,普通程序不建议使用。
(2)使用反射会模糊程序内内部逻辑:程序员希望在源代码中看到程序的逻辑,反射等绕过了源代码的技术,因而会带来维护问题。反射代码比相应的直接代码更复杂。
6)如何使用java的反射?
a. 通过一个全限类名创建一个对象
1)、Class.forName(“全限类名”); 例如:com.mysql.jdbc.Driver Driver类已经被加载到 jvm中,并且完成了类的初始化工作就行了
2)、类名.class; 获取Class<?> clz 对象
3)、对象.getClass();
b. 获取构造器对象,通过构造器new出一个对象
1). Clazz.getConstructor([String.class]);
2). Con.newInstance([参数]);
c. 通过class对象创建一个实例对象(就相当与new类名()无参构造器)
1). Clazz.newInstance();
d. 通过class对象获得一个属性对象
1)、Field c=clz.getFields():获得某个类的所有的公共(public)的字段,包括父类中的字段。
2)、Field c=clz.getDeclaredFields():获得某个类的所有声明的字段,即包括public、private和proteced,但是不包括父类的申明字段 e.
e、通过class对象获得一个方法对象
1). Clazz.getMethod(“方法名”,class……parameaType);(只能获取公共的)
2). Clazz.getDeclareMethod(“方法名”);(获取任意修饰的方法,不能执行私有)
3) M.setAccessible(true);(让私有的方法可以执行)
f. 让方法执行
1). Method.invoke(obj实例对象,obj可变参数);-----(是有返回值的)
什么是反射?
反射就是动态加载对象,并对对象进行剖析。在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法,这种动态获取信息以及动态调用对象方法的功能成为Java反射机制。
反射的基本操作
创建一个类,用于演示反射的基本操作,代码如下:
package fs;public class Student {private long id;
private String name;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
获取类中的所有方法
public static void main(String[] args) {try {
Class> clz = Class.forName("fs.Student");
Method[] methods = clz.getMethods();
for (Method method : methods) {
System.out.println("方法名:" + method.getName());
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Class.forName("fs.Student"):初始化指定的类
clz.getMethods():获取类中所有的方法(包括其继承类的方法)
如果只需要获取加载类中的方法,不要父类的方法,可以使用下面的代码:
Method[] methods = clz.getDeclaredMethods();
Method是方法类,可以获取方法相关的信息,除了我们上面的方法名称,我们还可以获取其他的一些信息,比如:
方法返回类型:method.getReturnType().getName()
方法修饰符:Modifier.toString(method.getModifiers())
方法参数信息: method.getParameters()
方法上的注解: method.getAnnotations()
等等.......
操作方法
除了可以获取Class中方法的信息,还可以通过反射来调用方法,接下来看看怎么调用方法:
try {Class> clz = Class.forName("fs.Student");
Student stu = (Student) clz.newInstance();
System.out.println(stu.getName());
Method method = clz.getMethod("setName", String.class);
method.invoke(stu, "猿天地");
System.out.println(stu.getName());
e.printStackTrace();
通过class的newInstance()方法构造一个Student对象,然后调用getName()方法,这个时候输出的是null,然后通过方法名获取到setName方法,通过invoke调用方法,传入参数,然后调用getName()方法可以看到输出的就是我们设置的值“猿天地”。
获取类中的所有属性
Class> clz = Class.forName("fs.Student");Field[] fields = clz.getFields();for (Field field : fields) {System.out.println("属性名:" + field.getName());
clz.getFields()只能获取public的属性,包括父类的。
如果需要获取自己声明的各种字段,包括public,protected,private得用clz.getDeclaredFields()
Field是属性类,可以获取属性相关的信息,比如:
属性类型:field.getType().getName()
属性修饰符:Modifier.toString(field.getModifiers())
属性上的注解: field.getAnnotations()
等等.......
操作属性
try {Class> clz = Class.forName("fs.Student");
Student stu = (Student) clz.newInstance();
Field field = clz.getDeclaredField("name");
field.setAccessible(true);
System.out.println(field.get(stu));
field.set(stu, "猿天地");
System.out.println(field.get(stu));
e.printStackTrace();
通过clz.getDeclaredField("name");获取name属性,调用get方法获取属性的值,第一次肯定是没有值的,然后调用set方法设置值,最后再次获取就有值了,在get之前有field.setAccessible(true);这个代码,如果不加的话就会报下面的错误信息:
Class fs.Test can not access a member of class fs.Student with modifiers "private"
setAccessible(true);以取消Java的权限控制检查,让我们在用反射时可以访问访问私有变量
反射的优缺点?
优点
反射提高了程序的灵活性和扩展性,在底层框架中用的比较多,业务层面的开发过程中尽量少用。
缺点
性能不好
反射是一种解释操作,用于字段和方法接入时要远慢于直接代码,下面通过2段简单的代码来比较下执行的时间就可以体现出性能的问题
直接创建对象,调用方法设置值,然后获取值,时间在300ms左右
long start = System.currentTimeMillis();for (int i = 0; i < 100000; i++) {Student stu = new Student();
stu.setName("猿天地");
System.out.println(stu.getName());
System.out.println(end - start);
利用反射来实现上面的功能,时间在500ms左右,我是在我本机测试的
long start = System.currentTimeMillis();for (int i = 0; i < 100000; i++) {Class> clz = Class.forName("fs.Student");
Student stu = (Student) clz.newInstance();
Method method = clz.getMethod("setName", String.class);
method.invoke(stu, "猿天地");
System.out.println(stu.getName());
System.out.println(end - start);
程序逻辑有影响
使用反射操作会模糊化程序的内部逻辑,从代码的维护角度来讲,我们更希望在源码中看到程序的逻辑,反射相当于绕过了源码的方式,因此会带来维护难度比较大的问题。
反射的使用场景有哪些?
实现RPC框架
实现ORM框架
拷贝属性值(BeanUtils.copyProperties)
......
实现RPC框架
RPC是远程过程调用的简称,广泛应用在大规模分布式应用中。提到RPC框架在我脑海里第一闪现的就是Dubbo,远程过程调用的实现原理简单无非就是当客户端调用的时候通过动态代理向服务提供方发送调用的信息(Netty通信),服务提供方收到后根据客户端需要调用的方法,调用本地方法,拿到结果组装返回。这里就涉及到动态方法的调用,反射也就可以排上用场了。
至于Dubbo中是怎么动态调用的我就不太清楚啦,没去研究过Dubbo的源码哈,我临时看了下,找到了2个相关的类JdkProxyFactory和JavassistProxyFactory。
JdkProxyFactory就是用的method.invoke(proxy, arguments);
public class JdkProxyFactory extends AbstractProxyFactory {@Override
@SuppressWarnings("unchecked")
public
JavassistProxyFactory是用的Javassist框架来实现的
public class JavassistProxyFactory extends AbstractProxyFactory {@Override
@SuppressWarnings("unchecked")
public
实现ORM框架
关于ORM的概念本文就不做过多的介绍了,主要给大家介绍下如何用反射实现ORM的核心功能,我们以保持操作来进行讲解,也就是定义一个与数据库表对应的实体类,写一个save方法,传入我们实体类就可以将这个对象中的属性值存储到数据库中,变成一条数据。
还是以上面的Student来作为与表对应的实体类,下面我们看如何实现save方法中的逻辑:
public static void save(Object data, Class> entityClass) throws Exception {String sql = "insert into {0}({1}) values({2})";
String tableName = entityClass.getSimpleName();
List
try {
Student stu = new Student();
stu.setId(1);
stu.setName("猿天地");
save(stu, Student.class);
} catch (Exception e) {
e.printStackTrace();
}
执行main方法,输出结果如下:
insert into Student(id,name) values(?,?)1
猿天地
当然我上面只是最简单的代码,考虑也没那么全面,为的只是让大家熟悉反射的使用方式和场景,接下来我们再配合注解做一个小小的优化,注解不熟的同学可以参考我的这篇文章:《注解面试题-请了解下》
优化2点,定义一个TableName注解,用于描述表的信息,上面我们是直接用的类名作为表名,实际使用中很有可能表名是stu_info这样的 ,还有就是定义一个Field用于描述字段的信息,原理同上。
定义TableName注解:
import java.lang.annotation.;/*
*/@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface TableName {
/**
* 表名
* @return
*/
String value();
定义Field注解:
import java.lang.annotation.;/*
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE })public @interface Field {
/**
* 字段名称
* @return
*/
String value();
修改实体类,增加注解的使用:
@TableName("stu_info")public class Student {
private long id;
@Field("stu_name")
private String name;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void save(Object data, Class> entityClass) throws Exception {String sql = "insert into {0}({1}) values({2})";
String tableName = entityClass.getSimpleName();
if (entityClass.isAnnotationPresent(TableName.class)) {
tableName = entityClass.getAnnotation(TableName.class).value();
}
List
通上面的修改,如果有注解的情况下以注解中的值为主,没有的话就用Class中的。
执行main方法,输出结果如下:
insert into stu_info(id,stu_name) values(?,?)1
猿天地
拷贝属性值(BeanUtils.copyProperties)
在开发过程中,我们会遇到各种bean之间的转换,比如用ORM框架查询出来的数据,对应的bean,需要转换成Dto返回给调用方,这个时候就需要进行bean的转换了,下面通过简单的伪代码来讲解下:
Student stu = dao.get();
StudentDto dto = new StudentDto();
dto.setName(stu.getName());
dto.setXXX(stu.getXXX());
dto.set......return dto;
如果属性多的话,光写set方法就要写很多行,有没有优雅的方式呢?
这个时候我们可以用Spring中的BeanUtils.copyProperties来实现上面的需求,只需要一行代码即可,关于BeanUtils.copyProperties的详细使用不做过多讲解:
Student stu = dao.get();
StudentDto dto = new StudentDto();
BeanUtils.copyProperties(stu, dto);
这个功能就是反射的功劳了,我们可以通过源码来验证下是否是通过反射来实现的
private static void copyProperties(Object source, Object target, Class> editable, String... ignoreProperties) throws BeansException {
Assert.notNull(source, "Source must not be null");
Assert.notNull(target, "Target must not be null");
Class> actualEditable = target.getClass();
if (editable != null) {
if (!editable.isInstance(target)) {
throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
"] not assignable to Editable class [" + editable.getName() + "]");
}
actualEditable = editable;
}
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List
源码不做过多解释,我们看最关键的2行代码,第一行是:
Object value = readMethod.invoke(source);
通过调用读的方法将source中的值读取出来
第二行关键的是:
writeMethod.invoke(target, value);
通过调用写的方法进行复制到target中。
public interface List void add(E x);
Iterator
public interface IteratorE next();
boolean hasNext();
Let's test your understanding of generics. Is the following code snippet legal?
List
List// Compile-time error!
shapes.add(0, new Rectangle());
You should be able to figure out why the code above is disallowed. The type of the second parameter to shapes.add() is ? extends Shape-- an unknown subtype of Shape. Since we don't know what type it is, we don't know if it is a supertype of Rectangle; it might or might not be such a supertype, so it isn't safe to pass a Rectangle there.
Generic Methods
Consider writing a method that takes an array of objects and a collection and puts all objects in the array into the collection. Here's a first attempt:
static void fromArrayToCollection(Object[] a, Collection> c) {for (Object o : a) {
c.add(o); // compile-time error
}
By now, you will have learned to avoid the beginner's mistake of trying to use Collectionfor (T o : a) {
c.add(o); // Correct
}
We can call this method with any kind of collection whose element type is a supertype of the element type of the array.
Object[] oa = new Object[100];
Collection
fromArrayToCollection(oa, co);
Collection
fromArrayToCollection(sa, cs);
fromArrayToCollection(sa, co);
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection
fromArrayToCollection(ia, cn);
fromArrayToCollection(fa, cn);
fromArrayToCollection(na, cn);
fromArrayToCollection(na, co);
fromArrayToCollection(na, cs);
Notice that we don't have to pass an actual type argument to a generic method. The compiler infers the type argument for us, based on the types of the actual arguments. It will generally infer the most specific type argument that will make the call type-correct.
One question that arises is: when should I use generic methods, and when should I use wildcard types? To understand the answer, let's examine a few methods from the Collection libraries.
interface Collectionpublic boolean containsAll(Collection> c);
public boolean addAll(Collection extends E> c);
We could have used generic methods here instead:
interface Collectionpublic
However, in both containsAll and addAll, the type parameter T is used only once. The return type doesn't depend on the type parameter, nor does any other argument to the method (in this case, there simply is only one argument). This tells us that the type argument is being used for polymorphism; its only effect is to allow a variety of actual argument types to be used at different invocation sites. If that is the case, one should use wildcards. Wildcards are designed to support flexible subtyping, which is what we're trying to express here.
Generic methods allow type parameters to be used to express dependencies among the types of one or more arguments to a method and/or its return type. If there isn't such a dependency, a generic method should not be used.
It is possible to use both generic methods and wildcards in tandem. Here is the method Collections.copy():
class Collections {public static
Note the dependency between the types of the two parameters. Any object copied from the source list, src, must be assignable to the element type T of the destination list, dst. So the element type of src can be any subtype of T—we don't care which. The signature of copy expresses the dependency using a type parameter, but uses a wildcard for the element type of the second parameter.
We could have written the signature for this method another way, without using wildcards at all:
class Collections {public static
src) {
...
This is fine, but while the first type parameter is used both in the type of dst and in the bound of the second type parameter, S, S itself is only used once, in the type of src—nothing else depends on it. This is a sign that we can replace S with a wildcard. Using wildcards is clearer and more concise than declaring explicit type parameters, and should therefore be preferred whenever possible.
Wildcards also have the advantage that they can be used outside of method signatures, as the types of fields, local variables and arrays. Here is an example.
Returning to our shape drawing problem, suppose we want to keep a history of drawing requests. We can maintain the history in a static variable inside class Shape, and have drawAll() store its incoming argument into the history field.
static List>
history = new ArrayList
>();
history.addLast(shapes);
for (Shape s: shapes) {
s.draw(this);
}
Finally, again let's take note of the naming convention used for the type parameters. We use T for type, whenever there isn't anything more specific about the type to distinguish it. This is often the case in generic methods. If there are multiple type parameters, we might use letters that neighbor T in the alphabet, such as S. If a generic method appears inside a generic class, it's a good idea to avoid using the same names for the type parameters of the method and class, to avoid confusion. The same applies to nested generic classes.
Spring
Annotation-based configuration: Spring 2.5 introduced support for annotation-based configuration metadata.
Java-based configuration: Starting with Spring 3.0, many features provided by the Spring JavaConfig project became part of the core Spring Framework. Thus, you can define beans external to your application classes by using Java rather than XML files. To use these new features, see the @Configuration, @Bean, @Import, and @DependsOn annotations.
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
It can be useful to have bean definitions span multiple XML files. Often, each individual XML configuration file represents a logical layer or module in your architecture.
You can use the application context constructor to load bean definitions from all these XML fragments. This constructor takes multiple Resource locations, as was shown in the previous section. Alternatively, use one or more occurrences of the class="examples.ClientService"
factory-method="createInstance"/>
public class ClientService {private static ClientService clientService = new ClientService();
private ClientService() {}
public static ClientService createInstance() {
return clientService;
}
Circular dependencies
If you use predominantly constructor injection, it is possible to create an unresolvable circular dependency scenario.
For example: Class A requires an instance of class B through constructor injection, and class B requires an instance of class A through constructor injection. If you configure beans for classes A and B to be injected into each other, the Spring IoC container detects this circular reference at runtime, and throws a BeanCurrentlyInCreationException.
One possible solution is to edit the source code of some classes to be configured by setters rather than constructors. Alternatively, avoid constructor injection and use setter injection only. In other words, although it is not recommended, you can configure circular dependencies with setter injection.
Unlike the typical case (with no circular dependencies), a circular dependency between bean A and bean B forces one of the beans to be injected into the other prior to being fully initialized itself (a classic chicken-and-egg scenario).
The ,
bean | ref | idref | list | set | map | props | value | null
1.4.6. Method Injection
In most application scenarios, most beans in the container are singletons. When a singleton bean needs to collaborate with another singleton bean or a non-singleton bean needs to collaborate with another non-singleton bean, you typically handle the dependency by defining one bean as a property of the other. A problem arises when the bean lifecycles are different. Suppose singleton bean A needs to use non-singleton (prototype) bean B, perhaps on each method invocation on A. The container creates the singleton bean A only once, and thus only gets one opportunity to set the properties. The container cannot provide bean A with a new instance of bean B every time one is needed.
A solution is to forego some inversion of control. You can make bean A aware of the container by implementing the ApplicationContextAware interface, and by making a getBean("B") call to the container ask for (a typically new) bean B instance every time bean A needs it. The following example shows this approach:
// a class that uses a stateful Command-style class to perform some processingpackage fiona.apple;
// Spring-API importsimport org.springframework.beans.BeansException;import org.springframework.context.ApplicationContext;import org.springframework.context.ApplicationContextAware;
public class CommandManager implements ApplicationContextAware {private ApplicationContext applicationContext;
public Object process(Map commandState) {
// grab a new instance of the appropriate Command
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}
protected Command createCommand() {
// notice the Spring API dependency!
return this.applicationContext.getBean("command", Command.class);
}
public void setApplicationContext(
ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
The preceding is not desirable, because the business code is aware of and coupled to the Spring Framework. Method Injection, a somewhat advanced feature of the Spring IoC container, lets you handle this use case cleanly.
@RequestScope
@Component
@SessionScope
@ApplicationScope
@Lookup
@Bean
@Primary
@Configuration
@Resource
@ComponentScan
@Qualifier
@Import
@Import@AspectJ
@EnableAspectJAutoProxy
@Pointcut
@Before
@AfterReturning
@AfterThrowing
@After
@Around
@Configurable
@EnableSpringConfigured
@Autowired
Alternatively, you can configure the Spring container to create standard JDK interface-based proxies for such scoped beans, by specifying false for the value of the proxy-target-class attribute of the
The @Required annotation applies to bean property setter methods, as in the following example:
public class SimpleMovieLister {private MovieFinder movieFinder;
@Required
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
Spring boot
@Value
Spring Boot uses a very particular PropertySource order that is designed to allow sensible overriding of values. Properties are considered in the following order:
1.Devtools global settings properties on your home directory (~/.spring-boot-devtools.properties when devtools is active).
2.@TestPropertySource annotations on your tests.
3.properties attribute on your tests. Available on @SpringBootTest and the test annotations for testing a particular slice of your application.
4.Command line arguments.
5.Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property).
6.ServletConfig init parameters.
7.ServletContext init parameters.
8.JNDI attributes from java:comp/env.
9.Java System properties (System.getProperties()).
10.OS environment variables.
11.A RandomValuePropertySource that has properties only in random.*.
12.Profile-specific application properties outside of your packaged jar (application-{profile}.properties and YAML variants).
13.Profile-specific application properties packaged inside your jar (application-{profile}.properties and YAML variants).
14.Application properties outside of your packaged jar (application.properties and YAML variants).
15.Application properties packaged inside your jar (application.properties and YAML variants).
16.@PropertySource annotations on your @Configuration classes.
17.Default properties (specified by setting SpringApplication.setDefaultProperties).
@PropertySource
1.A /config subdirectory of the current directory
2.The current directory
3.A classpath /config package
4.The classpath root
$ java -jar myproject.jar --spring.config.name=myproject
java -jar myproject.jar --spring.config.location=classpath:/default.properties,classpath:/override.properties
Config locations are searched in reverse order. By default, the configured locations are classpath:/,classpath:/config/,file:./,file:./config/. The resulting search order is the following:
1.file:./config/
2.file:./
3.classpath:/config/
4.classpath:/
YAML lists are represented as property keys with [index] dereferencers. For example, consider the following YAML:
my:servers:- dev.example.com
- another.example.com
my.servers[0]=dev.example.commy.servers[1]=another.example.com
@ConfigurationProperties(prefix="my")public class Config {private List
@Controller or @RestController
@RequestMapping
@GetMapping
@DeleteMapping
@PathVariable
@Path
@Entity
@Entitypublic class City implements Serializable {@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String state;
// ... additional members, often include @OneToMany mappings
protected City() {
// no-args constructor required by JPA spec
// this one is protected since it shouldn't be used directly
}
public City(String name, String state) {
this.name = name;
this.state = state;
}
public String getName() {
return this.name;
}
public String getState() {
return this.state;
}
// ... etc
@EnableScheduling
Spring cloud
数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。
超 键:
在关系中能唯一标识元组的属性集称为关系模式的超键。一个属性可以为作为一个超键,多个属性组合在一起也可以作为一个超键。超键包含候选键和主键。
候选键:
是最小超键,即没有冗余元素的超键。
外 键:
在一个表中存在的另一个表的主键称此表的外键。
2.数据库事务的四个特性及含义
数据库事务transanction正确执行的四个基本要素。ACID,原子性(Atomicity)、一致性(Correspondence)、隔离性(Isolation)、持久性(Durability)。
原子性:整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性:在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
隔离性:隔离状态执行事务,使它们好像是系统在给定时间内执行的唯一操作。如果有两个事务,运行在相同的时间内,执行 相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请 求,使得在同一时间仅有一个请求用于同一数据。
持久性:在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。
3.视图的作用,视图可以更改么?
视图是虚拟的表,与包含数据的表不一样,视图只包含使用时动态检索数据的查询;不包含任何列或数据。使用视图可以简化复杂的sql操作,隐藏具体的细节,保护数据;视图创建后,可以使用与表相同的方式利用它们。
视图不能被索引,也不能有关联的触发器或默认值,如果视图本身内有order by 则对视图再次order by将被覆盖。
创建视图:create view XXX as XXXXXXXXXXXXXX;
对于某些视图比如未使用联结子查询分组聚集函数Distinct Union等,是可以对其更新的,对视图的更新将对基表进行更新;但是视图主要用于简化检索,保护数据,并不用于更新,而且大部分视图都不可以更新。
4.drop,delete与truncate的区别
drop直接删掉表 truncate删除表中数据,再插入时自增长id又从1开始 delete删除表中数据,可以加where字句。
(1) DELETE语句执行删除的过程是每次从表中删除一行,并且同时将该行的删除操作作为事务记录在日志中保存以便进行进行回滚操作。TRUNCATE TABLE 则一次性地从表中删除所有的数据并不把单独的删除操作记录记入日志保存,删除行是不能恢复的。并且在删除的过程中不会激活与表有关的删除触发器。执行速度快。
(2) 表和索引所占空间。当表被TRUNCATE 后,这个表和索引所占用的空间会恢复到初始大小,而DELETE操作不会减少表或索引所占用的空间。drop语句将表所占用的空间全释放掉。
(3) 一般而言,drop > truncate > delete
(4) 应用范围。TRUNCATE 只能对TABLE;DELETE可以是table和view
(5) TRUNCATE 和DELETE只删除数据,而DROP则删除整个表(结构和数据)。
(6) truncate与不带where的delete :只删除数据,而不删除表的结构(定义)drop语句将删除表的结构被依赖的约束(constrain),触发器(trigger)索引(index);依赖于该表的存储过程/函数将被保留,但其状态会变为:invalid。
(7) delete语句为DML(data maintain Language),这个操作会被放到 rollback segment中,事务提交后才生效。如果有相应的 tigger,执行的时候将被触发。
(8) truncate、drop是DLL(data define language),操作立即生效,原数据不放到 rollback segment中,不能回滚
(9) 在没有备份情况下,谨慎使用 drop 与 truncate。要删除部分数据行采用delete且注意结合where来约束影响范围。回滚段要足够大。要删除表用drop;若想保留表而将表中数据删除,如果于事务无关,用truncate即可实现。如果和事务有关,或老师想触发trigger,还是用delete。
(10) Truncate table 表名 速度快,而且效率高,因为:
truncate table 在功能上与不带 WHERE 子句的 DELETE 语句相同:二者均删除表中的全部行。但 TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少。DELETE 语句每次删除一行,并在事务日志中为所删除的每行记录一项。TRUNCATE TABLE 通过释放存储表数据所用的数据页来删除数据,并且只在事务日志中记录页的释放。
(11) TRUNCATE TABLE 删除表中的所有行,但表结构及其列、约束、索引等保持不变。新行标识所用的计数值重置为该列的种子。如果想保留标识计数值,请改用 DELETE。如果要删除表定义及其数据,请使用 DROP TABLE 语句。
(12) 对于由 FOREIGN KEY 约束引用的表,不能使用 TRUNCATE TABLE,而应使用不带 WHERE 子句的 DELETE 语句。由于 TRUNCATE TABLE 不记录在日志中,所以它不能激活触发器。
5.索引的工作原理及其种类
数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。
在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。
为表设置索引要付出代价的:一是增加了数据库的存储空间,二是在插入和修改数据时要花费较多的时间(因为索引也要随之变动)。
图展示了一种可能的索引方式。左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址(注意逻辑上相邻的记录在磁盘上也并不是一定物理相邻的)。为了加快Col2的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据记录物理地址的指针,这样就可以运用二叉查找在O(log2n)的复杂度内获取到相应数据。
创建索引可以大大提高系统的性能。
第一,通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
第二,可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
第三,可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
第四,在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间。
第五,通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。
也许会有人要问:增加索引有如此多的优点,为什么不对表中的每一个列创建一个索引呢?因为,增加索引也有许多不利的方面。
第一,创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
第二,索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。
第三,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。
索引是建立在数据库表中的某些列的上面。在创建索引的时候,应该考虑在哪些列上可以创建索引,在哪些列上不能创建索引。一般来说,应该在这些列上创建索引:在经常需要搜索的列上,可以加快搜索的速度;在作为主键的列上,强制该列的唯一性和组织表中数据的排列结构;在经常用在连接的列上,这些列主要是一些外键,可以加快连接的速度;在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的;在经常需要排序的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间;在经常使用在WHERE子句中的列上面创建索引,加快条件的判断速度。
同样,对于有些列不应该创建索引。一般来说,不应该创建索引的的这些列具有下列特点:
第一,对于那些在查询中很少使用或者参考的列不应该创建索引。这是因为,既然这些列很少使用到,因此有索引或者无索引,并不能提高查询速度。相反,由于增加了索引,反而降低了系统的维护速度和增大了空间需求。
第二,对于那些只有很少数据值的列也不应该增加索引。这是因为,由于这些列的取值很少,例如人事表的性别列,在查询的结果中,结果集的数据行占了表中数据行的很大比例,即需要在表中搜索的数据行的比例很大。增加索引,并不能明显加快检索速度。
第三,对于那些定义为text, image和bit数据类型的列不应该增加索引。这是因为,这些列的数据量要么相当大,要么取值很少。
第四,当修改性能远远大于检索性能时,不应该创建索引。这是因为,修改性能和检索性能是互相矛盾的。当增加索引时,会提高检索性能,但是会降低修改性能。当减少索引时,会提高修改性能,降低检索性能。因此,当修改性能远远大于检索性能时,不应该创建索引。
根据数据库的功能,可以在数据库设计器中创建三种索引:唯一索引、主键索引和聚集索引。
唯一索引
唯一索引是不允许其中任何两行具有相同索引值的索引。
当现有数据中存在重复的键值时,大多数数据库不允许将新创建的唯一索引与表一起保存。数据库还可能防止添加将在表中创建重复键值的新数据。例如,如果在employee表中职员的姓(lname)上创建了唯一索引,则任何两个员工都不能同姓。 主键索引 数据库表经常有一列或列组合,其值唯一标识表中的每一行。该列称为表的主键。 在数据库关系图中为表定义主键将自动创建主键索引,主键索引是唯一索引的特定类型。该索引要求主键中的每个值都唯一。当在查询中使用主键索引时,它还允许对数据的快速访问。 聚集索引 在聚集索引中,表中行的物理顺序与键值的逻辑(索引)顺序相同。一个表只能包含一个聚集索引。
如果某索引不是聚集索引,则表中行的物理顺序与键值的逻辑顺序不匹配。与非聚集索引相比,聚集索引通常提供更快的数据访问速度。
局部性原理与磁盘预读
由于存储介质的特性,磁盘本身存取就比主存慢很多,再加上机械运动耗费,磁盘的存取速度往往是主存的几百分分之一,因此为了提高效率,要尽量减少磁盘I/O。为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。程序运行期间所需要的数据通常比较集中。
由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),因此对于具有局部性的程序来说,预读可以提高I/O效率。
预读的长度一般为页(page)的整倍数。页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页得大小通常为4k),主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,然后异常返回,程序继续运行。
B-/+Tree索引的性能分析
到这里终于可以分析B-/+Tree索引的性能了。
上文说过一般使用磁盘I/O次数评价索引结构的优劣。先从B-Tree分析,根据B-Tree的定义,可知检索一次最多需要访问h个节点。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。为了达到这个目的,在实际实现B-Tree还需要使用如下技巧:
每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了一个node只需一次I/O。
B-Tree中一次检索最多需要h-1次I/O(根节点常驻内存),渐进复杂度为O(h)=O(logdN)。一般实际应用中,出度d是非常大的数字,通常超过100,因此h非常小(通常不超过3)。
而红黑树这种结构,h明显要深的多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,所以红黑树的I/O渐进复杂度也为O(h),效率明显比B-Tree差很多。
综上所述,用B-Tree作为索引结构效率是非常高的。
6.连接的种类
查询分析器中执行:
--建表table1,table2:
create table table1(id int,name varchar(10))
create table table2(id int,score int)
insert into table1 select 1,'lee'
insert into table1 select 2,'zhang'
insert into table1 select 4,'wang'
insert into table2 select 1,90
insert into table2 select 2,100
insert into table2 select 3,70如表
table1 | table2 |
1 lee |1 90|
2 zhang| 2 100|4 wang| 3 70|
一、外连接
1.概念:包括左向外联接、右向外联接或完整外部联接
(1)左向外联接的结果集包括 LEFT OUTER 子句中指定的左表的所有行,而不仅仅是联接列所匹配的行。如果左表的某行在右表中没有匹配行,则在相关联的结果集行中右表的所有选择列表列均为空值(null)。
(2)sql 语句
select * from table1 left join table2 on table1.id=table2.id
-------------结果-------------idnameidscore
2zhang21004wangNULLNULL
(1)右向外联接是左向外联接的反向联接。将返回右表的所有行。如果右表的某行在左表中没有匹配行,则将为左表返回空值。
(2)sql 语句
select * from table1 right join table2 on table1.id=table2.id
-------------结果-------------idnameidscore
2zhang2100NULLNULL370
(1)完整外部联接返回左表和右表中的所有行。当某行在另一个表中没有匹配行时,则另一个表的选择列表列包含空值。如果表之间有匹配行,则整个结果集行包含基表的数据值。
(2)sql 语句
select * from table1 full join table2 on table1.id=table2.id
-------------结果-------------idnameidscore
2zhang2100
4wangNULLNULLNULLNULL370
1.概念:内联接是用比较运算符比较要联接列的值的联接
select * from table1 join table2 on table1.id=table2.id
-------------结果-------------idnameidscore
2zhang2100
A:select a.,b. from table1 a,table2 b where a.id=b.id
B:select * from table1 cross join table2 where table1.id=table2.id (注:cross join后加条件只能用where,不能用on)
select * from table1 cross join table2
-------------结果-------------idnameidscore
2zhang190
4wang190
1lee2100
2zhang2100
4wang2100
1lee370
2zhang3704wang370
A:select * from table1,table2
7.数据库范式
1 第一范式(1NF)
所谓第一范式(1NF)是指数据库表的每一列都是不可分割的基本数据项,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。如果出现重复的属性,就可能需要定义一个新的实体,新的实体由重复的属性构成,新实体与原实体之间为一对多关系。在第一范式(1NF)中表的每一行只包含一个实例的信息。简而言之,第一范式就是无重复的列。
2 第二范式(2NF)
第二范式(2NF)要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性,如果存在,那么这个属性和主关键字的这一部分应该分离出来形成一个新的实体,新实体与原实体之间是一对多的关系。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。简而言之,第二范式就是非主属性非部分依赖于主关键字。
3 第三范式(3NF)
8.数据库优化的思路
这个我借鉴了慕课上关于数据库优化的课程。
1.SQL语句优化
1)应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。
2)应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:
select id from t where num is null
可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:
select id from t where num=0
3)很多时候用 exists 代替 in 是一个好的选择
4)用Where子句替换HAVING 子句 因为HAVING 只会在检索出所有记录之后才对结果集进行过滤
2.索引优化
看上文索引
3.数据库结构优化
1)范式优化: 比如消除冗余(节省空间。。) 2)反范式优化:比如适当加冗余等(减少join) 3)拆分表: 分区将数据在物理上分隔开,不同分区的数据可以制定保存在处于不同磁盘上的数据文件里。这样,当对这个表进行查询时,只需要在表分区中进行扫描,而不必进行全表扫描,明显缩短了查询时间,另外处于不同磁盘的分区也将对这个表的数据传输分散在不同的磁盘I/O,一个精心设置的分区可以将数据传输对磁盘I/O竞争均匀地分散开。对数据量大的时时表可采取此方法。可按月自动建表分区。
4)拆分其实又分垂直拆分和水平拆分: 案例: 简单购物系统暂设涉及如下表: 1.产品表(数据量10w,稳定) 2.订单表(数据量200w,且有增长趋势) 3.用户表 (数据量100w,且有增长趋势) 以mysql为例讲述下水平拆分和垂直拆分,mysql能容忍的数量级在百万静态数据可以到千万 垂直拆分:解决问题:表与表之间的io竞争 不解决问题:单表中数据量增长出现的压力 方案: 把产品表和用户表放到一个server上 订单表单独放到一个server上 水平拆分: 解决问题:单表中数据量增长出现的压力 不解决问题:表与表之间的io争夺
方案: 用户表通过性别拆分为男用户表和女用户表 订单表通过已完成和完成中拆分为已完成订单和未完成订单 产品表 未完成订单放一个server上 已完成订单表盒男用户表放一个server上 女用户表放一个server上(女的爱购物 哈哈)
4.服务器硬件优化
这个么多花钱咯!
9.存储过程与触发器的区别
触发器与存储过程非常相似,触发器也是SQL语句集,两者唯一的区别是触发器不能用EXECUTE语句调用,而是在用户执行Transact-SQL语句时自动触发(激活)执行。触发器是在一个修改了指定表中的数据时执行的存储过程。通常通过创建触发器来强制实现不同表中的逻辑相关数据的引用完整性和一致性。由于用户不能绕过触发器,所以可以用它来强制实施复杂的业务规则,以确保数据的完整性。触发器不同于存储过程,触发器主要是通过事件执行触发而被执行的,而存储过程可以通过存储过程名称名字而直接调用。当对某一表进行诸如UPDATE、INSERT、DELETE这些操作时,SQLSERVER就会自动执行触发器所定义的SQL语句,从而确保对数据的处理必须符合这些SQL语句所定义的规则。
1)数据库范式
l 第一范式(1NF):强调的是列的原子性,即列不能够再分成其他几列。
如电话列可进行拆分---家庭电话、公司电话
l 第二范式(2NF):首先是 1NF,另外包含两部分内容,一是表必须有主键;二是没有包含在主键中的列必须完全依赖于主键,而不能只依赖于主键的一部分。
比如Student表(学号,姓名,年龄,性别,所在院校,院校地址,院校电话)
这样一个表结构,就存在上述关系。 学号--> 所在院校 --> (院校地址,院校电话)
这样的表结构,我们应该拆开来,如下。
(学号,姓名,年龄,性别,所在院校)--(所在院校,院校地址,院校电话)
满足这些规范的数据库是简洁的、结构明晰的;同时,不会发生插入(insert)、删除(delete)和更新(update)操作异常。
2)数据类型选择
l 数字类型
Float和double选择(尽量选择float)
区分开TINYINT / INT / BIGINT,能确定不会使用负数的字段,建议添加 unsigned定义
能够用数字类型的字段尽量选择数字类型而不用字符串类型的
l 字符类型
char,varchar,TEXT的选择:非万不得已不要使用 TEXT 数据类型,定长字段,建议使用 CHAR 类型(填空格),不定长字段尽量使用 VARCHAR(自动适应长度,超过阶段),且仅仅设定适当的最大长度
l 时间类型
按选择优先级排序DATE(精确到天)、TIMESTAMP、DATETIME(精确到时间)
l ENUM
对于状态字段,可以尝试使用 ENUM 来存放
l 避免使用NULL字段,很难查询优化且占用额外索引空间
3)字符编码
同样的内容使用不同字符集表示所占用的空间大小会有较大的差异,所以通过使用合适的字符集,可以帮助我们尽可能减少数据量,进而减少IO操作次数。
1.纯拉丁字符能表示的内容,选择 latin1 字符编码
2.中文可选用utf-8
3.MySQL的数据类型可以精确到字段,所以当我们需要大型数据库中存放多字节数据的时候,可以通过对不同表不同字段使用不同的数据类型来较大程度减小数据存储量,进而降低 IO 操作次数并提高缓存命中率
Sql优化
1) 只返回需要的数据
a) 不要写SELECT *的语句
b) 合理写WHERE子句,不要写没有WHERE的SQL语句。
2) 尽量少做重复的工作
可以合并一些sql语句
3) 适当建立索引(不是越多越好)但以下几点会进行全表扫描
a) 左模糊查询’%...’
b) 使用了不等操作符!=
c) Or使用不当,or两边都必须有索引才行
d) In 、not in
e) Where子句对字段进行表达式操作
f) 对于创建的复合索引(从最左边开始组合),查询条件用到的列必须从左边开始不能间隔。否则无效,复合索引的结构与电话簿类似
g) 全文索引:当于对文件建立了一个以词库为目录的索引(文件大全文索引比模糊匹配效果好)
能在char、varchar、text类型的列上面创建全文索引
MySQL 5.6 Innodb引擎也能进行全文索引
搜索语法:MATCH (列名1, 列名2,…) AGAINST (搜索字符串 [搜索修饰符])
如果列类型是字符串,但在查询时把一个数值型常量赋值给了一个字符型的列名name,那么虽然在name列上有索引,但是也没有用到。
4) 使用join代替子查询
5) 使用union代替手动创建临时表
索引优化
一、 创建索引,以下情况不适合建立索引
l 表记录太少
l 经常插入、删除、修改的表
l 数据重复且分布平均的表字段
二、 复合索引
如果一个表中的数据在查询时有多个字段总是同时出现则这些字段就可以作为复合索引
索引
索引是对数据库表中一列或多列的值进行排序的一种结构。
优点:
l 大大加快数据的检索速度
l 创建唯一性索引,保证数据库表中每一行数据的唯一性
l 可以加速表和表之间的连接
缺点:
l 索引需要占物理空间。
l 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,
降低了数据的维护速度。
索引分类:
l 普通索引
create index zjj_temp_index_1 on zjj_temp_1(first_name);
drop index zjj_temp_index_1;
l 唯一索引,索引列的值必须唯一,但允许有空值
create unique index zjj_temp_1 on zjj_temp_1(id);
l 主键索引,它是一种特殊的唯一索引,不允许有空值。
l 组合索引
事务
数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。
四大特征:
(1)原子性
事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行。
(2)一致性
事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。
(3) 隔离性(关于事务的隔离性数据库提供了多种隔离级别)
一个事务的执行不能干扰其它事务。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。
(4)持久性
事务完成之后,它对于数据库中的数据改变是永久性的。该修改即使出现系统故障也将一
直保持。
在介绍数据库提供的各种隔离级别之前,我们先看看如果不考虑事务的隔离性,会发生的几种问题:
l 脏读
脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
l 不可重复读
l 幻读
幻读和不可重复读都是读取了另一条已经提交的事务,不可重复读重点在于update和delete,而幻读的重点在于insert。
在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复 读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会 发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。
现在来看看MySQL数据库为我们提供的四种隔离级别:
① Serializable (串行化):可避免脏读、不可重复读、幻读的发生。
② Repeatable read (可重复读):可避免脏读、不可重复读的发生。
③ Read committed (读已提交):可避免脏读的发生。
④ Read uncommitted (读未提交):最低级别,任何情况都无法保证。
在MySQL数据库中默认的隔离级别为Repeatable read (可重复读)。
锁模式包括:
l 共享锁:(读取)操作创建的锁。其他用户可以并发读取数据,但任何事物都不能获取数据上的排它锁,直到已释放所有共享锁。
l 排他锁(X锁):对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。
l 更新锁:
更新 (U) 锁可以防止通常形式的死锁。如果两个事务获得了资源上的共享模式锁,然后试图同时更新数据,则两个事务需都要转换共享锁为排它 (X) 锁,并且每个事务都等待另一个事务释放共享模式锁,因此发生死锁。
若要避免这种潜 在的死锁问题,请使用更新 (U) 锁。一次只有一个事务可以获得资源的更新 (U) 锁。如果事务修改资源,则更新 (U) 锁转换为排它 (X) 锁。否则,锁转换为共享锁。
锁的粒度主要有以下几种类型:
l 行锁: 粒度最小,并发性最高
l 页锁:一次锁定一页。25个行锁可升级为一个页锁。
l 表锁:粒度大,并发性低
l 数据库锁:控制整个数据库操作
乐观锁:相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。一般的实现乐观锁的方式就是记录数据版本。
悲观锁:顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
索引
MyISAM、InnoDB区别
l MyISAM类型不支持事务处理等高级处理,而InnoDB类型支持。
l MyISAM表不支持外键,InnoDB支持
l MyISAM锁的粒度是表级,而InnoDB支持行级锁定。
l MyISAM支持全文类型索引,而InnoDB不支持全文索引。(mysql 5.6后innodb支持全文索引)
MyISAM相对简单,所以在效率上要优于InnoDB,小型应用可以考虑使用MyISAM。当你的数据库有大量的写入、更新操作而查询比较少或者数据完整性要求比较高的时
候就选择innodb表。当你的数据库主要以查询为主,相比较而言更新和写 入比较少,并且业务方面数据完整性要求不那么严格,就选择mysiam表。
MyISAM和InnoDB索引实现:
MyISAM索引实现
MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。
l 主索引
MyISAM引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址。
l 辅助索引
在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复。
MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录。
MyISAM的索引方式也叫做“非聚集”的,之所以这么称呼是为了与InnoDB的聚集索引区分。
InnoDB索引实现
然InnoDB也使用B+Tree作为索引结构,但具体实现方式却与MyISAM截然不同.
l 主索引
InnoDB表数据文件本身就是主索引。
InnoDB主索引(同时也是数据文件)的示意图,可以看到叶节点包含了完整的数据记录。这种索引叫做聚集索引。因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形。
l 辅助索引
InnoDB的所有辅助索引都引用主键作为data域。
聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。
不同存储引擎的索引实现方式对于正确使用和优化索引都非常有帮助,例如知道了InnoDB的索引实现后,就很容易明白
1、为什么不建议使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。再例如,
2、用非单调的字段作为主键在InnoDB中不是个好主意,因为InnoDB数据文件本身是一颗B+Tree,非单调的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。
InnoDB索引和MyISAM索引的区别:
l 一是主索引的区别,InnoDB的数据文件本身就是索引文件。而MyISAM的索引和数据是分开的。
l 二是辅助索引的区别:InnoDB的辅助索引data域存储相应记录主键的值而不是地址。而MyISAM的辅助索引和主索引没有多大区别。
红黑树 B树 B+树 B-树
二叉查找树(BST):
二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:
l 若左子树不空,则左子树上所有结点的值均小于它的根结点的值。
l 若右子树不空,则右子树上所有结点的值均大于它的根结点的值。
l 左、右子树也分别为二叉排序树。
l 没有键值相等的节点(因此,插入的时候一定是叶子节点)。
删除算法
l 要删除节点是叶子节点。
l 要删除的节点只有一个孩子(左孩子或右孩子),这种情况比较简单,只需要将该孩子连接到当前节点的父节点即可。
l 要删除的节点有两个孩子,这个时候的算法就比较复杂(相比较于只有一个孩子的情况)。首先我们需要找到待删除节点的左子树上的最大值节点,或者右子树上的最小值节点,然后将该节点的参数值与待删除的节点参数值进行交换,最后删除该节点,这样需要删除的参数就从该二叉树中删除了。
红黑树:
红黑树(Red Black Tree) 是一种自平衡二叉查找树 :
l 每个节点或者是黑色,或者是红色。
l 根节点是黑色。
l 每个叶子节点是黑色。
l 如果一个节点是红色的,则它的子节点必须是黑色的。
l 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
红黑树的各种操作的时间复杂度是O(log2N)。
红黑树 vs AVL
红黑树的查询性能略微逊色于AVL树,因为他比avl树会稍微不平衡最多一层,也就是说红黑树的查询性能只比相同内容的avl树最多多一次比较,但是,红黑树在插入和删除上完爆avl树,avl树每次插入删除会进行大量的平衡度计算,而红黑树为了维持红黑性质所做的红黑变换和旋转的开销,相较于avl树为了维持平衡的开销要小得多
插入操作
红父
如果新节点的父结点为红色,这时就需要进行一系列操作以保证整棵树红黑性质。如下图所示,由于父结点为红色,此时可以判定,祖父结点必定为黑色。这时需要根据叔父结点的颜色来决定做什么样的操作。青色结点表示颜色未知。由于有可能需要根结点到新点的路径上进行多次旋转操作,而每次进行不平衡判断的起始点(我们可将其视为新点)都不一样。所以我们在此使用一个蓝色箭头指向这个起始点,并称之为判定点。
l 红叔
当叔父结点为红色时,如下图所示,无需进行旋转操作,只要将父和叔结点变为黑色,将祖父结点变为红色即可。但由于祖父结点的父结点有可能为红色,从而违反红黑树性质。此时必须将祖父结点作为新的判定点继续向上(迭代)进行平衡操作。
需要注意的是,无论“父节点”在“叔节点”的左边还是右边,无论“新节点”是“父节点”的左孩子还是右孩子,它们的操作都是完全一样的(其实这种情况包括4种,只需调整颜色,不需要旋转树形)。
l 黑叔
当叔父结点为黑色时,需要进行旋转,以下图示了所有的旋转可能:
Case 1:
Case 2:
Case 3:
Case 4:
B-tree树即B树,B即Balanced,平衡的意思。因为B树的原英文名称为B-tree,而国内很多人喜欢把B-tree译作B-树,其实,这是个非常不好的直译,很容易让人产生误解。如人们可能会以为B-树是一种树,而B树又是另一种树。而事实上是,B-tree就是指的B树。
m阶B树是一棵平衡的m路搜索树。它是空树,或者是满足下列性质的树:
l 根结点的儿子数为[2, M];
l 除根结点以外的非叶子结点的儿子数为[M/2, M]; (M/2向上取整)
l 每个结点存放至少M/2-1(取上整)和至多M-1个关键字;
l 非叶子结点的关键字个数=指向儿子的指针个数-1;
l 非叶子结点的关键字:K[1], K[2], …, K[X-1];且K[i] < K[i+1];
l 非叶子结点的指针:P[1], P[2], …, P[X];其中P[1]指向关键字小于K[1]的
子树,P[X]指向关键字大于K[X-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树;
l 所有叶子结点位于同一层;
B+树:
B+树是B-树的变体,也是一种多路搜索树:
其定义基本与B-树同,除了:
l 非叶子结点的子树指针与关键字个数相同;
l 非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树
(B-树是开区间);
l 为所有叶子结点增加一个指针链;
l 所有关键字都在叶子结点出现;
基本SQL操作
l SELECT * FROM table ORDER BY field DESC; (ASC|DESC)
SELECT DISTINCT field from table where 范围
l INSERT INTO table_name (column1,column2,column3,...)
VALUES (value1,value2,value3,...);
l UPDATE table_name SET column1=value1,column2=value2,...
WHERE some_column=some_value;
l DELETE FROM table_name WHERE some_column=some_value;
LIKE 操作符
SELECT column_name(s) FROM table_name WHERE column_name LIKE pattern;
IN 操作符
SELECT column_name(s) FROM table_name WHERE column_name
IN (value1,value2,...);
BETWEEN 操作符
SELECT column_name(s) FROM table_name WHERE column_name BETWEEN
JOIN
左连接,右连接,内连接
left join(左联接): 返回包括左表中的所有记录和右表中联结字段相等的记录。
right join(右联接): 返回包括右表中的所有记录和左表中联结字段相等的记录。
inner join(等值连接): 只返回两个表中联结字段相等的行。(默认)
UNION 操作符
UNION 操作符用于合并两个或多个 SELECT 语句的结果集。
SELECT country, name FROM Websites WHERE country='CN'
UNION
SELECT country, app_name FROM apps WHERE country='CN'
ORDER BY country;
创建视图
CREATE VIEW view_name AS SELECT column_name(s) FROM table_name WHERE condition
SQL函数
Avg() Count() Max() Min() Sum()
Group By():
GROUP BY 语句用于结合聚合函数,根据一个或多个列对结果集进行分组。
SELECT column_name, aggregate_function(column_name)
FROM table_name WHERE column_name operator value
GROUP BY column_name;
HAVING 子句可以让我们筛选分组后的各组数据。
SELECT column_name, aggregate_function(column_name)
FROM table_name WHERE column_name operator value
GROUP BY column_name
HAVING aggregate_function(column_name) operator value;
如何查询数据库表结构,主键
desc tabl_name;
建表
CREATE TABLE 表名称
(
列名称1 数据类型,
....
)
Student(SID,Sname,Sage,Ssex) --SID 学生编号,Sname 学生姓名,Sage 出生年月,Ssex 学生性别
2.课程表
Course(CID,Cname,TID) --CID --课程编号,Cname 课程名称,TID 教师编号
3.教师表
Teacher(TID,Tname) --TID 教师编号,Tname 教师姓名
4.成绩表
SC(SID,CID,score) --SID 学生编号,CID 课程编号,score 分数
添加测试数据1.学生表
create table Student(SID varchar(10),Sname nvarchar(10),Sage datetime,Ssex nvarchar(10));
insert into Student values('01' , '赵雷' , '1990-01-01' , '男');
insert into Student values('02' , '钱电' , '1990-12-21' , '男');
insert into Student values('03' , '孙风' , '1990-05-20' , '男');
insert into Student values('04' , '李云' , '1990-08-06' , '男');
insert into Student values('05' , '周梅' , '1991-12-01' , '女');
insert into Student values('06' , '吴兰' , '1992-03-01' , '女');
insert into Student values('07' , '郑竹' , '1989-07-01' , '女');
insert into Student values('08' , '王菊' , '1990-01-20' , '女');
2.课程表
create table Course(CID varchar(10),Cname nvarchar(10),TID varchar(10));
insert into Course values('01' , '语文' , '02');
insert into Course values('02' , '数学' , '01');
insert into Course values('03' , '英语' , '03');
3.教师表
create table Teacher(TID varchar(10),Tname nvarchar(10));
insert into Teacher values('01' , '张三');
insert into Teacher values('02' , '李四');
insert into Teacher values('03' , '王五');
4.成绩表
create table SC(SID varchar(10),CID varchar(10),score decimal(18,1));
insert into SC values('01' , '01' , 80);
insert into SC values('01' , '02' , 90);
insert into SC values('01' , '03' , 99);
insert into SC values('02' , '01' , 70);
insert into SC values('02' , '02' , 60);
insert into SC values('02' , '03' , 80);
insert into SC values('03' , '01' , 80);
insert into SC values('03' , '02' , 80);
insert into SC values('03' , '03' , 80);
insert into SC values('04' , '01' , 50);
insert into SC values('04' , '02' , 30);
insert into SC values('04' , '03' , 20);
insert into SC values('05' , '01' , 76);
insert into SC values('05' , '02' , 87);
insert into SC values('06' , '01' , 31);
insert into SC values('06' , '03' , 34);
insert into SC values('07' , '02' , 89);
insert into SC values('07' , '03' , 98);
--1、查询"01"课程比"02"课程成绩高的学生的信息及课程分数--1.1、查询同时存在"01"课程和"02"课程的情况
select a.* , b.score 课程01的分数,c.score 课程02的分数 from Student a , SC b , SC c
where a.SID = b.SID and a.SID = c.SID and b.CID = '01' and c.CID = '02' and b.score > c.score
--1.2、查询同时存在"01"课程和"02"课程的情况和存在"01"课程但可能不存在"02"课程的情况(不存在时显示为null)(以下存在相同内容时不再解释)
select a.* , b.score 课程01的分数,c.score 课程02的分数 from Student a
left join SC b on a.SID = b.SID and b.CID = '01'
left join SC c on a.SID = c.SID and c.CID = '02'
where b.score > isnull(c.score)
--2、查询"01"课程比"02"课程成绩低的学生的信息及课程分数--2.1、查询同时存在"01"课程和"02"课程的情况
select a.* , b.score 课程01的分数 ,c.score 课程02的分数 from Student a , SC b , SC c
where a.SID = b.SID and a.SID = c.SID and b.CID = '01' and c.CID = '02' and b.score < c.score
--2.2、查询同时存在"01"课程和"02"课程的情况和不存在"01"课程但存在"02"课程的情况
select a.* , b.score 课程01的分数 ,c.score 课程02的分数 from Student a
left join SC b on a.SID = b.SID and b.CID = '01'
left join SC c on a.SID = c.SID and c.CID = '02'
where isnull(b.score,0) < c.score
--3、查询平均成绩大于等于60分的同学的学生编号和学生姓名和平均成绩
select a.SID , a.Sname , cast(avg(b.score) as decimal(18,2)) avg_score
from Student a , sc b
where a.SID = b.SID
group by a.SID , a.Sname
having cast(avg(b.score) as decimal(18,2)) >= 60
order by a.SID
--4、查询平均成绩小于60分的同学的学生编号和学生姓名和平均成绩--4.1、查询在sc表存在成绩的学生信息的SQL语句。
select a.SID , a.Sname , cast(avg(b.score) as decimal(18,2)) avg_score
from Student a , sc b
where a.SID = b.SID
group by a.SID , a.Sname
having cast(avg(b.score) as decimal(18,2)) < 60
order by a.SID
--4.2、查询在sc表中不存在成绩的学生信息的SQL语句。
select a.SID , a.Sname , isnull(cast(avg(b.score) as decimal(18,2)),0) avg_score
from Student a left join sc b
on a.SID = b.SID
group by a.SID , a.Sname
having isnull(cast(avg(b.score) as decimal(18,2)),0) < 60
order by a.SID
--5、查询所有同学的学生编号、学生姓名、选课总数、所有课程的总成绩--5.1、查询所有有成绩的SQL。
select a.SID 学生编号 , a.Sname 学生姓名 , count(b.CID) 选课总数, sum(score) 所有课程的总成绩
from Student a , SC b
where a.SID = b.SID
group by a.SID,a.Sname
order by a.SID
--5.2、查询所有(包括有成绩和无成绩)的SQL。
select a.SID 学生编号 , a.Sname 学生姓名 , count(b.CID) 选课总数, sum(score) 所有课程的总成绩
from Student a left join SC b
on a.SID = b.SID
group by a.SID,a.Sname
order by a.SID
--6、查询"李"姓老师的数量
--方法1
select count(Tname) 李姓老师的数量 from Teacher where Tname like '李%'
--方法2
select count(Tname) 李姓老师的数量 from Teacher where left(Tname,1) = '李'
--7、查询学过"张三"老师授课的同学的信息
select distinct Student.* from Student , SC , Course , Teacher
where Student.SID = SC.SID and SC.CID = Course.CID and Course.TID = Teacher.TID and Teacher.Tname = '张三'
order by Student.SID
--8、查询没学过"张三"老师授课的同学的信息
select m.* from Student m where SID not in (select distinct SC.SID from SC , Course , Teacher where SC.CID = Course.CID and Course.TID = Teacher.TID and Teacher.Tname = '张三') order by m.SID
--9、查询学过编号为"01"并且也学过编号为"02"的课程的同学的信息
--方法1
select Student.* from Student , SC where Student.SID = SC.SID and SC.CID = '01' and exists (Select 1 from SC SC_2 where SC_2.SID = SC.SID and SC_2.CID = '02') order by Student.SID
--方法2
select Student.* from Student , SC where Student.SID = SC.SID and SC.CID = '02' and exists (Select 1 from SC SC_2 where SC_2.SID = SC.SID and SC_2.CID = '01') order by Student.SID
--方法3
select m.* from Student m where SID in
(
select SID from
(
select distinct SID from SC where CID = '01'
union all
select distinct SID from SC where CID = '02'
) t group by SID having count(1) = 2
)
order by m.SID
--10、查询学过编号为"01"但是没有学过编号为"02"的课程的同学的信息
--方法1
select Student.* from Student , SC where Student.SID = SC.SID and SC.CID = '01' and not exists (Select 1 from SC SC_2 where SC_2.SID = SC.SID and SC_2.CID = '02') order by Student.SID
--方法2
select Student.* from Student , SC where Student.SID = SC.SID and SC.CID = '01' and Student.SID not in (Select SC_2.SID from SC SC_2 where SC_2.SID = SC.SID and SC_2.CID = '02') order by Student.SID
--11、查询没有学全所有课程的同学的信息
--11.1、
select Student.*
from Student , SC
where Student.SID = SC.SID
group by Student.SID , Student.Sname , Student.Sage , Student.Ssex having count(CID) < (select count(CID) from Course)
--11.2
select Student.*
from Student left join SC
on Student.SID = SC.SID
group by Student.SID , Student.Sname , Student.Sage , Student.Ssex having count(CID) < (select count(CID) from Course)
--12、查询至少有一门课与学号为"01"的同学所学相同的同学的信息
select distinct Student.* from Student , SC where Student.SID = SC.SID and SC.CID in (select CID from SC where SID = '01') and Student.SID <> '01'
--13、查询和"01"号的同学学习的课程完全相同的其他同学的信息
select Student.* from Student where SID in
(select distinct SC.SID from SC where SID <> '01' and SC.CID in (select distinct CID from SC where SID = '01')
group by SC.SID having count(1) = (select count(1) from SC where SID='01'))
--14、查询没学过"张三"老师讲授的任一门课程的学生姓名
select student.* from student where student.SID not in
(select distinct sc.SID from sc , course , teacher where sc.CID = course.CID and course.TID = teacher.TID and teacher.tname = '张三')
order by student.SID
--15、查询两门及其以上不及格课程的同学的学号,姓名及其平均成绩
select student.SID , student.sname , cast(avg(score) as decimal(18,2)) avg_score from student , sc
where student.SID = SC.SID and student.SID in (select SID from SC where score < 60 group by SID having count(1) >= 2)
group by student.SID , student.sname
--16、检索"01"课程分数小于60,按分数降序排列的学生信息
select student.* , sc.CID , sc.score from student , sc
where student.SID = SC.SID and sc.score < 60 and sc.CID = '01'
order by sc.score desc
--17、按平均成绩从高到低显示所有学生的所有课程的成绩以及平均成绩--17.1 SQL 2000 静态
select a.SID 学生编号 , a.Sname 学生姓名 ,
max(case c.Cname when '语文' then b.score else null end) 语文 ,
max(case c.Cname when '数学' then b.score else null end) 数学 ,
max(case c.Cname when '英语' then b.score else null end) 英语 ,
cast(avg(b.score) as decimal(18,2)) 平均分
from Student a
left join SC b on a.SID = b.SID
left join Course c on b.CID = c.CID
group by a.SID , a.Sname
order by 平均分 desc
--17.2 SQL 2000 动态
declare @sql nvarchar(4000)
set @sql = 'select a.SID ' + '学生编号' + ' , a.Sname ' + '学生姓名'
select @sql = @sql + ',max(case c.Cname when '''+Cname+''' then b.score else null end) '+Cname+' '
from (select distinct Cname from Course) as t
set @sql = @sql + ' , cast(avg(b.score) as decimal(18,2)) ' + '平均分' + ' from Student a left join SC b on a.SID = b.SID left join Course c on b.CID = c.CID
group by a.SID , a.Sname order by ' + '平均分' + ' desc'
exec(@sql)
--18、查询各科成绩最高分、最低分和平均分:以如下形式显示:课程ID,课程name,最高分,最低分,平均分,及格率,中等率,优良率,优秀率
--及格为>=60,中等为:70-80,优良为:80-90,优秀为:>=90
--方法1
select m.CID 课程编号 , m.Cname 课程名称 ,
max(n.score) 最高分 ,
min(n.score) 最低分 ,
cast(avg(n.score) as decimal(18,2)) 平均分 ,
cast((select count(1) from SC where CID = m.CID and score >= 60)*100.0 / (select count(1) from SC where CID = m.CID) as decimal(18,2)) 及格率 ,
cast((select count(1) from SC where CID = m.CID and score >= 70 and score < 80 )*100.0 / (select count(1) from SC where CID = m.CID) as decimal(18,2)) 中等率 ,
cast((select count(1) from SC where CID = m.CID and score >= 80 and score < 90 )*100.0 / (select count(1) from SC where CID = m.CID) as decimal(18,2)) 优良率 ,
cast((select count(1) from SC where CID = m.CID and score >= 90)*100.0 / (select count(1) from SC where CID = m.CID) as decimal(18,2)) 优秀率
from Course m , SC n
where m.CID = n.CID
group by m.CID , m.Cname
order by m.CID
--方法2
select m.CID 课程编号 , m.Cname 课程名称 ,
(select max(score) from SC where CID = m.CID) 最高分 ,
(select min(score) from SC where CID = m.CID) 最低分 ,
(select cast(avg(score) as decimal(18,2)) from SC where CID = m.CID) 平均分 ,
cast((select count(1) from SC where CID = m.CID and score >= 60)*100.0 / (select count(1) from SC where CID = m.CID) as decimal(18,2)) 及格率,
cast((select count(1) from SC where CID = m.CID and score >= 70 and score < 80 )*100.0 / (select count(1) from SC where CID = m.CID) as decimal(18,2)) 中等率 ,
cast((select count(1) from SC where CID = m.CID and score >= 80 and score < 90 )*100.0 / (select count(1) from SC where CID = m.CID) as decimal(18,2)) 优良率 ,
cast((select count(1) from SC where CID = m.CID and score >= 90)*100.0 / (select count(1) from SC where CID = m.CID) as decimal(18,2)) 优秀率
from Course m
order by m.CID
--19、按各科成绩进行排序,并显示排名--19.1 sql 2000用子查询完成
--Score重复时保留名次空缺
select t.* , px = (select count(1) from SC where CID = t.CID and score > t.score) + 1 from sc t order by t.cid , px
--Score重复时合并名次
select t.* , px = (select count(distinct score) from SC where CID = t.CID and score >= t.score) from sc t order by t.cid , px
--19.2 sql 2005用rank,DENSE_RANK完成
--Score重复时保留名次空缺(rank完成)
select t.* , px = rank() over(partition by cid order by score desc) from sc t order by t.CID , px
--Score重复时合并名次(DENSE_RANK完成)
select t.* , px = DENSE_RANK() over(partition by cid order by score desc) from sc t order by t.CID , px
--20、查询学生的总成绩并进行排名--20.1 查询学生的总成绩
select m.SID 学生编号 ,
m.Sname 学生姓名 ,
isnull(sum(score),0) 总成绩
from Student m left join SC n on m.SID = n.SID
group by m.SID , m.Sname
order by 总成绩 desc
--20.2 查询学生的总成绩并进行排名,sql 2000用子查询完成,分总分重复时保留名次空缺和不保留名次空缺两种。
select t1.* , px = (select count(1) from
(
select m.SID 学生编号 ,
m.Sname 学生姓名 ,
isnull(sum(score),0) 总成绩
from Student m left join SC n on m.SID = n.SID
group by m.SID , m.Sname
) t2 where 总成绩 > t1.总成绩) + 1 from
(
select m.SID 学生编号 ,
m.Sname 学生姓名 ,
isnull(sum(score),0) 总成绩
from Student m left join SC n on m.SID = n.SID
group by m.SID , m.Sname
) t1
order by px
select t1.* , px = (select count(distinct 总成绩) from
(
select m.SID 学生编号 ,
m.Sname 学生姓名 ,
isnull(sum(score),0) 总成绩
from Student m left join SC n on m.SID = n.SID
group by m.SID , m.Sname
) t2 where 总成绩 >= t1.总成绩) from
(
select m.SID 学生编号 ,
m.Sname 学生姓名 ,
isnull(sum(score),0) 总成绩
from Student m left join SC n on m.SID = n.SID
group by m.SID , m.Sname
) t1
order by px
--20.3 查询学生的总成绩并进行排名,sql 2005用rank,DENSE_RANK完成,分总分重复时保留名次空缺和不保留名次空缺两种。
select t.* , px = rank() over(order by 总成绩 desc) from
(
select m.SID 学生编号 ,
m.Sname 学生姓名 ,
isnull(sum(score),0) 总成绩
from Student m left join SC n on m.SID = n.SID
group by m.SID , m.Sname
) t
order by px
select t.* , px = DENSE_RANK() over(order by 总成绩 desc) from
(
select m.SID 学生编号 ,
m.Sname 学生姓名 ,
isnull(sum(score),0) 总成绩
from Student m left join SC n on m.SID = n.SID
group by m.SID , m.Sname
) t
order by px
--21、查询不同老师所教不同课程平均分从高到低显示
select m.TID , m.Tname , cast(avg(o.score) as decimal(18,2)) avg_score
from Teacher m , Course n , SC o
where m.TID = n.TID and n.CID = o.CID
group by m.TID , m.Tname
order by avg_score desc
--22、查询所有课程的成绩第2名到第3名的学生信息及该课程成绩--22.1 sql 2000用子查询完成
--Score重复时保留名次空缺
select from (select t. , px = (select count(1) from SC where CID = t.CID and score > t.score) + 1 from sc t) m where px between 2 and 3 order by m.cid , m.px
--Score重复时合并名次
select from (select t. , px = (select count(distinct score) from SC where CID = t.CID and score >= t.score) from sc t) m where px between 2 and 3 order by m.cid , m.px
--22.2 sql 2005用rank,DENSE_RANK完成
--Score重复时保留名次空缺(rank完成)
select from (select t. , px = rank() over(partition by cid order by score desc) from sc t) m where px between 2 and 3 order by m.CID , m.px
--Score重复时合并名次(DENSE_RANK完成)
select from (select t. , px = DENSE_RANK() over(partition by cid order by score desc) from sc t) m where px between 2 and 3 order by m.CID , m.px
--23、统计各科成绩各分数段人数:课程编号,课程名称, 100-85 , 85-70 , 70-60 , 0-60 及所占百分比 --23.1 统计各科成绩各分数段人数:课程编号,课程名称, 100-85 , 85-70 , 70-60 , 0-60
--横向显示
select Course.CID 课程编号 , Cname as 课程名称 ,
sum(case when score >= 85 then 1 else 0 end) 85-100 ,
sum(case when score >= 70 and score < 85 then 1 else 0 end) 70-85 ,
sum(case when score >= 60 and score < 70 then 1 else 0 end) 60-70 ,
sum(case when score < 60 then 1 else 0 end) 0-60
from sc , Course
where SC.CID = Course.CID
group by Course.CID , Course.Cname
order by Course.CID
--纵向显示1(显示存在的分数段)
select m.CID 课程编号 , m.Cname 课程名称 , 分数段 = (
case when n.score >= 85 then '85-100'
when n.score >= 70 and n.score < 85 then '70-85'
when n.score >= 60 and n.score < 70 then '60-70'
else '0-60'
end) ,
count(1) 数量
from Course m , sc n
where m.CID = n.CID
group by m.CID , m.Cname , (
case when n.score >= 85 then '85-100'
when n.score >= 70 and n.score < 85 then '70-85'
when n.score >= 60 and n.score < 70 then '60-70'
else '0-60'
end)
order by m.CID , m.Cname , 分数段
--纵向显示2(显示存在的分数段,不存在的分数段用0显示)
select m.CID 课程编号 , m.Cname 课程名称 , 分数段 = (
case when n.score >= 85 then '85-100'
when n.score >= 70 and n.score < 85 then '70-85'
when n.score >= 60 and n.score < 70 then '60-70'
else '0-60'
end) ,
count(1) 数量
from Course m , sc n
where m.CID = n.CID
group by all m.CID , m.Cname , (
case when n.score >= 85 then '85-100'
when n.score >= 70 and n.score < 85 then '70-85'
when n.score >= 60 and n.score < 70 then '60-70'
else '0-60'
end)
order by m.CID , m.Cname , 分数段
--23.2 统计各科成绩各分数段人数:课程编号,课程名称, 100-85 , 85-70 , 70-60 , <60 及所占百分比
--横向显示
select m.CID 课程编号, m.Cname 课程名称,
(select count(1) from SC where CID = m.CID and score < 60) 0-60 ,
cast((select count(1) from SC where CID = m.CID and score < 60)*100.0 / (select count(1) from SC where CID = m.CID) as decimal(18,2)) 百分比 ,
(select count(1) from SC where CID = m.CID and score >= 60 and score < 70) 60-70 ,
cast((select count(1) from SC where CID = m.CID and score >= 60 and score < 70)*100.0 / (select count(1) from SC where CID = m.CID) as decimal(18,2)) 百分比 ,
(select count(1) from SC where CID = m.CID and score >= 70 and score < 85) 70-85 ,
cast((select count(1) from SC where CID = m.CID and score >= 70 and score < 85)*100.0 / (select count(1) from SC where CID = m.CID) as decimal(18,2)) 百分比 ,
(select count(1) from SC where CID = m.CID and score >= 85) 85-100 ,
cast((select count(1) from SC where CID = m.CID and score >= 85)*100.0 / (select count(1) from SC where CID = m.CID) as decimal(18,2)) 百分比
from Course m
order by m.CID
--纵向显示1(显示存在的分数段)
select m.CID 课程编号 , m.Cname 课程名称 , 分数段 = (
case when n.score >= 85 then '85-100'
when n.score >= 70 and n.score < 85 then '70-85'
when n.score >= 60 and n.score < 70 then '60-70'
else '0-60'
end) ,
count(1) 数量 ,
cast(count(1) * 100.0 / (select count(1) from sc where CID = m.CID) as decimal(18,2)) 百分比
from Course m , sc n
where m.CID = n.CID
group by m.CID , m.Cname , (
case when n.score >= 85 then '85-100'
when n.score >= 70 and n.score < 85 then '70-85'
when n.score >= 60 and n.score < 70 then '60-70'
else '0-60'
end)
order by m.CID , m.Cname , 分数段
--纵向显示2(显示存在的分数段,不存在的分数段用0显示)
select m.CID 课程编号 , m.Cname 课程名称 , 分数段 = (
case when n.score >= 85 then '85-100'
when n.score >= 70 and n.score < 85 then '70-85'
when n.score >= 60 and n.score < 70 then '60-70'
else '0-60'
end) ,
count(1) 数量 ,
cast(count(1) * 100.0 / (select count(1) from sc where CID = m.CID) as decimal(18,2)) 百分比
from Course m , sc n
where m.CID = n.CID
group by all m.CID , m.Cname , (
case when n.score >= 85 then '85-100'
when n.score >= 70 and n.score < 85 then '70-85'
when n.score >= 60 and n.score < 70 then '60-70'
else '0-60'
end)
order by m.CID , m.Cname , 分数段
--24、查询学生平均成绩及其名次--24.1 查询学生的平均成绩并进行排名,sql 2000用子查询完成,分平均成绩重复时保留名次空缺和不保留名次空缺两种。
select t1.* , px = (select count(1) from
(
select m.SID 学生编号 ,
m.Sname 学生姓名 ,
isnull(cast(avg(score) as decimal(18,2)),0) 平均成绩
from Student m left join SC n on m.SID = n.SID
group by m.SID , m.Sname
) t2 where 平均成绩 > t1.平均成绩) + 1 from
(
select m.SID 学生编号 ,
m.Sname 学生姓名 ,
isnull(cast(avg(score) as decimal(18,2)),0) 平均成绩
from Student m left join SC n on m.SID = n.SID
group by m.SID , m.Sname
) t1
order by px
select t1.* , px = (select count(distinct 平均成绩) from
(
select m.SID 学生编号 ,
m.Sname 学生姓名 ,
isnull(cast(avg(score) as decimal(18,2)),0) 平均成绩
from Student m left join SC n on m.SID = n.SID
group by m.SID , m.Sname
) t2 where 平均成绩 >= t1.平均成绩) from
(
select m.SID 学生编号 ,
m.Sname 学生姓名 ,
isnull(cast(avg(score) as decimal(18,2)),0) 平均成绩
from Student m left join SC n on m.SID = n.SID
group by m.SID , m.Sname
) t1
order by px
--24.2 查询学生的平均成绩并进行排名,sql 2005用rank,DENSE_RANK完成,分平均成绩重复时保留名次空缺和不保留名次空缺两种。
select t.* , px = rank() over(order by 平均成绩 desc) from
(
select m.SID 学生编号 ,
m.Sname 学生姓名 ,
isnull(cast(avg(score) as decimal(18,2)),0) 平均成绩
from Student m left join SC n on m.SID = n.SID
group by m.SID , m.Sname
) t
order by px
select t.* , px = DENSE_RANK() over(order by 平均成绩 desc) from
(
select m.SID 学生编号 ,
m.Sname 学生姓名 ,
isnull(cast(avg(score) as decimal(18,2)),0) 平均成绩
from Student m left join SC n on m.SID = n.SID
group by m.SID , m.Sname
) t
order by px
--25、查询各科成绩前三名的记录--25.1 分数重复时保留名次空缺
select m.* , n.CID , n.score from Student m, SC n where m.SID = n.SID and n.score in
(select top 3 score from sc where CID = n.CID order by score desc) order by n.CID , n.score desc
--25.2 分数重复时不保留名次空缺,合并名次
--sql 2000用子查询实现
select from (select t. , px = (select count(distinct score) from SC where CID = t.CID and score >= t.score) from sc t) m where px between 1 and 3 order by m.Cid , m.px
--sql 2005用DENSE_RANK实现
select from (select t. , px = DENSE_RANK() over(partition by Cid order by score desc) from sc t) m where px between 1 and 3 order by m.CID , m.px
--26、查询每门课程被选修的学生数
select Cid , count(SID) 学生数 from sc group by CID
--27、查询出只有两门课程的全部学生的学号和姓名
select Student.SID , Student.Sname
from Student , SC
where Student.SID = SC.SID
group by Student.SID , Student.Sname
having count(SC.CID) = 2
order by Student.SID
--28、查询男生、女生人数
select count(Ssex) as 男生人数 from Student where Ssex = N'男'
select count(Ssex) as 女生人数 from Student where Ssex = N'女'
select sum(case when Ssex = N'男' then 1 else 0 end) 男生人数 ,sum(case when Ssex = N'女' then 1 else 0 end) 女生人数 from student
select case when Ssex = N'男' then N'男生人数' else N'女生人数' end 男女情况 , count(1) 人数 from student group by case when Ssex = N'男' then N'男生人数' else N'女生人数' end
--29、查询名字中含有"风"字的学生信息
select * from student where sname like N'%风%'
select * from student where charindex(N'风' , sname) > 0
--30、查询同名同性学生名单,并统计同名人数
select Sname 学生姓名 , count() 人数 from Student group by Sname having count() > 1
--31、查询1990年出生的学生名单(注:Student表中Sage列的类型是datetime)
select * from Student where year(sage) = 1990
select * from Student where datediff(yy,sage,'1990-01-01') = 0
select * from Student where datepart(yy,sage) = 1990
select * from Student where convert(varchar(4),sage,120) = '1990'
--32、查询每门课程的平均成绩,结果按平均成绩降序排列,平均成绩相同时,按课程编号升序排列
select m.CID , m.Cname , cast(avg(n.score) as decimal(18,2)) avg_score
from Course m, SC n
where m.CID = n.CID
group by m.CID , m.Cname
order by avg_score desc, m.CID asc
--33、查询平均成绩大于等于85的所有学生的学号、姓名和平均成绩
select a.SID , a.Sname , cast(avg(b.score) as decimal(18,2)) avg_score
from Student a , sc b
where a.SID = b.SID
group by a.SID , a.Sname
having cast(avg(b.score) as decimal(18,2)) >= 85
order by a.SID
--34、查询课程名称为"数学",且分数低于60的学生姓名和分数
select sname , score
from Student , SC , Course
where SC.SID = Student.SID and SC.CID = Course.CID and Course.Cname = N'数学' and score < 60
--35、查询所有学生的课程及分数情况
select Student.* , Course.Cname , SC.CID , SC.score
from Student, SC , Course
where Student.SID = SC.SID and SC.CID = Course.CID
order by Student.SID , SC.CID
--36、查询任何一门课程成绩在70分以上的姓名、课程名称和分数
select Student.* , Course.Cname , SC.CID , SC.score
from Student, SC , Course
where Student.SID = SC.SID and SC.CID = Course.CID and SC.score >= 70
order by Student.SID , SC.CID
--37、查询不及格的课程
select Student.* , Course.Cname , SC.CID , SC.score
from Student, SC , Course
where Student.SID = SC.SID and SC.CID = Course.CID and SC.score < 60
order by Student.SID , SC.CID
--38、查询课程编号为01且课程成绩在80分以上的学生的学号和姓名
select Student.* , Course.Cname , SC.CID , SC.score
from Student, SC , Course
where Student.SID = SC.SID and SC.CID = Course.CID and SC.CID = '01' and SC.score >= 80
order by Student.SID , SC.CID
--39、求每门课程的学生人数
select Course.CID , Course.Cname , count(*) 学生人数
from Course , SC
where Course.CID = SC.CID
group by Course.CID , Course.Cname
order by Course.CID , Course.Cname
--40、查询选修"张三"老师所授课程的学生中,成绩最高的学生信息及其成绩--40.1 当最高分只有一个时
select top 1 Student.* , Course.Cname , SC.CID , SC.score
from Student, SC , Course , Teacher
where Student.SID = SC.SID and SC.CID = Course.CID and Course.TID = Teacher.TID and Teacher.Tname = N'张三'
order by SC.score desc
--40.2 当最高分出现多个时
select Student.* , Course.Cname , SC.CID , SC.score
from Student, SC , Course , Teacher
where Student.SID = SC.SID and SC.CID = Course.CID and Course.TID = Teacher.TID and Teacher.Tname = N'张三' and
SC.score = (select max(SC.score) from SC , Course , Teacher where SC.CID = Course.CID and Course.TID = Teacher.TID and Teacher.Tname = N'张三')
--41、查询不同课程成绩相同的学生的学生编号、课程编号、学生成绩
--方法1
select m.* from SC m ,(select CID , score from SC group by CID , score having count(1) > 1) n
where m.CID= n.CID and m.score = n.score order by m.CID , m.score , m.SID
--方法2
select m.* from SC m where exists (select 1 from (select CID , score from SC group by CID , score having count(1) > 1) n
where m.CID= n.CID and m.score = n.score) order by m.CID , m.score , m.SID
--42、查询每门功成绩最好的前两名
select t.* from sc t where score in (select top 2 score from sc where CID = T.CID order by score desc) order by t.CID , t.score desc
--43、统计每门课程的学生选修人数(超过5人的课程才统计)。要求输出课程号和选修人数,查询结果按人数降序排列,若人数相同,按课程号升序排列
select Course.CID , Course.Cname , count(*) 学生人数
from Course , SC
where Course.CID = SC.CID
group by Course.CID , Course.Cname
having count(*) >= 5
order by 学生人数 desc , Course.CID
--44、检索至少选修两门课程的学生学号
select student.SID , student.Sname
from student , SC
where student.SID = SC.SID
group by student.SID , student.Sname
having count(1) >= 2
order by student.SID
--45、查询选修了全部课程的学生信息
--方法1 根据数量来完成
select student.* from student where SID in
(select SID from sc group by SID having count(1) = (select count(1) from course))
--方法2 使用双重否定来完成
select t.* from student t where t.SID not in
(
select distinct m.SID from
(
select SID , CID from student , course
) m where not exists (select 1 from sc n where n.SID = m.SID and n.CID = m.CID)
)
--方法3 使用双重否定来完成
select t.* from student t where not exists(select 1 from
(
select distinct m.SID from
(
select SID , CID from student , course
) m where not exists (select 1 from sc n where n.SID = m.SID and n.CID = m.CID)
) k where k.SID = t.SID
)
--46、查询各学生的年龄--46.1 只按照年份来算
select * , datediff(yy , sage , getdate()) 年龄 from student
--46.2 按照出生日期来算,当前月日 < 出生年月的月日则,年龄减一
select * , case when right(convert(varchar(10),getdate(),120),5) < right(convert(varchar(10),sage,120),5) then datediff(yy , sage , getdate()) - 1 else datediff(yy , sage , getdate()) end 年龄 from student
--47、查询本周过生日的学生
select * from student where datediff(week,datename(yy,getdate()) + right(convert(varchar(10),sage,120),6),getdate()) = 0
--48、查询下周过生日的学生
select * from student where datediff(week,datename(yy,getdate()) + right(convert(varchar(10),sage,120),6),getdate()) = -1
--49、查询本月过生日的学生
select * from student where datediff(mm,datename(yy,getdate()) + right(convert(varchar(10),sage,120),6),getdate()) = 0
--50、查询下月过生日的学生
select * from student where datediff(mm,datename(yy,getdate()) + right(convert(varchar(10),sage,120),6),getdate()) = -1
2.非关系型数据库通常指数据以对象的形式存储在数据库中,而对象之间的关系通过每个对象自身的属性来决定 ---常见的有:MongoDb、redis
· InnoDB:用于事务处理应用程序,具有众多特性,包括ACID事务支持。(提供行级锁)
· BDB:可替代InnoDB的事务引擎,支持COMMIT、ROLLBACK和其他事务特性。
· Memory:将所有数据保存在RAM中,在需要快速查找引用和其他类似数据的环境下,可提供极快的访问。
· Merge:允许MySQL DBA或开发人员将一系列等同的MyISAM表以逻辑方式组合在一起,并作为1个对象引用它们。对于诸如数据仓储等VLDB环境十分适合。
· Archive:为大量很少引用的历史、归档、或安全审计信息的存储和检索提供了完美的解决方案。
· Federated:能够将多个分离的MySQL服务器链接起来,从多个物理服务器创建一个逻辑数据库。十分适合于分布式环境或数据集市环境。
· Cluster/NDB:MySQL的簇式数据库引擎,尤其适合于具有高性能查找要求的应用程序,这类查找需求还要求具有最高的正常工作时间和可用性。 · Other:其他存储引擎包括CSV(引用由逗号隔开的用作数据库表的文件),Blackhole(用于临时禁止对数据库的应用程序输入),以及Example引擎(可为快速创建定制的插件式存储引擎提供帮助)。
一般来说不使用事务的话,请使用MyISAM引擎,使用事务的话,一般使用InnoDB ------仙子啊学习现在我们用的主要是InnoDB 注意,通过更改STORAGE_ENGINE配置变量,能够方便地更改MySQL服务器的默认存储引擎。
什么是三大范式:
第一范式:当关系模式R的所有属性都不能在分解为更基本的数据单位时,称R是满足第一范式的,简记为1NF。满足第一范式是关系模式规范化的最低要 求,否则,将有很多基本操作在这样的关系模式中实现不了。
第二范式:如果关系模式R满足第一范式,并且R得所有非主属性都完全依赖于R的每一个候选关键属性,称R满足第二范式,简记为2NF。
第三范式:设R是一个满足第一范式条件的关系模式,X是R的任意属性集,如果X非传递依赖于R的任意一个候选关键字,称R满足第三范式,简记为3NF. 注:关系实质上是一张二维表,其中每一行是一个元组,每一列是一个属性
事务具体四大特性,也就是经常说的ACID :
1.原子性(所有操作要么全部成功,要么全部失败回滚)
2.一致性(事务执行之前和执行之后都必须处于一致性状态。)
3.隔离性(数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离)
4.持久性(一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即使遭遇故障依然能够通过日志恢复最后一次更新) 在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务
MYSQL 事务处理主要有两种方法:
1、用 BEGIN, ROLLBACK, COMMIT来实现 BEGIN 开始一个事务 ROLLBACK 事务回滚 COMMIT 事务确认
2、直接用 SET 来改变 MySQL 的自动提交模式: SET AUTOCOMMIT=0 禁止自动提交 SET AUTOCOMMIT=1 开启自动提交
多对多:学生与课程---一个学生可以选择多个课程,一个课程也可以被多个学生选择
create table product
(id primary key auto_increment,
pname varchar(64),
pcount int);
create trigger trigger_name
after/before insert /update/delete on 表名
for each row
begin
sql语句:(触发的语句一句或多句)
end
函数:MySQL中提供了许多内置函数,还可以自定义函数(实现程序员需要sql逻辑处理)
自定义函数创建语法:
创建:CREATE FUNCTION 函数名称(参数列表)
RETURNS 返回值类型 函数体
修改: ALTER FUNCTION 函数名称 [characteristic ...]
删除:DROP FUNCTION [IF EXISTS] 函数名称
调用:SELECT 函数名称(参数列表)
视图:视图是由查询结果形成的一张虚拟表,是表通过某种运算得到的一个投影
create view view_name as select 语句
存储过程:把一段代码封装起来,当要执行这一段代码的时候,可以通过调用该存储过程来实现(经过第一次编译后再次调用不需要再次编译,比一个个执行sql语句效率高)
create procedure 存储过程名(参数,参数,…)
begin
//代码
end
MySQL索引的类型:
1. 普通索引:这是最基本的索引,它没有任何限制
2.唯一索引:索引列的值必须唯一,但允许有空值,如果是组合索引,则列值的组合必须唯一
3.全文索引:全文索引仅可用于 MyISAM 表,可以从CHAR、VARCHAR或TEXT列中作为CREATE TABLE语句的一部分被创建,或是随后使用ALTER TABLE 或CREATE INDEX被添加 (切记对于大容量的数据表,生成全文索引是一个非常消耗时间非常消耗硬盘空间的做法)
4. 单列索引、多列索引:多个单列索引与单个多列索引的查询效果不同,因为执行查询时,MySQL只能使用一个索引,会从多个索引中选择一个限制最为严格的索引。
5.组合索引(最左前缀):简单的理解就是只从最左面的开始组合(实在单列索引的基础上进一步压榨索引效率的一种方式)
2.外键用于与另一张表的关联,是能确定另一张表记录的字段,用于保持数据的一致性
AVG(col)返回指定列的平均值
COUNT(col)返回指定列中非NULL值的个数
MIN(col)返回指定列的最小值
MAX(col)返回指定列的最大值
SUM(col)返回指定列的所有值之和
GROUP_CONCAT(col) 返回由属于一组的列值连接组合而成的结果
数学函数:
ABS(x) 返回x的绝对值
BIN(x) 返回x的二进制(OCT返回八进制,HEX返回十六进制)
2.对于多列索引,不是使用的第一部分,则不会使用索引
3.like查询是以%开头
4.如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引
5.如果mysql估计使用全表扫描要比使用索引快,则不使用索引
6 对小表查询
7 提示不使用索引
8 统计数据不真实
9.单独引用复合索引里非第一位置的索引列.
2 修改配置文件( 放在[mysqld]下),重启 long_query_time 查询超过多少秒才记录
3 测试是否成功
4 慢查询日志文件的信息格式
mysqldump -u用户名 -p密码 数据库名 > 导出的文件名
例如:C:Usersjack> mysqldump -uroot -pmysql sva_rec > e:sva_rec.sql
2.导出一个表,包括表结构和数据
mysqldump -u用户名 -p 密码 数据库名 表名> 导出的文件名
例如:C:Usersjack> mysqldump -uroot -pmysql sva_rec date_rec_drv> e:date_rec_drv.sql
3.导出一个数据库结构
例如:C:Usersjack> mysqldump -uroot -pmysql -d sva_rec > e:sva_rec.sql
4.导出一个表,只有表结构
mysqldump -u用户名 -p 密码 -d数据库名 表名> 导出的文件名
例如:C:Usersjack> mysqldump -uroot -pmysql -d sva_rec date_rec_drv> e:date_rec_drv.sql
5.导入数据库
常用source 命令
进入mysql数据库控制台,
如mysql -u root -p mysql>use 数据库
然后使用source命令,后面参数为脚本文件(如这里用到的.sql)
mysql>source d:wcnc_db.sql
1、选取最适用的字段属性
2、使用连接(JOIN)来代替子查询(Sub-Queries)
3、使用联合(UNION)来代替手动创建的临时表
4、事务(当多个用户同时使用相同的数据源时,它可以利用锁定数据库的方法来为用户提供一种安全的访问方式,这样可以保证用户的操作不被其它的用户所干扰)
5.锁定表(有些情况下我们可以通过锁定表的方法来获得更好的性能)
6、使用外键(锁定表的方法可以维护数据的完整性,但是它却不能保证数据的关联性。这个时候我们就可以使用外键)
7、使用索引
8、优化的查询语句(绝大多数情况下,使用索引可以提高查询的速度,但如果SQL语句使用不恰当的话,索引将无法发挥它应有的作用)
varchar:不定长,存取速度相对慢
到底如何取舍可以根据一下几个方面考虑:
1、对于MyISAM表,尽量使用Char,对于那些经常需要修改而容易形成碎片的myisam和isam数据表就更是如此,它的缺点就是占用磁盘空间;
2、对于InnoDB表,因为它的数据行内部存储格式对固定长度的数据行和可变长度的数据行不加区分(所有数据行共用一个表头部分,这个标头部分存放着指向各有关数据列的指针), 所以使用char类型不见得会比使用varchar类型好。事实上,因为char类型通常要比varchar类型占用更多的空间,所以从减少空间占用量和减少磁盘i/o的角度,使用varchar类型反而更有利;
3、存储很短的信息,比如门牌号码101,201……这样很短的信息应该用char,因为varchar还要占个byte用于存储信息长度,本来打算节约存储的现在得不偿失。
4、固定长度的。比如使用uuid作为主键,那用char应该更合适。因为他固定长度,varchar动态根据长度的特性就消失了,而且还要占个长度信息。
5、十分频繁改变的column。因为varchar每次存储都要有额外的计算,得到长度等工作,如果一个非常频繁改变的,那就要有很多的精力用于计算,而这些对于char来说是不需要的。
这个功能有局限性,并不总会说出真相,但它的输出是可以获取的最好信息,通过输出结果反推执行过程
select * from tb where name = ‘Oldboy’ limit 1------查找到tb表中所有name = ‘Oldboy’的数据只取其中的第一条
分表
分库
级联函数
建表
1、Dubbo的底层实现原理和机制
–高性能和透明化的RPC远程服务调用方案
–SOA服务治理方案
Dubbo缺省协议采用单一长连接和NIO异步通讯,
适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况
2、描述一个服务从发布到被消费的详细过程
务。首先先获取zk的配置信息,然后获取需要暴露的url,然后调用registry.register方法将url注册到zookeeper上去。
3、分布式系统怎么做服务治理
针对互联网业务的特点,eg 突发的流量高峰、网络延时、机房故障等,重点针对大规模跨机房的海量服务进行运行态治理,保障线上服务的高SLA,满足用户的体验,常用的策略包括限流降级、服务嵌入迁出、服务动态路由和灰度发布等
4、接口的幂等性的概念
幂等的意思是同一个操作,重复执行多次,跟执行一次结果一致。消息幂等,即消息发送操作对于消息消费来说是幂等。也就是相同的消息发送多次,跟发送一次是一样的,这个消息只会被消费一次。
5、消息中间件如何解决消息丢失问题
为了解决消息丢失问题,我们引入了一些重发机制,但也带来的另外一个问题:消息重复,我们来看下都有哪些情况会导致消息重复:
消息发送超时,处于不确定状态,导致重试发送消息,有可能之前的消息已经发送成功,会出现消息重复的情况。解决的思路是,每个消息生成一个消息id,如果发送的消息Broker已经存在了,则丢弃。这种解决办法需要维护一个已经接收的消息的message id list。
消息在Broker中只有一份,但是consumer重启前,未及时更新offset,导致consumer重启之后重复消费消息。
上游业务给每个message 分配一个message ID,下游业务在接收到message之后,执行业务并且保存message ID,而且要讲两部分放到同一个事务中,保证业务执行成功,message ID肯定保存,业务执行失败,message ID肯定不会保存下来,利用db中存储的message id来做幂等。我们可以重新封装producer client和consumer client,将这部分message ID分配和判重的逻辑封装到client lib里面。
6、Dubbo的服务请求失败怎么处理
dubbo启动时默认有重试机制和超时机制。
超时机制的规则是如果在一定的时间内,provider没有返回,则认为本次调用失败,
重试机制在出现调用失败时,会再次调用。如果在配置的调用次数内都失败,则认为此次请求异常,抛出异常。
7、重连机制会不会造成错误
dubbo在调用服务不成功时,默认会重试2次。
Dubbo的路由机制,会把超时的请求路由到其他机器上,而不是本机尝试,所以 dubbo的重试机器也能一定程度的保证服务的质量。
但是如果不合理的配置重试次数,当失败时会进行重试多次,这样在某个时间点出现性能问题,调用方再连续重复调用,
系统请求变为正常值的retries倍,系统压力会大增,容易引起服务雪崩,需要根据业务情况规划好如何进行异常处理,何时进行重试。
8、对分布式事务的理解
本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
事务的ACID特性 原子性 一致性 隔离性 持久性
消息事务+最终一致性
CC提供了一个编程框架,将整个业务逻辑分为三块:Try、Confirm和Cancel三个操作。以在线下单为例,Try阶段会去扣库存,Confirm阶段则是去更新订单状态,如果更新订单失败,则进入Cancel阶段,会去恢复库存。总之,TCC就是通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,复杂度也不一样,因此,这种模式并不能很好地被复用。
9、如何实现负载均衡,有哪些算法可以实现?
经常会用到以下四种算法:随机(random)、轮训(round-robin)、一致哈希(consistent-hash)和主备(master-slave)。
10、Zookeeper的用途,选举的原理是什么?
11、数据的垂直拆分水平拆分。
12、zookeeper原理和适用场景
13、zookeeper watch机制
Znode发生变化(Znode本身的增加,删除,修改,以及子Znode的变化)可以通过Watch机制通知到客户端。那么要实现Watch,就必须实现org.apache.zookeeper.Watcher接口,并且将实现类的对象传入到可以Watch的方法中。Zookeeper中所有读操作(getData(),getChildren(),exists())都可以设置Watch选项。
14、redis/zk节点宕机如何处理
15、分布式集群下如何做到唯一序列号
Redis生成ID 这主要依赖于Redis是单线程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY来实现。
16、如何做一个分布式锁
17、用过哪些MQ,怎么用的,和其他mq比较有什么优缺点,MQ的连接是线程安全的吗
RabbitMQ 支持 AMQP(二进制),STOMP(文本),MQTT(二进制),HTTP(里面包装其他协议)等协议。Kafka 使用自己的协议。
Kafka 自身服务和消费者都需要依赖 Zookeeper。
RabbitMQ 在有大量消息堆积的情况下性能会下降,Kafka不会。毕竟AMQP设计的初衷不是用来持久化海量消息的,而Kafka一开始是用来处理海量日志的。
总的来说,RabbitMQ 和 Kafka 都是十分优秀的分布式的消息代理服务,只要合理部署,不作,基本上可以满足生产条件下的任何需求。
18、MQ系统的数据如何保证不丢失
在数据生产时避免数据丢失的方法:
只要能避免上述两种情况,那么就可以保证消息不会被丢失。
1)就是说在同步模式的时候,确认机制设置为-1,也就是让消息写入leader和所有的副本。
2)还有,在异步模式下,如果消息发出去了,但还没有收到确认的时候,缓冲池满了,在配置文件中设置成不限制阻塞超时的时间,也就说让生产端一直阻塞,这样也能保证数据不会丢失。
在数据消费时,避免数据丢失的方法:如果使用了storm,要开启storm的ackfail机制;如果没有使用storm,确认数据被完成处理之后,再更新offset值。低级API中需要手动控制offset值。
数据重复消费的情况,如果处理
(1)去重:将消息的唯一标识保存到外部介质中,每次消费处理时判断是否处理过;
(2)不管:大数据场景中,报表系统或者日志信息丢失几条都无所谓,不会影响最终的统计分析结
19、列举出你能想到的数据库分库分表策略;分库分表后,如何解决全表查询的问题
业务拆分、主从复制,数据库分库与分表
使用用户ID是最常用的分库的路由策略。用户的ID可以作为贯穿整个系统用的重要字段。因此,使用用户的ID我们不仅可以方便我们的查询
垂直分表
水平分表
20、zookeeper的选举策略
在zookeeper集群中也是一样,每个节点都会投票,如果某个节点获得超过半数以上的节点的投票,则该节点就是leader节点了。
zookeeper中有三种选举算法,分别是LeaderElection,FastLeaderElection,AuthLeaderElection,
FastLeaderElection此算法和LeaderElection不同的是它不会像后者那样在每轮投票中要搜集到所有结果后才统计投票结果,而是不断的统计结果,一旦没有新的影响leader结果的notification出现就返回投票结果。这样的效率更高。
21、全局ID
Snowflake
redis
指事务的每个操作步骤都位于不同的节点上,需要保证事务的 AICD 特性。
SOA 架构,比如一个电商网站将订单业务和库存业务分离出来放到不同的节点上。
支付:买家账户扣款同时卖家账户入账。买家和卖家账户信息不在同一个数据库,因此涉及分布式事务。
两阶段提交协议可以很好得解决分布式事务问题,它可以使用 XA 来实现,XA 它包含两个部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如 Oracle、DB2 这些商业数据库都实现了 XA 接口,而事务管理器作为全局的协调者,负责各个本地资源的提交和回滚。
3.2 消息中间件
消息中间件也可称作消息系统 (MQ),它本质上是一个暂存转发消息的一个中间件。在分布式应用当中,我们可以把一个业务操作转换成一个消息,比如支付宝的余额转如余额宝操作,支付宝系统执行减少余额操作之后向消息系统发一个消息,余额宝系统订阅这条消息然后进行增加账户金额操作。
3.2.1 消息处理模型
点对点
发布/订阅
3.2.2 消息的可靠性
消息的发送端的可靠性:发送端完成操作后一定能将消息成功发送到消息系统。
消息的接收端的可靠性:接收端仅且能够从消息中间件成功消费一次消息。
发送端的可靠性
在本地数据建一张消息表,将消息数据与业务数据保存在同一数据库实例里,这样就可以利用本地数据库的事务机制。事务提交成功后,将消息表中的消息转移到消息中间件,若转移消息成功则删除消息表中的数据,否则继续重传。
接收端的可靠性
保证接收端处理消息的业务逻辑具有幂等性:只要具有幂等性,那么消费多少次消息,最后处理的结果都是一样的。
保证消息具有唯一编号,并使用一张日志表来记录已经消费的消息编号。
负载均衡的算法与实现
轮询算法把每个请求轮流发送到每个服务器上。下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。最后,(1, 3, 5) 的请求会被发送到服务器 1,(2, 4, 6) 的请求会被发送到服务器 2。
该算法比较适合每个服务器的性能差不多的场景,如果有性能存在差异的情况下,那么性能较差的服务器可能无法承担多大的负载。下图中,服务器 2 的性能比服务器 1 差,那么服务器 2 可能无法承担多大的负载。
1.2 加权轮询(Weighted Round Robbin)
加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值。例如下图中,服务器 1 被赋予的权值为 5,服务器 2 被赋予的权值为 1,那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1,(6) 请求会被发送到服务器 2。
1.3 最少连接(least Connections)
由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数多大,而另一台服务器的连接多小,造成负载不均衡。例如下图中,(1, 3, 5) 请求会被发送到服务器 1,但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1;(2, 4, 6) 请求被发送到服务器 2,它们的连接都还没有断开,继续运行时,服务器 2 会承担多大的负载。
最少连接算法就是将请求发送给当前最少连接数的服务器上。例如下图中,服务器 1 当前连接数最小,那么请求 6 就会被发送到服务器 1 上。
1.4 加权最小连接(Weighted Least Connection)
在最小连接的基础上,根据服务器的性能为每台服务器分配权重,然后根据权重计算出每台服务器能处理的连接数。
1.5 随机算法(Random)
把请求随机发送到服务器上。和轮询算法类似,该算法比较适合服务器性能差不多的场景。
使用 DNS 作为负载均衡器,会根据负载情况返回不同服务器的 IP 地址。大型网站基本使用了这种方式最为第一级负载均衡手段,然后在内部在第二级负载均衡。
2.2 修改 MAC 地址
使用 LVS(Linux Virtual Server)这种链路层负载均衡器,根据负载情况修改请求的 MAC 地址。
2.3 修改 IP 地址
在网络层修改请求的目的 IP 地址。
2.4 HTTP 重定向
HTTP 重定向负载均衡服务器收到 HTTP 请求之后会返回服务器的地址,并将该地址写入 HTTP 重定向响应中返回给浏览器,浏览器收到后再次发送请求。
2.5 反向代理
正向代理:发生在客户端,是由用户主动发起的。比如FQ,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端。
反向代理:发生在服务器端,用户不知道发生了代理。
分布式锁
Java 提供了两种内置的锁的实现,一种是由 JVM 实现的 synchronized 和 JDK 提供的 Lock,当你的应用是单机或者说单进程应用时,可以使用 synchronized 或 Lock 来实现锁。当应用涉及到多机、多进程共同完成时,那么这时候就需要一个全局锁来实现多个进程之间的同步。
基于 MySQL 锁表
该实现方式完全依靠数据库唯一索引来实现。当想要获得锁时,就向数据库中插入一条记录,释放锁时就删除这条记录。如果记录具有唯一索引,就不会同时插入同一条记录。这种方式存在以下几个问题:
锁没有失效时间,解锁失败会导致死锁,其他线程无法再获得锁。
只能是非阻塞锁,插入失败直接就报错了,无法重试。
不可重入,同一线程在没有释放锁之前无法再获得锁。
采用乐观锁增加版本号
根据版本号来判断更新之前有没有其他线程更新过,如果被更新过,则获取锁失败。
2.2 Redis 分布式锁
基于 SETNX、EXPIRE
使用 SETNX(set if not exist)命令插入一个键值对时,如果 Key 已经存在,那么会返回 False,否则插入成功并返回 True。因此客户端在尝试获得锁时,先使用 SETNX 向 Redis 中插入一个记录,如果返回 True 表示获得锁,返回 False 表示已经有客户端占用锁。
EXPIRE 可以为一个键值对设置一个过期时间,从而避免了死锁的发生。
RedLock 算法
ReadLock 算法使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时还可用。
尝试从 N 个相互独立 Redis 实例获取锁,如果一个实例不可用,应该尽快尝试下一个。
计算获取锁消耗的时间,只有当这个时间小于锁的过期时间,并且从大多数(N/2+1)实例上获取了锁,那么就认为锁获取成功了。
如果锁获取失败,会到每个实例上释放锁。
2.3 Zookeeper 分布式锁
Zookeeper 是一个为分布式应用提供一致性服务的软件,例如配置管理、分布式协同以及命名的中心化等,这些都是分布式系统中非常底层而且是必不可少的基本功能,但是如果自己实现这些功能而且要达到高吞吐、低延迟同时还要保持一致性和可用性,实际上非常困难。
抽象模型
Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示它的父节点为 /app1。
节点类型
永久节点:不会因为会话结束或者超时而消失;
临时节点:如果会话结束或者超时就会消失;
有序节点:会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000,它的下一个有序节点则为 /lock/node-0000000001,依次类推。
监听器
为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。
分布式锁实现
创建一个锁目录 /lock。
在 /lock 下创建临时的且有序的子节点,第一个客户端对应的子节点为 /lock/lock-0000000000,第二个为 /lock/lock-0000000001,以此类推。
客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁;
执行业务代码,完成后,删除对应的子节点。
会话超时
如果一个已经获得锁的会话超时了,因为创建的是临时节点,因此该会话对应的临时节点会被删除,其它会话就可以获得锁了。可以看到,Zookeeper 分布式锁不会出现数据库分布式锁的死锁问题。
羊群效应
在步骤二,一个节点未获得锁,需要监听监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知,而我们只希望它的下一个子节点收到通知。
分布式 Session
如果不做任何处理的话,用户将出现频繁登录的现象,比如集群中存在 A、B 两台服务器,用户在第一次访问网站时,Nginx 通过其负载均衡机制将用户请求转发到 A 服务器,这时 A 服务器就会给用户创建一个 Session。当用户第二次发送请求时,Nginx 将其负载均衡到 B 服务器,而这时候 B 服务器并不存在 Session,所以就会将用户踢到登录页面。这将大大降低用户体验度,导致用户的流失,这种情况是项目绝不应该出现的。
粘性 Session 是指将用户锁定到某一个服务器上,比如上面说的例子,用户第一次请求时,负载均衡器将用户的请求转发到了 A 服务器上,如果负载均衡器设置了粘性 Session 的话,那么用户以后的每次请求都会转发到 A 服务器上,相当于把用户和 A 服务器粘到了一块,这就是粘性 Session 机制。
优点
简单,不需要对 Session 做任何处理。
缺点
缺乏容错性,如果当前访问的服务器发生故障,用户被转移到第二个服务器上时,他的 Session 信息都将失效。
适用场景
发生故障对客户产生的影响较小;
服务器发生故障是低概率事件。
任何一个服务器上的 Session 发生改变,该节点会把这个 Session 的所有内容序列化,然后广播给所有其它节点,不管其他服务器需不需要 Session,以此来保证 Session 同步。
优点
可容错,各个服务器间 Session 能够实时响应。
缺点
会对网络负荷造成一定压力,如果 Session 量大的话可能会造成网络堵塞,拖慢服务器性能。
实现方式
设置 Tomcat 的 server.xml 开启 tomcat 集群功能。
在应用里增加信息:通知应用当前处于集群环境中,支持分布式,即在 web.xml 中添加 选项。
使用 Session 共享也分两种机制,两种情况如下:
3.1 粘性 Session 共享机制
和粘性 Session 一样,一个用户的 Session 会绑定到一个 Tomcat 上。Memcached 只是起到备份作用。
3.2 非粘性 Session 共享机制
原理
Tomcat 本身不存储 Session,而是存入 Memcached 中。Memcached 集群构建主从复制架构。
优点
可容错,Session 实时响应。
实现方式
用开源的 msm 插件解决 Tomcat 之间的 Session 共享:Memcached_Session_Manager(MSM)
拿出一个数据库,专门用来存储 Session 信息。保证 Session 的持久化。
优点
服务器出现问题,Session 不会丢失
缺点
如果网站的访问量很大,把 Session 存储到数据库中,会对数据库造成很大压力,还需要增加额外的开销维护数据库。
Terracotta 的基本原理是对于集群间共享的数据,当在一个节点发生变化的时候,Terracotta 只把变化的部分发送给 Terracotta 服务器,然后由服务器把它转发给真正需要这个数据的节点。它是服务器 Session 复制的优化。
优点
这样对网络的压力就非常小,各个节点也不必浪费 CPU 时间和内存进行大量的序列化操作。把这种集群间数据共享的机制应用在 Session 同步上,既避免了对数据库的依赖,又能达到负载均衡和灾难恢复的效果。
分库与分表带来的分布式困境与应对之策
为每个分片指定一个 ID 范围。
有两种通信方式,第一种是利用HttpClient,HttpClient提供了http服务的能力,其工作原理就类似于我们去打开浏览器访问一个网页去获取数据,最终网页将数据展现出来。HttpClient可以利用get或者post请求去抓取一个接口的数据,从而得到我们需要的数据。
服务器的数量:zookeeper:3台服务器
solr:4台服务器
先搭建zookeeper集群,因为zookeeper集群有存活过半机制,一般服务器选用奇数台最少3台,因为一台就不叫集群了叫zookeeper服务器了,一个leader主节点,两个follower节点
搭建完zookeeper集群,启动zookeeper,启动4台tomcat实例,更改tomcat端口号一般改为8080,8081,8082,8083,再搭建4个单机版solr实例,让zookeeper集群集中管理配置文件,将配置文件上传到zookeeper,将conf下面的内容上传到zookeeper集群中,修改solr.xml的文件,告诉每个solr实例zookeeper集群的位置,在每台Tomcat的bin目录下catalina.bat文件中加入DzkHost指定的zookeeper服务器地址
MQ(消息队列)是一种应用程序对应应用程序的通信方法,由于在高并发环境下,由于来不及同步处理,请求往往发生堵塞,通过消息队列,我们可以异步处理请求,缓解系统压力;MQ( Message Queue) ,即消息队列是在消息的传输过程中保存消息的容器。
通俗的说, 就是一个容器, 你把消息丢进去, 不需要立即处理。 然后有个程序去从你的容器里面把消息一条条读出来处理。一般用于应用系统解耦、 消息异步分发, 能够提高系统吞吐量。
注册用户,发邮件(异步)
登录,发短信通知(异步),加积分(异步)
商品添加,异步更新solr,异步更新静态页面
接口,库存修改后,重新生成新的静态页面
Redis是一种基于键值对的NoSQL数据库(非关系型数据库);是一个key-value存储系统
Redis有两个特点:高能性 可靠性
高能性:Redis将所有数据都存储在内存中,所有读写性特别高
可靠性:Redis将内存中的数据利用RDB和AOF的形式保存到硬盘中,这样就可以避免发生断点或机器故障时内存数据丢失的问题
功能应用
1.数据缓存功能,减少对数据库的访问压力
2.消息队列功能(轻量级)
Redis提供了发布订阅功能和阻塞队列功能
3.计数器-应用保存用户凭证
比如计算浏览数,如果每次操作都要做数据库的对应更新操作,那将会给数据库的性能带来极大的挑战
缓存:优化网站性能,首页 (不常变的信息)
存储:单点登陆,购物车
计数器:登陆次数限制,incr
时效性:验证码expire
订单号:数字
淘汰策略:在redis.conf 里面配置,来保证热点数据保存在reids里面。
1.缓存数据服务器
SSO单点登录
2.应对高速读写的场景
秒杀高可用
3.分布式锁
秒杀数据一致性
4.数据共享
库存数据
方便解耦,简化开发:通过spring提供的ioc容器,可以将对象之间的依赖关系交由spring进行控制,避免硬编码所造成的过度程序耦合
aop编程的支持;通过spring提供的aop功能,方便进行面向切面的编程
声明式事务的支持
方便几次各种优秀框架
降低JavaEE API的使用难度
IOC是一个生产和管理bean的容器,原来调用类中new的东西,现在在IOC容器中产生;
ioc是控制反转,是spring的核心思想,通过面向接口编程来实现对业务组件的动态依赖
Spring的IOC有三种方式注入
1、根据属性注入—set方法注入
2、根据构造方法注入
3、根据注解注入
单点登录使用了Redis+Cookie实现
把用户信息放在Redis中,Key作为用户凭证存放在Cookie中放在客户端,通过获取Cookie凭证判断用户是否有登录
Cookie的安全性,我们的凭证是唯一的UUID,使用工具类统一字符串命名,并且设置了Cookie,关闭document.cookie的取值功能
Cookie的跨域问题,在二级域名使用共享Cookie的将多个系统的域名统一作为二级域名,统一平台提供使用主域名,cookie.setPath("/")设置Cooie路径为根路径,通过cookie.setDomain(".父域名")使得项目之间跨域互相访问他们的Cookie
现实中购物车有两种情况,未登录时的购物车和登录时的购物车。我们用Redis+Cookie的方法来实现购物车。当点击“加入购物车”按钮时,先获取用户登录凭证,如果没有登录,就将商品的id保存在Redis未登录购物车中,当拦截器拦截到用户登录时,把购物车的内容合并到数据库中登录后购物车里,通过json解析商品id查到商品信息,所以购物车中的商品信息是可以变化的。
1.HTML静态化,消耗最小的纯静态化的html页面避免大量的数据库访问请求
2.分离图片服务器,对于web服务器来说,图片是最消耗资源的将图片资源和页面资源进行分离,进行不同的配置优化,保证更改的系统消耗和执行效率
3.数据库集群和库表散列,数据库集群由于在架构、成本、扩张性方面都会受到所采用的关系型的限制,在应用程序安装业务和功能模块将数据库进行分离,不同的模块对应不同的数据库或者表,再进行更小的数据库散列,最终可以再配置让系统随时增加数据库补充系统性能;
4.缓存,使用外加的redis模块进行缓存,减轻数据库访问压力
5.负载均衡,在服务器集群中需一台服务器调度角色Nginx,用户所有请求先由它接收,在分配某台服务器去处理;实现负载均衡:http重定向实现,DNS匹配,反向代理
6.动静态分离,对于动态请求交给Tomcat而其他静态请求,搭建专门的静态资源服务器,使用nginx进行请求分发
1.统一配置管理
持久化节点存放配置信息,监听内容修改
2.集群管理
临时节点机器(节点)退出或者加入,Master选举投票
临时顺序节点选举时候直接使用编号最小的即可
3.分布式锁
创建临时节点,创建成功者获得锁,执行业务操作,独占操作
也可以进行顺序执行,通过最顺序临时节点的编号
4.命名服务
/dubbo
/consumer:存放消费地址(没实际意义),
/conf:存放配置信息
…
consumer通过监听provider节点的内容修改实现动态读取地址,并且支持集群,只需要在provider中存放多个地址然后程序中通过代码实现随机调用即可
12.Nginx应用场景
1.Http服务器,具有提供http服务的能力
location /{
root /home
}
2.虚拟主机,可以对不同的进行端口映射
server{
port:端口
server_name:域名
…
}
tomcatlist{
ip1:port [weight=n1];
ip2:port [weight=n2];
}
location{
proxy_pass : tomcatlist;
}
4.动静态分离
location ~.(jpg|css|js|png|html|htm)
1.系统间异步调用
添加商品时,索引工程创建索引,详情工程生成静态页面
2.顺序消费
队列的特点
3.定时任务
订单的30分钟后关闭
4.请求削峰
双十一和平时的时候的处理
通过消息中间件的消息存储到队列中,服务层只拿取指定数量的消息进行消费从而保证服务层的稳定性,用时间的代价换取性能和稳定的保证再通过成本可以接受的集群搭建提高时间基础
答:线程局部变量ThreadLocal为每个使用该变量的线程提供独立的变量副本,每次调用set()方法的时候,每个当前线程都有一个ThreadLocal。应用场景:当很多线程需要多次使用同一个对象,并且需要改对象具有相同初始化值的时候最适合使用ThreadLocal
作用:解决多线程程序并发问题
1.Erueka是一个是服务端,ZooKeeper是一个进程
2.Erueka是自我保护机制,ZooKeeper是过半存活机制
3.Erueka是AP设计,ZooKeeper是CP设计
4.Erueka没有角色的概念,ZooKeeper有leader和follow
对象池技术基本原理的核心有两点:缓存和共享,即对于那些被频繁使用的对象,在使用完后,不立即将它们释放,而是将它们缓存起来,以供后续的应用程序重复使用,从而减少创建对象和释放对象的次数,进而改善应用程序的性能。事实上,由于对象池技术将对象限制在一定的数量,也有效地减少了应用程序内存上的开销。
一、概念
NIO即New IO,这个库是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但实现方式不同,NIO主要用到的是块,所以NIO的效率要比IO高很多。在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。
二、NIO和IO的主要区别
下表总结了Java IO和NIO之间的主要区别:
IO NIO
面向流 面向缓冲
阻塞IO 非阻塞IO
无 选择器
1、面向流与面向缓冲
Java IO和NIO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
2、阻塞与非阻塞IO
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
3、选择器(Selectors)
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
三、NIO和IO如何影响应用程序的设计
无论您选择IO或NIO工具箱,可能会影响您应用程序设计的以下几个方面:
1.对NIO或IO类的API调用。
2.数据处理。
3.用来处理数据的线程数。
1、API调用
当然,使用NIO的API调用时看起来与使用IO时有所不同,但这并不意外,因为并不是仅从一个InputStream逐字节读取,而是数据必须先读入缓冲区再处理。
2、数据处理
使用纯粹的NIO设计相较IO设计,数据处理也受到影响。
在IO设计中,我们从InputStream或 Reader逐字节读取数据。假设你正在处理一基于行的文本数据流,例如:
Name: Anna
Age: 25
Email: [email protected]
Phone: 1234567890
该文本行的流可以这样处理:
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
String ageLine = reader.readLine();
String emailLine = reader.readLine();
String phoneLine = reader.readLine();
请注意处理状态由程序执行多久决定。换句话说,一旦reader.readLine()方法返回,你就知道肯定文本行就已读完, readline()阻塞直到整行读完,这就是原因。你也知道此行包含名称;同样,第二个readline()调用返回的时候,你知道这行包含年龄等。 正如你可以看到,该处理程序仅在有新数据读入时运行,并知道每步的数据是什么。一旦正在运行的线程已处理过读入的某些数据,该线程不会再回退数据(大多如此)。下图也说明了这条原则:
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
注意第二行,从通道读取字节到ByteBuffer。当这个方法调用返回时,你不知道你所需的所有数据是否在缓冲区内。你所知道的是,该缓冲区包含一些字节,这使得处理有点困难。假设第一次 read(buffer)调用后,读入缓冲区的数据只有半行,例如,“Name:An”,你能处理数据吗?显然不能,需要等待,直到整行数据读入缓存,在此之前,对数据的任何处理毫无意义。所以,你怎么知道是否该缓冲区包含足够的数据可以处理呢?好了,你不知道。发现的方法只能查看缓冲区中的数据。其结果是,在你知道所有数据都在缓冲区里之前,你必须检查几次缓冲区的数据。这不仅效率低下,而且可以使程序设计方案杂乱不堪。例如:
int bytesRead = inChannel.read(buffer);
while(! bufferFull(bytesRead) ) { bytesRead = inChannel.read(buffer);
bufferFull()方法必须跟踪有多少数据读入缓冲区,并返回真或假,这取决于缓冲区是否已满。换句话说,如果缓冲区准备好被处理,那么表示缓冲区满了。
bufferFull()方法扫描缓冲区,但必须保持在bufferFull()方法被调用之前状态相同。如果没有,下一个读入缓冲区的数据可能无法读到正确的位置。这是不可能的,但却是需要注意的又一问题。
如果缓冲区已满,它可以被处理。如果它不满,并且在你的实际案例中有意义,你或许能处理其中的部分数据。但是许多情况下并非如此。下图展示了“缓冲区数据循环就绪”:
NIO可让您只使用一个(或几个)单线程管理多个通道(网络连接或文件),但付出的代价是解析数据可能会比从一个阻塞流中读取数据更复杂。
如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器,实现NIO的服务器可能是一个优势。同样,如果你需要维持许多打开的连接到其他计算机上,如P2P网络中,使用一个单独的线程来管理你所有出站连接,可能是一个优势。一个线程多个连接的设计方案如下图所示:
如果你有少量的连接使用非常高的带宽,一次发送大量的数据,也许典型的IO服务器实现可能非常契合。下图说明了一个典型的IO服务器设计:
在进入Java NIO编程之前,我们今天先来讨论一些比较基础的知识:I/O模型。本文先从同步和异步的概念 说起,然后接着阐述了阻塞和非阻塞的区别,接着介绍了阻塞IO和非阻塞IO的区别,然后介绍了同步IO和异步IO的区别,接下来介绍了5种IO模型,最后介绍了两种和高性能IO设计相关的设计模式(Reactor和Proactor)。
一.什么是同步?什么是异步?
二.什么是阻塞?什么是非阻塞?
三.什么是阻塞IO?什么是非阻塞IO?
四.什么是同步IO?什么是异步IO?
五.五种IO模型
六.两种高性能IO设计模式
若有不正之处,请多多谅解并欢迎批评指正。
同步和异步的概念出来已经很久了,网上有关同步和异步的说法也有很多。以下是我个人的理解:
同步就是:如果有多个任务或者事件要发生,这些任务或者事件必须逐个地进行,一个事件或者任务的执行会导致整个流程的暂时等待,这些事件没有办法并发地执行;异步就是:如果有多个任务或者事件发生,这些事件可以并发地执行,一个事件或者任务的执行不会导致整个流程的暂时等待。这就是同步和异步。举个简单的例子,假如有一个任务包括两个子任务A和B,对于同步来说,当A在执行的过程中,B只有等待,直至A执行完毕,B才能执行;而对于异步就是A和B可以并发地执行,B不必等待A执行完毕之后再执行,这样就不会由于A的执行导致整个任务的暂时等待。
void fun1() {
}
}
fun1();
fun2()
.....
.....
}
这段代码就是典型的同步,在方法function中,fun1在执行的过程中会导致后续的fun2无法执行,fun2必须等待fun1执行完毕才可以执行。
void fun1() {
}
}
new Thread(){
public void run() {
fun1();
}
}.start();
public void run() {
fun2();
}
}.start();
.....
.....
}
这段代码是一种典型的异步,fun1的执行不会影响到fun2的执行,并且fun1和fun2的执行不会导致其后续的执行过程处于暂时的等待。
在前面介绍了同步和异步的区别,这一节来看一下阻塞和非阻塞的区别。
在了解阻塞IO和非阻塞IO之前,先看下一个具体的IO操作过程是怎么进行的。
1)查看数据是否就绪;
2)进行数据拷贝(内核将数据拷贝到用户线程)。
那么阻塞(blocking IO)和非阻塞(non-blocking IO)的区别就在于第一个阶段,如果数据没有就绪,在查看数据是否就绪的过程中是一直等待,还是直接返回一个标志信息。
我们先来看一下同步IO和异步IO的定义,在《Unix网络编程》一书中对同步IO和异步IO的定义是这样的:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes.
An asynchronous I/O operation does not cause the requesting process to be blocked.
在《Unix网络编程》一书中提到了五种IO模型,分别是:阻塞IO、非阻塞IO、多路复用IO、信号驱动IO以及异步IO。
最传统的一种IO模型,即在读写数据过程中会发生阻塞现象。
data = socket.read();
如果数据没有就绪,就会一直阻塞在read方法。
当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。
while(true){
data = socket.read();
if(data!= error){
处理数据
break;
}
}
但是对于非阻塞IO就有一个非常严重的问题,在while循环中需要不断地去询问内核数据是否就绪,这样会导致CPU占用率非常高,因此一般情况下很少使用while循环这种方式来读取数据。
多路复用IO模型是目前使用得比较多的模型。Java NIO实际上就是多路复用IO。
在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。
异步IO模型才是最理想的IO模型,在异步IO模型中,当用户线程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个asynchronous read之后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了。也就说用户线程完全不需要实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。
在传统的网络服务设计模式中,有两种比较经典的模式:一种是 多线程,一种是线程池。