基础知识
多线程
1、线程与进程
在开始之前先把进程与线程进行区分一下,一个程序最少需要一个进程,而一个进程最少需要一个线程。关系是线程–>进程–>程序的大致组成结构。所以线程是程序执行流的最小单位,而进程是系统进行资源分配和调度的一个独立单位。
2、Thread的几个重要方法
我们先了解一下Thread的几个重要方法。
a、start()方法,调用该方法开始执行该线程;
b、stop()方法,调用该方法强制结束该线程执行;
c、join方法,调用该方法等待该线程结束。
d、sleep()方法,调用该方法该线程进入等待。
e、run()方法,调用该方法直接执行线程的run()方法,但是线程调用start()方法时也会运行run()方法,区别就是一个是由线程调度运行run()方法,一个是直接调用了线程中的run()方法!!
看到这里,可能有些人就会问啦,那wait()和notify()呢?要注意,其实wait()与notify()方法是Object的方法,不是Thread的方法!!同时,wait()与notify()会配合使用,分别表示线程挂起和线程恢复。
这里还有一个很常见的问题,顺带提一下:wait()与sleep()的区别,简单来说wait()会释放对象锁而sleep()不会释放对象锁。这些问题有很多的资料,不再赘述。
3、线程状态
线程总共有5大状态,通过上面第二个知识点的介绍,理解起来就简单了。
新建状态:新建线程对象,并没有调用start()方法之前
就绪状态:调用start()方法之后线程就进入就绪状态,但是并不是说只要调用start()方法线程就马上变为当前线程,在变为当前线程之前都是为就绪状态。值得一提的是,线程在睡眠和挂起中恢复的时候也会进入就绪状态哦。
运行状态:线程被设置为当前线程,开始执行run()方法。就是线程进入运行状态
阻塞状态:线程被暂停,比如说调用sleep()方法后线程就进入阻塞状态
死亡状态:线程执行结束
4、锁类型
可重入锁:在执行对象中所有同步方法不用再次获得锁
可中断锁:在等待获取锁过程中可中断
公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利
读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写
5、多线程特性
多线程要保证并发程序正确执行,必须遵循三个原则:原子性、有序性、可见性
synchronized与Lock的区别与使用
我把两者的区别分类到了一个表中,方便大家对比:
类别 |
Synchronized |
Lock |
存在层次 |
Java的关键字,在jvm层面上 |
是一个类 |
锁的释放 |
1、等待获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 |
在finally中必须释放锁,不然容易造成 线程死锁 |
锁的获取 |
假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 |
分情况而定,Lock有多个锁获取的方式 ,具体下面会说道,大致就是可以尝试 获得锁,线程可以不用一直等待 |
锁状态 |
无法判断 |
可以判断 |
锁类型 |
可重入 不可中断 非公平 |
可重入 可中断 可公平(两者皆可) |
性能 |
少量同步 |
大量同步 |
volatile和ThreadLocal
volatile:其变量修改后将立即刷新到主内存,其他线程即可读取到新值。编译器利用内存屏障的概念禁止上述三条指令的重排序。
ThreadLocal:当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
悲观锁和乐观锁
独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
CAS在Java中的实现
AtomicInteger,在没有锁的机制下借助volatile原语,保证线程间的数据是可见的(共享的),然后利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。
原理:CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。 CAS通过调用JNI的代码实现的。JNI:Java Native Interface为JAVA本地调用,允许java调用其他语言。
线程池的原理和实现
https://blog.csdn.net/he90227/article/details/52576452
阻塞队列和非阻塞队列
阻塞队列
获取元素的时候等待队列里有元素,否则阻塞,保存元素的时候等待队列里有空间,否则阻塞 。
ArrayBlockingQueue:FIFO、数组实现
LinkedBlockingQueue :FIFO、Node链表结构
SynchronousQueue :无内部容量的阻塞队列,put必须等待take,同样take必须等待put。比较适合两个线程间的数据传递。
DelayQueue :有界阻塞延时队列,当队列里的元素延时期未到时,通过take方法不能获取,会被阻塞,直到有元素延时到期为止
PriorityBlockingQueue :插入的对象必须是可比较的或者通过构造方法实现插入对象的比较器,用来处理一些有优先级的事物
注:ArrayBlockingQueue在put,take操作使用了同一个锁,两者操作不能同时进行,而LinkedBlockingQueue使用了不同的锁,put操作和take操作可同时进行,以此来提高整个队列的并发性能。
非阻塞队列
ConcurrentHashMap:弱一致性模型
ConcurrentSkipListMap:并发程序中可以保证顺序
ConcurrentSkipListSet:支持排序且不允许重复的元素
ConcurrentLinkedQueue:提供了并发环境的队列操作。
方法poll当没有获得数据时返回null,如果有数据则移除表头,并将表头进行返回;
方法element当没有数据时,出现异常,如果有数据则返回表头项 ;
方法peek当没有数据时返回null,有数据则不移除表头,返回表头项
ConcurrentLinkedDeque:
CopyOnWriteArrayList
CopyOnWriteArraySet
注:HashTable、Vector调用iterator()方法返回Iterator对象后,再调用remove()时会出现ConcurrentModificationException异常,也就是并不支持Iterator并发的删除。HashTable、Vector调用iterator()方法返回Iterator对象后,再调用remove()时会出现ConcurrentModificationException异常,也就是并不支持Iterator并发的删除。
线程间通信
Condition可以替代传统的线程间通信(synchronize),用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll()。
注:
为什么方法名不直接叫wait()/notify()/nofityAll()?因为Object的这几个方法是final的,不可重写!
Condition的强大之处在于它可以为多个线程间建立不同的Condition
CountDownLatch、CyclicBarrier、Semaphore共同之处与区别以及各自使用场景
区别:https://blog.csdn.net/jackyechina/article/details/52931453
1.CountDownLatch使一个线程A或者组线程A等待其他线程执行完毕后,
并发工具类/比较 |
CountDownLatch |
CyclicBarrier |
区别 |
一组线程A或者组线程A等待其他线程执行完毕后才继续执行 |
一组线程使用await指定barrier,所有线程都到达各自barrier后,再同时执行barrier下面的代码 |
减计数的方式,计数为0释放所有等待线程 |
加计数方式,计数达到构造方法中参数指定的值时释放所有等待线程 |
|
当计数为0时,无法被重置 |
计数达到指定值时,计数置位0重新开始 |
|
countDown方法计数减一,调用await方法只进行阻塞,对计数没任何影响 |
只有一个await方法,一旦调用计数加一,若加一后不等于构造方法的值,则线程阻塞 |
|
相同 |
有一个int类型参数的构造方法,该值作为计数用 |
|
都具有await()方法,并且执行此方法会引起线程的阻塞,达到某种条件才能继续执行(这种条件也是两者的不同) |
Semaphore:有一个int类型参数的构造方法,该参数用来控制同时访问特定资源的线程数量,当达到semaphore指定资源数量时就不能再访问,线程处于阻塞
JVM
运行机制
1、运行过程概述
https://www.cnblogs.com/lishun1005/p/6019678.html
开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件),再然后字节码被装入内存,一旦字节码进入虚拟机,就会被解释器执行或者是被即时代码发生器有选择的转换成机器码执行。
(1)Java代码到JVM字节码
(2)Java字节码的执行
(3)运行过程包含的机制
(a)Java源码编译机制
Java源码编译由以下三个过程组成:分析和输入到符号表、注解处理、语义分析和生成class文件
最后生成的class文件由以下部分组成:
结构信息:包含class文件格式版本号及各部分的数量和大小的信息
元数据:对应于Java源码中声明与常量的信息。包含类、继承的超类、实现的接口的声明信息、域与方法声明信息和常量池
方法信息:对应于Java源码中语句和表达式对应的信息。包含字节码、异常处理器表、求值栈与局部变量去大小、求值栈的类型记录、调试符号信息
(b)类加载机制
- 加载
JVM的类加载时通过ClassLoader及其子类来完成的,类的层次关系和加载顺序右下图描述
①Bootstrap ClassLoader
负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类
②Extension ClassLoader
负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包
③App ClassLoader
负责记载classpath中指定的jar包及其目录下的class文件
④Custom ClassLoader
属于应用程序根据自身需要自定义ClassLoader,如Tomcat、Jboss都会根据j2ee规范自行实现ClassLoader
加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
- 连接
主要是将已经读到内存的类的二进制数据合并到虚拟机的运行时环境中去。分为三个阶段,
分别为
验证:文件格式验证、元数据验证、字节码验证、符号引用验证等。
准备:主要是为对象和变量分配内存,并为类设置初始值(方法区中)。
对于static类型变量在这个阶段会为其赋值为默认值,比如public static int v=5,在这个阶段会为其赋值为v=0,而对于static final类型的变量,在准备阶段就会被赋值为正确的值
解析:将符号引用转换为直接引用。
符号引用仅仅是一个字符串,而引用的对象不一定被加载,直接引用指的是将对象的指针或者地址偏移量指向真正的对象,将字符串所指向的对象加载到内存中
- 初始化
主要执行类的构造方法,并且为静态变量赋值为初始值,执行静态块代码。
步骤:
假如该类没有被加载和链接,那就先加载和链接
假如该类存在直接的父类,并且这个父类还没被初始化,那就先初始化父类
假如类中存在初始化语句时,那就依次执行这些初始化语句
顺序:类的静态成员 --> 类的实例成员 --> 类的构造方法
时机:
所有的java类只有在对类的首次主动使用时才会被初始化。主动使用的情况有六种,分别如下。
创建类的实例、访问某个类或接口的静态变量,或者对该静态变量赋值、调用类的静态方法、反射、初始化一个类的子类、Java虚拟机启动时被标注为启动类的类
注意:1、当Java虚拟机初始化一个类时,要求他的所有父类都已经被初始化,但是这条规则并不适合接口。在初始化一个类或接口时,并不会先初始化它所实现的接口。
2、只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用。如果静态方法或变量在parent中定义,从子类进行调用,则不会初始化子类。
(c)类执行机制
JVM是基于堆栈的虚拟机。JVM为每个新创建的线程都分配一个堆栈。
JVM执行class字节码,线程创建后,都会产生程序计数器和栈,程序计数器存放下一条要执行的指令在方法内的偏移量,栈中存放一个个栈帧,每个栈帧对应着每个方法的每次调用,而栈帧又是由局部变量区和操作数栈两部分组成,局部变量区用于存放方法中的局部变量和参数,操作数栈用于存放方法执行过程中产生的中间结果。
内存空间管理
内存模型
https://blog.csdn.net/suifeng3051/article/details/48292193#t7
其中,方法区和堆是所有线程共享的。
(1) 方法区
存放了要加载的类的信息、类中的静态变量、final定义的常量、类中的field、方法信息等。由线程共享。
(2) 堆区
JavaGC机制最重要的区域,由所有线程共享,在虚拟机启动时创建。
存储对象实例及数组,可以认为java中所有通过new创建的对象都在此分配
分代管理方式
(3) 本地方法栈
用于支持native方法的执行,存储了每个native方法调用的状态
(4) 程序计数器
一个较小的内存区域,可能是CPU寄存器或者操作系统内存,所以它是线程私有的。
如果程序执行的是Java方法,则计数器记录的时正在执行的虚拟机字节码指令地址;如果程序执行的是本地方法,则计数器的值为Undefined。由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况。
(5) 虚拟机栈
占用的是操作系统内存,每个线程都对应着一个虚拟机栈,是线程私有,而且分配非常高效。一个线程的每个方法在执行的同时,都会创建一个栈帧,栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。
虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError(栈溢出);不过多数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,直到内存不足,此时,会抛出OutOfMemoryError(内存溢出)。
(6) Java对象访问方式
句柄:堆区中,会划分一块单独内存区域作为句柄池,句柄池存储了对象实例数据(堆区)和对象类型数据(方法区)的指针,该方式表示地址十分稳定。
直接指针:reference中存储的就是对象实例数据在堆中的实际地址,对象实例数据包含对象类型数据的指针,HotSpot虚拟机用的就是这种方式。
JVM内存分配
Java对象所占用的内存主要在堆上实现,因为堆是线程共享的,因此在堆上分配内存时需要进行加锁,这就导致了创建对象的开销比较大。当堆上空间不足时,会触发GC,如果GC后空间仍然不足,则会抛出OutOfMemory异常。
内存分配技术
bump-the-pointer:由于Eden区是连续的,因此该技术的核心就是跟踪最后创建的对象,在对象创建时,只需要检查最后一个对象后面是否有足够的内存即可。
TLAB(Thread-LocalAllocation Buffers):针对多线程的,为每个新创建的线程在Eden区分配一块独立的空间,这快空间称为TLAB。TLAB分配内存时,不需要加锁。
内存回收
(1)方式
引用计数器:采用分散式管理,通过计数器记录对象是否被引用,当计数器为0,说明此对象已经不再被使用,可进行回收。弊端有引用计数器需要在每次对象赋值时进行引用计数器的增减,他有一定消耗。另外,引用计数器对于循环引用的场景没有办法实现回收。
跟踪收集器:采用集中式管理,会全局记录数据引用的状态。基于一定条件的触发,执行时需要从根集合来扫描对象的引用关系,这可能会造成应用程序暂停。主要有复制(Copying)、标记-清除(Mark-Sweep)、标记-压缩(Mark-Compact)三种算法。
根集合(GC Roots):方法区、栈和本地方法区不被GC所管理,因而选择这些区域内的对象作为GC roots,被GC roots引用的对象不被GC回收。
A.复制(Copying)
从根集合扫描出存活的对象,并将找到的存活的对象复制到一块新的完全未被使用的空间中。年轻代(Eden)就是采用这个算法,其带来的成本就是增加一块空的内存空间进行对象的移动。
B. 标记-清除(Marking-Deleting)
从根集合扫描,对存活的对象标记,标记完毕后,再扫描整个空间未标记的对象进行删除。由于直接回收不存活对象占用的内存,因此会造成内存碎片。
C.标记-压缩(Mark-Compact)
跟标记清除一样,是对活的对象进行标记,但清除对象占用的内存后,会把所有活的对象向左端空闲空间移动,然后再更新引用其对象的指针。
(2)过程
1.在初始阶段,新创建的对象分配到Eden区,survivor的两块空间都为空
2.当Eden区满了,minor garbage被触发
3.经过扫描与标记,存活的对象被复制到S0,不存活的对象被收回
4.下一次Minor GC中,</