Java 高性能编程
导读:Java作为一门解释型语言,拥有无与伦比的跨平台优势。但是同时也造成了效率上的不足。虽然Java解释器经过多次优化,但是在很多场景上的执行效率依旧赶不上原生的语言,比如C、C++。
本文所指的高性能编程,是指借助Java的多线程并发,高网络并发等特性实现Java高性能编程。
1. 多线程并发编程
多线程编程主要分析Java线程编程的原理,遇到的问题,以及解决方法和注意事项。
1.1 Java 程序运行原理分析
Java 运行程序简介:
.java 源码经过编译生成.class字节码,然后交由java解释器运行,具体内容如下:
1.1.1 JVM 运行时数据
线程独占
:每个线程都会有它独立的空间,随着线程生命周期创建和销毁。线程共享
:所有线程能访问这块内存数据,随虚拟机或者GC而创建和销毁。
1.1.2 方法区
JVM用来存储加载的类信息、常量、静态变量、编译后的代码等数据
虚拟机规范中这是一个逻辑区划。具体实现根据不同虚拟机来实现。
如:oracle的Hotspot在java7中方法区放在永久代,java8放在元数据空间,并
且通过GC机制对这个区域逬行管理
1.1.3 堆内存
堆内存还可以细分为:老年代、新生代(Eden、From Survivor、To Survivor) JVM启动时创建,存放对象的实例。垃圾回收器主要就是管理堆内存。
如果满了,就会出现OutOfMemroyError,后续在内存模型中,详细讲解。
1.1.4 虚拟机栈
虚拟机栈,每个线程都在这个空间有一个私有的空间。
线程栈由多个栈帧(Stack Frame)组成。
一个线程会执行一个或多个方法,一个方法对应一个栈帧
栈帧内容包含:局部变量表、操作数栈、动态链接、方法返回地址、附加信息等。
钱内存默认最大是1M,超出则抛出StackOverflowError
1.1.5 本地方法栈
和虚拟机栈功能类似,虚拟机栈是为虚拟机执行JAVA方法而准备的,本地方法 栈是为虚拟机使用Native本地方法而准备的。
虚拟机规范没有规定具体的实现,由不同的虚拟机厂商去实现。
HotSpot虚拟机中虚拟机栈和本地方法栈的实现式一样的。同样,超出大小以后也会抛出StackOverflowError
1.1.6 程序计数器
程序计数器(Program Counter Register)记录当前线程执行字节码的位置,存储的
是字节码指令地址,如果执行Native方法,则计数器值为空。
每个线程都在这个空间有一个私有的空间,占用内存空间很少。
CPU同一时间,只会执行一条线程中的指令。JVM多线程会轮流切换并分配CPU执行
时间的方式。为了线程切换后,需要通过程序计数器,来恢复正确的执行位置。
1.2 线程状态 —— New、Runnable、Blocked、Watting、Timed Watting、Terminated
6种线程的状态的定义:java.lang.Thread.State
- New :尚未启动的线程的线程状态
- Runnable :可运行线程的线程状态,等待CPU调度
- Blocked:线程阻塞,等待监视器锁定的线程状态
处于synchronized同步代码块的或方法中被阻塞 - Watting:等待线程的线程状态,下列不带超时的方式
Object.wait、Thread.join、LockSupport.park - Timed Watting:具有指定等待时间的等待线程的线程状态。下列带超时的方式:
Thread.sleep、Object.wait、Thread.join、LockSupport.parkNanos、LockSupport.parkUntil - Terminated:终止线程的线程状态。线程正常完成执行或者出现异常。
1.3 线程终止 ——stop、interrupt、标志位
- 不正确的线程终止 -Stop
stop:中止线程,并且清除监控器锁的信息,但是可能导致线程安全问题,JDK不建议使用
Destory:JDK 未实现该方法。
- 正确的线程终止-interrupt
如果目标线程在调用Object class的wait()、wait(long) 或wait(long,int)方法、join()、
join(long,int) 或sleep(long,int)方法时被阻塞,那么Interrupt会生效,该线程的中断状态将
被清除,抛出InterruptedException 异常。
如果目标线程是被I/O或者NIO中的Channel所阻塞,同样,丨/〇操作会被中断$者返回特殊异
常值。达到终止线程的目的。
如果以上条件都不满足,则会设置此线程的中断状态。
/**
* 示例3 - 线程stop强制性中止,破坏线程安全的示例
*/
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
StopThread thread = new ();
thread.start();
// 休眠1秒,确保i变量自增成功
Thread.sleep(1000);
// 暂停线程
thread.stop(); // 错误的终止
// thread.interrupt(); // 正确终止
while (thread.isAlive()) {
// 确保线程已经终止
} // 输出结果
thread.print();
}
}
public class StopThread extends Thread {
private int i = 0, j = 0;
@Override
public void run() {
synchronized (this) {
// 增加同步锁,确保线程安全
++i;
try {
// 休眠10秒,模拟耗时操作
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
++j;
}
}
/** * 打印i和j */
public void print() {
System.out.println("i=" + i + " j=" + j);
}
}
- 正确的线程中止-标志位
标志位是在代码逻辑中,增加一个判断,用来控制线程执行的中止。
public class Demo4 extends Thread {
public volatile static boolean flag=true;
public static void main(String[] args) throws
Interrupted Exception {
newThread(() ->{
try{
while(flag) {//判断是否运行
System.out.println("运行中
Thread.sleep(1000L);
}
} catch (InterruptedException e) {
e.printStackTraceO;
}
}).start();
//3秒之后,将状态标志改为False,代表不继续运行
Thread.sleepf(3000L);
flag=false;
Sysem.out.println("程序运行结束");
}
}
1.4 内存屏障和CPU缓存
- CPU缓存简介
为了提高程序运行的性能,现代CPU在很多方面对程序进行了优化。
例如:CPU高速缓存。尽可能地避免处理器访问主内存的时间幵销,处理器大多会利用缓
存(cache)以提局性能。
- 多级缓存简介
L1 Cache(_级缓存)是CPU第一层高速缓存,分为数据缓存和指令缓存。一般服务器CPU的L1
缓存的容量通常在32—4096KB。
L2由于L1级高速缓存容量的限制,为了再次提高CPU的运算速度,在CPU外部放置一高速存
储器,即二级缓存。
L3现在的都是内置的。而它的实际作用即是,L3缓存的应用可以进一步降低内存延迟,同时
提升大数据量计算时处理器的性能。具有较大L3缓存的处理器提供更有效的文件系统缓存行为
及较短消息和处理器队列长度。一般是多核共享一个L3缓存!CPU在读取数据时,先在L1中寻找,再从L2寻找,再从L3寻找,然后是内存,再后是外存储器。
- 缓存同步协议
多CPU读取同样的数据进行缓存,进行不同运算之后,最终写入主内存以哪个CPU为准?
在这种高速缓存回写的场景下,有一个缓存一致性协议多数cpur^商对它进行了实现。MESI协议
,它规定每条缓存有个状态位,同时定义了下面四个状修改态( Modified)
——此 cache行已被修改过(脏行),内容已不同于主存,为此cache专有;专有态( Exclusive)
——此 cache行内容同于主存,但不出现于其它cache中;共享态( Shared)
——此 cache行内容同于主存,但也出现于其它cache中;无效态( Invalid)
——此 cache行内容无效(空行)。
多处理器时,单个CPU对缓存中数据逬行了改动,需要通知绐其他CPU。
也就是意味着,CPU处理要控制自己的读写操作,还要监听其他CPU发出的通知,从而保证最终一致
。
- CPU性能优化手段——运行时指令重排
指令重排的场景:当CP∪写缓存时发现缓存区块正被其他CPU占用,为了提高CPU处理性能,可能将后面的读缓存命令优先执行。
并非随便重排,需要遵守as-if- seria语义
as- - if-seria语乂的意思指:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器, runtime和处理器都必须遵守as-if-serial语义。
也就是说:编译器和处理器不会对存在数据依赖关系的操作做重排序。
CPU缓存和指令重排带来以下两个问题:
1、CPU高速缓存下有一个问题
缓存中的数据与主内存的数据并不是实时同步的,各CPU(或CPU核心)间缓存的数据也不是
实时同步。在同一个时间点,各CPU所看到同一内存地址的数据的值可能是不一致的。
2、CPU执行指令重排序优化下有一个问题
虽然遵守了as-f- serial语义,单仅在单CPU自己执行的情况下能保证结果正确。
多核多线程中,指令逻辑无法分辨因果关联,可能岀现乱序执行,导致程序运行结果错误。
为了解决以上两个问题,处理器提供了两个内存屏障指令( Memory Barrier):
写内存屏障( Store Memory Barrier)
:在指令后插入 Store barrier,能让写入缓存中的最新
数据更新写入主内存,让其他线程可见。
强制写入主内存,这种显示调用,CPU就不会因为性能考虑而去对指令重排。读内存屏障( Load Memory Barrier)
:在指令前插入 Load Barrier,可以让高速缓存中的数
据失效,强制从新从主內存加载数据。
强制读取主內存內容,让CPU缓存与主內存保持一致,避免了缓存导致的一致性问题
1.5 线程间通信
要想实现多个线程之间的协同,如:线程执行先后顺序、获取某个线程执行的结果等等
涉及到线程之间相互通信,分为下面四类:
- 文件共享
- 网络共享
- 共享变量
- jdk提供的线程协调API
细分为:suspend/resumes、 wait/notify、 park/unpark
未完待续…