复习java基础知识的笔记java
进程和线程主要区别在于他们是操做系统不一样的资源管理方式. 进程是程序的一次执行过程(运行中的程序),是系统运行程序的基本单位. 一个进程至少包含一个线程(main), 能够包含多个线程.(换言之,线程是进程内的执行单元) 线程与进程类似,它是比进程更小的执行单位.一个进程在执行过程当中能够产生多个线程. 同类线程有共享的堆和方法区(jdk8以后的元空间(MetaSpace)), 每一个线程又有本身的程序计数器,虚拟机栈,本地方法栈. 系统在各个线程之间的切换工做要比进程负担低,所以线程又被称为轻量级进程
NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED
并发是指计算机在同一时间段内处理多任务的执行. 如我和小明同时访问淘宝网站,那么淘宝服务器就同时处理我和小明的访问请求 并行是指多任务同时执行,可是任务之间没有任何关系,不涉及共享资源. 好比我一边看电视一边喝水,2件事互不干扰
公平锁:c++
指根据线程在队列中的优先级获取锁,好比线程优先加入阻塞队列,那么线程就优先获取锁
非公平锁:算法
指在获取锁的时候,每一个线程都会去争抢,而且都有机会获取到锁,无关线程的优先级
可重入锁:数据库
一个线程获取到锁后,若是继续遇到被相同锁修饰的资源或方法,那么能够继续获取该锁. 对synchronized来讲,每一个锁都有线程持有者和锁计数器,每次线程获取到锁,会记录下 改线程,而且锁的计数器就+1,当线程退出synchronized代码块的时候,线程计数就会-1, 当锁计数为0的时候,就释放锁.
自旋锁:缓存
指当锁被获取后,其余线程并不会中止获取,而是一直去尝试获取.这样作的好处是减小上下文开销, 缺点是增长cpu消耗. CAS底层就使用了自旋操做(不是自旋锁,而是若是预期值和原值比较不成功就会一直比较)
独占锁:安全
锁一次只能被一个线程占有使用,Synchronized和ReetrantLock都是独占锁
共享锁:服务器
锁能够被多个线程持有,对于ReentrantReadWriteLock而言,它的读锁是共享锁,写锁是独占锁
Volatile是JVM提供的轻量级的同步机制
1: volatile保证内存可见性并发
JMM内存模型实现老是线程从主内存(共享内存)读取数据,线程把主存的变量存储到本地, 在本地进行修改,而后写回主内存,而不是直接在主存中进行操做. 那么这就可能形成可见性问题:假设2个线程从主存读取同一个变量,一个线程修改了它的本地变量, 并写回了主存,可是另外一个线程仍然使用的是以前的值,这就形成了数据的不一致. volatile关键字修饰的变量就解决了这个问题:被volatile修饰的变量要求线程使用时, 都从主内存中读取,而不使用本地的拷贝.
2: volatile不保证原子性jvm
原子性指一个操做的完整性,它是不可分割的,要么同时成功执行,要么同时失败回滚. 当多个线程同时对volatie关键字修饰的变量进行非原子性操做(++,--)的时候, 变量可能会被多++一次,少++一次,多--一次,少--一次. volatile并不能保证操做的原子性,最后获得的结果可能不尽人意
如何解决原子性:测试
1: 最简单的方法就是加锁 2: 使用CAS原子类
3: volatile禁止指令重排序
指令重排序是编译器和cpu为了尽量高效的执行程序而采起的一种优化手段, 它会致使程序实际执行的顺序和代码的顺序不必定相符, 而volatil就是在执行的代码先后加入屏障,使cpu在执行时 没法重排序,就按代码的顺序执行
CAS:CompareAndSet,比较并交换,它将指定内存位置的值与给定值进行比较, 若是两个值相等,就将内存位置的值 改成给定值.CAS涉及3个元素:内存地址,期盼值和目标值, 只有内存地址对应的值和指望的值相同时,才把内存地址对应的值修改成目标值.
CAS在JAVA中的底层实现(Atomic原子类实现)
1:Unsafe类:
Unsafe类是CAS的核心类,由jdk自动加载,它的方法都是native方法. 由于Java没法像c/c++同样直接使用底层指针操做对象 内存,Unsafe类的做用就是专门解决这个问题,它能够直接操做对象在内存中的地址. 具体步骤是:首先获取当前Atomic对象的value在内存中真实的偏移地址,再根据这个偏移地址 获取value的真实值,而后再重复这个步骤,把两次获取到的值进行比较, 若是比较成功,则继续操做,不然继续循环比较. 而获取value在内存中真实的偏移地址和比较设置值方法都是native的.
2: volatile:
Atomic原子类内部的value值是volatile修饰的,这就保证了value的可见性.
CAS的缺点:
1: 循环时间开销大
若是预期的值和当前值比较不成功,那么CAS会一直进行循环.若是长时间比较不成功 就会一直循环,致使CPU开销过大
2: 只能保证一个共享变量的原子操做
在获取内存地址和设置值的时候都是当前Atomic对象的volatile值,若是要保证多个共享变量, 那么能够经过加锁来保证线程安全和原子性.
3: ABA问题
尽管一个线程CAS操做成功,但并不表明这个过程就是没有问题的. 假设2个线程读取了主内存中的共享变量,若是一个线程对主内存中的值进行了修改后, 又把新值改回了原来的值,而此时另外一个线程进行CAS操做,发现原值和期盼的值是 同样的,就顺利的进行了CAS操做.这就是CAS引起的ABA问题.
4: 解决ABA问题
juc的atomic包下提供了AtomicStampedReference这个类来解决CAS的原子引用 更新的ABA问题,它相较于普通的Atomic原子类多增长了一个版本号的字段, 每次修改引用就更新版本号,这样即便发生ABA问题,也能经过版本号判断引用 是否被修改过了.
池化技术家常便饭:数据库链接池,Http链接池,线程池都是这种思想. 池化技术的好处很是明显,以往单个的new Thread,不易于线程之间的管理, 而池化技术把全部线程都放在一个池子里,要用就取出,用完就回收,这样很是易于管理, 而且能够下降资源的消耗,使线程能够重复利用,提升任务的响应速度,当任务到来时,就能够 处理.
ThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
corePoolSize:
线程池的核心线程数(常驻线程数),也就是线程池的最小线程数,这部分线程不会被回收.
maximumPoolSize:
线程池最大线程数,线程池中容许同时执行的最大线程数量
keepAliveTime:
当线程池中的数量超过 corePoolSize(最小线程池数量),而且此时没有新的任务执行,那么 会保持 keepAliveTime 的时间才会回收线程
unit:
keepAliveTime的时间单位
workQueue:
任务队列,当有新任务来临时,若是核心线程数corePoolSize被用完,此时若是workQueue有空间, 任务就会被放入workQueue
threadFactory:
建立工做线程的工厂,也就是如何建立线程的,通常采用默认的
handler:
拒绝策略. 若是线程池陷入一种极端状况:工做队列满了,没法再容纳新的任务,最大工做线程也到达限制了, 此时线程池如何处理这种极端状况. ThreadPoolExecutor 提供了四种策略:
AbortPolicy(是线程池的默认拒绝策略):
若是还有新任务到来,那么拒绝,并抛出RejectedExecutionException异常
CallerRunsPolicy:
这种策略不会拒绝执行新任务,可是由发出任务的线程执行,也就是说当线程池没法 执行新任务的时候,就由请求线程本身执行任务
DiscardPolicy:
这种策略会拒绝新任务,可是不会抛出异常
DiscardOldestPolicy:
这种策略不会拒绝策略,他会抛弃队列中等待最久那个任务,来执行新任务
阿里巴巴开发者手册不建议开发者使用Executors建立线程池:
newFixedThreadPool和newSingleThreadExecutor: 会建立固定数量线程的线程池和单线程线程池,尽管两者线程池数量有限, 可是它会建立长度为Integer.MAX_VALUE长度的阻塞队列,这样可能会致使阻塞队列 的任务过多而致使OOM(OutOfMemoryError) newCachedThreadPool和newScheduledThreadPool: 会建立缓存线程池和周期任务线程池,两者线程池的最大线程为Integer.MAX_VALUE, 也可能会致使OOM
JDK8以前:线程私有的部分有:程序计数器(PC寄存器),JAVA虚拟机栈, 本地方法栈(native),线程共享部分有: GC堆,方法区(永久代包含运行时常量池) JDK8以后:线程私有的部分不变, 线程共享部分的方法区改成了元空间(MetaSpace), 运行时常量池也移动到了 heap空间
程序计数器:
程序计数器又称PC寄存器,它是记录着当前线程执行的字节码的行号指示器. cpu是经过时间片轮换制度执行每一个线程的任务,而在多个线程之间切换时, 程序计数器就记录当前线程执行的位置,当cpu又开始执行此线程的时候,它须要知道 上次运行的位置,那么就是经过程序计数器直到上次字节码的执行位置, 因此每一个线程都会有属于本身的程序计数器
Java虚拟机栈:
Java虚拟机栈描述的是方法执行的内存模型,每一个方法在执行时会在虚拟机栈中建立一个 栈帧,每一个方法的执行,就对应着栈帧在虚拟机栈中的入栈和出栈. 栈帧由局部变量表,操做数栈,方法出口,动态连接等数据组成.
Java虚拟机栈的错误
StackOverflowError:
当Java虚拟机栈没法动态扩容的时候,当前线程执行或请求的栈的大小超过了Java虚拟机栈的最大空间 (好比递归嵌套调用太深),那么抛出StackOverflowError错误
OutOfMemoryError:
1: 当Java虚拟机栈容许动态扩容的时候,当前虚拟机栈执行请求的栈的大小 仍然超过了扩容以后的最大空间,没法继续为栈分配空间(堆内存分配空间太小), 那么抛出OutOfMemoryError错误 2: Java堆存放对象实例,当须要为对象分配内存时,而堆空间大小已经达到最大值, 没法为对象实例继续分配空间时,抛出 OutOfMemoryError错误 3: GC时间过长可能会抛出OutOfMemoryError.也大部分的时间都用在GC上了,并 且每次回收都只回收一点内存,而清理的一点内存很快又被消耗殆尽,这样就恶性循环, 不断长时间的GC,就可能抛出GC Overhead limit,可是这点在个人机器上测试不出来, 可能与jdk版本或gc收集器或Xmx分配内存的大小有关,一直抛出的是java heap sapce 4: 由于jvm内存依赖于本地物理内存,那么给程序分配超额的物理内存,而堆内存充足, 那么GC就不会执行回收,DirectByteBuffer对象就不会被回收,若是继续分配 直接物理内存,那么可能会出现DirectBufferMemoryError 5: 一个应用程序能够建立的线程数有限,若是建立的线程的数量达到相应平台的上限, 那么可能会出现 unable to create new native thread 错误 6:jdk8以后的Metaspace元空间也有可能抛出OOM,Metasapce受限于物理内存,它存储 着类的元信息,当Metaspace里的类的信息过多时,Metaspace可能会发生OOM. 这里是可使用cglb的字节码生成类的技术测试的.
虚拟机栈栈的动态扩容:
上面说过若是当虚拟机栈容许动态扩容,当动态扩容的空间都不够用的时候,就抛出OutOfMemory异常. 虚拟机栈的动态扩容是指在栈空间不够用的时候,自动增长栈的内存大小. 动态扩容栈有2种方法: Stack Copying: 就是分配一个更大的栈空间,把原来的栈拷贝到新栈空间去. Segmented Stack: 能够理解为一个双向链表把多个栈连接起来,一开始只分配一个栈,当 这个空间不够时.再分配一个栈空间,用链表连接起来.
局部变量表:存放编译期可知的各类数据类型(常见的8大数据类型)和引用类型(reference,能够是指向对象的指针,也能够是句柄)
操做数栈:与局部变量相似,它也能够存听任意类型的数据,但它是做为数据在计算时的临时存储空间,入站和出栈
动态连接:由于每一个方法在运行时都会建立相应的栈帧,那么栈帧也会保存一份方法的引用,栈帧保存方法的引用是为了支持方法调用过程当中的动态连接.
动态连接是指将符号引用转为直接引用的过程. 若是方法调用另外一个方法或者调用另外一个类的成员变量就须要知道其名字, 符号引用就至关于名字,运行时就将这个名字解析成相应的直接引用.
方法出口:方法执行后,有2种方式退出: 1: 执行方法的过程当中遇到了异常; 2: 遇到正常的返回字节码指令.
不管何种方式退出,在方法退出后,都须要回到方法被调用时的位置, 方法在返回时就须要在栈帧中保存一些信息,用来帮助它恢复上层方法的执行状态.
本地方法栈:
与Java虚拟机栈相似,Java虚拟机栈是对Java方法执行的描述, 可是本地方法栈是对jvm的native方法描述,也就是第三方c/c++ 编写的方法,本地方法栈也有相应的 局部变量表,操做数栈,方法出口,动态连接
堆
堆是jvm内存区域中最大的一块区域. 堆区的做用是为对象分配内存,存储他们,并负责回收它们之中无用的对象. 堆能够分为新生代和老年代,新生代占堆区的1/3,老年代占堆区的2/3. 新生代包括eden,from survivor, to survivor三个空间 其中 eden空间最大占新生代的80%内存,from和to都是1:1. 新生代是GC发生最频繁的区域. 发生在新生代的GC被称为MinorGC,老年代的GC被称为Major GC / Full GC 由于老年代的空间比新生代的空间大,因此一般老年代的GC时间会比新生代的GC时间慢不少. 对象优先在eden区域分配,当eden区域没有空间时,虚拟机就会发起一次Minor GC
1: 引用计数法:
给每一个对象添加一个引用计数器,当对象被引用的时候,引用计数器就+1,当引用失效时,引用计数器 就-1,直到引用计数器为0,就表明对象再也不被引用. 引用计数的主要缺陷是很难解决循环引用的问题:也就是当2个对象互相引用的时候,除了彼此, 就没有其余地方引用这2个对象,那么他们的引用计数都为1,就没法被回收
2: 可达性算法:
经过一系列被称为GC ROOTS的对象节点往下搜索,节点走过的地方被称为引用链, 若是一个对象不被任何引用链走过,那么称 此对象不可达.
什么是GC Root
上面说经过GC Root对象搜索引用链,那么GC Root对象是什么对象,或者什么样的对象是GC Root对象. 能够做为GC Root对象的有: 1: 虚拟机栈和本地方法栈区(native)的引用对象; 2: 堆区里的静态变量引用的对象; 3: 堆区里的常量池的常量引用的对象
复制算法:
将内存分为2块大小的内存空间,每次使用其中一块空间,当一块使用完后, 将还存活的对象复制到另外一块空间去,而后清楚已经使用过的空间. 根据GC角度来讲就是:在新生代的eden空间和From Survivor空间经历过MinorGC后, 仍然存活的对象采用复制算法复制到To Survivor, 并将To Survivor(其实就是空间不空闲的那块Survivor区域)的对象年龄+1, 默认对象年龄撑过15岁,那么进入老年代. 复制算法的缺点就是太耗空间内存.
标记-清除算法:
标记出全部须要被回收的对象,而后统一回收全部被标记的对象. 标记清除算法的最大缺点就是会形成不连续的内存空间, 也就是内存碎片,由于对象在内存中的分布是不均匀的.
标记-整理算法:
是对标记-清除算法作出的改进,标记整理算法也是首先标记出全部须要被回收的对象, 不一样的是,它会使全部仍然存活的对象向空间的一段移动,而后对其它端进行清理. 此算法虽然不会产生内存碎片,可是它的效率会比标记清楚算法慢
分代收集算法:
分代收集算法不是一种具体的收集算法. 由于堆是分为新生代(包括eden空间,from survivor,to survivor) 老年代的,分代收集算法就是在不一样的分代空间采用不一样的垃圾回收算法: 如复制算法应用于新生代,标记清除和标记整理应用于老年代
通常经常使用的new方式建立对象,建立的就是强引用. 只要强引用存在, 垃圾回收器就不会回收.
SoftReference , 非必须引用,若是内存足够或正常,就不回收,可是当 内存不够,快发生OOM的时候就回收掉软引用对象.
WeakReference , 对于弱引用的对象来讲,只要垃圾回收器开始回收, 不管空间是否充足,都回收弱引用的对象.
和其余几种引用不一样,虚引用不会决定对象生命周期,垃圾回收时,没法经过虚引用获取对象 值.虚引用在任什么时候候均可能被垃圾回收掉.虚引用必须和引用队列(ReferenceQueue)使用.
软引用,弱引用,虚引用在被GC前会被加入到与其关联的引用队列中.