从底层数据结构以及访问、添加、删除的效率分别说明
注意: ArrayList当数组大小不知足时须要增长存储能力,就要将已经有数组的数据复制到新的存储空间java
Vector也是经过数组实现的,但它支持线程同步,避免同时写引发的不一致性,实现同步须要很高的开销,所以它的访问比ArrayList慢算法
jdk1.7中的HashMap
HashMap是一个数组,数组的每个元素是一个Entry的链表
每个Entry对象包括了Key,value,hash值和指向下一个元素的next数组
HashMap包括两个构造参数:
1.capacity
:当前数组的容量,可扩容,扩容后的大小为当前数组大小的两倍
2.loadFactor
:负载因子,默认0.75缓存
HashMap(int initialCapacity, float loadFactor)
安全
threshold
:扩容的阈值,等于capacity * loadFactor
数据结构
JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,在这些位置进行查找的时候能够下降时间复杂度为 O(logN)。多线程
(1). HashMap 容许有一个null的key,容许多个value为null
(2). HashMap 是线程不安全的,可经过Collections的synchronizedMap 方法使HashMap 具备线程安全的能力,或者使用ConcurrentHashMap
构造参数:concurrencyLevel
:并行数,也即segment的个数,默认16,初始化后不可更改或扩容;一个segment同时只容许一个线程操做, 16个segment容许16个线程在各类不一样的segment上并发写;并发
HashTable是上古版本的遗留类,如今不多使用,app
HashSet 底层是由HashMap实现, 因此也是非线程安全,
其实HashSet也是<K,V> 只不过对每一个元素都是同一个object对象做为Vjvm
它继承HashMap、底层使用哈希表与双向链表来保存全部元素
Entry元素比HashMap多了:Entry<K, V> before
Entry<K, V> after
before、After是用于维护Entry插入的前后顺序的。
大概的图:
1.减小了建立和销毁线程的次数,每一个工做线程均可以被重复利用,可执行多个任务。
2.能够根据系统的承受能力,调整线程池中工做线线程的数目,防止消耗过多的内存
建立一个线程池,它可安排在给定延迟后运行命令或者按期地执行。
ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3); scheduledThreadPool.schedule(newRunnable(){ @Override public void run() { System.out.println("延迟三秒"); }}, 3, TimeUnit.SECONDS); scheduledThreadPool.scheduleAtFixedRate(newRunnable(){ @Override public void run() { System.out.println("延迟1 秒后每三秒执行一次"); }},1,3,TimeUnit.SECONDS);
在线程的生命周期中,它要通过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态
当程序使用new 关键字建立了一个线程以后,该线程就处于新建状态,此时仅由JVM 为其分配内存,并初始化其成员变量的值
当线程对象调用了start()方法以后,该线程处于就绪状态。Java 虚拟机会为其建立方法调用栈和程序计数器,等待调度运行。
若是处于就绪状态的线程得到了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。
阻塞状态是指线程由于某种缘由放弃了cpu 使用权,暂时中止运行
阻塞的状况分三种:
运行(running)的线程执行o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)
中。
运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM 会把该线程放入锁池(lock pool)中。
运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O 请求时,JVM 会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O
处理完毕时,线程从新转入可运行(runnable)状态。
正常结束
异常结束
调用 stop
在访问volatile 变量时不会执行加锁操做,所以也就不会使执行线程阻塞,所以volatile 变量是一种比sychronized 关键字更轻量级的同步机制。volatile 适合这种场景:一个变量被多个线程共享,线程直接给这个变量赋值
简单原理:
当对非 volatile 变量进行读写的时候,每一个线程先从内存拷贝变量到CPU 缓存中。若是计算机有
多个CPU,每一个线程可能在不一样的CPU 上被处理,这意味着每一个线程能够拷贝到不一样的 CPU
cache 中。而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache这一步。
指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果是正确的,可是没法保证程序的操做顺序与代码顺序一致。这在单线程中不会构成问题,可是在多线程中就会出现问题。很是经典的例子是在单例方法中同时对字段加入voliate,就是为了防止指令重排序。
public class Singleton { private volatile static Singleton singleton; private Singleton() {} public static Singleton getInstance() { if (singleton == null) { // 1 synchronized(Singleton.class) { if (singleton == null) { singleton = new Singleton(); // 2 } } } return singleton; } }
实际上当程序执行到2处的时候,若是咱们没有使用volatile关键字修饰变量singleton,就可能会形成错误。这是由于使用new关键字初始化一个对象的过程并非一个原子的操做,它分红下面三个步骤进行:
a. 给 singleton 分配内存
b. 调用 Singleton 的构造函数来初始化成员变量
c. 将 singleton 对象指向分配的内存空间(执行完这步 singleton 就为非 null 了)
若是虚拟机存在指令重排序优化,则步骤b和c的顺序是没法肯定的。若是A线程率先进入同步代码块并先执行了c而没有执行b,此时由于singleton已经非null。这时候线程B到了1处,判断singleton非null并将其返回使用,由于此时Singleton实际上还未初始化,天然就会出错。synchronized能够解决内存可见性,可是不能解决重排序问题。
举例说明:
以i++为例,其包括读取、操做、赋值三个操做,下面是两个线程的操做顺序
假如说线程A在作了i+1,但未赋值的时候,线程B就开始读取i,那么当线程A赋值i=1,并回写到主内存,而此时线程B已经再也不须要i的值了,而是直接交给处理器去作+1的操做,因而当线程B执行完并回写到主内存,i的值仍然是1,而不是预期的2。也就是说,volatile缩短了普通变量在不一样线程之间执行的时间差,但仍然存有漏洞,依然不能保证原子性。
参考文章 : java的可见性、有序性和原子性
参考资料: Java:CAS(乐观锁)
在JDK 5以前Java语言是靠synchronized关键字保证同步的,该机制存在如下问题:
(1)在多线程竞争下,加锁、释放锁会致使比较多的上下文切换和调度延时,引发性能问题。
(2)一个线程持有锁会致使其它全部须要此锁的线程挂起。
(3)若是一个优先级高的线程等待一个优先级低的线程释放锁会致使优先级倒置,引发性能风险。
独占锁是一种悲观锁,synchronized就是一种独占锁,会致使其它全部须要锁的线程挂起,等待持有锁的线程释放锁。
而另外一个更加有效的锁就是乐观锁。
所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操做,若是由于冲突失败就重试,直到成功为止。
乐观锁用到的机制就是CAS,Compare and Swap。
CAS机制当中使用了3个基本操做数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改成B。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法做用是首先检查当前引用是否等于预期引用,而且当前标志是否等于预期标志,若是所有相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
参加我的文章 java内存模型
同上
JVM在Server模式下的逃逸分析能够分析出某个对象是否永远只在某个方法、线程的范围内,并无“逃逸”出这个范围,逃逸分析的一个结果就是对于某些未逃逸对象能够直接在栈上分配,因为该对象必定是局部的,因此栈上分配不会有问题。
假设有一个Person类,以及两个子类Boy和Girl
当这三个类加载到虚拟机后,方法区就包含了这三个类的信息,包括各自的方法表
Girl 和 Boy 在方法区中的方法表可表示以下:
方法表中的条目指向的具体的方法地址
如 Girl 继承自 Object 的方法中,只有 toString() 指向本身的实现(Girl 的方法代码,被重写),其他皆指向 Object 的方法代码;其继承自于 Person 的方法 eat() [未重写]和 speak() [被重写]分别指向 Person 的方法实现和自己的实现。
若是子类改写了父类的方法,那么子类和父类的那些同名的方法仍然共享一个方法表项。(能够理解为这个方法在方法表里的叫法/直接引用仍是不变的)
所以,方法在方法表中的偏移量老是固定的,全部继承父类的子类的方法表中,其父类所定义的方法的偏移量也老是一个定值。(能够理解为即便被重写,子类还能够经过和父类的一样直接引用找到该方法)
这样 JVM 在调用实例方法其实只须要指定调用方法表中的第几个方法便可
假设代码以下:
class Party { void happyHour() { Person girl = new Girl(); girl.speak(); } }
(1)在常量池中找到方法调用的符号引用。
(2)查看Person的方法表,获得speak方法在该方法表的偏移量(假设为15),这样就获得该方法的直接引用。
(3)根据this指针获得具体的对象(即 girl 所指向的位于堆中的对象)。
(4)根据对象获得该对象对应的方法表,根据偏移量15查看有无重写(override)该方法,若是重写,则能够直接调用(Girl的方法表的speak项指向自身的方法而非父类);若是没有重写,则须要拿到按照继承关系从下往上的基类(这里是Person类)的方法表,一样按照这个偏移量15查看有无该方法。
下面代码的执行结果是?
class Person{ { System.out.println("P1"); } static { System.out.println("P2"); } public Person(){ System.out.println("P3"); } } class Student extends Person{ static { System.out.println("S1"); } { System.out.println("S2"); } public Student(){ System.out.println("S3"); } } public class Main { public static void main(String[] args) { Student s = new Student(); } }
答案:P2,S1,P1,P3,S2,S3
参考 java中类加载与静态变量、静态方法与静态代码块详解与初始化顺序