学习

线程池最佳实践

转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!

线程池知识回顾 #

1. 为什么要使用线程池 #

  • 池化技术的思想,主要是为了减少每次获取资源(线程资源)的消耗,提高对资源的利用率
  • 线程池提供了一种限制管理资源(包括执行一个任务)的方法,每个线程池还维护一些基本统计信息,例如已完成任务的数量

好处:

  1. 降低资源消耗
  2. 提高响应速度
  3. 提高线程的可管理性

2. 线程池在实际项目的使用场景 #

线程池一般用于执行多个不相关联的耗时任务,没有多线程的情况下,任务顺序执行,使用了线程池的话可让多个不相关联的任务同时执行

ly-20241212141942375

3. 如何使用线程池 #

一般是通过 ThreadPoolExecutor 的构造函数来创建线程池,然后提交任务给线程池执行就可以了。构造函数如下:

  /**
     * 用给定的初始参数创建一个新的ThreadPoolExecutor。
     */
    public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                              int maximumPoolSize,//线程池的最大线程数
                              long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                              TimeUnit unit,//时间单位
                              BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
                              ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
                              RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
                               ) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

使用代码:

...

java线程池详解

转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!

一 使用线程池的好处 #

  • 池化技术:减少每次获取资源的消耗,提高对资源的利用率
  • 线程池提供一种限制管理资源(包括执行一个任务)的方式,每个线程池还维护一些基本统计信息,例如已完成任务的数量
  • 线程池的好处
    • 降低资源消耗(重复利用,降低线程创建和销毁造成的消耗)
    • 提高响应速度(任务到达直接执行,无需等待线程创建
    • 提高线程可管理性避免无休止创建,使用线程池统一分配调优监控

二 Executor框架 #

Java5之后,通过Executor启动线程,比使用Thread的start方法更好,更易于管理效率高,还能有助于避免this逃逸的问题

this逃逸,指的是构造函数返回之前其他线程就持有该对象的引用,会导致调用尚未构造完全的对象
例子:

public class ThisEscape { 
  public ThisEscape() { 
    new Thread(new EscapeRunnable()).start(); 
    // ... 
  } 
    
  private class EscapeRunnable implements Runnable { 
    @Override
    public void run() { 
      // 通过ThisEscape.this就可以引用外围类对象, 但是此时外围类对象可能还没有构造完成, 即发生了外围类的this引用的逃逸 
    } 
  } 
}

处理办法 //不要在构造函数中运行线程

public class ThisEscape { 
  private Thread t; 
  public ThisEscape() { 
    t = new Thread(new EscapeRunnable()); 
    // ... 
  } 
    
  public void init() { 
    //也就是说对象没有构造完成前,不要调用ThisEscape.this即可
    t.start(); 
  } 
    
  private class EscapeRunnable implements Runnable { 
    @Override
    public void run() { 
      // 通过ThisEscape.this就可以引用外围类对象, 此时可以保证外围类对象已经构造完成 
    } 
  } 
}

Executor框架不仅包括线程池的管理,提供线程工厂队列以及拒绝策略

...

java内存模型

引用自https://github.com/Snailclimb/JavaGuide

从CPU缓存模型说起 #

  • redis是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题,CPU缓存则是为了解决CPU处理速度和内存处理速度不对等的问题

    我们把内存看作外存的高速缓存,程序运行时把外存的数据复制到内存,由于内存的处理速度远高于外存,这样提高了处理速度

  • 总结,CPU Cache缓存的是内存数据,用于解决CPU处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题 CPU Cache示意图:

    ly-20241212141939038 CPU Cache通常分为三层,分别叫L1,L2,L3 Cache 工作方式: 先复制一份数据到CPUCache中,当CPU需要用的时候就可以从CPUCache中读取数据,运算完成后,将运算得到的数据,写回MainMemory中,此时,会出现内存缓存不一致的问题,例子:执行了i++,如果两个线程同时执行,假设两个线程从CPUCach中读取的i=1,两个线程做了1++运算完之后再写回MainMemory,此时i=2 而正确结果为3

  • CPU为了解决内存缓存不一致问题,可以通过制定缓存一致协议(比如MESI协议)或其他手段。这个缓存一致协议,指的是在 CPU 高速缓存与主内存交互的时候需要遵守的原则和规范 ly-20241212141939314 操作系统,通过内存模型MemoryModel定义一系列规范来解决这个问题

Java内存模型 #

ly-20241212141939466

指令重排序 #

  • 什么是指令重排序? 简单来说就是系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行

  • 指令重排有下面2种

    • 编译器优化重排:编译器(包括 JVM、JIT 编译器等)在不改变单线程程序语义的前提下,重新安排语句的执行顺序。
    • 指令并行重排:现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序

另外,内存系统也会有“重排序”,但又不是真正意义上的重排序。在 JMM 里表现为主存和本地内存的内容可能不一致,进而导致程序在多线程下执行可能出现问题

即Java源代码会经历 编译器优化重排—>指令并行重排—>内存系统重排,最终编程操作系统可执行的指令序列

极其重要★:指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致,所以在多线程下指令重排可能导致一些问题

编译器和处理器的指令重排序的处理方式不一样。对于编译器,通过禁止特定类型的编译器重排序的方式来禁止重排序。对于处理器,通过插入内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)的方式来禁止特定类型的处理器重排序。指令并行重排和内存系统重排都属于是处理器级别的指令重排序

内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种 CPU 指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障指令执行的有序性。另外,为了达到屏障的效果,它也会使处理器写入、读取值之前,将主内存的值写入高速缓存清空无效队列,从而保障变量的可见性。

JMM(JavaMemoryMode) #

什么是 JMM?为什么需要 JMM? #

一般来说,编程语言也可以直接复用操作系统层面的内存模型。不过,不同的操作系统内存模型不同。如果直接复用操作系统层面的内存模型,就可能会导致同样一套代码换了一个操作系统就无法执行了Java 语言是跨平台的,它需要自己提供一套内存模型以屏蔽系统差异。

实际上,对于Java来说,可以把JMM看作是Java定义的并发编程相关的一组规范,除了抽象了线程和主内存之间的关系之外,还规定了从Java源代码到CPU可执行指令的转化过程要遵守哪些和并发相关的原则和规范,主要目的是为了简化多线程编程增强程序可移植性

为什么要遵守这些并发相关的原则和规范呢?因为在并发编程下,CPU多级缓存和指令重排这类设计会导致程序运行出问题,比如指令重排,为此JMM抽象了happens-before原则

JMM 说白了就是定义了一些规范来解决这些问题,开发者可以利用这些规范更方便地开发多线程程序。对于 Java 开发者说,你不需要了解底层原理直接使用并发相关的一些关键字(比如 volatilesynchronized、各种 Lock)即可开发出并发安全的程序。

...

并发03

转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!

线程池 #

  • 为什么要使用线程池

    • 池化技术:线程池数据库连接池Http连接池
    • 池化技术思想意义:为了减少每次获取资源消耗,提高对资源的利用率
    • 线程池提供了限制管理 资源(包括执行一个任务)的方式
    • 每个线程池还维护基本统计信息,例如已完成任务的数量
    • 好处:
      1. 降低资源消耗 重复利用已创建线程降低线程创建销毁造成的消耗
      2. 提高响应速度 任务到达时,任务可以不需等到线程创建就能继续执行
      3. 提高线程的可管理性 线程是稀缺资源,如果无限制创建,不仅消耗系统资源,还会降低系统的稳定性,使用线程池统一管理分配调优监控
  • 实现Runnable接口和Callable接口的区别

    //Callable的用法 
    public class TestLy {
    
        //如果加上volatile,就能保证可见性,线程1 才能停止
          boolean stop = false;//对象属性
    
        public static void main(String[] args) throws InterruptedException, ExecutionException {
            FutureTask<String> futureTask=new FutureTask<>(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    System.out.println("等3s再把结果给你");
                    TimeUnit.SECONDS.sleep(3);
                    return "hello world";
                }
            });
            new Thread(futureTask).start();
            String s = futureTask.get();
            System.out.println("3s后获取到了结果"+s);
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("abc");
                }
            }).start();
        }
    }
    /*
    等3s再把结果给你
    3s后获取到了结果hello world
    abc
    */
    
    • Runnable接口不会返回结果或抛出检查异常,Callable接口可以

      ...

锁升级

以下内容均转自 https://www.cnblogs.com/wuqinglong/p/9945618.html,部分疑惑参考自另一作者 https://github.com/farmerjohngit/myblog/issues/12 ,感谢原作者。

目前还是存有部分疑虑(轻量级锁那块),可能需要详细看源码才能释疑】

概述 #

传统的synchronized为重量级锁(使用操作系统互斥量(mutex)来实现的传统锁),但是随着JavaSE1.6对synchronized优化后,部分情况下他就没有那么重了。本文介绍了JavaSE1.6为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁轻量级锁,以及锁结构、及锁升级过程

实现同步的基础 #

Java中每个对象都可以作为锁,具体变现形式

  1. 对于普通同步方法,锁是当前实例对象
  2. 对于静态同步方法,锁是当前类的Class对象
  3. 对于同步方法块,锁是synchronized括号里配置的对象

一个线程试图访问同步代码块时,必须获取锁;在退出或者抛出异常时,必须释放锁

实现方式 #

JVM 基于进入和退出 Monitor 对象来实现方法同步代码块同步,但是两者的实现细节不一样

  1. 代码块同步:通过使用 monitorentermonitorexit 指令实现的
  2. 同步方法:ACC_SYNCHRONIZED 修饰

monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 指令是在编译后插入到同步代码块的结束处或异常处

对于同步方法,进入方法前添加一个 monitorenter 指令,退出方法后添加一个 monitorexit 指令。

demo:

public class Demo {

    public void f1() {
        synchronized (Demo.class) {
            System.out.println("Hello World.");
        }
    }

    public synchronized void f2() {
        System.out.println("Hello World.");
    }

}

编译之后的字节码(使用 javap )

public void f1();
  descriptor: ()V
  flags: ACC_PUBLIC
  Code:
    stack=2, locals=3, args_size=1
       0: ldc           #2                  // class me/snail/base/Demo
       2: dup
       3: astore_1
       4: monitorenter
       5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       8: ldc           #4                  // String Hello World.
      10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      13: aload_1
      14: monitorexit
      15: goto          23
      18: astore_2
      19: aload_1
      20: monitorexit
      21: aload_2
      22: athrow
      23: return
    Exception table:
       from    to  target type
           5    15    18   any
          18    21    18   any
    LineNumberTable:
      line 6: 0
      line 7: 5
      line 8: 13
      line 9: 23
    StackMapTable: number_of_entries = 2
      frame_type = 255 /* full_frame */
        offset_delta = 18
        locals = [ class me/snail/base/Demo, class java/lang/Object ]
        stack = [ class java/lang/Throwable ]
      frame_type = 250 /* chop */
        offset_delta = 4

public synchronized void f2();
  descriptor: ()V
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED
  Code:
    stack=2, locals=1, args_size=1
       0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #4                  // String Hello World.
       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
    LineNumberTable:
      line 12: 0
      line 13: 8

先说 f1() 方法,发现其中一个 monitorenter 对应了两个 monitorexit,这是不对的。但是仔细看 #15: goto 语句,直接跳转到了 #23: return 处,再看 #22: athrow 语句发现,原来第二个 monitorexit保证同步代码块抛出异常时锁能得到正确的释放而存在的,这就理解了。

...

(该文弃用)锁升级

本文主要讲解synchronized原理和偏向锁、轻量级锁、重量级锁的升级过程,基本都转自

https://blog.csdn.net/MariaOzawa/article/details/107665689 原作者: MariaOzawa

简介 #

  • 为什么需要锁
    并发编程中,多个线程访问同一共享资源时,必须考虑如何维护数据的原子性
  • 历史
    • JDK1.5之前,Java依靠Synchronized关键字实现锁功能,Synchronized是Jvm实现的内置锁,锁的获取与释放由JVM隐式实现
    • JDK1.5,并发包新增Lock接口实现锁功能,提供同步功能,使用时显式获取和释放锁
  • 区别
    • Lock同步锁基于Java实现,Synchronized基于底层操作系统的MutexLock实现 /ˈmjuːtɛks/ ,每次获取和释放锁都会带来用户态和内核态的切换,从而增加系统性能开销,性能糟糕,又称重量级锁
    • JDK1.6之后,对Synchronized同步锁做了充分优化

Synchronized同步锁实现原理 #

  • Synchronized实现同步锁的两种方式:修饰方法;修饰方法块

      // 关键字在实例方法上,锁为当前实例
      public synchronized void method1() {
          // code
      }
    
      // 关键字在代码块上,锁为括号里面的对象
      public void method2() {
          Object o = new Object();
          synchronized (o) {
              // code
          }
      }
    

    这里使用编译–及javap 打印字节文件

    javac -encoding UTF-8 SyncTest.java  //先运行编译class文件命令
    
    javap -v SyncTest.class //再通过javap打印出字节文件
    

    结果如下,Synchronized修饰代码块时,由monitorenter和monitorexist指令实现同步。进入monitorenter指令后线程持有Monitor对象;退出monitorenter指令后,线程释放该Monitor对象

      public void method2();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=4, args_size=1
             0: new           #2                  
             3: dup
             4: invokespecial #1                  
             7: astore_1
             8: aload_1
             9: dup
            10: astore_2
            11: monitorenter //monitorenter 指令
            12: aload_2
            13: monitorexit  //monitorexit  指令
            14: goto          22
            17: astore_3
            18: aload_2
            19: monitorexit
            20: aload_3
            21: athrow
            22: return
          Exception table:
             from    to  target type
                12    14    17   any
                17    20    17   any
          LineNumberTable:
            line 18: 0
            line 19: 8
            line 21: 12
            line 22: 22
          StackMapTable: number_of_entries = 2
            frame_type = 255 /* full_frame */
              offset_delta = 17
              locals = [ class com/demo/io/SyncTest, class java/lang/Object, class java/lang/Object ]
              stack = [ class java/lang/Throwable ]
            frame_type = 250 /* chop */
              offset_delta = 4
    

    如果Synchronized修饰同步方法,代替monitorenter和monitorexit的是 ACC_SYNCHRONIZED标志,即:JVM使用该访问标志区分方法是否为同步方法。方法调用时,调用指令检查是否设置ACC_SYNCHRONIZED标志,如有,则执行线程先持有该Monitor对象,再执行该方法;运行期间其他线程无法获取到该Monitor对象;方法执行完成后,释放该Monitor对象 javap -v xx.class 字节文件查看

    ...

(该文弃用)锁升级

简介 #

无锁 => 偏向锁 => 轻量锁 => 重量锁

复习Class类锁和实例对象锁,说明Class类锁和实例对象锁不是同一把锁,互相不影响

public static void main(String[] args) throws InterruptedException { 
        Object object=new Object();
        new Thread(()->{
           synchronized (Customer.class){
               System.out.println(Thread.currentThread().getName()+"Object.class类锁");
               try {
                   TimeUnit.SECONDS.sleep(5);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
            System.out.println(Thread.currentThread().getName()+"结束并释放锁");
        },"线程1").start();
        //保证线程1已经获得类锁
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            synchronized (object){
                System.out.println(Thread.currentThread().getName()+"获得object实例对象锁");
            }
            System.out.println(Thread.currentThread().getName()+"结束并释放锁");
        },"线程2").start();

    }

/* 输出
线程1Object.class类锁
线程2获得object实例对象锁
线程2结束并释放锁
线程1结束并释放锁
*/

总结图 , 00 , 01 , 10 ,没有11

...

对象内存布局和对象头

对象布局 #

  • heapwhere): new (eden ,s0 ,s1) ,old, metaspace

  • 对象的构成元素(what) HotSpot虚拟机里,对象在堆内存中的存储布局分为三个部分 ly-20241212141948626

    • 对象头(Header)
      • 对象标记 MarkWord
      • 类元信息(类型指针 Class Pointer,指向方法区的地址)
      • 对象头多大 length(数组才有)
    • 实例数据(Instance Data)
    • 对其填充(Padding,保证整个对象大小,是8个字节的倍数)

对象头 #

  • 对象标记

    1. Object o= new Object(); //new一个对象,占内存多少
    2. o.hashCode() //hashCode存在对象哪个地方
    3. synchronized(o){ } //对象被锁了多少次(可重入锁)
    4. System.gc(); //躲过了几次gc(次数)

    上面这些,哈希码gc标记gc次数同步锁标记偏向锁持有者,都保存在对象标记里面 ly-20241212141948895

    1. 如果在64位系统中,对象头中,**mark word(对象标记)**占用8个字节(64位);**class pointer(类元信息)**占用8个字节,总共16字节(忽略压缩指针)
    2. 无锁的时候, ly-20241212141949076
  • 类型指针 注意下图,指向方法区中(模板)的地址 ly-20241212141949249

实例数据和对齐填充 #

  • 实例数据

  • 用来存放类的属性(Filed)数据信息,包括父类的属性信息

  • 对齐填充

  • 填充到长度为8字节,因为虚拟机要求对象起始地址必须是8字节的整数倍(对齐填充不一定存在)

  • 示例

    class Customer{
        int id;//4字节
        boolean flag=false; //1字节
    }
    //Customer customer=new Customer();
    //该对象大小:对象头(对象标记8+类型指针8)+实例数据(4+1)=21字节 ===> 为了对齐填充,则为24字节
    

源码查看 #

ly-20241212141949397

...

并发02

转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!

JMM(JavaMemoryModel) #

详见-知识点 Java内存模型

volatile关键字 #

  • 保证变量可见性

    • 使用volatile关键字保证变量可见性,如果将变量声明为volatile则指示JVM该变量是共享且不稳定的,每次使用它都到主存中读取
      ly-20241212141935463

      volatile关键字并非Java语言特有,在C语言里也有,它最原始的意义就是禁用CPU缓存

    • volatile关键字只能保证数据可见性不能保证数据原子性synchronized关键字两者都能保证

    • 不可见的例子

      package com.concurrent; 
      import java.util.concurrent.TimeUnit;
      
      public class TestLy {
      
          //如果加上volatile,就能保证可见性,线程1 才能停止
            boolean stop = false;//对象属性
      
          public static void main(String[] args) throws InterruptedException {
             TestLy atomicTest = new TestLy();
              new Thread(() -> {
                  while (!atomicTest.stop) {
                      //这里不能加System.out.println ,因为这个方法内部用了synchronized修饰,会导致获取主内存的值,
                      //就没法展示效果了
                      /*System.out.println("1还没有停止");*/
                  }
                  System.out.println(Thread.currentThread().getName()+"停止了");
              },"线程1").start();
      
              new Thread(() -> {
                  try {
                      TimeUnit.SECONDS.sleep(1);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  atomicTest.stop= true;
                  System.out.println(Thread.currentThread().getName()+"让线程1停止");
              },"线程2").start();
              while (true){}
          }
      
      
      }
      
  • 如何禁止指令重排 使用volatile关键字,除了可以保证变量的可见性,还能防止JVM指令重排。当我们对这个变量进行读写操作的时候,-会通过插入特定的内存屏障来禁止指令重排

    ...