本文共 23833 字,大约阅读时间需要 79 分钟。
java虚拟机(java virtual machine,JVM),一种能够运行java字节码的虚拟机。作为一种编程语言的虚拟机,实际上不只是专用于Java语言,只要生成的编译文件匹配VM对加载编译文件格式要求,任何语言都可以由JVM编译运行。
1.类的加载机制2.jvm内存结构3.GC算法垃圾回收4.GC分析命令调优
1.什么是类的加载?
类的加载指的是将类的.cas文件中的二进制数据读入到内存中,将其汝在运行时数据区的方法区内,然后在堆区创建一个jrn.tln. Cas对象,用来封装类在方法区内的数据结构。类的加或的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
2.类的生命周期?
1.加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象
2.连接,建按又包含三块内容:验证、准备、初始化。1)驻证,文件格式、元数据、字节码、符号引用验任﹔2》准备,为类的静态变量分配内存,并格其初始化为默认值;3)解析把类中的符号引用转换为直接引用
3.初始化,为类的静态变量赋予正确的初始值4.使用,new出对象程序中使用
5.卸载,执行垃圾回收
3.类加载器
1.启动类加载器:Botstrp ClassLoader,负责加载存放在JDKireli(DK代表JDK的安装目录,下同)下,或被-Ybochssuath参数指定的路径中的,并且能被虚拟机识别的类库
2扩展类加载器,Eetnon(ClsLader,该加较器出由sn.mosc LamthrnExolasLader实现。它负责加载DK re ntrec目录中,或者tjyoaetdis系纷变量指定的路径中的所有类库(如in at.*开头的类》,开发者可以直接使用扩展类加载器。
3.应用程序类加载器。Aplicaton (CiasLader,该类加载器由sum.mivcLamchrSAypnC'ssLoader来实现,它负责加载用户类路径(Cisstah)所指定的类,开发者可以直接使用该类加载器4.用户自定义类加载器,通过继承java.lang.ClassLoader类的方式实现。
4.双亲委派模型
当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载.反馈给子类,由子类去完成类的加载。自底向上的检查,自顶向下的加载注意双亲委派模式的问题:无法识别应用程序类加载器中的类。
解决方案:设置一个上下文加载器角色解决
5.类加载机制
1.全盘负责,省一个尖加软莽页贡加软果个CiBs时,该CKBs所依赖的和引用的共他CiBs世将田该癸加软器页贡载入,陈非显示使用另外一个尖加载褴米载入
2.父类委托,让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
3.缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用呆个Class时类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成
Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
Minor GC触发条件:当Eden区满时,触发Minor GC。
Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法去空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
静态变量先于静态代码块执行,整个执行顺序是:
1.父类静态变量初始化。
2.父类静态代码块。
3.子类静态变量初始化。
4.子类静态语句块。
5.父类变量初始化。
6.父类代码块。
7.父类构造函数。
8.子类变量初始化。
9.子类语句块。
10.子类构造函数。
你写的代码有可能,根本没有按照你期望的顺序执行,因为编译器和 CPU 会尝试指令重排来让代码运行更高效,这就是指令重排。
我们都知道CPU执行指令的时候,访问内存的速度远慢于 CPU 速度。为了尽可能减少内存操作带来的 CPU 空置的影响,虚拟机会按照自己的一些规则将程序编写顺序打乱:即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行。当然这样的前提是不会产生错误。不管谁先开始,总之后面的代码在一些情况下存在先结束的可能。
在硬件层面,CPU会将接收到的一批指令按照其规则重排序,同样是基于 CPU 速度比缓存速度快的原因。和上一点的目的类似,只是硬件处理的话,每次只能在接收到的有限指令范围内重排序,而虚拟机可以在更大层面、更多指令范围内重排序。
如果两个操作访问的是同一个变量,且其中有一个是写操作,那么这两个操作之间就存在数据依赖。数据依赖分为读后写、写后写、写后读。
基于上面的重排序原则,不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守 as-if-serial 语义。为了遵守 as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。
定义:如果一个操作 happens-before 另一个操作,那么意味着第一个操作的结果对第二个操作可见,而且第一个操作的执行顺序将排在第二个操作的前面。如果重排序之后的结果,与按照happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。
具体规则如下:
总结:这么些规则总的来说就在体现一件事,也就是单线程里,程序有关的各个层面都会做指令重排的优化,而且会用这些规则来保证结果正确。
如果是多线程,无法保证正确性,指令重排可能会造成结果错误。我个人是这样理解的:多线程最大问题就是一个 先来后到 可能破坏正确性的问题,因此有同步、加锁等等机制来保障先来后到,可是指令重排让线程本身的指令乱了,就可能让整体的结果功亏一篑。
/*** 指令重排问题*/public class HappenBefore { private static int i = 0, j = 0; private static int a = 0, b = 0; public static void main(String[] args) throws InterruptedException { while (true){ i = 0; j = 0; a = 0; b = 0; Thread t1 = new Thread(()->{ a = 1; i = b; }); Thread t2 = new Thread(()->{ b = 1; j = a; }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("i = " + i +" ; j = " + j); if (i == 0 && j == 0){ break; } } }}
上面程序里开了两个线程,交替赋值;join保障这两个线程都在 main 线程之前执行完,确保最后在执行完后输出。(注意并不是保证 t1 在 t2 之前执行)
线程t1 | 线程t2 |
---|---|
a = 1; | b = 1; |
i = b; | j = a; |
如果没有指令重排,按照我们的分析,多线程情况下,t1 和 t2 分别执行完,或者经过cpu的调度,t1 和 t2 线程经过了切换,那么可能出现的情况是,最终的 i 和 j 为:
但是结果应该是不会出现 i = 0,j = 0 的情况的。可是代码跑起来就会发现,会出现 i = 0,j = 0 的情况,这就是指令重排在多线程情况下带来的问题。在各自单线程里,因为没有依赖关系,所以编译器、虚拟机以及 cpu 可以进行乱序重排,最后导致了这种结果。
Volatile 英文翻译:易变的、可变的、不稳定的。
volatile保证变量可见:简单来说就是,当线程 A 对变量 X 进行了修改后,在线程 A 后面执行的其他线程能够看到 X 的变动,就是保证了 X 永远是最新的。
更详细的说,就是要符合以下两个规则:
另一个角度,结合指令重排序,volatile 修饰的内存空间,在这上面执行的指令是禁止乱序的。因此,在单例模式的 DCL 写法中,volatile 也是必须的元素。
private static int num = 0;public static void main(String[] args) throws InterruptedException { new Thread(()->{ while (num == 0){ } }).start(); Thread.sleep(1000); num = 1;}
代码死循环,因为主线程里的 num = 1,不能及时将数据变化更新到主存,因此上面的代码 while 条件持续为真。
因此可以给变量加上 volatile:这样就在执行几秒后就会停止运行。
private volatile static int num = 0;
在设计模式里的单例模式,如果在多线程的情况下,仍然要保证始终只有一个对象,就要进行同步和锁。利用双重校验锁,实现线程安全的单例锁。但是:volatile不能保证原子性。
class DCL{ private static volatile DCL instance; private DCL(){ } public static DCL getInstance(){ if (instance == null){//check1 synchronized (DCL.class){ if (instance == null){//check2 instance = new DCL(); } } } return instance; }}
原子操作就是这个操作要么执行完,要么不执行,不可能卡在中间。
比如在 Java 里, i = 2,这个指令是具有原子性的,而 i++ 则不是,事实上 i++ 也是先拿 i,再修改,再重新赋值给 i 。
例如你让一个volatile的integer自增(i++),其实要分成3步:
1)读取volatile变量值到local;
2)增加变量的值;
3)把local的值写回,让其它的线程可见。
这 3 步的jvm指令为:
mov 0xc(%r10),%r8d ; Loadinc %r8d ; Incrementmov %r8d,0xc(%r10) ; Storelock addl $0x0,(%rsp) ; StoreLoad Barrier
最后一步是内存屏障。
**什么是内存屏障?**内存屏障告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行,同时强制更新一次不同CPU的缓存,也就是通过这个操作,使得 volatile 关键字达到了所谓的变量可见性。这个时候我们就知道,如果一个操作有好几步,如果其他的线程修改了值,将会都产生覆盖,还是会出现不安全的情况,所以, volatile 关键字本身无法保证原子性。
volatile 无法保证原子性。我们还是用两数之和来示例:
public class NoAtomic { private static volatile int num = 0; public static void main(String[] args) throws InterruptedException { for (int i=0; i<100; i++){ new Thread(()->{ for (int j=0; j < 100; j++){ num++; } }).start(); } Thread.sleep(3000); System.out.println(num); }}
输出结果会小于预期,虽然 volatile 保证了可见性,但是却不能保证操作的原子性。因此想要保证原子性,还是得回去找 synchronized 或者使用juc下的原子数据类型。
不过:由于硬件层面,从工作内存到主存的更新速度已经提升的很快,加上 synchronized 的改进,也已经不用考虑太过重量的问题,所以 volatile 很少使用。
基本数据类型: byte boolean char short int float long double
基本类型 | byte | boolean | char | short | int | float | long | double |
应用类型 | Byte | Boolean | Character | Short | Integer | Float | Long | Double |
所占字节 | 1 | 1或者4 | 2 | 2 | 4 | 4 | 8 | 8 |
包装类和基本数据类型的区别是:
| 包装类 | 基本数据类型 |
| 包装类是对象,拥有方法和字段, 对象的调用都是通过引用对象的地址 | 基本类型不是 |
参数传递 | 包装类型是引用的传递 | 基本类型是值的传递 |
声明不同 | 包装类型需要new在堆内存进行new来分配内存空间 | 基本数据类型不需要new关键字 |
存储位置不同 | 包装类型是把对象放在堆中,然后通过对象的引用来调用他们 | 基本数据类型直接将值保存在值栈中 |
初始值不同 | 包装类型的初始值为null | int的初始值为0、boolean的初始值为false |
使用方式不同 | 基本数据类型直接赋值使用就好 | 包装类型是在集合如 coolection Map时会使用 |
== 和 equals 的区别是什么?
| 基本数据类型 | 包装数据类型 |
== | 比较是值是否相同 | 比较的是地址指向是否相同 |
equal | 如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;但是String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容 |
基本知识:我们知道,如果两个引用指向同一个对象,用==表示它们是相等的。如果两个引用指向不同的对象,用==表示它们是不相等的,即使它们的内容相同。因此,后面一条语句也应该是false 。
这就是它有趣的地方了。如果你看去看 Integer.java 类,你会发现有一个内部私有类,IntegerCache.java,它缓存了从-128到127之间的所有的整数对象。所以事情就成了,所有的小整数在内部缓存,然后当我们声明类似——
操作字符串的类有:String、StringBuffer、StringBuilder
| String | Stringbuffter | Stringbuilder | |
相同 | 都是继承与AbstractStringBuilder | |||
区别 | 定长度的不变 | 会自动进行扩容工作,扩展为原数组长度的2倍加2。 | ||
效率最小 | 线程安全/效率其次 | 线程不安全/ 效率最高 |
StringBuffer初始化及扩容机制
StringBuffer()的初始容量可以容纳16个字符,当该对象的实体存放的字符的长度大于16时,实体容量就自动增加。StringBuffer对象可以通过length()方法获取实体中存放的字符序列长度,通过capacity()方法来获取当前实体的实际容量。底层都是一个字符数组的来实现的。Stringbuffer中就是比Stringbuder多了synchronize关键字。
重写(Overriding) | 重载(Overloading) | |
类的数量 | 父子类、接口与实现类 | 本类 |
方法名称 | 一致 | 一致 |
参数列表 | 一定不能修改 | 必须修改 |
返回类型 | 一定不能修改 | 可以修改 |
异常 | 可以减少或删除,但不能扩展 | 可以修改 |
| For | while |
从内存角度考虑 | 局部变量在栈内存中存在,当for循环语句结束,那么变量会及时被gc(垃圾回收器)及时的释放掉,不浪费空间 | 如果使用循环之后还想去访问循环语句中控制那个变量,使用while循环 |
从内存角度考虑 | 需求明确循环的次数,那么使用for循环 | 如果一个需求,不知道循环了多少次,使用while循环 |
从适用范围考虑 | for对于初学使用的要多于while | 所有的for循环都可以用while来表示,不是所有的while循环都可以用for循环来表示 |
Const/goto | 保留关键字,没有具体含义 |
instanceof | 用来在运行时判断对象是否是指定类及其父类的一个实例,不能比较基本类型。 |
final | 修饰的类叫最终类,该类不能被继承。修饰的方法不能被重写。final修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改 |
voilate | 可见性,volatile 解决了内存可见,所有对volatile关键字的读写都会直接刷到主存中去,保证了变量的可见性,适用于对变量可见性有要求而对读取顺序没有要求的场景。 |
Native | 本地(操作系统函数调用) |
Synchronized | 同步锁 |
final | static |
都可以修饰类、方法、成员变量。 | 都可以修饰类、方法、成员变量。 |
都不能用于修饰构造方法。 | 都不能用于修饰构造方法。 |
Final不可以修饰代码块 | static 可以修饰类的代码块 |
Final可以修饰方法内的局部变量 | static 不可以修饰方法内的局部变量 |
final 修饰表示常量、一旦创建不可改变 | static 修饰表示静态或全局,被修饰的属性和方法属于类,可以用类名.静态属性 / 方法名 访问 |
final 标记的成员变量必须在声明的同时赋值,或在该类的构造方法中赋值,不可以重新赋值 | static 修饰的代码块表示静态代码块,当 Java 虚拟机(JVM)加载类时,就会执行该代码块,只会被执行一次 |
final 方法不能被子类重写 | static 修饰的属性,也就是类变量,是在类加载时被创建并进行初始化,只会被创建一次 |
final 类不能被继承,没有子类,final 类中的方法默认是 final 的 | static 修饰的变量可以重新赋值 |
| static 方法中不能用 this 和 super 关键字 |
| static 方法必须被实现,而不能是抽象的abstract |
| static 方法不能被重写 |
Final | final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。 |
finally | finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用存放一些关闭资源的代码。 |
finalize | finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。 |
抽象性:抽象是将类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。
继承性:指子类拥有父类的全部特征和行为,这是类之间的一种关系。Java 只支持单继承(C++支持多继承多实现)。
封装性:封装是将代码及其处理的数据绑定在一起的一种编程机制,该机制保证了程序和数据都不受外部干扰且不被误用。封装的目的在于保护信息。
多态性:多态性体现在父类的属性和方法被子类继承后或接口被实现类实现后,可以具有不同的属性或表现方式。
普通类和抽象类有哪些区别?
抽象类 | 普通类 |
不能被实例化 | 可以实例化 |
有抽象方法,抽象方法只需申明,无需实现,含有抽象方法的类必须申明为抽象类 |
|
抽象类的子类必须实现抽象类中所有抽象方法,否则这个子类也是抽象类 |
|
抽象方法不能被声明为静态 |
|
抽象方法不能用 private 修饰 |
|
抽象方法不能用 final 修饰 |
|
一个类只能继承一个抽象类,而一个类却可以实现多个接口。(单继承多实现方式)
| 接口 | 抽象类 |
相同点 | 接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。实现接口或继承抽象类的普通子类都必须实现这些抽象方法 | |
不同点 | 1只能包含抽象方法,静态方法和默认方法,不能为普通方法提供方法实现(在JDK1.8可以使用default和static关键字来修饰接口中定义的普通方法) 2接口中的成员变量只能是 public static final 类型 3接口不能包含构造器 4接口里不能包含初始化块 | 1完全可以包含普通方法,接口中的普通方法默认为抽象方法 2抽象类中的成员变量可以是各种类型的 3抽象类可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。 4抽象类里完全可以包含初始化块。 |
类的实例化创建/创建一个类的实例都有哪些办法?
使用new关键字//创建对象方式1:使用new关键字 User u1 = new User("1",2,"3"); System.err.println(u1.toString());//创建对象方式2:使用反射//发射方式创建对象要求被创建的对象编写空构造 try { User u2 = User.class.newInstance(); System.err.println(u2.toString()); } catch (InstantiationException | IllegalAccessException e) { System.out.println("反射创建失败"+e.getMessage()); }//使用clone方法创建对象:要求被创建或者被克隆的对象实现Cloneable接口//(3)是在内存上对已有对象的影印,所以不会调用构造函数 try { User u3 = (User) u1.clone(); System.err.println("u3:"+u3.toString()); System.out.println(u1==u3);//false } catch (CloneNotSupportedException e) { System.out.println("克隆创建失败"+e.getMessage()); }
浅拷贝 | 当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。 |
深拷贝 | 除了对象本身被复制外,对象所包含的所有成员变量也将复制。 |
不通过构造方法能创建对象吗?
1 | 用 new 语句创建对象 | 会调用构造函数 |
2 | 运用反射,调用 java.lang.Class 或 java.lang.reflect.Constructor 类的 newInstance() 方法。 | 会调用构造函数 |
3 | 调用对象的 clone() 方法 | 不会调用构造函数 |
4 | 运用反序列化手段,调用 java.io.ObjectInputStream 对象的 readObject() 方法、不会调用构造函数 | 不会调用构造函数 |
多态的定义 | 是一种的动态的绑定技术是指的是在执行期间而不是编译期间判断对象的实际类型调用相关的方法。多态就是一个行为具有多个不同的变现的形式或者是形态的能力。多态就是同一个接口使用不同实例进而实现不同的操作。 |
多态的前提条件 | 1继承或是实现父类 |
2对父类方法的重写 | |
3 父类的引用指向子类的对象 | |
多态的优点: | 1消除类型的会见的耦合性 |
2 可以实现高扩展 | |
3灵活性、简化性 |
线程的状态:Java 中的线程有四种状态分别是:创建、就绪,运行、、挂起、结束。
进程的转态:
运行态:进程实际占用cpu的时间的运行时
就绪态:可以运行的 但是其他线程在运行而处于就绪态
阻塞态:除非某种外部事件发生,否则进程不能运行
Java的线程的通信方式:
1同步:这里讲的同步是指多个线程通过synchronized关键字这种方式来实现线程间的通信。
2 while轮询的方式:在这种方式下,线程A不断地改变条件,线程ThreadB不停地通过while语句检测这个条件(list.size()==5)是否成立 ,从而实现了线程间的通信。但是这种方式会浪费CPU资源。之所以说它浪费资源,是因为JVM调度器将CPU交给线程B执行时,它没做啥“有用”的工作,只是在不断地测试 某个条件是否成立。
3 利用volatile:volatile修饰的变量值直接存在主内存里面,子线程对该变量的读写直接写住内存,而不是像其它变量一样在local thread里面产生一份copy。volatile能保证所修饰的变量对于多个线程可见性,即只要被修改,其它线程读到的一定是最新的值。
4利用BlockingQueue:BlockingQueue定义的常用方法如下:
add(Object):把Object加到BlockingQueue里,如果BlockingQueue可以容纳,则返回true,否则抛出异常。
offer(Object):表示如果可能的话,将Object加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false。
put(Object):把Object加到BlockingQueue里,如果BlockingQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里有空间再继续。
poll(time):获取并删除BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null。当不传入time值时,立刻返回。
peek():立刻获取BlockingQueue里排在首位的对象,但不从队列里删除,如果队列为空,则返回null。
take():获取并删除BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的对象被加入为止。
进程间如何通讯方法/线程间如何通讯:
操作系统层面进程通信 | 操作系统层面线程通信 | Java层面线程通信 |
1 socket通信 | 临界区 | wait/notify 等待 |
2 消息队列 | 互斥量 | Volatile 内存共享 |
3 信号量 | 信号量 | CountDownLatch 并发工具 |
4 共享内存 | 事件 | CyclicBarrier 并发工具 |
5 管道通信 |
| 同步、blockingqueue |
线程的调度策略
线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:
(1)线程体中调用了 yield 方法让出了对 cpu 的占用权利
(2)线程体中调用了 sleep 方法使线程进入睡眠状态
(3)线程由于 IO 操作受到阻塞
(4)另外一个更高优先级线程出现
(5)在支持时间片的系统中,该线程的时间片用完
说一下 synchronized 底层实现原理?
synchronized 是由一对 monitorenter/monitorexit 指令实现的,monitor 对象是同步的基本实现单元。在 Java 6 之前,monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。但在 Java 6 的时候,Java 虚拟机 对此进行了大刀阔斧地改进,提供了三种不同的 monitor 实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。
无锁的状态是:没有任务线程执行这样的任务,
偏向说是:只有一个线程来执行这个任务
轻量锁 :在偏向锁的基础上的多个线程竞争这个所资源。在利用的锁自旋的操作到一定次数的时候就会变成轻量锁。
重量锁:子啊轻量锁的基础上 在自旋自定的次数的时候在变成重量锁
synchronized关键字锁住的是什么?
无锁 --> 偏向锁(偏向锁实际上需要先获得轻量级锁,然后在锁重入时才会执行偏向锁优化) --> 轻量级锁(CAS设置markword+自旋) --> 重量级锁(OS层面,在升级为重量级锁时,若在多核cpu上,会出尝试多次自旋,若还是获取不到锁,才就会膨胀为重量级锁)
其中无锁,偏向锁,轻量级锁都是JVM层面所做的工作; 而重量级锁是OS层面的,这就涉及到用户态到内核态的转换.轻量级锁连续自旋等待超过一定次数时(JVM默认设置为10次),为了避免CPU占用过高,会升级成重量级锁, 对于重量级锁, 在CPU层面是通过CAS指令来实现的.
synchronized在JVM层面是通过设置对象头来实现上述的锁升级/锁优化的,针对64bit的JVM,其对象头中的mark word格式如下:
Synchronized 与volatile 关键字的区别?
Synchronized 解决执行控制问题,它会其它线程获取当前对象的监控锁,使得当前对象中被Synchronized关键字保护的代码块无法被并发执行,并且Synchronized 还会创建一个内存屏障,保证了所有CPU操作结果都会直接刷到主存中去,从而保证了可见性。
内存可见:控制的是线程执行结果在内存中对其它线程的可见性,根据Java内存模型的实现,Java线程在具体执行时,会先拷贝主存中的数据 到线程本地(CPU缓存),操作完成后再把结果从线程刷到主存中。
volatile 解决了内存可见,所有对volatile关键字的读写都会直接刷到主存中去,保证了变量的可见性,适用于对变量可见性有要求而对读取顺序没有要求的场景。
Synchronized 与volatile 关键字的区别?
| volatile | Synchronized |
阻塞 | 不会造成线程的阻塞 | 会造成线程的阻塞 |
范围 | 仅能使用在变量级别 | 可以使用在变量,方法,类级别上 |
优化 | 标记的变量不会被编译器优化 | 标记的变量会被编译器优化 |
可见性 | 仅能保证变量的修改可见性,不能保证原子性 | 可以保证变量修改的可见性和原子性 |
层面 | 本质告诉JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中去读取 | 锁定当前变量,只有当前线程可以访问该变量,其它线程不可以。 |
同步和异步有何异同,在什么情况下分别使用他们?
答:如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率
java层面线程的通信的方法:
Java的层面的是通信方式有:
方式一:使用 volatile 关键字
基于 volatile 关键字来实现线程间相互通信是使用共享内存的思想,大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。这也是最简单的一种实现方式
方式二:使用Object类的wait() 和 notify() 方法
众所周知,Object类提供了线程间通信的方法:wait()、notify()、notifyaAl(),它们是多线程通信的基础,而这种实现方式的思想自然是线程间通信。注意: wait和 notify必须配合synchronized使用,wait方法释放锁,notify方法不释放锁。
方式三:使用JUC工具类 CountDownLatch
jdk1.5之后在java.util.concurrent包下提供了很多并发编程相关的工具类,简化了我们的并发编程代码的书
方式五:基本LockSupport实现线程间的阻塞和唤醒
LockSupport 是一种非常灵活的实现线程间阻塞和唤醒的工具,使用它不用关注是等待线程先进行还是唤醒线程先运行,但是得知道线程的名字
操作系统的层面线程同步方式:
互斥量(互斥锁/mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。互斥对象和临界区(代码的一个区间)对象非常相似,只是其允许在进程间使用,而临界区只限制于同一进程的各个线程之间使用,但是更节省资源,更有效率。
信号量/semaphore:它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。Mutex互斥量可以说是semaphore在仅取值0/1时的特例
事件(信号):通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。允许一个线程在处理完一个任务后,主动唤醒另外一个线程执行任务。比如在某些网络应用程序中,一个线程如A负责侦听通信端口,另外一个线程B负责更新用户数据,利用事件机制,则线程A可以通知线程B何时更新用户数据。
临界区(CCriticalSection)(已被舍弃):当多个线程访问一个独占性共享资源时,可以使用临界区对象。拥有临界区的线程可以访问被保护起来的资源或代码段,其他线程若想访问,则被挂起,直到拥有临界区的线程放弃临界区为止。具体应用方式:
1、 定义临界区对象CcriticalSection g_CriticalSection;
2、 在访问共享资源(代码或变量)之前,先获得临界区对象,g_CriticalSection.Lock();
3、 访问共享资源后,则放弃临界区对象,g_CriticalSection.Unlock();
4.3 操作系统层面的通信方式:
线程通讯:由于多线程共享地址空间和数据空间,所以多个线程间的通信是一个线程的数据可以直接提供给其他线程使用,而不必通过操作系统(也就是内核的调度)。
线程间的通信目的主要是用于线程同步。所以线程没有像进程通信中的用于数据交换的通信机制。也就是说,线程的通讯与线程的同步一样
1.(互斥)锁机制
包括互斥锁、条件变量、读写锁;
互斥锁提供了以排他方式防止数据结构被并发修改的方法。
条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
2.信号量机制(Semaphore)
包括无名线程信号量和命名线程信号量
3.信号机制(Signal)
类似进程间的信号处理
4.4线程同步的方法
1同步方法,有synchronized关键字修饰的方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
2同步代码块:即有synchronized关键字修饰的语句块。 被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。 注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
3使用局部变量实现线程同步。如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
4使用特殊域变量(volatile)实现线程同步
a.volatile关键字为域变量的访问提供了一种免锁机制
b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新
c.因此每次使用该域就要重新计算,而不是使用寄存器中的值
d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
5在java中新增了一个java.util.concurrent包来支持同步。
ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
6wait与notify关键字的来控制线程的同步。
7使用阻塞队列实现线程同步,前面5种同步方式都是在底层实现的线程同步,
8使用原子变量实现线程同步。在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。
线程的创建方式:
1)继承Thread类创建线程
2)实现Runnable接口创建线程
3)使用Callable和Future创建线程
4)使用线程池例如用Executor框架
Callable和Runnable接口的区别
区别1: 两者最大的区别,实现Callable接口的任务线程能返回执行结果,而实现Runnable接口的任务线程不能返回执行结果
注意点:Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞线程直到获取“将来”的结果,当不调用此方法时,主线程不会阻塞
区别2:Callable接口实现类中run()方法允许将异常向上抛出,也可以直接在内部处理(try. catch); 而Runnable接口实现类中run()方法的异常必须在内部处理掉,不能向上抛出。
线程池的种类
newCachedThreadPool:(缓存线程池)
底层:返回ThreadPoolExecutor实例,corePoolSize为0;maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为60L;unit为TimeUnit.SECONDS;workQueue为SynchronousQueue(同步队列)
通俗:当有新任务到来,则插入到SynchronousQueue中,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可以线程则执行,若没有可用线程则创建一个线程来执行该任务;若池中线程空闲时间超过指定大小,则该线程会被销毁。
适用:执行很多短期异步的小程序或者负载较轻的服务器
newFixedThreadPool:(固定数量的线程池)
底层:返回ThreadPoolExecutor实例,接收参数为所设定线程数量nThread,corePoolSize为nThread,maximumPoolSize为nThread;keepAliveTime为0L(不限时);unit为:TimeUnit.MILLISECONDS;WorkQueue为:new LinkedBlockingQueue<Runnable>() 无解阻塞队列
通俗:创建可容纳固定数量线程的池子,每隔线程的存活时间是无限的,当池子满了就不在添加线程了,如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列)
适用:执行长期的任务,性能好很多
newSingleThreadExecutor(单个线程的线程池)
底层:FinalizableDelegatedExecutorService包装的ThreadPoolExecutor实例,corePoolSize为1;maximumPoolSize为1;keepAliveTime为0L;unit为:TimeUnit.MILLISECONDS;workQueue为:new LinkedBlockingQueue<Runnable>() 无解阻塞队列
通俗:创建只有一个线程的线程池,且线程的存活时间是无限的;当该线程正繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列)
适用:一个任务一个任务执行的场景
NewScheduledThreadPool:(延时缓存线程池)
底层:创建ScheduledThreadPoolExecutor实例,corePoolSize为传递来的参数,maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为0;unit为:TimeUnit.NANOSECONDS;workQueue为:new DelayedWorkQueue() 一个按超时时间升序排序的队列
通俗:创建一个固定大小的线程池,线程池内线程存活时间无限制,线程池可以支持定时及周期性任务执行,如果所有线程均处于繁忙状态,对于新任务会进入DelayedWorkQueue队列中,这是一种按照超时时间排序的队列结构
适用:周期性执行任务的场景
五种线程池的使用场景
newSingleThreadExecutor:一个单线程的线程池,可以用于需要保证顺序执行的场景,并且只有一个线程在执行。
newFixedThreadPool:一个固定大小的线程池,可以用于已知并发压力的情况下,对线程数做限制。
newCachedThreadPool:一个可以无限扩大的线程池,比较适合处理执行时间比较小的任务。
newScheduledThreadPool:可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景。
newWorkStealingPool:一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行。
ThreadPoolExecutor线程池
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
1线程池状态
在ThreadPoolExecutor中定义了一个volatile变量,另外定义了几个static final变量表示线程池的各个状态:
volatile int runState | runState表示当前线程池的状态,一个volatile变量用来保证线程之间的可见性 |
Static final int RUNNING= 0; |
|
static final int SHUTDOWN = 1; | 如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕; |
static final int STOP = 2; | 如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务; |
static final int TERMINATED = 3; | 当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。 |
2线程池任务的执行
rivate final BlockingQueue<Runnable> workQueue | //任务缓存队列,用来存放等待执行的任务 |
private final ReentrantLock mainLock = new ReentrantLock() | //线程池的主要状态锁,对线程池状态(比如线程池大小//、runState等)的改变都要使用这个锁 |
private final HashSet<Worker> workers = new HashSet<Worker>(); | //用来存放工作集 |
private volatile long keepAliveTime; | //线程存货时间 |
private volatile boolean allowCoreThreadTimeOut | //是否允许为核心线程设置存活时间 |
private volatile int corePoolSize; | //核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列) |
private volatile int maximumPoolSize; | //线程池最大能容忍的线程数 |
private volatile int poolSize | //线程池中当前的线程数 |
private volatile RejectedExecutionHandler handler | //任务拒绝策略 |
private volatile ThreadFactory threadFactory | //线程工厂,用来创建线程 |
private int largestPoolSize | //用来记录线程池中曾经出现过的最大线程数 |
private long completedTaskCount | //用来记录已经执行完毕的任务个数 |
1)首先,要清楚corePoolSize和maximumPoolSize的含义;
2)其次,要知道Worker是用来起到什么作用的;
3)要知道任务提交给线程池之后的处理策略,这里总结一下主要有4点:
如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
如果线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。
3线程池中的线程初始化
默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。
4任务缓存队列及排队策略
在前面我们多次提到了任务缓存队列,即workQueue,它用来存放等待执行的任务
5任务拒绝策略
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
6线程池的关闭
ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:
shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务
7线程池容量的动态调整
ThreadPoolExecutor提供了动态调整线程池容量大小的方法:setCorePoolSize()和setMaximumPoolSize(),
setCorePoolSize:设置核心池大小
setMaximumPoolSize:设置线程池最大能创建的线程数目大小
当上述参数从小变大时,ThreadPoolExecutor进行线程赋值,还可能立即创建新的线程来执行任务。
8线程池任务执行流程:
当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。
线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行 当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务 当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理
当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程 当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭
如果是不采用是这个那就在队列中的线程是不可能出队列的,就是如果是的非公平的锁的话那就永远不能出队列。那可能能执行不到该线程。
volatile底层原理
JMM内存模型和采用是缓存一致性协议(总线嗅探机制)来实现变量的可见性。
synchronized底层原理及其锁的升级与降级
无锁—偏向锁(CAS+自旋(比较的对象头部))—轻量锁(CAS+自旋(比较的对象头部))—重型锁
Lock(ReentrantLock)底层原理
通过设立标志位和自旋的方式来判断能够获取对象呢的锁
ThreadLocal
它是一种为共享变量在每一个线程中创建一个副本,每一个线程都是可以访问自己的副本的变量。通过绑定线程来实现对线程副本的操作。而不影响其他线程。目的就是为了解决在多线程下访问一个变量的下数据的一致性。也是可用用来保证线程的同步。
CAS 与 AtomicInteger类型实现原理
AtomicIntger 是对 int类型的一个封装,提供原子性的访问和更新操作,其原子性操作的实现是基于CAS(compare-and-swap)技术。java对于CAS支持的操作是利用Unsafe类库的compareAndSwapInt备注(该类不是提供给用户的类Unsafe,getUnsafe的代码中限制了只有启动类加载器Bootstrap ClassLoader加载的class才能访问获取)对当前数值,进行一些运算,利用 CAS指令试图进行更新。如果当前数值未变,代表没有其他线程进行并发修改,则成功更新。否则,可能出现不同的选择,要么进行重试,要么就返回一个成功或者失败的结果。从 AtomicInteger 的内部属性可以看出,它依赖于 Unsafe 提供的一些底层能力,进行底层操作;以 volatile 的 value。CAS的实现是通常是靠一对儿指令(如“load and reserve”和“store conditional”)实现的
AQS:Semaphore,CyclicBarrier, CountDownLatch
Semaphore | 可以控制某个资源可被同时访问的个数, 通过构造函数设定一定数量的许可, 通过 acquire() 获取一个许可, 如果没有就等待, 而 release() 释放一个许可。 |
CyclicBarrier | 让一组线程达到一个同步点时被阻塞,知道最后一个线程达到同步点时候,屏障才会开们,所有被拦截的线程才会继续执行。await()函数每被调用一次,计数便会减少 1(CyclicBarrier 设置了初始值),并阻塞住当前线程。 当计数减至 0 时, 阻塞解除, 所有在此 CyclicBarrier 上面阻塞的线程开始运行。 |
CountDownLatch | CountDownLatch 允许一个或多个线程等待其他线程完成操作。主线程必须在启动其他线程后立即调用 await()方法。 这样主线程的操作就会在这个方法上阻塞, 直到其他线程完成各自的任务。 当某线程调用该 CountDownLatch 对象的await()方法时, 该线程会等待“共享锁” 可用时, 才能获取“共享锁” 进而继续运行。 |
CountDownLatch:发令枪的作用
CyclicBarrier:
notify和notifyAll的区别
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的notifyAll()方法(唤醒所有wait线程)或 notify()方法(只随机唤醒一个wait线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
Fork-jion线程
采用功能的是的多线程的时候存在上下文的切换的还有任务的切分的时候的存在时间的消耗,但是如果在任务的较多的和负复杂的时候单线程可能有就很大的时间
转载地址:http://rmch.baihongyu.com/