【程序员翻身计划】Java高性能编程第一章-Java多线程概述

java

  1. 重点:

    • 线程安全的概念
    • 线程通信的方式与应用
    • reactor线程模型
    • 线程数量的优化
    • jdk常用命令
    • Netty框架的作用

  2. 难点

    • java运行的原理
    • 同步关键字的原理
    • AQS的抽象
    • JUC的源码
    • 网络编程的概念
    • GC机制

class文件内容

文件开头有一个0xcafebabe特殊的标志。

包含版本、访问标志、常量池、当前类、超级类、接口、字段、方法、属性

把class文件的信息存在方法区里面,有了类 根据类创建对象,存储在堆内存中,垃圾回收就是这里。这是线程共享的部分,随虚拟机或者GC创建或销毁。除了这个区域 还有线程独占空间,随线程生命周期而创建和销毁。

  • 方法区:用来存储加载的类信息、常量、静态变量、编译后的代码等数据。虚拟机规范中,这是一个逻辑区划,不同的虚拟机不同的实现。oracle的HotSpot在java7中,方法区放在永久代,java8放在元数据空间,并且通过GC机制对这个区域进行管理。

  • 堆内存:分为老年代、新生代(Eden、From Survivor、To Survivor) JVM启动时创建,存放对象的实例。垃圾回收主要管理堆内存。

  • 虚拟机栈:每个线程都有一个私有的空间。线程栈由多个栈帧(Stack Frame)组成,一个线程会执行一个或多个方法,一个方法对应一个栈帧。

    栈帧包括:局部变量表、操作数栈、动态链接、方法返回地址、附加信息。栈内存默认最大1M,超出抛出StackOverflowError

  • 本地方法栈:使用Native本地方法准备的,超出也会报StackOverflowError,不同虚拟机厂商不同的实现。

  • 程序计数器:记录当前线程执行字节码的位置,存储的是字节码指令地址,如果执行Native方法,计数器值会为空。

    CPU同一时间只会执行一条线程中的指令。JVM多线程会轮流切换并分配CPU执行时间的方式。为了线程切换后,需要通过程序计数器来恢复正确的执行位置。

接下来是源文件编译后字节码相关的东西,暂不在本次笔记中记录。【记得有本书是字节码相关的解读,立个flag,日后学习!】

线程状态

6个状态

  1. new:尚未启动的线程的状态
  2. runnable:可运行线程的线程状态,等待CPU调度
  3. blocked:线程阻塞等待监视器锁定的状态,处于synchronized同步代码块或方法中被阻塞。
  4. waiting:等待线程的状态,不带超时的方式:object.wait Thread.join LockSupport.pard
  5. timed waiting : 具有指定等待时间的等待线程的线程状态。带超时的方式:Thread.sleep Object.wait Thread.join LockSupport.parkNanos LockSupport.parkUntil
  6. Terminated:终止线程的状态,执行完毕或出现异常。

案例1

//新建  运行  终止

System.out.println("#####第一种状态新建 运行 终止");

Thread thread1 = new Thread(new Runnable() {

@Override

public void run() {

System.out.println("thread1当前状态:"+Thread.currentThread().getState().toString());

System.err.println("thread1执行了");

}

});

System.out.println("没调用start方法,thread1当前状态:"+thread1.getState().toString());

thread1.start();

Thread.sleep(2000);

System.out.println("等待两秒,thread1当前状态:"+thread1.getState().toString());

复制代码

#####第一种状态新建  运行  终止

没调用start方法,thread1当前状态:NEW

thread1当前状态:RUNNABLE

thread1执行了

等待两秒,thread1当前状态:TERMINATED

复制代码

案例2

System.out.println("######第二种 新建 运行  等待 运行  终止(sleep)");

Thread thread2 = new Thread(new Runnable() {

@Override

public void run() {

try {

Thread.sleep(1500L);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

System.out.println("thread2当前状态:"+Thread.currentThread().getState().toString());

System.err.println("thread2执行了");

}

});

Thread.sleep(2000);

System.out.println("没调用start方法,thread2当前状态:" + thread2.getState().toString());

thread2.start();

System.out.println("调用start方法,thread2当前状态:" + thread2.getState().toString());

Thread.sleep(200);

System.out.println("等待200毫秒,thread2当前状态:" + thread2.getState().toString());

Thread.sleep(3000);

System.out.println("等待3秒,thread2当前状态:" + thread2.getState().toString());

复制代码

######第二种 新建 运行  等待 运行  终止(sleep)

没调用start方法,thread2当前状态:NEW

调用start方法,thread2当前状态:RUNNABLE

等待200毫秒,thread2当前状态:TIMED_WAITING

thread2当前状态:RUNNABLE

thread2执行了

等待3秒,thread2当前状态:TERMINATED

复制代码

案例3

System.out.println("###第三种 新建  运行  阻塞  运行 终止");

Thread thread = new Thread(new Runnable() {

@Override

public void run() {

synchronized (Test.class) {

System.out.println("当前状态:"+Thread.currentThread().getState().toString());

System.out.println("执行了");

}

}

});

synchronized (Test.class) {

System.out.println("没调用start方法,当前状态:"+thread.getState().toString());

thread.start();

System.out.println("调用start方法,当前状态:"+thread.getState().toString());

Thread.sleep(200);

System.out.println("200毫秒后,当前状态:"+thread.getState().toString());

}

Thread.sleep(3000);

System.out.println("3秒后,当前状态:"+thread.getState().toString());

复制代码

###第三种 新建  运行  阻塞  运行 终止

没调用start方法,当前状态:NEW

调用start方法,当前状态:RUNNABLE

200毫秒后,当前状态:BLOCKED

当前状态:RUNNABLE

执行了

3秒后,当前状态:TERMINATED

复制代码

线程终止

  • stop()

    线程不安全,会强行终止线程的所有锁定。

  • interrupt()

    如果目标线程在调用Object class的wait join sleep方法时被阻塞,那么interrupt会生效,该线程的中断状态将被清除,抛出interruptedException异常。

    如果目标线程是被IO或者NIO中的channel阻塞,IO操作会被中断或者返回特殊异常值。达到终止的目的。

    如果以上条件都不满足,则会设置此线程的中断状态。

  • 通过状态位来判断

    public class StopThread extends Thread{

    public volatile static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {

    new Thread(()-> {

    while(flag) {

    try {

    System.out.println("运行中");

    Thread.sleep(10000);

    } catch (InterruptedException e) {

    // TODO Auto-generated catch block

    e.printStackTrace();

    }

    }

    }).start();

    Thread.sleep(3000);

    flag = false;

    System.out.println("结束");

    }

    }

    复制代码

CPU缓存及内存屏障

CPU有三级缓存,从123到内存再到硬盘。但是存在一个问题,如果多核cpu读取同样的数据进行缓存计算,最终写入主内存的是以哪个为准?

这个时候就出来了一个缓存一致性协议,单个cpu对缓存中的数据做了改动,需要通知给其他cpu。

CPU还有一个性能优化手段,运行时指令重排,把读缓存命令优先执行。

两个问题:

  1. 缓存中的数据与主内存中的数据并不是实时同步的,各个cpu间缓存的数据也不是实时同步的,在同一个时间点,各个cpu看到的同一内存地址的数据的值可能是不一致的。
  2. 多核多线程中,指令逻辑无法分辨因果关系,可能出现乱序执行。

解决办法:内存屏障

  1. 写内存屏障:在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。
  2. 读内存屏障:在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制重新从主内存中加载数据。

线程通信

要想实现多个线程之间的协同,如 线程执行先后顺序,获取某个线程执行的结果等,设计线程之间相互通信。

  1. 文件共享

  2. 网络共享

  3. 共享变量

  4. jdk提供的线程协调API

    suspend/resume、wait/notify、park/unpark

JDK中对于需要多线程协作的,提供了对应API支持,典型场景是:生产者-消费者模型(线程阻塞、线程唤醒)

suspend/resume

  1. 同步代码中使用,suspend挂起之后并不会释放锁,容易出现死锁。

  2. suspend比resume后执行

    被弃用。

wait/notify notifyAll

只能由同一对象锁的持有者线程调用,也就是写在同步块里,否则会抛出illegalMonitorStateException异常。

wait:加入该对象的等待集合中,并且放弃当前持有的对象锁。

虽然wait会自动解锁,但是对顺序有要求,如果在notify被调用之后才开始wait方法的调用,线程会永远处于WAITING状态。

//正常的wait

public void waitNotify() throws Exception {

new Thread(()->{

if(baozidian == null) {

synchronized (this) {

System.out.println("进入等待");

}

}

System.out.println("买到包子");

}).start();

Thread.sleep(3000);

baozidian = new Object();

synchronized (this) {

this.notify();

System.out.println("通知");

}

}

结果:

进入等待

买到包子

通知

复制代码

park/unpark

线程调用park则等待许可,unpark为指定线程提供许可。

不要求方法的调用顺序。但不会释放锁,所以在同步代码块中使用可能会死锁。

/** 死锁的park/unpark */

public void parkUnparkDeadLockTest() throws Exception {

// 启动线程

Thread consumerThread = new Thread(() -> {

if (baozidian == null) { // 如果没包子,则进入等待

System.out.println("1、进入等待");

// 当前线程拿到锁,然后挂起

synchronized (this) {

LockSupport.park();

}

}

System.out.println("2、买到包子,回家");

});

consumerThread.start();

// 3秒之后,生产一个包子

Thread.sleep(3000L);

baozidian = new Object();

// 争取到锁以后,再恢复consumerThread

synchronized (this) {

LockSupport.unpark(consumerThread);

}

System.out.println("3、通知消费者");

}

结果:

1、进入等待

复制代码

注意:最好不要使用if语句来判断是否进入等待状态。

官方建议应该在循环体中检查等待状态,原因是处于等待状态的线程可能会收到错误警报和伪唤醒。

伪唤醒是指线程因为更底层的原因导致的。

线程封闭

并不是所有时候 都要用到共享数据,shuju被封闭在各自的线程中,就不需要同步。

具体体现有:ThreadLocal、局部变量

ThreadLocal

是一个线程级别的变量,每个线程都有一个ThreadLocal,就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了,在并发模式下,是绝对安全的变量。

线程池

  1. 线程在java中是一个对象,更是操作系统的资源,创建销毁都需要时间。

  2. java对象占用堆内存,操作系统线程占用系统内存,根据jvm规范,一个线程默认最大栈大小是1M,线程过多会消耗很多内存。

  3. 操作系统需要频繁切换线程上下文。

    ----->线程池就是为了解决这些问题。

线程池概念

  1. 线程池管理器:创建并管理,创建、销毁线程池、添加新任务
  2. 工作线程:在没有任务时处于等待状态,可以循环执行任务
  3. 任务接口:每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等。
  4. 任务队列:存放没有处理的任务。提供一种缓冲机制。

线程池API-接口定义和实现类

类型名称描述
接口Executor最上层的接口,定义了**执行任务的方法execute**
接口ExecutorService继承了Executor接口,拓展了Callable、Future、关闭方法
接口ScheduledExecutorService继承了ExecutorService接口,增加了定时任务相关的方法
实现类ThreadPoolExecutor基础、标准的线程池实现
实现类ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,实现了
ScheduledExecutorService中相关定时任务的方法

代码示例

公共代码块:

/**

* 测试:提交15个执行时间需要三秒,看线程池的情况

* @param threadPoolExecutor

*/

public void testCommon(ThreadPoolExecutor threadPoolExecutor) throws Exception{

for (int i=0;i<15;i++){

int n = i;

threadPoolExecutor.submit(new Runnable() {

@Override

public void run() {

try {

System.out.println("开始执行:"+n);

Thread.sleep(3000l);

System.err.println("执行结束:"+n);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

});

System.out.println("提交任务成功:"+i);

}

Thread.sleep(500l);

System.out.println("当前线程池的数量:"+threadPoolExecutor.getPoolSize());

System.out.println("当前等待队列的数量:"+threadPoolExecutor.getQueue().size());

Thread.sleep(15000l);

System.out.println("当前线程池的数量:"+threadPoolExecutor.getPoolSize());

System.out.println("当前等待队列的数量:"+threadPoolExecutor.getQueue().size());

}

复制代码

测试方法1:

/**

* 1、线程池信息: 核心线程数量5,最大数量10,无界队列,超出核心线程数量的线程存活时间:5秒, 指定拒绝策略的

*

* @throws Exception

*/

public void threadPoolExecutorTest1() throws Exception{

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5,10,5, TimeUnit.SECONDS,

new LinkedBlockingQueue<Runnable>());

testCommon(threadPoolExecutor);

}

//预计的结果:线程池数量5,其他进入等待队列

复制代码

测试方法1输出结果:

提交任务成功:0

开始执行:0

提交任务成功:1

开始执行:1

提交任务成功:2

开始执行:2

提交任务成功:3

提交任务成功:4

提交任务成功:5

提交任务成功:6

提交任务成功:7

提交任务成功:8

提交任务成功:9

提交任务成功:10

提交任务成功:11

提交任务成功:12

提交任务成功:13

提交任务成功:14

开始执行:3

开始执行:4

当前线程池的数量:5

当前等待队列的数量:10

执行结束:2

执行结束:0

执行结束:4

执行结束:1

执行结束:3

开始执行:5

开始执行:6

开始执行:7

开始执行:8

开始执行:9

开始执行:10

开始执行:11

开始执行:12

开始执行:13

开始执行:14

执行结束:5

执行结束:6

执行结束:8

执行结束:7

执行结束:9

执行结束:13

执行结束:10

执行结束:14

执行结束:12

执行结束:11

当前线程池的数量:5

当前等待队列的数量:0

复制代码

这里有一个问题就是,最大线程数量设置的是10,当前线程池的数量为什么达不到最大线程数量?

这就需要对execute的过程有个了解。

测试方法2:

/**

* 2、 线程池信息: 核心线程数量5,最大数量10,队列大小3,超出核心线程数量的线程存活时间:5秒, 指定拒绝策略的

*

* @throws Exception

*/

public void threadPoolExecutorTest2() throws Exception{

// 创建一个 核心线程数量为5,最大数量为10,等待队列最大是3 的线程池,也就是最大容纳13个任务。

// 如果不指定拒绝策略,默认的策略是抛出RejectedExecutionException异常,java.util.concurrent.ThreadPoolExecutor.AbortPolicy

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,

new LinkedBlockingQueue<Runnable>(3), new RejectedExecutionHandler() {

@Override

public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {

System.out.println("任务决绝执行。");

}

});

testCommon(threadPoolExecutor);

}

//执行预期结果:

//线程池数量5,3个进入等待,这时候核心线程数量和队列都满了,会加开5个任务线程(注意,5秒后没任务执行会销毁),因为最大线程是10

//最大10+等待队列3 总共13,剩下两个拒绝执行

复制代码

测试方法3:Executors.newFixedThreadPool(int nThreads)

对于无界队列,最大线程数量实际上是不起作用的。

    /**

* 3、 线程池信息: 核心线程数量5,最大数量5,无界队列,超出核心线程数量的线程存活时间:5秒

*

* @throws Exception

*/

private void threadPoolExecutorTest3() throws Exception {

// 和Executors.newFixedThreadPool(int nThreads)一样的

ThreadPoolExecutor threadPoolExecutor1 = (ThreadPoolExecutor) Executors.newFixedThreadPool(5);

// ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,

// new LinkedBlockingQueue<Runnable>());

testCommon(threadPoolExecutor1);

// 预计结:线程池线程数量为:5,超出数量的任务,其他的进入队列中等待被执行

}

复制代码

Executors.newFixedThreadPool()的内部实现实际上就是 注释掉的部分:

    public static ExecutorService newFixedThreadPool(int nThreads) {

return new ThreadPoolExecutor(nThreads, nThreads,

0L, TimeUnit.MILLISECONDS,

new LinkedBlockingQueue<Runnable>());

}

复制代码

测试方法4:Executors.newCachedThreadPool()

此种方法适用于不可预估数量的情况

/**

* 4、 线程池信息:

* 核心线程数量0,最大数量Integer.MAX_VALUE,SynchronousQueue队列,超出核心线程数量的线程存活时间:60秒

*

* @throws Exception

*/

private void threadPoolExecutorTest4() throws Exception {

// SynchronousQueue,实际上它不是一个真正的队列,因为它不会为队列中元素维护存储空间。与其他队列不同的是,它维护一组线程,这些线程在等待着把元素加入或移出队列。

// 在使用SynchronousQueue作为工作队列的前提下,客户端代码向线程池提交任务时,

// 而线程池中又没有空闲的线程能够从SynchronousQueue队列实例中取一个任务,

// 那么相应的offer方法调用就会失败(即任务没有被存入工作队列)。

// 此时,ThreadPoolExecutor会新建一个新的工作者线程用于对这个入队列失败的任务进行处理(假设此时线程池的大小还未达到其最大线程池大小maximumPoolSize)。

// 和Executors.newCachedThreadPool()一样的

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,

new SynchronousQueue<Runnable>());

testCommon(threadPoolExecutor);

// 预计结果:

// 1、 线程池线程数量为:15,超出数量的任务,其他的进入队列中等待被执行

// 2、 所有任务执行结束,60秒后,如果无任务可执行,所有线程全部被销毁,池的大小恢复为0

Thread.sleep(60000L);

System.out.println("60秒后,再看线程池中的数量:" + threadPoolExecutor.getPoolSize());

}

复制代码

 测试方法4输出结果:

提交任务成功:0

提交任务成功:1

提交任务成功:2

提交任务成功:3

提交任务成功:4

提交任务成功:5

提交任务成功:6

提交任务成功:7

提交任务成功:8

提交任务成功:9

提交任务成功:10

提交任务成功:11

提交任务成功:12

提交任务成功:13

提交任务成功:14

开始执行:3

开始执行:2

开始执行:6

开始执行:7

开始执行:10

开始执行:11

开始执行:0

开始执行:1

开始执行:5

开始执行:4

开始执行:8

开始执行:9

开始执行:12

开始执行:13

开始执行:14

当前线程池的数量:15

当前等待队列的数量:0

执行结束:3

执行结束:2

执行结束:6

执行结束:7

执行结束:10

执行结束:9

执行结束:0

执行结束:1

执行结束:5

执行结束:4

执行结束:14

执行结束:8

执行结束:11

执行结束:12

执行结束:13

当前线程池的数量:15

当前等待队列的数量:0

60秒后,再看线程池中的数量:0

复制代码

测试方法5:一次性定时任务

/**

* 5、 定时执行线程池信息:3秒后执行,一次性任务,到点就执行 <br/>

* 核心线程数量5,最大数量Integer.MAX_VALUE,DelayedWorkQueue延时队列,超出核心线程数量的线程存活时间:0秒

*

* @throws Exception

*/

public void threadPoolExecutorTest5() throws Exception{

//Executors.newScheduledThreadPool() 一样的

ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(5);

scheduledThreadPoolExecutor.schedule(new Runnable() {

@Override

public void run() {

System.out.println("任务被执行,现在时间:"+ DateUtil.now());

}

},3000,TimeUnit.MILLISECONDS);

System.out.println("定时任务,提交成功,时间是:"+DateUtil.now());

}

复制代码

测试方法5输出结果:

定时任务,提交成功,时间是:2021-11-27 13:42:43

任务被执行,现在时间:2021-11-27 13:42:46

复制代码

测试方法6:周期定时任务

/**

* 6、 定时执行线程池信息:线程固定数量5 ,<br/>

* 核心线程数量5,最大数量Integer.MAX_VALUE,DelayedWorkQueue延时队列,超出核心线程数量的线程存活时间:0秒

*

* @throws Exception

*/

public void threadPoolExecutorTest6() throws Exception{

ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(5);

//第一种方式:scheduleAtFixedRate,如果执行时间超过了周期时间

//执行完毕后,立即执行,不考虑延迟时间

scheduledThreadPoolExecutor.scheduleAtFixedRate(new Runnable() {

@Override

public void run() {

try {

Thread.sleep(3000l);

}catch (InterruptedException e){

e.printStackTrace();

}

System.out.println("任务1被执行,现在时间:"+DateUtil.now());

}

},2000,1000,TimeUnit.MILLISECONDS);

//第二种方式,scheduleWithFixedDelay

//如果执行时间超过了周期时间,执行完毕后,加上延迟时间后再执行

scheduledThreadPoolExecutor.scheduleWithFixedDelay(new Runnable() {

@Override

public void run() {

try {

Thread.sleep(3000l);

}catch (InterruptedException e){

e.printStackTrace();

}

System.out.println("任务2被执行,现在时间:"+DateUtil.now());

}

},2000,1000,TimeUnit.MILLISECONDS);

}

复制代码

测试方法6输出结果:

//可以看出,任务1每隔3秒执行一次,任务2每隔4秒执行一次

任务1被执行,现在时间:2021-11-27 14:08:45

任务2被执行,现在时间:2021-11-27 14:08:45

任务1被执行,现在时间:2021-11-27 14:08:48

任务2被执行,现在时间:2021-11-27 14:08:49

任务1被执行,现在时间:2021-11-27 14:08:51

任务2被执行,现在时间:2021-11-27 14:08:53

任务1被执行,现在时间:2021-11-27 14:08:54

任务1被执行,现在时间:2021-11-27 14:08:57

任务2被执行,现在时间:2021-11-27 14:08:57

复制代码

终止线程的两种方式

scheduledThreadPoolExecutor.shutdown();

//第二种会返回尚未执行的任务

List<Runnable> runnableList = scheduledThreadPoolExecutor.shutdownNow();

复制代码

本文同步公众号【刘墨泽】,欢迎大家关注聊天!

以上是 【程序员翻身计划】Java高性能编程第一章-Java多线程概述 的全部内容, 来源链接: utcz.com/z/394679.html

回到顶部