Java 高性能编程

导读:Java作为一门解释型语言,拥有无与伦比的跨平台优势。但是同时也造成了效率上的不足。虽然Java解释器经过多次优化,但是在很多场景上的执行效率依旧赶不上原生的语言,比如C、C++。
本文所指的高性能编程,是指借助Java的多线程并发,高网络并发等特性实现Java高性能编程。

1. 多线程并发编程

多线程编程主要分析Java线程编程的原理,遇到的问题,以及解决方法和注意事项。

1.1 Java 程序运行原理分析

Java 运行程序简介:

.java 源码经过编译生成.class字节码,然后交由java解释器运行,具体内容如下:

java 高性能集合 java高性能编程_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)以提局性能。

java 高性能集合 java高性能编程_Java_02

  • 多级缓存简介

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 线程间通信

要想实现多个线程之间的协同,如:线程执行先后顺序、获取某个线程执行的结果等等
涉及到线程之间相互通信,分为下面四类:

  1. 文件共享
  2. 网络共享
  3. 共享变量
  4. jdk提供的线程协调API
    细分为:suspend/resumes、 wait/notify、 park/unpark

未完待续…