java工程师小结1

https://www.cnblogs.com/signheart/p/acde4d7ebebb6a6ead403e312af0c76b.html
http://blog.csdn.net/baidu_37464759/article/details/77683588
http://blog.csdn.net/hhh594521/article/details/72829487
https://www.cnblogs.com/lkdirk/p/6719248.html
http://blog.csdn.net/wodewutai17quiet/article/details/78187386
https://www.cnblogs.com/cenyu/p/8047691.html
http://blog.csdn.net/fighterandknight/article/details/61476335
https://www.cnblogs.com/WJQ2017/p/7588681.html
http://www.importnew.com/22007.html


//上面是部分参考的网站,其实参考了很多很多,不一一说明了。




编译型语言解释型语言?动态类型语言,静态类型语言?动态语言静态语言?强类型语言弱类型语言?Java是什么样的计算机语言?
编译型语言 需要用编译器编译源代码 通常还需要链接 即把代码模块和一些头文件模块以及其他模块链接成目标文件
一次编译 到处运行
解释型语言 每次解释执行一行,每一次运行都需要解释一次
动态类型语言表示变量的类型是不确定的 静态类型原因在使用变量之前必须声明数据类型
动态语言表示结构可以改变 运行时可以加入新的对象,变量或者其他模块
Java是强类型 综合型虽然编译了一次 但是在不同机器上通过jvm每次还是解释一次class文件
静态类型 本身是静态语言 后来加入了一些动态特性。
比如多态:存储器生成的对象不在编译过程中决定,而是延迟到运行时由解释器确定。


什么是变量?
编程的本质 就是对内存中的数据的访问和修改
程序所用的数据都保存在内存中 程序员需要一种机制来访问或者修改内存中的数据 这种机制就是变量
每个变量都代表了一块内存,而且变量是有名字的 程序对变量赋值实际上就是把数据装入该变量所代表的内存中




Java平台无关性?
java是跨平台的编程语言,之所以能跨平台 是因为java语言有jvm的存在 
Java语言不需要像其他某些语言一样 在不同的平台上编译成不同的目标代码
而是编译成为jvm能够解释执行的class文件 在不同平台下 都有jvm解释成相对应的
可执行目标文件 然后执行


匿名内部类?接口?抽象类?
https://www.cnblogs.com/chenssy/p/3388487.html
抽象类其实是可以实例化的,但是他的实例化方式不是通过new方式来创建对象,
而是通过父类的引用来指向子类的实例来间接地实现父类的实例化
(因为子类要实例化前,一定会先实例化他的父类。这样创建了继承抽象类的子类的对象,也就把其父类(抽象类)给实例化了).
但是:接口是不能被实例化的(接口压根就没有构造函数)。
使用内部类最吸引人的原因是:每个内部类都能独立地继承一个(接口的)实现,
所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。


Java四大特性?
封装:即包装的意思,也就是信息隐藏,通过抽象数据类型将数据和基于数据的操作封装在一起,构成不可分割的整体
这样一来,数据被保护在抽象数据类型里面,内部细节被隐藏,只留下和外部的一些接口,用户无需知道内部实现细节
只需要通过接口调用就行。
封装好处:良好的封装能减少耦合,内部的结构可以自由修改,可以对成员精确控制,能隐藏细节信息
抽象:抽象就是将一类实体的共同特性提炼出来 就是将客观事物的共通,共同的地方统一起来
抽象包括两个方面:过程抽象和数据抽象。
过程抽象把一个系统按功能划分成若干个子系统,进行"自顶向下逐步求精"的程序设计
数据抽象以数据为中心,把数据类型和施加在该类型对象上的操作作为一个整体(对象)来进行描述,形成抽象数据类型ADT。
其实抽象是一种习惯性思维,不仅仅是面向对象方法论中的特性。

继承:继承就像是我们现实生活中的父子关系,儿子可以遗传父亲的一些特性,在面向对象语言中,
就是一个类可以继承另一个类的一些特性,从而可以代码重用,其实继承体现的是is-a关系,父类同子类在本质上还是一类实体;
想到组合 组合其实就是has a的关系
多态:多态就是通过传递给父类引用不同的子类对象从而表现出不同的行为 
java在程序运行时才确定具体的类
也就是说引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定
而是在程序运行期间才确定 


C语言数组和java数组的区别?
定义:c type arrayname[length];  int a[10] = {xxx};int a[] = {xxx};
  java type arrayname; type[] arrayname;都可以 int a[] = new int[10]; int[] a = new int[10];
  
初始化: 编译阶段C语言不会检查数组越界,C语言不会给数组名分配内存空间,会给数组分配连续的内存空间
Java 会给数组名分配内存空间 不会给数组分配  要通过new关键字实现分配
可以是引用类型 不一定是连续分配的内存空间
Java数组被看做是一个类,有length属性和相对应的方法 而C语言sizeof(data)/siezof(data[0])

内存本质:Java中数组看做对象 存储是不一样的。//重点掌握
会画图解释 一维数组 二维数组 对象数组 内存图解析 栈 堆 方法区

Java里面的引用机制?C语言里面的指针?c++引用?
java引用机制是可以用来管理对象的 也就是内存回收  c++中纯粹的引用别名
Java引用不是显示定义的 存储的是堆中对面的地址 对他操作就是对堆中对象的操作 c++通过运算符重载提供的大幅度的支持 
主要是用类模拟基本对象的要求 而且他是显示定义的 
java中的引用地址会改变 c++中的引用不会改变,改变的是指向地址的内容 


再说说c指针:
java不可以对引用进行+ -之类的操作为了安全没有这样的权限  c指针就是内存的编号,可以加减,指哪打哪
c指针在函数调用时可以只传递一个指针而不是整份数据
java普通数据类型是不能进行引用定义的,如果要对普通数据类型进行函数调用时的地址传递(即java中的引用传递),
必须把数据封装到类中。java的这种特性使得在java的函数或类的参数传递时可以实现与c中指针相同的功能。


总体来说 Java的引用更像是c++/c中的指针
所占内存:引用声明时没有实体(没有绑定堆中的具体对象),不占空间()。C指针如果声明后会用到才会赋值,如果用不到不会分配内存。
内存溢出:JAVA引用的使用权限比较小,不会产生内存溢出。C指针是容易产生内存溢出的,所以程序员要小心使用,及时回收。
从本质上讲:JAVA中的引用和C中的指针本质上都是想通过一个别名,找到要操作的目标(变量对象等),方便在程序里操作;
所不同的是JAVA的办法更安全,使用更加方便些,但没有了C的灵活,高效。


类的初始化?
一个类的初始化过程:
在main方法开始之前,所有的类都会编译生成class文件
但是只有使用的类的class才会被加载
从main()方法开始
1.若要加载类A,则先加载执行其父类B(Object)的静态变量以及静态语句块(执行先后顺序按排列的先后顺序)。
2.然后再加载执行类A的静态变量以及静态语句块。(并且1、2步骤只会执行1次)
3.若需实例化类A,则先调用其父类B的构造函数,并且在调用其父类B的构造函数前,
依次先调用父类B中的非静态变量及非静态语句块.最后再调用父类B中的构造函数初始化。
4.然后再依次调用类A中的非静态变量及非静态语句块.最后调用A中的构造函数初始化。( 并且3、4步骤可以重复执行)
5.而对于静态方法和非静态方法都是被动调用,即系统不会自动调用执行,所以用户没有调用时都不执行,
主要区别在于静态方法可以直接用类名直接调用(实例化对象也可以),而非静态方法只能先实例化对象后才能调用。
//写一个例子
注意:Java中对象存在 不能够说先给对象分配内存然后初始化成员变量
如果说这个对象表示的那片内存的话 那么只要jvm给对象分配了内存
这个对象就算是存在了 但是我们口头所说的一般都是可以用的对象
也就是初始化后有一个对象引用可以使用的对象


构造器是创建Java对象的途径,是不是说构造器完全负责创建Java对象?
答:不是的。构造器是创建Java对象的重要途径,通过new关键字调用构造器时,构造器也确实返回了该类的对象,
但是这个对象并不是完全由构造器负责创建的。实际上,当程序员调用构造器时,系统会先为该对象分配内存空间,
并为这个对象执行默认初始化,这个对象已经产生了——这些操作在构造器执行之前都完成了。
也就是说,当系统开始执行构造器的构造体之前,系统已经创建一个对象,只是这个对象还不能被外部程序访问,
只能在该构造器中通过this来引用。当构造器的执行体执行结束后,这个对象作为构造器的返回值被返回
通常还会赋给另一个引用变量,从而让程序外部可以访问该对象。

为对象分配空间——>实例变量默认初始化——>执行构造器的执行体——>通过this给实例变量赋值——>构造器返回对象给引用变量
在类定义中的属性若不赋予初值,则JAVA会自动赋予这个属性一个默认值

不可变类?
类不可变
不是指final修饰这个类 而是final修饰这个类的成员变量 final修饰这个类的话表示这个类不能被继承也就是没有子类
1、使用private和final修饰符来修饰该类的属性。
2、提供带参数的构造器,用于根据传入的参数来初始化属性。
3、仅为该类属性提供getter方法,不要提供setter方法。
4、如果有必要,重写hashCode和equals方法,同时应保证两个用equals方法判断为相等的对象,其hashCode也应相等
但是值得注意的是,该类的属性虽然是被final修饰的,但若属性是非String的其他引用类型的话,
那么虽然该属性的内容(所指对象的地址)不会改变,但其指向的对象却有可能会改变,这样的类当然并不能成为不可变类
为什么String是不可变的
通过查看源码 我们知道String其实封装了char[] value    还有hash 全部用final修饰
所以无法改变 (char数组是基本类型的数组)任何一个属性 整个String也就无法改变
实际上 string可以通过反射改变 但我们通常不这么做
有什么用处?
不可变类因为是不可变的特性 可用于数据共享 还可以安全的被多个线程访问,反正不能改变属性值
如果说是为了节省内存空间,优化程序性能,不可变类可以起到重用的功能
关于可缓存实例的不可变类:
public class CacheImmutable//定义了一个不可变类,使用这个类的数组实现  
{  
   //数组来缓存已有的实例  
   private static CacheImmutable[] cache=new CacheImmutable[10];  
   //设置一个变量来指向数组的偏移量  
   private static int pos=0;  
   private final String name;  
   public CacheImmutable(String name)  
   {  
       this.name=name;  
   }  
   public String getName()  
   {  
       return this.name;  
   }  
   public static CacheImmutable valueOf(String name)  
   {  //遍历缓冲池  
       for(int i=0;i<10;i++)  
       {  //如果有相同的实例,直接返回已有实例  
           if(cache[i] !=null && cache[i].getName().equals(name))  
           {  
               return cache[i];  
           }  
           //如果已满,则就要把第一个覆盖  
           if(pos==10)  
           {   pos=0;  
               cache[pos++]=new CacheImmutable(name);  
           }  
           //否则就放在最后一个位置  
           else  
           {  
               cache[pos++]=new CacheImmutable(name);  
           }  
       }  
       return cache[pos-1];  
   }  
   //重写equals方法  
   public boolean equals(Object obj)  
   {  
       if(this ==obj)  
       {  
           return true;  
       }  
       if(obj!=null&&obj.getClass()==CacheImmutable.class)  
       {  
           CacheImmutable ci =(CacheImmutable)obj;  
           return obj.equals(ci.getName());  
       }  
    return false;  
   }  
   //重写hashCode方法  
   public int hashCode()  
   {  
       return name.hashCode();  
   }  
}  
public class CacheImmutableText  
{   
    public static void main(String[] args) throws NullPointerException//为了避免出空指针错误,这里把错误抛出!!!!  
    {   
       CacheImmutable c1=CacheImmutable.valueOf("Hello");  
       CacheImmutable c2=CacheImmutable.valueOf("Hello");  
       System.out.println(c1==c2);  
    }  
}  //简单缓存不可变类的实现
1)为了防止耗尽内存,在实例缓存中存放的是对象的软引用(SoftReference),
如果一个对象仅仅持有软引用,Java虚拟机会在内存不足的情况下回收它的内存
2)在程序的生命周期中,对于程序不需要经常访问的实例,应该使用new语句创建它,使它能及时结束生命周期
3)对于程序需要经常访问的实例,那就用valueOf()方法来获得它,因为该方法能把实例放到缓存中,使它可以被重用
4)只有满足以下条件的不可变类才需要实例缓存
A、不可变类的实例的数量有限
B、在程序运行过程中,需要频繁访问不可变类的一些特定实例。这些实例拥有与程序本身同样长的生命周期




单例类和单例模式?//都要会写
Java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例、饿汉式单例、登记式单例。
  单例模式有以下特点:
  1、单例类只能有一个实例。
  2、单例类必须自己创建自己的唯一实例。
  3、单例类必须给所有其他对象提供这一实例。
  
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例


//懒汉式单例类.在第一次调用的时候实例化自己   
public class Singleton {  
    private Singleton() {}  
    private static Singleton single=null;  
    //静态工厂方法   
    public static Singleton getInstance() {  
         if (single == null) {    
             single = new Singleton();  
         }    
        return single;  
    }  
}  
//在getInstance方法上加同步   每次都要同步
public static synchronized Singleton getInstance() {
         if (single == null) {
             single = new Singleton();
         }
        return single;
}
//双重检查锁定  第一次同步 以后存在了就不需要同步
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
   if (single == null) {
  single = new Singleton();
   } 
}

return single;   

//静态内部类   classloader机制  线程安全 
public class Singleton {    
    private static class LazyHolder {    
       private static final Singleton INSTANCE = new Singleton();    
    } 
    private Singleton (){}
    public static final Singleton getInstance() { 
       return LazyHolder.INSTANCE; 
    }    
}  


//饿汉式单例类.在类初始化时,已经自行实例化   不需要使用synchronized
public class Singleton1 {  
    private Singleton1() {}  
    private static final Singleton1 single = new Singleton1();  
    //静态工厂方法   
    public static Singleton1 getInstance() {  
        return single;  
    }  
}
登记式暂时不考虑
饿汉式和懒汉式区别
从名字上来说,饿汉和懒汉,
饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了,
而懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例。
另外从以下两点再区分以下这两种方式:
1、线程安全:
饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,
懒汉式本身是非线程安全的,为了实现线程安全有几种写法,分别是上面的1、2、3,
这三种实现在资源加载和性能方面有些区别。
2、资源加载和性能:
饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,
但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成,
而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,
如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。
至于1、2、3这三种实现又有些区别,
第1种,在方法调用上加了同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟99%的情况下是不需要同步的
第2种,在getInstance中做了两次null检查,确保了只有第一次调用单例的时候才会做同步,
这样也是线程安全的,同时避免了每次都同步的性能损耗
第3种,利用了classloader的机制来保证初始化instance时只有一个线程,所以也是线程安全的,同时没有性能损耗。


登记式单例。  了解
//类似Spring里面的方法,将类名注册,下次从里面直接获取。
登记式单例实际上维护了一组单例类的实例,将这些实例存放在一个Map(登记薄)中,
对于已经登记过的实例,则从Map直接返回,对于没有登记的,则先登记,然后返回。 
首先它用的比较少,另外其实内部实现还是用的饿汉式单例,因为其中的static方法块,
它的单例在类被装载的时候就被实例化了。


浅复制与深复制概念
⑴浅复制(浅克隆)
被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。
换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。
⑵深复制(深克隆)
被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。
那些引用其他对象的变量将指向被复制过的新对象,而不再是原
有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。

this引用的理解?静态成员和非静态成员?
this指针:this是封装在某个定义里面的变量,还是非静态变量
this指针其实是类的非静态成员,一个时刻jvm只会对一个this引用的对象进行处理,同类的其他对象是不会进行处理的
当前的this所引用的对象就是jvm正在处理的,而且运行到其他时间,this引用的对象也会是不同的。


数组和集合的区别?
A:长度区别
数组的长度固定
集合长度可变
B:内容不同
数组存储的是同一种类型的元素
而集合可以存储不同类型的元素
C:元素的数据类型问题
数组可以存储基本数据类型,也可以存储引用数据类型
集合只能存储引用类型  基本类型 自动拆装箱


ArrayList 
ArrayList是动态数组集合实现,既然是数组,当然支持下标随机访问
不过是 通过get(index)这样访问,而不是说[index]
ArrayList的动态性在于add 和remove 底层依然是数组的增删 只是ArrayList是集合,他只能够装入引用类型
所以 他做的必须的优化就是remove会把后面元素往前面移动以后,置为null,方便jvm gc
add有可能扩容,初始默认容量是10,每一次扩容是约为1.5倍 oldcap = oldcap + oldcap>>2;
扩容调用ensureCapacity->ensureExplicitCapacity(minCapacity)->grow(minCapacity);
如果你去查看源码,你会发现大部分集合都是实现了fail-fast机制的,这个机制是什么呢?


fail-fast机制
  fail-fast机制也叫作”快速失败”机制,是Java集合中的一种错误检测机制。
  在对集合进行迭代过程中,除了迭代器可以对集合进行数据结构上的修改,其他的对集合的数据结构进行修改,
都会抛出ConcurrentModificationException错误。
  这里,所谓的进行数据结构上进行修改,是指对存储的对象,进行add、set、remove操作,进而对数据发生改变。
  ArrayList中,有个modCount的变量,每次进行add、set、remove等操作,都会执行modCount++。
  在获取ArrayList的迭代器时,会将ArrayList中的modCount保存在迭代中,每次执行add,set,remove等操作,
都会执行一次检查,调用checkForComodification方法,对modCount进行比较。
如果迭代器中的modCount和List中的modCount不同,则抛出ConcurrentModificationException。
面对并发的修改,迭代器很快就会完全失败,也就是说,迭代器的快速失败行为不能得到保证。比如
我有可能刚好遍历到最后倒数第二个元素,删除了最后一个元素,size-1,于是跳出最后一层循环,
这样逃避掉了ConcurrentModificationException异常,因为这个异常抛出的检测机制是在删除等操作完毕后遍历的下一轮
才开始判断mod 和expectedMod 的 所以,迭代器的快速失败行为应该仅用于检测程序错误。
//所有实现了fail-fast机制的集合,做法都类似于此。


为了避免这个问题,我们可以
1>采用List集合专用的迭代器遍历,ListIterator,《《《《《只能是单线程,否则仍然需要同步操作
ListIterator相比Iterator而言,功能更加强大,
1.使用范围不同,Iterator可以应用于所有的集合,Set、List和Map和这些集合的子类型。
而ListIterator只能用于List及其子类型。
2.ListIterator有add方法,可以向List中添加对象,而Iterator不能。
3.ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,
但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator不可以。
4.ListIterator可以定位当前索引的位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。
5.都可实现删除操作,但是ListIterator可以实现对象的修改,set()方法可以实现。Iterator仅能遍历,不能修改。


2>在遍历过程中所有涉及到改变modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList
这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。手动的添加synchronized同步块
可以指定锁对象。
SynchronizedList对部分操作加上了synchronized关键字以保证线程安全。但其iterator()操作还不是线程安全的。
但是它可以包装不同的List集合,弹性,扩展性兼容性好。可以去参考如下网址;
http://blog.csdn.net/zljjava/article/details/48139465
3>使用CopyOnWriteArrayList来替换ArrayList。
CopyOnWriteArrayList是java.util.concurrent包下的类,支持多线程操作。
其底层实现和ArrayList一样也是数组实现,同样有add remove等操作方法。 
CopyOnWriterArrayList所代表的核心概念就是:
任何对array在结构上有所改变的操作(add、remove、clear等),CopyOnWriterArrayList都会copy现有的数据,
再在copy的数据上修改,这样就不会影响COWIterator中的数据了,修改完成之后改变原有数据的引用即可。
同时这样造成的代价就是产生大量的对象,同时数组的copy也是相当有损耗的

内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,
旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,
只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。
如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,
那么这个时候很有可能造成频繁的Yong GC和Full GC。频繁的GC是因为修改CopyOnWriteArrayList里大量的元素造成的

使用了重入锁ReentrantLock, 使用了重入锁ReentrantLock 
如果锁具备可重入性,则称作为可重入锁 
一个线程A执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,
假如synchronized不具备可重入性,此时线程A需要重新申请锁
ReentrantLock锁除了synchronized的功能,多了三个高级功能.
等待可中断,公平锁,绑定多个Condition.
1.等待可中断
在持有锁的线程长时间不释放锁的时候,等待的线程可以选择放弃等待.  
2.公平锁
按照申请锁的顺序来一次获得锁称为公平锁.FIFO队列
公平锁在很多时候都是不推荐使用的   因为效率不高 




Vector:
我们说不需要学,已经过时了,没什么用,因为太暴力,直接在方法上加的同步操作 同步对象为this
//SynchronizedList 和 CopyOnwriteList  Vector区别
写操作:在线程数目增加时CopyOnWriteArrayList的写操作性能下降非常严重,
而Collections.synchronizedList虽然有性能的降低,但下降并不明显。
        读操作:在多线程进行读时,Collections.synchronizedList和CopyOnWriteArrayList均有性能的降低,
但是Collections.synchronizedList的性能降低更加显著。

mutex就是当前的SynchronizedCollection对象,而SynchronizedRandomAccessList继承自SynchronizedList,
SynchronizedList又继承自SynchronizedCollection,所以SynchronizedRandomAccessList中的mutex
也就是SynchronizedRandomAccessList的this对象。所以在GoodListHelper中使用的锁list对象,
和SynchronizedRandomAccessList内部的锁是一致的,所以它可以实现线程安全性。

CopyOnWriteArrayList,发生修改时候做copy,新老版本分离,保证读的高性能,适用于以读为主,
读操作远远大于写操作的场景中使用,比如缓存。
而Collections.synchronizedList则可以用在CopyOnWriteArrayList不适用,但是有需要同步列表的地方,
读写操作都比较均匀的地方


LinkedList:
可参考http://blog.csdn.net/fisherwan/article/details/25796625
1.是List接口的双向链表非同步实现,并允许包括null在内的所有元素。
实际上可以作为队列 双端队列 双向链表使用 主要用处是双向链表
2.底层的数据结构是基于双向链表的,该数据结构我们称为节点 
3.双向链表节点对应的内部类Node的实例,Node中包含成员变量:prev,next,item。
4.它的查找是分两半查找,先判断index是在链表的哪一半,然后再去对应区域查找,
这样最多只要遍历链表的一半节点即可找到
对于LinkedList,只要你清楚双向链表的操作就知道这个集合大概的实现了。


不得不提一下三种遍历的区别:
for()  
不适合链式结构的遍历,因为每次获得节点都要重新遍历一遍,复杂度是O(n^2)
foreach()
底层实现也是基于迭代器的,反而说比直接使用迭代器效率差一些,但是简化了代码
Iterator:
适用于各种的集合迭代遍历,注意不能进行修改等操作,除了原生的remove()方法。
ListIterator
List专用迭代器,实现了很多丰富的功能,而且避免了单线程下的并发修改异常。
此外这个是List的迭代方式,我们还有set,map
他们java8 已经自己实现了一些专用的迭代器,有兴趣可以查看API文档
比如HashMap 他就有
HashIterator 迭代hash数组
KeyIterator  迭代key
ValueIterator 迭代 value
EntryIterator 迭代 k-v



TreeMap   就是红黑树原理说清楚   http://shmilyaw-hotmail-com.iteye.com/blog/1836431
1.底层数据结构是红黑树,平衡排序二叉树
要么空 要么左边节点小于中间节点 右边节点大于中间节点
左旋转 右旋转 着色
put():
以根节点为初始节点进行检索。
与当前节点进行比对,若新增节点值较大,则以当前节点的右子节点作为新的当前节点。
否则以当前节点的左子节点作为新的当前节点。
循环递归2步骤知道检索出合适的叶子节点为止
将新增节点与3步骤中找到的节点进行比对,如果新增节点较大,则添加为右子节点;否则添加为左子节点。
delete():
1、有两个儿子。这种情况比较复杂,但还是比较简单。
2、没有儿子,即为叶结点。直接把父结点的对应儿子指针设为NULL,删除儿子结点就OK了。
3、只有一个儿子。那么把父结点的相应儿子指针指向儿子的独生子,删除儿子结点也OK了。
删除完节点后,就要根据情况来对红黑树进行复杂的调整:fixAfterDeletion()。
2.因为是排序了的所以能保证数据是有序的 
排序方式有两种 自然排序和定制排序
如果是本身继承了Comparable接口的  自然排序是升序
如果定制排序可以选择Comparable 实现compareTo()方法 
Comparator接口实现compare()方法  推荐使用后一种
3.比起HashMap来说 它在性能上差些,因为hash有一个顺序存储的数组,TreeMap则是直接红黑树查找
但是HashMap保证了插入元素是有序的


https://www.cnblogs.com/xyhuangjinfu/p/5429644.html
http://blog.csdn.net/gooooa/article/details/77530112

TreeSet是SortedSet接口的唯一实现类,TreeSet可以确保集合元素处于排序状态。TreeSet支持两种排序方式
自然排序 和定制排序,其中自然排序为默认的排序方式。向TreeSet中加入的应该是同一个类的对象。
TreeSet判断两个对象不相等的方式是两个对象通过equals方法返回false,或者通过CompareTo方法比较没有返回0
自然排序
自然排序使用要排序元素的CompareTo(Object obj)方法来比较元素之间大小关系,然后将元素按照升序排列。
Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,
该方法返回一个整数值,实现了该接口的对象就可以比较大小。
obj1.compareTo(obj2)方法如果返回0,则说明被比较的两个对象相等,如果返回一个正数,
则表明obj1大于obj2,如果是负数,则表明obj1小于obj2。
如果我们将两个对象的equals方法总是返回true,则这两个对象的compareTo方法返回应该返回0
定制排序
用Comparator接口,实现 int compare(T o1,T o2)方法。
TreeSet(Comparator comparator) 
最重要:
1、TreeSet 是红黑树(平衡二叉树的一种实现)实现的,Treeset中的数据是自动排好序的,不允许放入null值。 
2、HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,
两者中的值都不能重复,就如数据库中唯一约束。 都是set
3、HashSet要求放入的对象必须实现HashCode()方法,放入的对象,是以hashcode码作为标识的,
而具有相同内容的 String对象,hashcode是一样,所以放入的内容不能重复。但是同一个类的对象可以放入不同的实例 。
http://blog.csdn.net/xiaofei__/article/details/53138681
http://blog.csdn.net/HHcoco/article/details/53117525
红黑树
http://blog.csdn.net/sun_tttt/article/details/65445754
http://blog.csdn.net/chenhuajie123/article/details/11951777
http://blog.chinaunix.net/uid-26548237-id-3480169.html


public class Student{
private String name;
private int age;
public Student() {
super();
// TODO Auto-generated constructor stub
}
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}


public class TreeSetDemo {  
        public static void main(String[] args) {  
        // 如果一个方法的参数是接口或者抽象类,那么真正要的是接口或抽象类的实现类的对象  
        // 而匿名内部类就可以实现这个东西  
        TreeSet ts = new TreeSet(new Comparator() {  
            @Override  
            public int compare(Student s1, Student s2) {  
                // 姓名长度  
                int num = s1.getName().length() - s2.getName().length();  
                // 姓名内容  
                int num2 = num == 0 ? s1.getName().compareTo(s2.getName())  
                        : num;  
                // 年龄  
                int num3 = num2 == 0 ? s1.getAge() - s2.getAge() : num2;  
                return num3;  
            }  
        });  
  
        // 创建元素  
        Student s1 = new Student("linqingxia", 27);  
        Student s2 = new Student("zhangguorong", 29);  
        Student s3 = new Student("wanglihong", 23);  
        Student s4 = new Student("linqingxia", 27);  
        Student s5 = new Student("liushishi", 22);  
        Student s6 = new Student("wuqilong", 40);  
        Student s7 = new Student("fengqingy", 22);  
        Student s8 = new Student("linqingxia", 29);  
  
        // 添加元素  
        ts.add(s1);  
        ts.add(s2);  
        ts.add(s3);  
        ts.add(s4);  
        ts.add(s5);  
        ts.add(s6);  
        ts.add(s7);  
        ts.add(s8);  
  
        // 遍历  
        for (Student s : ts) {  
            System.out.println(s.getName() + "---" + s.getAge());  
        }  
    }  

Javaj集合遍历的最佳实践是什么?
Java数据集合框架中,提供了一个RandomAccess接口,该接口没有方法,只是一个标记。通常被List接口的实现使用,
用来标记该List的实现是否支持Random Access。
一个数据集合实现了该接口,就意味着它支持Random Access,按位置读取元素的平均时间复杂度为O(1)。比如ArrayList。
而没有实现该接口的,就表示不支持Random Access。比如LinkedList。
所以看来JDK开发者也是注意到这个问题的,那么推荐的做法就是,如果想要遍历一个List,
那么先判断是否支持Random Access,也就是 list instanceof RandomAccess。
比如:
if (list instanceof RandomAccess) {
    //使用传统的for循环遍历。
} else {
    //使用Iterator或者foreach。
}


遍历:
  //Collection And Map
    public static void testCM(){
        //Collection
        Map hs = new HashMap();
        int i = 0;
        hs.put(199, "序号:"+201);
        while(i<50){
            hs.put(i, "序号:"+i);
            i++;
        }
        hs.put(-1, "序号:"+200);
        hs.put(200, "序号:"+200);


        //遍历方式一:for each遍历HashMap的entryset,注意这种方式在定义的时候就必须写成
        //Map hs,不能写成Map hs;
        for(Entry entry : hs.entrySet()){
            System.out.println("key:"+entry.getKey()+"  value:"+entry.getValue());
        }
        //遍历方式二:使用EntrySet的Iterator
        Iterator> iterator = hs.entrySet().iterator();
        while(iterator.hasNext()){
            Entry entry =  iterator.next();
            System.out.println("key:"+entry.getKey()+"  value:"+entry.getValue());
        };
        //遍历方式三:for each直接使用HashMap的keyset
        for(Integer key : hs.keySet()){
            System.out.println("key:"+key+"  value:"+hs.get(key));
        };
        //遍历方式四:使用keyset的Iterator
        Iterator keyIterator = hs.keySet().iterator();
        while(keyIterator.hasNext()){
            Integer key = (Integer)keyIterator.next();
            System.out.println("key:"+key+"  value:"+hs.get(key));
        }   
    }


线程安全集合:
http://blog.csdn.net/gldemo/article/details/44653787
http://blog.csdn.net/mexican_ok/article/details/12859351




第一种:
Map hs = new HashMap();
        hs = Collections.synchronizedMap(hs);


第二种:
ConcurrentHashMap hs = new ConcurrentHashMap();

了解hash:
hash算法就是把被查找的键转换为数组的索引的一种算法
通过hash函数进行映射
构造hash函数的方法很多: 记住前面四个
字符串数值哈希法  把字符串的前10个字符的ASCII值之和对N取摸作为Hash地址,只要N较小,Hash地址将较均匀分布[0,N]区间内 是可行的
除留余数法   除留余数法的模p取不大于表长且最接近表长m素数时效果最好,且p最好取1.1n~1.7n之间的一个素数(n为存在的数据元素个数)
直接定址法        地址集合的大小 = = 关键字集合的大小,其中大小是常数
数字分析法        能预先估计出全体关键字的每一位上各种数字出现的频度

折叠法            关键字的数字位数特别多
平方取中法       关键字中的每一位都有某些数字重复出现频度很高的现象
减去法 基数转换法  随机数法 随机乘数法  旋转法
性能指标:
平衡性:就是说尽可能hash均匀到所有的空间
单调性:能不说就不说。。。。。。。
要么我不走,要么我就搬家不在这个家里的其他位置
分散性:就是同样的内容存储在同一个缓冲区 而不是不同的缓冲区,尽量避免不一致性
负载:  就是降低缓冲区的负荷  不要太多的hash在同一个缓冲区

hash算法用处很多 比如快速查找 O(1)时间复杂度 加密 保证不可逆性  也就是放过来计算键是多少在数学上是很难很难的

hash 冲突 不可避免的 比如因为ASCII码里面a和97  
不同的键(无限个)可能出现同样的hashcode这个就叫做hash冲突
怎么解决呢?
只能尽量减少不可能根本上解决
方法:再哈希法(不同的哈希函数) 链地址法(链式开桶 拉链法) 建立公共溢出区



hashMap


底层:HashMap是Map接口基于哈希表的实现。数组 红黑树 链表
是否允许null:HashMap允许key和value为null。只允许一个 不重复
是否有序:HashMap不保证映射的顺序,特别是它不保证该顺序恒久不变。
何时rehash:超出当前允许的initial capacity*load factor之后,
HashMap就会进行rehashed操作来进行扩容,扩容后的的容量为之前的两倍。
初始化容量对性能的影响:
小了会增大时间开销(频繁rehash),大了会增大空间开销(占用了更多空间)和时间开销(影响遍历)。
加载因子对性能的影响:
加载因子过高虽然减少了空间开销,但同时也增加了查询成本。0.75是个折中的选择 可以大于1不过应该和=1是一样的
小了会增大时间开销(频繁rehash);大了会也增大时间开销(影响遍历)。
是否同步:HashMap不是同步的。
迭代器:迭代器是fast-fail的。

从四个构造函数讲起  默认初始化 指定cap 指定cap和factor 指定collection初始化
关于指定collection调用的是putMapEntries(m, false);
首先判断hash表是否初始化 没有就初始化  什么cap 阈值threshold这些
这里不需要再判断扩容了  因为cap是根据collection的size初始化的
若已经是初始化完毕的 就要 判断是否需要扩容 就putVal() 
然后 get put remove
参数:
8  6  12 = 0.75*16 64
/*
java8 对扩容操作进行了优化  hash散列更加均匀 不用担心 在cap为64之前有过多的节点在同一个链表上
避免(resizing 和树形结构化 treeification thresholds)的冲突设置成64
就是说防止太多的节点分配在一链表节点上 
但是可以转换为红黑树结构的前提是table capacity至少为64 
 */
static final int MIN_TREEIFY_CAPACITY = 64;


ModCount 记录HashMap内部结构变化 注意put新的键值对 如果是覆盖或者不覆盖没插入的情况不属于结构变化

Node
HashMap的节点类型。既是HashMap底层数组的组成元素,又是每个单向链表的组成元素


get方法总结:
get(E e)可以分为三个步骤:(e = getNode(hash(key), key)) == null ? null : e.value;
通过hash(Object key)方法计算key的哈希值hash。
通过getNode( int hash, Object key)方法获取node。 
如果node为null,返回null,否则返回node.value。


hash( Object key) 
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
第一步,取key的hashCode
第二步,key的hashCode高16位异或低16位 //好处
取模:HashMap定位哈希桶索引位置时,也加入了高位参与运算的过程。
如果只有低位参与运算那么可能高位变化大低位变化小 hash冲突会明显。
底层的移位和异或运算肯定比加减乘除取模等效率好
所谓hash冲突是指不同的key计算出的hash值是一样的,比如a和97,这个肯定是存在的
定位操作 (n - 1) & hash
不管增加、删除、查找键值对,定位到哈希桶数组的位置都是很关键的第一步。


getNode(int hash,Object key):
如果哈希表不为空,而且key对应的桶不为空
如果桶中的第一个节点就和指定参数hash和key匹配上了,返回这个节点。
如果桶中的第一个节点没有匹配上,而且有后续节点 
如果当前的桶采用红黑树,则调用红黑树的get方法去获取节点
如果当前的桶不采用红黑树,即桶中节点结构为链式结构,遍历链表,直到key匹配
找到节点返回节点
否则就是没找到或者hash表为空或者桶为空,返回null。


put(K key, V value)
通过hash(Object key)方法计算key的哈希值。
通过putVal(hash(key), key, value, false, true)方法实现功能。
返回putVal方法返回的结果
putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
如果哈希表为空,调用resize()创建一个哈希表。
如果指定参数hash在表中没有对应的桶,即为没有碰撞,直接将键值对插入到哈希表中即可。
如果有碰撞,遍历桶,找到key映射的节点 
如果桶中的第一个节点就匹配了,将桶中的第一个节点记录起来。
如果桶中的第一个节点没有匹配,且桶中结构为红黑树,则调用红黑树对应的方式插入键值对。
如果不是红黑树,那么就肯定是链表。遍历链表,如果找到了key映射的节点,就记录这个节点,退出循环。
如果在链表中没有找到,在链表尾部插入节点。
插入后,如果链的长度大于TREEIFY_THRESHOLD这个临界值,则使用treeifyBin方法把链表转为红黑树。
如果找到了key映射的节点,且节点不为null 记录节点的Oldvlaue。
如果参数onlyIfAbsent为false,或者oldValue为null,替换value,否则不替换。返回记录下来的节点的value。

检查size是否大于临界值threshold,如果大于会使用resize方法进行扩容
注意:插入后检查,我们说 插入后判断可能刚好达到threshold 然后进行扩容 ++size > threshold????
但是可能扩容以后不在进行插入操作了 可能会导致浪费一次扩容操作


resize()  
计算扩容后的容量,临界值。
中间就是判断了容量参数是否合法  第一次resize默认初始化  最正常的情况就是扩大两倍不大于最大cap
将hashMap的临界值修改为扩容后的临界值
根据扩容后的容量新建数组,然后将hashMap的table的引用指向新数组。
将旧数组的元素复制到table中。
(为什么就数组的引用都指向新数组了还能进行拷贝?)
因为这个方法里面其实用一个新的引用指向了旧数组 方法结束以后这个变量也就不存在了
在进行resize的时候 链表 或者 红黑树 节点也会被一个一个分离split成两个链表
两个链表都是顺序结构 

remove  和上面put都差不多了  只要知道怎么get定位查询 put remove这些大致也就知道了
通过hash(Object key)方法计算key的哈希值。
通过removeNode方法实现功能。
返回被删除的node的value。
removeNode
如果数组table为空或key映射到的桶为空,返回null。
如果key映射到的桶上第一个node的就是要删除的node,记录下来。
如果桶内不止一个node,且桶内的结构为红黑树,记录key映射到的node。按红黑树进行remove remove后 判断是否转换为链表
桶内的结构不为红黑树,那么桶内的结构就肯定为链表,遍历链表,找到key映射到的node,记录下来。
如果被记录下来的node不为null,删除node。
返回被删除的node。

/**
 * 返回大于等于cap的最小的二次幂数值。   设计的非常巧妙 底层位运算效率高 而且保证总是2的n次方
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
关于桶的大小为2^n  怎么优化的?
常规的设计是把桶的大小设计为素数。因为有证明说素数导致冲突的概率要小于合数
但是HashMap采用这种非常规设计,主要是为了在扩容时做优化 减少冲突


扩容:使用2次幂的扩展(指长度扩为原来2倍),然后重定位 元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。
之所以hashmap的初始值取得是2的整数次幂,是为了减少h & (length-1)的碰撞,
length-1确保每一位都是1,这样h的每一位都参与&运算,如果length-1有一位为0,
那么h相同的那一位无论是0或1都没有参与的必要,因为都是0。
所以length-1是为了确保h所有的为都参与运算,减少碰撞值
所以用这种方式散列更加均匀

此外:
可以resize的时候不用就是复制所有的元素   只有部分元素需要移动


isEmpty
size 等等方法


Hashtable: 主要区别Hashtable的不同之处
int hash;
    Object key;
    Object value;
    HashtableEntry next;
普通的链表操作 因为没加final修饰符

rehash的总体思路为:
新建变量新的容量,值为旧的容量的2倍+1
如果新的容量大于容量的最大值MAX_ARRAY_SIZE 
如果旧容量为MAX_ARRAY_SIZE,容量不变,中断方法的执行
如果旧容量不为MAX_ARRAY_SIZE,新容量变为MAX_ARRAY_SIZE
创建新的数组,容量为新容量
将旧的数组中的键值对转移到新数组中
index = (e.hash & 0x7FFFFFFF) % newCapacity;  因为不是2^n所以说没有n-1 取余操作也没有优化移位操作
                e.next = newTable[index];
                newTable[index] = e;
Hashtable的put方法的总体思路:
确认value不为null。如果为null,则抛出异常
找到key在table中的索引,获取key所在位置的entry
遍历entry,判断key是否已经存在
如果key已经存在,替换value,返回旧的value
如果key在hashtable不是已经存在,就直接添加,否则直接将键值对添加到table中,返回null
在遍历桶中元素时,是按照链表的方式遍历的。Hashtable的桶中只可能是链表。

Hashtable的remove方法的总体思路:
找到key在table中的索引,获取key所在位置的entry
遍历entry,判断key是否已经存在
如果key存在,删除key映射的键值对,返回旧的value
如果key在hashtable不存在,返回null


关于Hashtable 和HashMap的区别 在文件夹里面有


ConcuurentHashMap1.7  类似于多个Hashtable 也没有红黑树
ConcurrentHashMap可以做到读取数据不加锁
允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。 
ConcurrentHashMap内部使用段来表示这些不同的部分,每个段其实就类似一个小的Hashtable,它们有自己的锁。
只要多个修改操作发生在不同的段上,它们就可以并发进行。


有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而不仅仅是某个段,
这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。
这里“按顺序”是很重要的,否则极有可能出现死锁。
思考不同的线程可能都在size()  顺序锁定就可以不会导致死锁了 a在size 1 bsize2 后等待1  a等待2 死锁就发生了


这里的锁不是synchronized锁 而是retrantLock 可重入锁
我们知道8以后则不是重入锁了 而是对象锁synchronized


ConcurrentHashMap采用了二次hash的方式,
第一次hash将key映射到对应的segment,segments[(hash>>>segmentShift)&segmentMask]
(hash>>>segmentShift)获得高位 然后和掩码与操作定位到seg数组  
而第二次hash则是映射到segment的不同桶(bucket)中。int index = (tab.length - 1) & hash;
  为什么要用二次hash,主要原因是为了构造分离锁,使得对于map的修改不会锁住整个容器,提高并发能力。
当然,没有一种东西是绝对完美的,二次hash带来的问题是整个hash的过程比hashmap单次hash要长,
其次两次hash可以使散列更加均匀

get: 通下面put一样
对于get操作而言,基本没有锁,只有当找到了e且e.value等于null,
有可能是当下的这个HashEntry刚刚被创建,value属性还没有设置成功,
这时候我们读到是该HashEntry的value的默认值null,所以这里加锁,等待put结束后,返回value值

因为ConcurrentHashMap里面的共享变量全部用volatile修饰 单单就用volatile修饰是不能保证原子性操作的
因为volatile只保证在内存中直接存取 但不保证原子性 也就是多线程的时候多个线程一起写会导致数据出错
ReentrantLock的用法:必须手工释放锁。可实现Synchronized的效果,原子性。 可重入锁
volatile需要配合锁去使用才能实现原子性,否则在多线程操作的情况下依然不够用
count变量通过volatile修饰,实现内存可见性 在有锁保证了原子性的情况下
当我们读取count变量的时候,会强制从主内存中读取count的最新值
当我们对count变量进行赋值之后,会强制将最新的count值刷到主内存中去
通过以上两点,我们可以保证在高并发的情况下,执行这段流程的线程可以读取到最新值
总之要么读后写  要么写后读 这个都没问题 要么就是读的时候写  
这个时候读可能读不到数据 是null 因为put可能只是刚好分配完内存还没来得及赋值
保证了线程之间的可见性 这个是内存模型里面的happen before 原则
(这里用到的规则就是说前面的线程a的写操作对于后面线程b是可见的)
总之关于happen before他就是一些规则,不保证概括所有的线程并发运行情况 但是实现了线程之间的可见性
put:
* 1)计算key.hashCode()的hash值
     * 2)根据hash值定位到某个Segment
     * 3)调用Segment的put()方法
     * Segment的put()方法:
     * 1)上锁
     * 2)从主内存中读取key-value对个数count
     * 3)count+1如果大于threshold,执行rehash()  先判断是否需要扩容 然后再插入 不同于HashMap 插入后扩容
     * 4)计算将要插入的HashEntry[]的下标index
     * 5)获取HashEntry的头节点HashEntry[index]-->first
     * 6)从头结点开始遍历整个HashEntry链表
     * 6.1)若找到与key和hash相同的节点,则判断onlyIfAbsent如果为false,新值覆盖旧值,返回旧值;如果为true,则直接返回旧值
     * 6.2)若没有找到与key和hash相同的节点,则创建新节点HashEntry,并将之前的有节点作为新节点的next,
即将新节点放入链头,然后将新节点赋值给HashEntry[index],将count强制写入主内存,最后返回null。

remove:
前面的操作和put差不太多
找到entry remove 只是一些判断有些区别 而且这链表的next是final的  
所以删除是要定位到删除元素然后拷贝删除元素的后面的元素和前面的元素 然后再删除这个旧的链表的  

rehash 主要就是因为扩展是按照2的幂次方 
     * 进行扩展的,所以扩展前在同一个桶中的元素,现在要么还是在原来的 
     * 序号的桶里,或者就是原来的序号再加上一个2的幂次方,就这两种选择。 
     * 所以原桶里的元素只有一部分需要复制,其余的都不要复制。该函数为了 
     * 提高效率,就是找到最后一个不在原桶序号的元素,那么连接到该元素后面 
     * 的子链表中的元素的序号都是与找到的这个不在原序号的元素的序号是一样的 
     * 那么就只需要把最后一个不在原序号的元素移到新桶里,那么后面跟的一串 
     * 子元素自然也就连接上了,而且序号还是相同的。在找到的最后一个不在 
     * 原桶序号的元素之前的元素就需要逐个的去遍历,加到和原桶序号相同的新桶上 
     * 或者加到偏移2的幂次方的序号的新桶上。这个都是新创建的元素,因为 
     * 只能在表头插入元素
     所以是一个顺序 一个逆序  这个和java8 是一样的    不一样的原因可能是next指针问题 7的next hashentry指针是final修饰的
可能作者为了让读者更加方便理解 重新改了方法 名字什么的 实现的功能是一样的 
 
size:遍历一次segmengts 求 sum+= segments[i].count 和 modcount 
然后遍历一次segments  比较segments[i].modcount 只要有一个发生了变化
重复上面操作两次
这样一共三次 还不行就加锁来一次

volatile使用:
运算结果并不依赖当前值,例如Boolean就可,而number++这样的就不行,这样的情况使用锁
运算结果依赖当前值但是能够确保只有单一线程修改变量的值,例如ConcurrentHashMap中Segment的count变量
变量不需要与其他的状态变量共同参与不变约束,例如low 在访问变量时需要使用锁,就不要使用volatile(《java并发编程实战》)

java 8 的ConcurrentHashMap

摒弃了锁段的概念 启用CAS算法 沿用java8 的HashMap
为了做到并发 加了很多辅助类 Treebin Traverser等

首先CAS: 全称在这里可以理解为CompareAndSwap  它是一种java并发技术 
比较和替换是使用一个期望值和一个变量的当前值进行比较,
如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。
经常出现的一种是check and act模式 先检查后操作模式发生在代码中首先检查一个变量的值,然后再基于这个值做一些操作。
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,
将内存值V修改为B,否则什么都不做。


比如我们举个例子
public final int get(){
return value;
}
public final incrementAndGet(){
for(;;){
int current = get();
int next = current + 1;
if(compareAndSet(current,next)){
return next;
}
}
}
我们的compareAndSet算法如下:
public final boolean compareAndSet(int expect, int update) {   
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
unsafe.compareAndSwapInt(this, valueOffset, expect, update); 
为什么unsafe呢  因为用到了指针 指针本身就不安全为了效率只好用指针
类似:
if (this == expect) {
  this = update
 return true;
} else {
return false;


整个过程利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法
所谓非阻塞算法:一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
JNI:Java Native Interface为JAVA本地调用接口,允许java调用其他语言 比如底层的c jvm大部分都是用c写的
CAS缺点
 CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作
1.  ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,
但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。


atomic包里提供了一个类AtomicStampedReference来解决ABA问题。
这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,
如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。


2. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。


3. 只能保证一个共享变量的原子操作。
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,
循环CAS就无法保证操作的原子性,这个时候就可以用锁,
或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。
比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。
JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。


原子性:
原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,
那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。

Java中的原子操作包括:
1)除long和double之外的基本类型的赋值操作
2)所有引用reference的赋值操作
3)java.concurrent.Atomic.* 包中所有类的一切操作  这里面就是通过CAS实现了原子性


讲了这么多的atomic  java的atomic包下有什么? 对于atomic包 大概了解是基于CAS 实现的 大概有些什么类
AtomicBoolean:原子更新布尔类型
AtomicInteger:原子更新整型
AtomicLong:原子更新长整型




好了 了解了这么多 可以开始看看java 8 究竟怎么优化的了
 重要的属性:
transient volatile sizeCtl 一个控制标识符 不同的值有不同的含义
负数代表正在进行初始化或扩容操作
-1代表正在初始化
-N 表示有N-1个线程正在进行扩容操作
正数或0代表hash表还没有被初始化,这个数值表示初始化或扩容后的阈值,这一点类似于扩容阈值的概念
还后面可以看到,它的值始终是当前ConcurrentHashMap容量的0.75倍,这与loadfactor是对应的。

这里我们可以看出的是java 8  使用的是并发扩容
volatile Node[] table;//2^n
/**
     * The number of bits used for generation stamp in sizeCtl.
     * Must be at least 6 for 32bit arrays.
     */
private static int RESIZE_STAMP_BITS = 16;扩容标记数
    /*   
     * The bit shift for recording size stamp in sizeCtl.
     */
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;扩容偏移量




static final int MOVED     = -1; // hash值是-1,表示这是一个forwardNode节点
static final int TREEBIN   = -2; // hash值是-2  表示这时一个TreeBin节点

unsafe静态块   我们在上面已经介绍了unsafe 和 CAS
unsafe代码块控制了一些属性的修改工作,比如最常用的SIZECTL 。
在这一版本的concurrentHashMap中,大量应用来的CAS方法进行变量、属性的修改工作。利用CAS进行无锁操作,可以大大提高性能。
private static final sun.misc.Unsafe U;
   private static final long SIZECTL;
   private static final long TRANSFERINDEX;
   private static final long BASECOUNT;
   private static final long CELLSBUSY;
   private static final long CELLVALUE;
   private static final long ABASE;
   private static final int ASHIFT;
 
重要的类:
Node
Node是最核心的内部类,它包装了key-value键值对,所有插入ConcurrentHashMap的数据都包装在这里面。
它与HashMap中的定义很相似,但是但是有一些差别它对value和next属性设置了volatile同步锁(与JDK7的Segment相同),
它不允许调用setValue方法直接改变Node的value域,它增加了find方法辅助map.get()方法。
通过putVal 的onlyIfAbsent 参数设置改变


TreeNode
树节点类,另外一个核心的数据结构。当链表长度过长的时候,会转换为TreeNode
但是与HashMap不相同的是,它并不是直接转换为红黑树,而是把这些结点包装成TreeNode放在TreeBin对象中
由TreeBin完成对红黑树的包装。而且TreeNode在ConcurrentHashMap继承自Node类
而并非HashMap中的继承自LinkedHashMap.Entry
也就是说TreeNode带有next指针,这样做的目的是方便基于TreeBin的访问。


TreeBin
这个类并不负责包装用户的key、value信息,而是包装的很多TreeNode节点。
它代替了TreeNode的根节点,也就是说在实际的ConcurrentHashMap“hash数组”中,存放的是TreeBin对象
而不是TreeNode对象,这是与HashMap的区别。另外这个类还带有了读写锁。


ForwardingNode
一个用于连接两个table的节点类。它包含一个nextTable指针,用于指向下一张表。
而且这个节点的key value next指针全部为null,它的hash值为-1. 
这里面定义的find的方法是从nextTable里进行查询节点,而不是以自身为头节点进行查找。


三个重要的方法:
ConcurrentHashMap定义了三个原子操作,用于对指定位置的节点进行操作。正是这些原子操作保证了ConcurrentHashMap的线程安全。
//获得在i位置上的Node节点
    static final Node tabAt(Node[] tab, int i) {
        return (Node)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }
        //利用CAS算法设置i位置上的Node节点。之所以能实现并发是因为他指定了原来这个节点的值是多少
        //在CAS算法中,会比较内存中的值与你指定的这个值是否相等,如果相等才接受你的修改,否则拒绝你的修改
        //因此当前线程中的值并不是最新的值,这种修改可能会覆盖掉其他线程的修改结果  有点类似于SVN
    static final boolean casTabAt(Node[] tab, int i,
                                        Node c, Node v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }
        //利用volatile方法设置节点位置的值
    static final void setTabAt(Node[] tab, int i, Node v) {
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
    }
构造函数:
对于ConcurrentHashMap来说,调用它的构造方法仅仅是设置了一些参数而已
而整个table的初始化是在向ConcurrentHashMap中插入元素的时候发生的
initial:
初始化方法主要应用了关键属性sizeCtl 如果这个值〈0,表示其他线程正在进行初始化,就放弃这个操作。
在这也可以看出ConcurrentHashMap的初始化只能由一个线程完成。
如果获得了初始化权限,就用CAS方法将sizeCtl置为-1,防止其他线程进入。初始化数组后,将sizeCtl的值改为0.75*n。


transfer整个扩容操作分为两个部分


第一部分是构建一个nextTable,它的容量是原来的两倍,这个操作是单线程完成的。
这个单线程的保证是通过RESIZE_STAMP_SHIFT这个常量经过一次运算来保证的,    这个地方在后面会有提到;
第二个部分就是将原来table中的元素复制到nextTable中,这里允许多线程进行操作。


先来看一下单线程是如何完成的:
它的大体思想就是遍历、复制的过程。首先根据运算得到需要遍历的次数i,然后利用tabAt方法获得i位置的元素:
如果这个位置为空,就在原table中的i位置放入forwardNode节点,这个也是触发并发扩容的关键点;
如果这个位置是Node节点(fh>=0),如果它是一个链表的头节点,就构造一个反序链表,把他们分别放在nextTable的i和i+n的位置上
如果这个位置是TreeBin节点(fh<0),也做一个反序处理,并且判断是否需要untreefi,把处理的结果分别放在nextTable的i和i+n的位置上
这两者都是记录最后一个需要移动的节点 这样遍历前面到这个节点 一个个判断重新插入 后面的直接链接到i+n的位置 减少了要拷贝的消耗
遍历过所有的节点以后就完成了复制工作,这时让nextTable作为新的table,并且更新sizeCtl为新容量的0.75倍 ,完成扩容。


再看一下多线程是如何完成的:
任何线程如果遍历到的节点是forward节点,就向后继续遍历,再加上给节点上锁的机制,就完成了多线程的控制。
多线程遍历节点,处理了一个节点,就把对应点的值set为forward,另一个线程看到forward,就向后遍历。
这样交叉就完成了复制工作。而且还很好的解决了线程安全的问题。 


put:
put方法依然沿用HashMap的put方法的思想,
根据hash值计算这个新插入的点在table中的位置i,如果i位置是空的,直接放进去,否则进行判断,
如果i位置是树节点,按照树的方式插入新的节点,否则把i插入到链表的末尾。ConcurrentHashMap中依然沿用这个思想
有一个最重要的不同点就是ConcurrentHashMap不允许key或value为null值。
另外由于涉及到多线程,put方法就要复杂一点。在多线程中可能有以下两个情况


如果一个或多个线程正在对ConcurrentHashMap进行扩容操作,当前线程也要进入扩容的操作中。
这个扩容的操作之所以能被检测到,是因为transfer方法中在空结点上插入forward节点,
如果检测到需要插入的位置被forward节点占有,就帮助进行扩容;
如果检测到要插入的节点是非空且不是forward节点,就对这个节点加锁,这样就保证了线程安全。
尽管这个有一些影响效率,但是还是会比hashTable的synchronized要好得多。
整体流程就是首先定义不允许key或value为null的情况放入  
对于每一个放入的值,首先利用spread方法对key的hashcode进行一次hash计算,由此来确定这个值在table中的位置。


如果这个位置是空的,那么直接放入,而且不需要加锁操作。
如果这个位置存在结点,说明发生了hash碰撞,首先判断这个节点的类型。
如果是链表节点(fh>0),则得到的结点就是hash值相同的节点组成的链表的头节点。
需要依次向后遍历确定这个新加入的值所在位置。
如果遇到hash值与key值都与新加入节点是一致的情况,则只需要更新value值即可。
否则依次向后遍历,直到链表尾插入这个结点。如果加入这个节点以后链表长度大于8,就把这个链表转换成红黑树。
如果这个节点的类型已经是树节点的话,直接调用树节点的插入方法进行插入新的值。


get:我们说这次更新后方法封装更加完善  多了find TabAt这些 在get里面调用
总体和HashMap是一样的  

remove:
也和HashMap是一样的

helptransfer 其他线程获取到newtab 然后调用transfer


mappingCount对比size
这里的count是一个估计值 也就是说因为多线程的原因 
得到的size之后其他线程修改了map的结构  这里估计是留个用户思考 度量的
sumCount  被调用  也是实现主要count的函数

在put方法结尾处调用了addCount方法,把当前ConcurrentHashMap的元素个数+1
这个方法一共做了两件事,更新baseCount的值,检测是否进行扩容。


put remove 这些操作 是通过volatile + synchronized锁机制实现的同步
而get是CAS操作  比起弱一致性的volatile更加靠谱


主要设计上的变化有以下几点:
不采用segment而采用node,锁住node来实现减小锁粒度。
设计了MOVED状态 当resize的中过程中 线程2还在put数据,线程2会帮助resize。
使用3个CAS操作来确保node的一些操作的原子性,这种方式代替了锁。
sizeCtl的不同值来代表不同含义,起到了控制的作用。


为什么元素不能是null?
ConcurrentHashmap和Hashtable都是支持并发的,这样会有一个问题,当你通过get(k)获取对应的value时,
如果获取到的是null时,你无法判断,它是put(k,v)的时候value为null,还是这个key从来没有做过映射。
HashMap是非并发的,可以通过contains(key)来做这个判断。而支持并发的Map在调用m.contains(key)和m.get(key),m可能已经不同了。
为什么锁机制换回来了synchronized 而不是重入锁?
因为java8 对synchronized 优化了许多  性能提升了 而且也用不到RetrantLock这么强大的锁

你可能感兴趣的:(java工程师面试)