在大学自学 Java。看视频教程和代码能看懂 自己写就写不出来,怎么样能够入门?
Java技术江湖
Java技术江湖
公众号【Java技术江湖】分享最硬核实用的Java技术干货
已拿BAT等一些年薪近30W的java的offer,来回答一发。我把所有需要的知识点罗列了出来,从Java基础到Java进阶,每个部分都有对应的文章和解读,以及对于这块知识的总结,可以说把这个GitHub参考的内容搞懂,你就可以自诩精通Java后端了。
https://github.com/h2pl/Java-Tutorial 这个github仓库,内容很丰富,可以说是最完整最实用的Java后端技术仓库了,对我当初面试的时候帮助很大,也帮助了很多Java方向的小伙伴,下面是本仓库的readme,相信看了这些内容之后你就会知道它的价值!
如果对你有用,记得给我一个star,非常感谢!!!!
本仓库为【Java工程师技术指南】力求打造最完整最实用的Java工程师学习指南!
这些文章和总结都是我近几年学习Java总结和整理出来的,非常实用,对于学习Java后端的朋友来说应该是最全面最完整的技术仓库。 我靠着这些内容进行复习,拿到了BAT等大厂的offer,这个仓库也已经帮助了很多的Java学习者,如果对你有用,希望能给个star支持我,谢谢!
下图是仓库的首页和目录
接下来是每部分的具体内容!附上我的学习总结,由于篇幅比较长,只放上一部分学习总结,更多内容请到仓库查看。
1、基础知识
面向对象基础
Java基本数据类型
string和包装类
final关键字特性
Java类和包
抽象类和接口
代码块和代码执行顺序
Java自动拆箱装箱里隐藏的秘密
Java中的Class类和Object类
Java异常
解读Java中的回调
反射
泛型
枚举类
Java注解和最佳实践
JavaIO流
多线程
深入理解内部类
javac和javap
Java8新特性终极指南
Java类和包
序列化和反序列化
继承、封装、多态的实现原理
Java基础学习总结:
Java基础学习总结
每部分内容会重点写一些常见知识点,方便复习和记忆,但是并不是全部内容,详细的内容请参见具体的文章地址。
面向对象三大特性
继承:一般类只能单继承,内部类实现多继承,接口可以多继承
封装:访问权限控制public > protected > 包 > private 内部类也是一种封装
多态:编译时多态,体现在向上转型和向下转型,通过引用类型判断调用哪个方法(静态分派)。
运行时多态,体现在同名函数通过不同参数实现多种方法(动态分派)。
基本数据类型
基本类型位数,自动装箱,常量池
例如byte类型是1byte也就是8位,可以表示的数字是-128到127,因为还有一个0,加起来一共是256,也就是2的八次方。
32位和64位机器的int是4个字节也就是32位,char是1个字节就是8位,float是4个字节,double是8个字节,long是8个字节。
所以它们占有字节数是相同的,这样的话两个版本才可以更好地兼容。(应该)
基本数据类型的包装类只在数字范围-128到127中用到常量池,会自动拆箱装箱,其余数字范围的包装类则会新建实例
String及包装类
String类型是final类型,在堆中分配空间后内存地址不可变。
底层是final修饰的char[]数组,数组的内存地址同样不可变。
但实际上可以通过修改char[n] = 'a’来进行修改,不会改变String实例的内存值,不过在jdk中,用户无法直接获取char[],也没有方法能操作该数组。 所以String类型的不可变实际上也是理论上的不可变。所以我们在分配String对象以后,如果将其 = “abc”,那也只是改变了引用的指向,实际上没有改变原来的对象。
StringBuffer和StringBuilder底层是可变的char[]数组,继承父类AbstractStringBuilder的各种成员和方法,实际上的操作都是由父类方法来完成的。
final关键字
final修饰基本数据类型保证不可变
final修饰引用保证引用不能指向别的对象,否则会报错。
final修饰类,类的实例分配空间后地址不可变,子类不能重写所有父类方法。因此在cglib动态代理中,不能为一个类的final修饰的函数做代理,因为cglib要将被代理的类设置为父类,然后再生成字节码。
final修饰方法,子类不能重写该方法。
抽象类和接口
1 抽象类可以有方法实现。 抽象类可以有非final成员变量。 抽象方法要用abstract修饰。 抽象类可以有构造方法,但是只能由子类进行实例化。
2 接口可以用extends加多个接口实现多继承。 接口只能有public final类型的成员变量。 接口只能有抽象方法,不能有方法体、 接口不能实例化,但是可以作为引用类型。
代码块和加载顺序
假设该类是第一次进行实例化。那么有如下加载顺序 静态总是比非静态优先,从早到晚的顺序是: 1 静态代码块 和 静态成员变量的顺序根据代码位置前后来决定。 2 代码块和成员变量的顺序也根据代码位置来决定 3 最后才调用构造方法构造方法
包、内部类、外部类
1 Java项目一般从src目录开始有com…A.java这样的目录结构。这就是包结构。所以一般编译后的结构是跟包结构一模一样的,这样的结构保证了import时能找到正确的class引用包访问权限就是指同包下的类可见。
import 一般加上全路径,并且使用.*时只包含当前目录的所有类文件,不包括子目录。
2 外部类只有public和default两种修饰,要么全局可访问,要么包内可访问。
3 内部类可以有全部访问权限,因为它的概念就是一个成员变量,所以访问权限设置与一般的成员变量相同。
非静态内部类是外部类的一个成员变量,只跟外部类的实例有关。
静态内部类是独立于外部类存在的一个类,与外部类实例无关,可以通过外部类.内部类直接获取Class类型。
异常
1 异常体系的最上层是Throwable类 子类有Error和Exception Exception的子类又有RuntimeException和其他具体的可检查异常。
2 Error是jvm完全无法处理的系统错误,只能终止运行。
运行时异常指的是编译正确但运行错误的异常,如数组越界异常,一般是人为失误导致的,这种异常不用try catch,而是需要程序员自己检查。
可检查异常一般是jvm处理不了的一些异常,但是又经常会发生,比如Ioexception,Sqlexception等,是外部实现带来的异常。
3 多线程的异常流程是独立的,互不影响。 大型模块的子模块异常一般需要重新封装成外部异常再次抛出,否则只能看到最外层异常信息,难以进行调试。
日志框架是异常报告的最好帮手,log4j,slf4j中,在工作中必不可少。
泛型
Java中的泛型是伪泛型,只在编译期生效,运行期自动进行泛型擦除,将泛型替换为实际上传入的类型。
泛型类用class A {
}这样的形式表示,里面的方法和成员变量都可以用T来表示类型。泛型接口也是类似的,不过泛型类实现泛型接口时可以选择注入实际类型或者是继续使用泛型。
泛型方法可以自带泛型比如void E go();
泛型可以使用?通配符进行泛化 Object>可以接受任何类型
也可以使用 extends Number> super Integer>这种方式进行上下边界的限制。
Class类和Object类
Java反射的基础是Class类,该类封装所有其他类的类型信息,并且在每个类加载后在堆区生成每个类的一个Class<类名>实例,用于该类的实例化。
Java中可以通过多种方式获取Class类型,比如A.class,new A().getClass()方法以及Class.forName(“com.?.?.A”)方法。
Object是所有类的父类,有着自己的一些私有方法,以及被所有类继承的9大方法。
有人讨论Object和Class类型谁先加载谁后加载,因为每个类都要继承Object,但是又得先被加载到堆区,事实上,这个问题在JVM初始化时就解决了,没必要多想。
javac和java
javac 是编译一个java文件的基本命令,通过不同参数可以完成各种配置,比如导入其他类,指定编译路径等。
java是执行一个java文件的基本命令,通过参数配置可以以不同方式执行一个java程序或者是一个jar包。
javap是一个class文件的反编译程序,可以获取class文件的反编译结果,甚至是jvm执行程序的每一步代码实现。
反射
Java反射包reflection提供对Class,Method,field,constructor等信息的封装类型。
通过这些api可以轻易获得一个类的各种信息并且可以进行实例化,方法调用等。
类中的private参数可以通过setaccessible方法强制获取。
反射的作用可谓是博大精深,JDK动态代理生成代理类的字节码后,首先把这个类通过defineclass定义成一个类,然后用class.for(name)会把该类加载到jvm,之后我们就可以通过,A.class.GetMethod()获取其方法,然后通过invoke调用其方法,在调用这个方法时,实际上会通过被代理类的引用再去调用原方法。
枚举类
枚举类继承Enum并且每个枚举类的实例都是唯一的。
枚举类可以用于封装一组常量,取值从这组常量中取,比如一周的七天,一年的十二个月。
枚举类的底层实现其实是语法糖,每个实例可以被转化成内部类。并且使用静态代码块进行初始化,同时保证内部成员变量不可变。
序列化
序列化的类要实现serializable接口
transient修饰符可以保证某个成员变量不被序列化
readObject和writeOject来实现实例的写入和读取。
待更新。
事实上,一些拥有数组变量的类都会把数组设为transient修饰,这样的话不会对整个数组进行序列化,而是利用专门的方法将有数据的数组范围进行序列化,以便节省空间。
动态代理
jdk自带的动态代理可以代理一个已经实现接口的类。
cglib代理可以代理一个普通的类。
动态代理的基本实现原理都是通过字节码框架动态生成字节码,并且在用defineclass加载类后,获取代理类的实例。
一般需要实现一个代理处理器,用来处理被代理类的前置操作和后置操作。在JDK动态代理中,这个类叫做invocationHandler。
JDK动态代理首先获取被代理类的方法,并且只获取在接口中声明的方法,生成代理类的字节码后,首先把这个类通过defineclass定义成一个类,然后把该类加载到jvm,之后我们就可以通过,A.class.GetMethod()获取其方法,然后通过invoke调用其方法,在调用这个方法时,实际上会通过被代理类的引用再去调用原方法。
而对于cglib动态代理,一般会把被代理类设为代理类的父类,然后获取被代理类中所有非final的方法,通过asm字节码框架生成代理类的字节码,这个代理类很神奇,他会保留原来的方法以及代理后的方法,通过方法数组的形式保存。
cglib的动态代理需要实现一个enhancer和一个interceptor,在interceptor中配置我们需要的代理内容。如果没有配置interceptor,那么代理类会调用被代理类自己的方法,如果配置了interceptor,则会使用代理类修饰过的方法。
多线程
这里先不讲juc包里的多线程类。juc相关内容会在Java并发专题讲解。
线程的实现可以通过继承Thread类和实现Runable接口 也可以使用线程池。callable配合future可以实现线程中的数据获取。
Java中的线程有7种状态,new runable running blocked waiting time_waiting terminate blocked是线程等待其他线程锁释放。 waiting是wait以后线程无限等待其他线程使用notify唤醒 time_wating是有限时间地等待被唤醒,也可能是sleep固定时间。
Thread的join是实例方法,比如a.join(b),则说明a线程要等b线程运行完才会运行。
o.wait方法会让持有该对象o的线程释放锁并且进入阻塞状态,notify则是持有o锁对象的线程通知其他等待锁的线程获取锁。notify方法并不会释放锁。注意这两个方法都只能在synchronized同步方法或同步块里使用。
synchronized方法底层使用系统调用的mutex锁,开销较大,jvm会为每个锁对象维护一个等待队列,让等待该对象锁的线程在这个队列中等待。当线程获取不到锁时则让线程阻塞,而其他检查notify以后则会通知任 Thread.sleep(),Thread.interrupt()等方法都是类方法,表示当前调用该方法的线程的操作。
一个线程实例连续start两次会抛异常,这是因为线程start后会设置标识,如果再次start则判断为错误。
IO流
IO流也是Java中比较重要的一块,Java中主要有字节流,字符流,文件等。其中文件也是通过流的方式打开,读取和写入的。
IO流的很多接口都使用了装饰者模式,即将原类型通过传入装饰类构造函数的方式,增强原类型,以此获得像带有缓冲区的字节流,或者将字节流封装成字符流等等,其中需要注意的是编码问题,后者打印出来的结果可能是乱码哦。
IO流与网络编程息息相关,一个socket接入后,我们可以获取它的输入流和输出流,以获取TCP数据包的内容,并且可以往数据报里写入内容,因为TCP协议也是按照流的方式进行传输的,实际上TCP会将这些数据进行分包处理,并且通过差错检验,超时重传,滑动窗口协议等方式,保证了TCP数据包的高效和可靠传输。
网络编程
承接IO流的内容
IO流与网络编程息息相关,一个socket接入后,我们可以获取它的输入流和输出流,以获取TCP数据包的内容,并且可以往数据报里写入内容,因为TCP协议也是按照流的方式进行传输的,实际上TCP会将这些数据进行分包处理,并且通过差错检验,超时重传,滑动窗口协议等方式,保证了TCP数据包的高效和可靠传输。
除了使用socket来获取TCP数据包外,还可以使用UDP的DatagramPacket来封装UDP数据包,因为UDP数据包的大小是确定的,所以不是使用流方式处理,而是需要事先定义他的长度,源端口和目标端口等信息。
为了方便网络编程,Java提供了一系列类型来支持网络编程的api,比如URL类,InetAddress类等。
后续文章会带来NIO相关的内容,敬请期待。
Java8
接口中的默认方法,接口终于可以有方法实现了,使用注解即可标识出默认方法。
lambda表达式实现了函数式编程,通过注解可以声明一个函数式接口,该接口中只能有一个方法,这个方法正是使用lambda表达式时会调用到的接口。
Option类实现了非空检验
新的日期API
各种api的更新,包括chm,hashmap的实现等
Stream流概念,实现了集合类的流式访问,可以基于此使用map和reduce并行计算。
2、集合类
Java集合类总结
Java集合详解1:一文读懂ArrayList,Vector与Stack使用方法和实现原理
Java集合详解2:Queue和LinkedList
Java集合详解3:Iterator,fail-fast机制与比较器
Java集合详解5:深入理解LinkedHashMap和LRU缓存
Java集合详解4:HashMap和HashTable
Java集合详解6:TreeMap和红黑树
Java集合详解7:HashSet,TreeSet与LinkedHashSet
Java集合详解8:Java集合类细节精讲
Java集合类技术总结
这篇总结是基于之前博客内容的一个整理和回顾。
这里先简单地总结一下,更多详细内容请参考我的专栏:深入浅出Java核心技术
https://blog.csdn.net/column/details/21930.html
里面有包括Java集合类在内的众多Java核心技术系列文章。
以下总结不保证全对,如有错误,还望能够指出。谢谢
Colletion,iterator,comparable
一般认为Collection是最上层接口,但是hashmap实际上实现的是Map接口。iterator是迭代器,是实现iterable接口的类必须要提供的一个东西,能够使用for(i : A) 这种方式实现的类型能提供迭代器,以前有一个enumeration,现在早弃用了。
List
List接口下的实现类有ArrayList,linkedlist,vector等等,一般就是用这两个,用法不多说,老生常谈。 ArrayList的扩容方式是1.5倍扩容,这样扩容避免2倍扩容可能浪费空间,是一种折中的方案。 另外他不是线程安全,vector则是线程安全的,它是两倍扩容的。
linkedlist没啥好说的,多用于实现链表。
Map
map永远都是重头戏。
hashmap是数组和链表的组合结构,数组是一个Entry数组,entry是k-V键值对类型,所以一个entry数组存着很entry节点,一个entry的位置通过key的hashcode方法,再进行hash(移位等操作),最后与表长-1进行相与操作,其实就是取hash值到的后n - 1位,n代表表长是2的n次方。
hashmap的默认负载因子是0.75,阈值是16 * 0.75 = 12;初始长度为16;
hashmap的增删改查方式比较简单,都是遍历,替换。有一点要注意的是key相等时,替换元素,不相等时连成链表。
除此之外,1.8jdk改进了hashmap,当链表上的元素个数超过8个时自动转化成红黑树,节点变成树节点,以提高搜索效率和插入效率到logn。
还有一点值得一提的是,hashmap的扩容操作,由于hashmap非线程安全,扩容时如果多线程并发进行操作,则可能有两个线程分别操作新表和旧表,导致节点成环,查询时会形成死锁。chm避免了这个问题。
另外,扩容时会将旧表元素移到新表,原来的版本移动时会有rehash操作,每个节点都要rehash,非常不方便,而1.8改成另一种方式,对于同一个index下的链表元素,由于一个元素的hash值在扩容后只有两种情况,要么是hash值不变,要么是hash值变为原来值+2^n次方,这是因为表长翻倍,所以hash值取后n位,第一位要么是0要么是1,所以hash值也只有两种情况。这两种情况的元素分别加到两个不同的链表。这两个链表也只需要分别放到新表的两个位置即可,是不是很酷。
最后有一个比较冷门的知识点,hashmap1.7版本链表使用的是节点的头插法,扩容时转移链表仍然使用头插法,这样的结果就是扩容后链表会倒置,而hashmap.1.8在插入时使用尾插法,扩容时使用头插法,这样可以保证顺序不变。
CHM
concurrenthashmap也稍微提一下把,chm1.7使用分段锁来控制并发,每个segment对应一个segmentmask,通过key的hash值相与这个segmentmask得到segment位置,然后在找到具体的entry数组下标。所以chm需要维护多个segment,每个segment对应一个数组。分段锁使用的是reetreetlock可重入锁实现。查询时不加锁。
1.8则放弃使用分段锁,改用cas+synchronized方式实现并发控制,查询时不加锁,插入时如果没有冲突直接cas到成功为止,有冲突则使用synchronized插入。
Set
set就是hashmap将value固定为一个object,只存key元素包装成一个entry即可,其他不变。
Linkedhashmap
在原来hashmap基础上将所有的节点依据插入的次序另外连成一个链表。用来保持顺序,可以使用它实现lru缓存,当访问命中时将节点移到队头,当插入元素超过长度时,删除队尾元素即可。
collections和Arrays工具类
两个工具类分别操作集合和数组,可以进行常用的排序,合并等操作。
comparable和comparator
实现comparable接口可以让一个类的实例互相使用compareTo方法进行比较大小,可以自定义比较规则,comparator则是一个通用的比较器,比较指定类型的两个元素之间的大小关系。
treemap和treeset
主要是基于红黑树实现的两个数据结构,可以保证key序列是有序的,获取sortedset就可以顺序打印key值了。其中涉及到红黑树的插入和删除,调整等操作,比较复杂,这里就不细说了。
3、设计模式
设计模式学习总结
初探Java设计模式1:创建型模式(工厂,单例等).md
初探Java设计模式2:结构型模式(代理模式,适配器模式等).md
初探Java设计模式3:行为型模式(策略,观察者等).md
初探Java设计模式4:JDK中的设计模式.md
初探Java设计模式5:Spring涉及到的9种设计模式.md
设计模式学习总结
设计模式基础学习总结 这篇总结主要是基于我之前设计模式基础系列文章而形成的的。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢
设计模式
创建型模式
创建型模式 创建型模式的作用就是创建对象,说到创建一个对象,最熟悉的就是 new 一个对象,然后 set 相关属性。但是,在很多场景下,我们需要给客户端提供更加友好的创建对象的方式,尤其是那种我们定义了类,但是需要提供给其他开发者用的时候。
单例
单例模式保证全局的单例类只有一个实例,这样的话使用的时候直接获取即可,比如数据库的一个连接,Spring里的bean,都可以是单例的。
单例模式一般有5种写法。
第一种是饿汉模式,先把单例进行实例化,获取的时候通过静态方法直接获取即可。缺点是类加载后就完成了类的实例化,浪费部分空间。
第二种是饱汉模式,先把单例置为null,然后通过静态方法获取单例时再进行实例化,但是可能有多线程同时进行实例化,会出现并发问题。
第三种是逐步改进的方法,一开始可以用synchronized关键字进行同步,但是开销太大,而后改成使用volatile修饰单例,然后通过一次检查判断单例是否已初始化,如果未初始化就使用synchronized代码块,再次检查单例防止在这期间被初始化,而后才真正进行初始化。
第四种是使用静态内部类来实现,静态内部类只在被使用的时候才进行初始化,所以在内部类中进行单例的实例化,只有用到的时候才会运行实例化代码。然后外部类再通过静态方法返回静态内部类的单例即可。
第五种是枚举类,枚举类的底层实现其实也是内部类。枚举类确保每个类对象在全局是唯一的。所以保证它是单例,这个方法是最简单的。
工厂模式
简单工厂一般是用一个工厂创建多个类的实例。
工厂模式一般是指一个工厂服务一个接口,为这个接口的实现类进行实例化
抽象工厂模式是指一个工厂服务于一个产品族,一个产品族可能包含多个接口,接口又会包含多个实现类,通过一个工厂就可以把这些绑定在一起,非常方便。
原型模式
一般通过一个实例进行克隆从而获得更多同一原型的实例。使用实例的clone方法即可完成。
建造者模式
建造者模式中有一个概念叫做链式调用,链式调用为一个类的实例化提供便利,一般提供系列的方法进行实例化,实际上就是将set方法改造一下,将原本返回为空的set方法改为返回this实例,从而实现链式调用。
建造者模式在此基础上加入了builder方法,提供给外部进行调用,同样使用链式调用来完成参数注入。
结构型模式
结构型模式 前面创建型模式介绍了创建对象的一些设计模式,这节介绍的结构型模式旨在通过改变代码结构来达到解耦的目的,使得我们的代码容易维护和扩展。
桥接模式
有点复杂。建议参考原文
适配器模式
适配器模式用于将两个不同的类进行适配。
适配器模式和代理模式的异同
比较这两种模式,其实是比较对象适配器模式和代理模式,在代码结构上, 它们很相似,都需要一个具体的实现类的实例。 但是它们的目的不一样,代理模式做的是增强原方法的活; 适配器做的是适配的活,为的是提供“把鸡包装成鸭,然后当做鸭来使用”, 而鸡和鸭它们之间原本没有继承关系。
适配器模式可以分为类适配器,对象适配器等。
类适配器通过继承父类就可以把自己适配成父类了。 而对象适配器则需要把对象传入另一个对象的构造方法中,以便进行包装。
享元模式
/ 享元模式的核心在于享元工厂类, // 享元工厂类的作用在于提供一个用于存储享元对象的享元池, // 用户需要对象时,首先从享元池中获取, // 如果享元池中不存在,则创建一个新的享元对象返回给用户, // 在享元池中保存该新增对象。
//享元模式 // 英文是 Flyweight Pattern,不知道是谁最先翻译的这个词,感觉这翻译真的不好理解,我们试着强行关联起来吧。Flyweight 是轻量级的意思,享元分开来说就是 共享 元器件,也就是复用已经生成的对象,这种做法当然也就是轻量级的了。 // // 复用对象最简单的方式是,用一个 HashMap 来存放每次新生成的对象。每次需要一个对象的时候,先到 HashMap 中看看有没有,如果没有,再生成新的对象,然后将这个对象放入 HashMap 中。 // // 这种简单的代码我就不演示了。
代理模式
// 我们发现没有,代理模式说白了就是做 “方法包装” 或做 “方法增强”。 // 在面向切面编程中,算了还是不要吹捧这个名词了,在 AOP 中, // 其实就是动态代理的过程。比如 Spring 中, // 我们自己不定义代理类,但是 Spring 会帮我们动态来定义代理, // 然后把我们定义在 @Before、@After、@Around 中的代码逻辑动态添加到代理中。
外观模式
外观模式一般封装具体的实现细节,为用户提供一个更加简单的接口。
通过一个方法调用就可以获取需要的内容。
组合模式
//组合模式用于表示具有层次结构的数据,使得我们对单个对象和组合对象的访问具有一致性。
//直接看一个例子吧,每个员工都有姓名、部门、薪水这些属性, // 同时还有下属员工集合(虽然可能集合为空), // 而下属员工和自己的结构是一样的, // 也有姓名、部门这些属性, // 同时也有他们的下属员工集合。
class Employee {
private String name;
private String dept;
private int salary;
private List subordinates; // 下属
}
装饰者模式
装饰者
装饰者模式把每个增强类都继承最高级父类。然后需要功能增强时把类实例传入增强类即可,然后增强类在使用时就可以增强原有类的功能了。
和代理模式不同的是,装饰者模式每个装饰类都继承父类,并且可以进行多级封装。
行为型模式
行为型模式 行为型模式关注的是各个类之间的相互作用,将职责划分清楚,使得我们的代码更加地清晰。
策略模式
策略模式一般把一个策略作为一个类,并且在需要指定策略的时候传入实例,于是我们可以在需要使用算法的地方传入指定算法。
命令模式
命令模式一般分为命令发起者,命令以及命令接受者三个角色。
命令发起者在使用时需要注入命令实例。然后执行命令调用。
命令调用实际上会调用命令接收者的方法进行实际调用。
比如遥控器按钮相当于一条命令,点击按钮时命令运行,自动调用电视机提供的方法即可。
模板方法模式
模板方法一般指提供了一个方法模板,并且其中有部分实现类和部分抽象类,并且规定了执行顺序。
实现类是模板提供好的方法。而抽象类则需要用户自行实现。
模板方法规定了一个模板中方法的执行顺序,非常适合一些开发框架,于是模板方法也广泛运用在开源框架中。
状态模式
少见。
观察者模式和事件监听机制
观察者模式一般用于订阅者和消息发布者之间的数据订阅。
一般分为观察者和主题,观察者订阅主题,把实例注册到主题维护的观察者列表上。
而主题更新数据时自动把数据推给观察者或者通知观察者数据已经更新。
但是由于这样的方式消息推送耦合关系比较紧。并且很难在不打开数据的情况下知道数据类型是什么。
知道后来为了使数据格式更加灵活,使用了事件和事件监听器的模式,事件包装的事件类型和事件数据,从主题和观察者中解耦。
主题当事件发生时,触发该事件的所有监听器,把该事件通过监听器列表发给每个监听器,监听得到事件以后,首先根据自己支持处理的事件类型中找到对应的事件处理器,再用处理器处理对应事件。
责任链模式
责任链通常需要先建立一个单向链表,然后调用方只需要调用头部节点就可以了,后面会自动流转下去。比如流程审批就是一个很好的例子,只要终端用户提交申请,根据申请的内容信息,自动建立一条责任链,然后就可以开始流转了。
4、JavaWeb
走进JavaWeb技术世界1:JavaWeb的由来和基础知识
走进JavaWeb技术世界2:JSP与Servlet的曾经与现在
走进JavaWeb技术世界3:JDBC的进化与连接池技术
走进JavaWeb技术世界4:Servlet 工作原理详解
走进JavaWeb技术世界5:初探Tomcat的HTTP请求过程
走进JavaWeb技术世界6:Tomcat5总体架构剖析
走进JavaWeb技术世界7:Tomcat和其他WEB容器的区别
走进JavaWeb技术世界8:浅析Tomcat9请求处理流程与启动部署过程
走进JavaWeb技术世界9:Java日志系统的诞生与发展
走进JavaWeb技术世界10:从JavaBean讲到Spring
走进JavaWeb技术世界11:单元测试框架Junit
走进JavaWeb技术世界12:从手动编译打包到项目构建工具Maven
走进JavaWeb技术世界13:Hibernate入门经典与注解式开发
走进JavaWeb技术世界14:Mybatis入门
深入JavaWeb技术世界15:深入浅出Mybatis基本原理
走进JavaWeb技术世界16:极简配置的SpringBoot
JavaWeb学习总结
这篇总结主要是基于我之前两个系列的文章而来。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢
Servlet及相关类
servlet是一个接口,它的实现类有GenericServlet,而httpservlet是GenericServlet的一个子类,一般我们都会使用这个类。
servletconfig是用于保存servlet配置信息的数据结构,而servletcontext则负责保持servlet的上下文,web应用启动时加载web.xml信息于servletconfig中。
Jsp和ViewResolver
jsp页面需要编译成class文件并通过tomcat的类加载器进行加载,形成servlet实例,请求到来时实际上执行的是servlet代码,然后最终再通过viewresolver渲染成页面。
filter,listener
filter是过滤器,也需要在web.xml中配置,是责任链式的调用,在servlet执行service方法前执行。 listener则是监听器,由于容器组件都实现了lifecycle接口,所以可以在组件上添加监听器来控制生命周期。
web.xml
web.xml用来配置servlet和servlet的配置信息,listener和filter。也可以配置静态文件的目录等。
war包
waWAR包 WAR(Web Archive file)网络应用程序文件,是与平台无关的文件格式,它允许将许多文件组合成一个压缩文件。war专用在web方面 。
JAVA WEB工程,都是打成WAR包进行发布。
典型的war包内部结构如下:
webapp.war
| index.jsp
|
|— images
|— META-INF
|— WEB-INF
| web.xml // WAR包的描述文件
|
|— classes
| action.class // java类文件
|
|— lib
other.jar // 依赖的jar包
share.jar
tomcat基础
上一篇文章关于网络编程和NIO已经讲过了,这里按住不表。
log4j
log4j是非常常用的日志组件,不过现在为了使用更通用的日志组件,一般使用slf4j来配置日志管理器,然后再介入日志源,比如log4j这样的日志组件。
数据库驱动和连接池
一般我们会使用class.forname加载数据库驱动,但是随着Spring的发展,现在一般会进行数据源DataSource这个bean的配置,bean里面填写你的数据来源信息即可,并且在实现类中可以选择支持连接池的数据源实现类,比如c3poDataSource,非常方便。
数据库连接池本身和线程池类似,就是为了避免频繁建立数据库连接,保存了一部分连接并存放在集合里,一般可以用队列来存放。
除此之外,还可以使用tomcat的配置文件来管理数据库连接池,只需要简单的一些配置,就可以让tomcat自动管理数据库的连接池了。 应用需要使用的时候,通过jndi的方式访问即可,具体方法就是调用jndi命名服务的look方法。
单元测试
单元测试是工程中必不可少的组件,maven项目在打包期间会自动运行所有单元测试。一般我们使用junit做单元测试,统一地在test包中分别测试service和dao层,并且使用mock方法来构造假的数据,以便跳过数据库或者其他外部资源来完成测试。
Maven
maven是一个项目构建工具,基于约定大于配置的方式,规定了一个工程各个目录的用途,并且根据这些规则进行编译,测试和打包。 同时他提供了方便的包管理方式,以及快速部署的优势。
Git
git是分布式的代码管理工具,比起svn有着分布式的优势。太过常见了,略了。
Json和xml
数据描述形式不同,json更简洁。
hibernate和mybatis
由于jdbc方式的数据库连接和语句执行太过繁琐,重复代码太多,后来提出了jdbctemplate对数据进行bean转换。
但是还是差强人意,于是转而出现了hibernate这类的持久化框架。可以做到数据表和bean一一映射,程序只需要操作bean就可以完成数据库的curd。
mybatis比hibernate更轻量级,mybatis支持原生sql查询,并且也可以使用bean映射,同时还可以自定义地配置映射对象,更加灵活,并且在多表查询上更有优势。
5、Spring
Spring源码剖析1:Spring概述
Spring源码剖析2:初探Spring IOC核心流程
Spring源码剖析3:Spring IOC容器的加载过程
Spring源码剖析4:懒加载的单例Bean获取过程分析
Spring源码剖析5:JDK和cglib动态代理原理详解
Spring源码剖析6:Spring AOP概述
Spring源码剖析7:AOP实现原理详解
Spring源码剖析8:Spring事务概述
Spring源码剖析9:Spring事务源码剖析
SpringMVC
SpringMVC源码分析1:SpringMVC概述
SpringMVC源码分析2:SpringMVC设计理念与DispatcherServlet
SpringMVC源码分析3:DispatcherServlet的初始化与请求转发
SpringMVC源码分析4:DispatcherServlet如何找到正确的Controller
SpringMVC源码剖析5:消息转换器HttpMessageConverter与@ResponseBody注解
SpringMVC源码分析6:SpringMVC的视图解析原理
SpringBoot
todo
SpringCloud
todo
Spring和SpringMVC源码学习总结
这篇总结主要是基于我之前Spring和SpringMVC源码系列文章而形成的的。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢
Spring是一个框架,除了提供IOC和AOP以外,还加入了web等众多内容。
1 IOC:控制反转,改变类实例化的方式,通过xml等配置文件指定接口的实现类,让实现类和代码解耦,通过配置文件灵活调整实现类。
2 AOP: 面向切面编程,将切面代码封装,比如权限验证,日志模块等,这些逻辑重复率大,通过一个增强器封装功能,然后定义需要加入这些功能的切面,切面一般用表达式或者注解去匹配方法,可以完成前置和后置的处理逻辑。
3 SpringMVC是一个web框架,基于Spring之上,实现了web相关的功能,使用dispatcherservlet作为一切请求的处理入口。通过配置viewresolver解析页面,通过配置管理静态文件,还可以注入其他的配置信息,除此之外,springmvc可以访问spring容器的所有bean。
Spring源码总结
IOC:
1 Spring的bean容器也叫beanfactory,我们常用的applicationcontext实际上内部有一个listablebeanfactory实际存储bean的map。
2 bean加载过程:spring容器加载时先读取配置文件,一般是xml,然后解析xml,找到其中所有bean,依次解析,然后生成每个bean的beandefinition,存在一个map中,根据beanid映射实际bean的map。
3 bean初始化:加载完以后,如果不启用懒加载模式,则默认使用单例加载,在注册完bean以后,可以获取到beandefinition信息,然后根据该信息首先先检查依赖关系,如果依赖其他bean则先加载其他bean,然后通过反射的方式即newinstance创建一个单例bean。
为什么要用反射呢,因为实现类可以通过配置改变,但接口是一致的,使用反射可以避免实现类改变时无法自动进行实例化。
当然,bean也可以使用原型方式加载,使用原型的话,每次创建bean都会是全新的。
AOP:
AOP的切面,切点,增强器一般也是配置在xml文件中的,所以bean容器在解析xml时会找到这些内容,并且首先创建增强器bean的实例。
基于上面创建bean的过程,AOP起到了什么作用呢,或者是是否有参与到其中呢,答案是有的。
在获得beandefinition的时候,spring容器会检查该bean是否有aop切面所修饰,是否有能够匹配切点表达式的方法,如果有的话,在创建bean之前,会将bean重新封装成一个动态代理的对象。
代理类会为bean增加切面中配置的advisor增强器,然后返回bean的时候实际上返回的是一个动态代理对象。
所以我们在调用bean的方法时,会自动织入切面的增强器,当然,动态代理既可以选择jdk增强器,也可以选择cglib增强器。
Spring事务:
spring事务其实是一种特殊的aop方式。在spring配置文件中配置好事务管理器和声明式事务注解后,就可以使用@transactional进行事务方法的处理了。
事务管理器的bean中会配置基本的信息,然后需要配置事务的增强器,不同方法使用不同的增强器。当然如果使用注解的话就不用这么麻烦了。
然后和aop的动态代理方式类似,当Spring容器为bean生成代理时,会注入事务的增强器,其中实际上实现了事务中的begin和commit,所以执行方法的过程实际上就是在事务中进行的。
SpringMVC源码总结
1 dispatcherservlet概述 SpringMVC使用dispatcherservlet作为唯一如果,在web.xml中进行配置,他继承自frameworkservlet,向上继承自httpservletbean。
httpservletbean为dispatcherservlet加载了来自web.xml配置信息中的信息,保存在servletcontext上下文中,而frameworkservletbean则初始化了spring web的bean容器。
这个容器一般是配置在spring-mvc.xml中的,他独立于spring容器,但是把spring容器作为父容器,所以SpringMVC可以访问spring容器中的各种类。
而dispatcherservlet自己做了什么呢,因为springmvc中配置了很多例如静态文件目录,自动扫描bean注解,以及viewresovler和httpconverter等信息,所以它需要初始化这些策略,如果没有配置则会使用默认值。
2 dispatcherservlet的执行流程
首先web容器会加载指定扫描bean并进行初始化。
当请求进来后,首先执行service方法,然后到dodispatch方法执行请求转发,事实上,spring web容器已经维护了一个map,通过注解@requestmapping映射到对应的bean以及方法上。通过这个map可以获取一个handlerchain,真正要执行的方法被封装成一个handler,并且调用方法前要执行前置的一些过滤器。
最终执行handler方法时实际上就是去执行真正的方法了。
3 viewresolver
解析完请求和执行完方法,会把modelandview对象解析成一个view对象,让后使用view.render方法执行渲染,至于使用什么样的视图解析器,就是由你配置的viewresolver来决定的,一般默认是jspviewresolver。
4 httpmessageconverter
一般配合responsebody使用,可以将数据自动转换为json和xml,根据http请求中适配的数据类型来决定使用哪个转换器。
Java进阶
6、并发
Java并发指南1:并发基础与Java多线程
Java并发指南2:深入理解Java内存模型JMM
Java并发指南3:并发三大问题与volatile关键字,CAS操作
Java并发指南4:Java中的锁 Lock和synchronized
Java并发指南5:JMM中的final关键字解析
Java并发指南6:Java内存模型JMM总结
Java并发指南7:JUC的核心类AQS详解
Java并发指南8:AQS中的公平锁与非公平锁,Condtion
Java并发指南9:AQS共享模式与并发工具类的实现
Java并发指南10:Java 读写锁 ReentrantReadWriteLock 源码分析
Java并发指南11:解读 Java 阻塞队列 BlockingQueue
Java并发指南12:深度解读 java 线程池设计思想及源码实现
Java并发指南13:Java 中的 HashMap 和 ConcurrentHashMap 全解析
Java并发指南14:JUC中常用的Unsafe和Locksupport
Java并发指南15:Fork join并发框架与工作窃取算法剖析
Java并发编程学习总结
线程安全
线程安全一般指多线程之间的操作结果不会因为线程调度的顺序不同而发生改变。
互斥和同步
互斥一般指资源的独占访问,同步则要求同步代码中的代码顺序执行,并且也是单线程独占的。
JMM内存模型
JVM中的内存分区包括堆,栈,方法区等区域,这些内存都是抽象出来的,实际上,系统中只有一个主内存,但是为了方便Java多线程语义的实现,以及降低程序员编写并发程序的难度,Java提出了JMM内存模型,将内存分为主内存和工作内存,工作内存是线程独占的,实际上它是一系列寄存器,编译器优化后的结果。
as-if-Serial,happens-before
as if serial语义提供单线程代码的顺序执行保证,虽然他允许指令重排序,但是前提是指令重排序不会改变执行结果。
volatile
volatile语义实际上是在代码中插入一个内存屏障,内存屏障分为读写,写读,读读,写写四种,可以用来避免volatile变量的读写操作发生重排序,从而保证了volatile的语义,实际上,volatile修饰的变量强制要求线程写时将数据从缓存刷入主内存,读时强制要求线程从主内存中读取,因此保证了它的可见性。
而对于volatile修饰的64位类型数据,可以保证其原子性,不会因为指令重排序导致一个64位数据被分割成两个32位数据来读取。
synchronized和锁优化
synchronized是Java提供的同步标识,底层是操作系统的mutex lock调用,需要进行用户态到内核态的切换,开销比较大。
synchronized经过编译后的汇编代码会有monitor in和monitor out的字样,用于标识进入监视器模块和退出监视器模块,
监视器模块watcher会监控同步代码块中的线程号,只允线程号正确的线程进入。
Java在synchronized关键字中进行了多次优化。
比如轻量级锁优化,使用锁对象的对象头做文章,当一个线程需要获得该对象锁时,线程有一段空间叫做lock record,用于存储对象头的mask word,然后通过cas操作将对象头的mask word改成指向线程中的lockrecord。
如果成功了就是获取到了锁,否则就是发生了互斥。需要锁粗化,膨胀为互斥锁。
偏向锁,去掉了更多的同步措施,检查mask word是否是可偏向状态,然后检查mask word中的线程id是否是自己的id,如果是则执行同步代码,如果不是则cas修改其id,如果修改失败,则出现锁争用,偏向锁失效,膨胀为轻量级锁。
自旋锁,每个线程会被分配一段时间片,并且听候cpu调度,如果发生线程阻塞需要切换的开销,于是使用自旋锁不需要阻塞,而是忙等循环,一获取时间片就开始忙等,这样的锁就是自旋锁,一般用于并发量比较小,又担心切换开销的场景。
CAS操作
CAS操作是通过硬件实现的原子操作,通过一条指令完成比较和赋值的操作,防止发生因指令重排导致的非原子操作,在Java中通过unsafe包可以直接使用,在Java原子类中使用cas操作来完成一系列原子数据类型的构建,保证自加自减等依赖原值的操作不会出现并发问题。
cas操作也广泛用在其他并发类中,通过循环cas操作可以完成线程安全的并发赋值,也可以通过一次cas操作来避免使用互斥锁。
Lock类
AQS
AQS是Lock类的基石,他是一个抽象类,通过操作一个变量state来判断线程锁争用的情况,通过一系列方法实现对该变量的修改。一般可以分为独占锁和互斥锁。
AQS维护着一个CLH阻塞队列,这个队列主要用来存放阻塞等待锁的线程节点。可以看做一个链表。
一:独占锁 独占锁的state只有0和1两种情况(如果是可重入锁也可以把state一直往上加,这里不讨论),state = 1时说明已经有线程争用到锁。线程获取锁时一般是通过aqs的lock方法,如果state为0,首先尝试cas修改state=1,成功返回,失败时则加入阻塞队列。非公共锁使用时,线程节点加入阻塞队列时依然会尝试cas获取锁,最后如果还是失败再老老实实阻塞在队列中。
独占锁还可以分为公平锁和非公平锁,公平锁要求锁节点依据顺序加入阻塞队列,通过判断前置节点的状态来改变后置节点的状态,比如前置节点获取锁后,释放锁时会通知后置节点。
非公平锁则不一定会按照队列的节点顺序来获取锁,如上面所说,会先尝试cas操作,失败再进入阻塞队列。
二:共享锁 共享锁的state状态可以是0到n。共享锁维护的阻塞队列和互斥锁不太一样,互斥锁的节点释放锁后只会通知后置节点,而共享锁获取锁后会通知所有的共享类型节点,让他们都来获取锁。共享锁用于countdownlatch工具类与cyliderbarrier等,可以很好地完成多线程的协调工作
锁Lock和Conditon
Lock 锁维护这两个内部类fairsync和unfairsync,都继承自aqs,重写了部分方法,实际上大部分方法还是aqs中的,Lock只是重新把AQS做了封装,让程序员更方便地使用Lock锁。
和Lock锁搭配使用的还有condition,由于Lock锁只维护着一个阻塞队列,有时候想分不同情况进行锁阻塞和锁通知怎么办,原来我们一般会使用多个锁对象,现在可以使用condition来完成这件事,比如线程A和线程B分别等待事件A和事件B,可以使用两个condition分别维护两个队列,A放在A队列,B放在B队列,由于Lock和condition是绑定使用的,当事件A触发,线程A被唤醒,此时他会加入Lock自己的CLH队列中进行锁争用,当然也分为公平锁和非公平锁两种,和上面的描述一样。
Lock和condtion的组合广泛用于JUC包中,比如生产者和消费者模型,再比如cyliderbarrier。
###读写锁
读写锁也是Lock的一个子类,它在一个阻塞队列中同时存储读线程节点和写线程节点,读写锁采用state的高16位和低16位分别代表独占锁和共享锁的状态,如果共享锁的state > 0可以继续获取读锁,并且state-1,如果=0,则加入到阻塞队列中,写锁节点和独占锁的处理一样,因此一个队列中会有两种类型的节点,唤醒读锁节点时不会唤醒写锁节点,唤醒写锁节点时,则会唤醒后续的节点。
因此读写锁一般用于读多写少的场景,写锁可以降级为读锁,就是在获取到写锁的情况下可以再获取读锁。
并发工具类
countdownlatch主要通过AQS的共享模式实现,初始时设置state为N,N是countdownlatch初始化使用的size,每当有一个线程执行countdown,则state-1,state = 0之前所有线程阻塞在队列中,当state=0时唤醒队头节点,队头节点依次通知所有共享类型的节点,唤醒这些线程并执行后面的代码。
cycliderbarrier主要通过lock和condition结合实现,首先设置state为屏障等待的线程数,在某个节点设置一个屏障,所有线程运行到此处会阻塞等待,其实就是等待在一个condition的队列中,并且每当有一个线程到达,state -=1 则当所有线程到达时,state = 0,则唤醒condition队列的所有结点,去执行后面的代码。
samphere也是使用AQS的共享模式实现的,与countlatch大同小异,不再赘述。
exchanger就比较复杂了。使用exchanger时会开辟一段空间用来让两个线程进行交互操作,这个空间一般是一个栈或队列,一个线程进来时先把数据放到这个格子里,然后阻塞等待其他线程跟他交换,如果另一个线程也进来了,就会读取这个数据,并把自己的数据放到对方线程的格子里,然后双双离开。当然使用栈和队列的交互是不同的,使用栈的话匹配的是最晚进来的一个线程,队列则相反。
原子数据类型
原子数据类型基本都是通过cas操作实现的,避免并发操作时出现的安全问题。
同步容器
同步容器主要就是concurrenthashmap了,在集合类中我已经讲了chm了,所以在这里简单带过,chm1.7通过分段锁来实现锁粗化,使用的死LLock锁,而1.8则改用synchronized和cas的结合,性能更好一些。
还有就是concurrentlinkedlist,ConcurrentSkipListMap与CopyOnWriteArrayList。
第一个链表也是通过cas和synchronized实现。
而concurrentskiplistmap则是一个跳表,跳表分为很多层,每层都是一个链表,每个节点可以有向下和向右两个指针,先通过向右指针进行索引,再通过向下指针细化搜索,这个的搜索效率是很高的,可以达到logn,并且它的实现难度也比较低。通过跳表存map就是把entry节点放在链表中了。查询时按照跳表的查询规则即可。
CopyOnWriteArrayList是一个写时复制链表,查询时不加锁,而修改时则会复制一个新list进行操作,然后再赋值给原list即可。 适合读多写少的场景。
阻塞队列
BlockingQueue 实现之 ArrayBlockingQueue
ArrayBlockingQueue其实就是数组实现的阻塞队列,该阻塞队列通过一个lock和两个condition实现,一个condition负责从队头插入节点,一个condition负责队尾读取节点,通过这样的方式可以实现生产者消费者模型。
BlockingQueue 实现之 LinkedBlockingQueue
LinkedBlockingQueue是用链表实现的阻塞队列,和arrayblockqueue有所区别,它支持实现为无界队列,并且它使用两个lock和对应的condition搭配使用,这是因为链表可以同时对头部和尾部进行操作,而数组进行操作后可能还要执行移位和扩容等操作。
所以链表实现更灵活,读写分别用两把锁,效率更高。
BlockingQueue 实现之 SynchronousQueue
SynchronousQueue实现是一个不存储数据的队列,只会保留一个队列用于保存线程节点。详细请参加上面的exchanger实现类,它就是基于SynchronousQueue设计出来的工具类。
BlockingQueue 实现之 PriorityBlockingQueue
PriorityBlockingQueue
PriorityBlockingQueue是一个支持优先级的无界队列。默认情况下元素采取自然顺序排列,也可以通过比较器comparator来指定元素的排序规则。元素按照升序排列。
DelayQueue
DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。我们可以将DelayQueue运用在以下应用场景:
缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。
线程池
类图
首先看看executor接口,只提供一个run方法,而他的一个子接口executorservice则提供了更多方法,比如提交任务,结束线程池等。
然后抽象类abstractexecutorservice提供了更多的实现了,最后我们最常使用的类ThreadPoolExecutor就是继承它来的。
ThreadPoolExecutor可以传入多种参数来自定义实现线程池。
而我们也可以使用Executors中的工厂方法来实例化常用的线程池。
常用线程池
比如newFixedThreadPool
newSingleThreadExecutor newCachedThreadPool
newScheduledThreadPool等等,这些线程池即可以使用submit提交有返回结果的callable和futuretask任务,通过一个future来接收结果,或者通过callable中的回调函数call来回写执行结果。也可以用execute执行无返回值的runable任务。
在探讨这些线程池的区别之前,先看看线程池的几个核心概念。
任务队列:线程池中维护了一个任务队列,每当向线程池提交任务时,任务加入队列。
工作线程:也叫worker,从线程池中获取任务并执行,执行后被回收或者保留,因情况而定。
核心线程数和最大线程数,核心线程数是线程池需要保持存活的线程数量,以便接收任务,最大线程数是能创建的线程数上限。
newFixedThreadPool可以设置固定的核心线程数和最大线程数,一个任务进来以后,就会开启一个线程去执行,并且这部分线程不会被回收,当开启的线程达到核心线程数时,则把任务先放进任务队列。当任务队列已满时,才会继续开启线程去处理,如果线程总数打到最大线程数限制,任务队列又是满的时候,会执行对应的拒绝策略。
拒绝策略一般有几种常用的,比如丢弃任务,丢弃队尾任务,回退给调用者执行,或者抛出异常,也可以使用自定义的拒绝策略。
newSingleThreadExecutor是一个单线程执行的线程池,只会维护一个线程,他也有任务队列,当任务队列已满并且线程数已经是1个的时候,再提交任务就会执行拒绝策略。
newCachedThreadPool比较特别,第一个任务进来时会开启一个线程,而后如果线程还没执行完前面的任务又有新任务进来,就会再创建一个线程,这个线程池使用的是无容量的SynchronousQueue队列,要求请求线程和接受线程匹配时才会完成任务执行。 所以如果一直提交任务,而接受线程来不及处理的话,就会导致线程池不断创建线程,导致cpu消耗很大。
ScheduledThreadPoolExecutor内部使用的是delayqueue队列,内部是一个优先级队列priorityqueue,也就是一个堆。通过这个delayqueue可以知道线程调度的先后顺序和执行时间点。
Fork/Join框架
又称工作窃取线程池。
我们在大学算法课本上,学过的一种基本算法就是:分治。其基本思路就是:把一个大的任务分成若干个子任务,这些子任务分别计算,最后再Merge出最终结果。这个过程通常都会用到递归。
而Fork/Join其实就是一种利用多线程来实现“分治算法”的并行框架。
另外一方面,可以把Fori/Join看作一个单机版的Map/Reduce,只不过这里的并行不是多台机器并行计算,而是多个线程并行计算。
与ThreadPool的区别 通过上面例子,我们可以看出,它在使用上,和ThreadPool有共同的地方,也有区别点: (1) ThreadPool只有“外部任务”,也就是调用者放到队列里的任务。 ForkJoinPool有“外部任务”,还有“内部任务”,也就是任务自身在执行过程中,分裂出”子任务“,递归,再次放入队列。 (2)ForkJoinPool里面的任务通常有2类,RecusiveAction/RecusiveTask,这2个都是继承自FutureTask。在使用的时候,重写其compute算法。
工作窃取算法 上面提到,ForkJoinPool里有”外部任务“,也有“内部任务”。其中外部任务,是放在ForkJoinPool的全局队列里面,而每个Worker线程,也有一个自己的队列,用于存放内部任务。
窃取的基本思路就是:当worker自己的任务队列里面没有任务时,就去scan别的线程的队列,把别人的任务拿过来执行
7、JVM
JVM总结
深入理解JVM虚拟机1:JVM内存的结构与消失的永久代
深入理解JVM虚拟机2:JVM垃圾回收基本原理和算法
深入理解JVM虚拟机3:垃圾回收器详解
深入理解JVM虚拟机4:Javaclass介绍与解析实践
深入理解JVM虚拟机5:虚拟机字节码执行引擎
深入理解JVM虚拟机6:深入理解JVM类加载机制
深入理解JVM虚拟机7:JNDI,OSGI,Tomcat类加载器实现
深入了解JVM虚拟机8:Java的编译期优化与运行期优化
深入理解JVM虚拟机9:JVM监控工具与诊断实践
深入理解JVM虚拟机10:JVM常用参数以及调优实践
深入理解JVM虚拟机11:Java内存异常原理与实践
深入理解JVM虚拟机12:JVM性能管理神器VisualVM介绍与实战
深入理解JVM虚拟机13:再谈四种引用及GC实践
深入理解JVM虚拟机14:GC调优思路与常用工具
JVM介绍和源码
首先JVM是一个虚拟机,当你安装了jre,它就包含了jvm环境。JVM有自己的内存结构,字节码执行引擎,因此class字节码才能在jvm上运行,除了Java以外,Scala,groovy等语言也可以编译成字节码而后在jvm中运行。JVM是用c开发的。
JVM内存模型
内存模型老生常谈了,主要就是线程共享的堆区,方法区,本地方法栈。还有线程私有的虚拟机栈和程序计数器。
堆区存放所有对象,每个对象有一个地址,Java类jvm初始化时加载到方法区,而后会在堆区中生成一个Class对象,来负责这个类所有实例的实例化。
栈区存放的是栈帧结构,栈帧是一段内存空间,包括参数列表,返回地址,局部变量表等,局部变量表由一堆slot组成,slot的大小固定,根据变量的数据类型决定需要用到几个slot。
方法区存放类的元数据,将原来的字面量转换成引用,当然,方法区也提供常量池,常量池存放-128到127的数字类型的包装类。 字符串常量池则会存放使用intern的字符串变量。
JVM OOM和内存泄漏
这里指的是oom和内存泄漏这类错误。
oom一般分为三种,堆区内存溢出,栈区内存溢出以及方法区内存溢出。
堆内存溢出主要原因是创建了太多对象,比如一个集合类死循环添加一个数,此时设置jvm参数使堆内存最大值为10m,一会就会报oom异常。
栈内存溢出主要与栈空间和线程有关,因为栈是线程私有的,如果创建太多线程,内存值超过栈空间上限,也会报oom。
方法区内存溢出主要是由于动态加载类的数量太多,或者是不断创建一个动态代理,用不了多久方法区内存也会溢出,会报oom,这里在1.7之前会报permgem oom,1.8则会报meta space oom,这是因为1.8中删除了堆中的永久代,转而使用元数据区。
内存泄漏一般是因为对象被引用无法回收,比如一个集合中存着很多对象,可能你在外部代码把对象的引用置空了,但是由于对象还被集合给引用着,所以无法被回收,导致内存泄漏。测试也很简单,就在集合里添加对象,添加完以后把引用置空,循环操作,一会就会出现oom异常,原因是内存泄漏太多了,导致没有空间分配新的对象。
常见调试工具
命令行工具有jstack jstat jmap 等,jstack可以跟踪线程的调用堆栈,以便追踪错误原因。
jstat可以检查jvm的内存使用情况,gc情况以及线程状态等。
jmap用于把堆栈快照转储到文件系统,然后可以用其他工具去排查。
visualvm是一款很不错的gui调试工具,可以远程登录主机以便访问其jvm的状态并进行监控。
class文件结构
class文件结构比较复杂,首先jvm定义了一个class文件的规则,并且让jvm按照这个规则去验证与读取。
开头是一串魔数,然后接下来会有各种不同长度的数据,通过class的规则去读取这些数据,jvm就可以识别其内容,最后将其加载到方法区。
JVM的类加载机制
jvm的类加载顺序是bootstrap类加载器,extclassloader加载器,最后是appclassloader用户加载器,分别加载的是jdk/bin ,jdk/ext以及用户定义的类目录下的类(一般通过ide指定),一般核心类都由bootstrap和ext加载器来加载,appclassloader用于加载自己写的类。
双亲委派模型,加载一个类时,首先获取当前类加载器,先找到最高层的类加载器bootstrap让他尝试加载,他如果加载不了再让ext加载器去加载,如果他也加载不了再让appclassloader去加载。这样的话,确保一个类型只会被加载一次,并且以高层类加载器为准,防止某些类与核心类重复,产生错误。
defineclass findclass和loadclass
类加载classloader中有两个方法loadclass和findclass,loadclass遵从双亲委派模型,先调用父类加载的loadclass,如果父类和自己都无法加载该类,则会去调用findclass方法,而findclass默认实现为空,如果要自定义类加载方式,则可以重写findclass方法。
常见使用defineclass的情况是从网络或者文件读取字节码,然后通过defineclass将其定义成一个类,并且返回一个Class对象,说明此时类已经加载到方法区了。当然1.8以前实现方法区的是永久代,1.8以后则是元空间了。
JVM虚拟机字节码执行引擎
jvm通过字节码执行引擎来执行class代码,他是一个栈式执行引擎。这部分内容比较高深,在这里就不献丑了。
编译期优化和运行期优化
编译期优化主要有几种
1 泛型的擦除,使得泛型在编译时变成了实际类型,也叫伪泛型。
2 自动拆箱装箱,foreach循环自动变成迭代器实现的for循环。
3 条件编译,比如if(true)直接可得。
运行期优化主要有几种
1 JIT即时编译
Java既是编译语言也是解释语言,因为需要编译代码生成字节码,而后通过解释器解释执行。
但是,有些代码由于经常被使用而成为热点代码,每次都编译太过费时费力,干脆直接把他编译成本地代码,这种方式叫做JIT即时编译处理,所以这部分代码可以直接在本地运行而不需要通过jvm的执行引擎。
2 公共表达式擦除,就是一个式子在后面如果没有被修改,在后面调用时就会被直接替换成数值。
3 数组边界擦除,方法内联,比较偏,意义不大。
4 逃逸分析,用于分析一个对象的作用范围,如果只局限在方法中被访问,则说明不会逃逸出方法,这样的话他就是线程安全的,不需要进行并发加锁。
1
JVM的垃圾回收
1 GC算法:停止复制,存活对象少时适用,缺点是需要两倍空间。标记清除,存活对象多时适用,但是容易产生随便。标记整理,存活对象少时适用,需要移动对象较多。
2 GC分区,一般GC发生在堆区,堆区可分为年轻代,老年代,以前有永久代,现在没有了。
年轻代分为eden和survior,新对象分配在eden,当年轻代满时触发minor gc,存活对象移至survivor区,然后两个区互换,等待下一场gc, 当对象存活的阈值达到设定值时进入老年代,大对象也会直接进入老年代。
老年代空间较大,当老年代空间不足以存放年轻代过来的对象时,开始进行full gc。同时整理年轻代和老年代。 一般年轻代使用停止复制,老年代使用标记清除。
3 垃圾收集器
serial串行
parallel并行
它们都有年轻代与老年代的不同实现。
然后是scanvage收集器,注重吞吐量,可以自己设置,不过不注重延迟。
cms垃圾收集器,注重延迟的缩短和控制,并且收集线程和系统线程可以并发。
cms收集步骤主要是,初次标记gc root,然后停顿进行并发标记,而后处理改变后的标记,最后停顿进行并发清除。
g1收集器和cms的收集方式类似,但是g1将堆内存划分成了大小相同的小块区域,并且将垃圾集中到一个区域,存活对象集中到另一个区域,然后进行收集,防止产生碎片,同时使分配方式更灵活,它还支持根据对象变化预测停顿时间,从而更好地帮用户解决延迟等问题。
JVM的锁优化
在Java并发中讲述了synchronized重量级锁以及锁优化的方法,包括轻量级锁,偏向锁,自旋锁等。详细内容可以参考我的专栏:Java并发技术指南