复习

(该文弃用)锁升级

本文主要讲解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指令重排。当我们对这个变量进行读写操作的时候,-会通过插入特定的内存屏障来禁止指令重排

    ...

并发01

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

  • 什么是进程和线程

    • 进程:是程序的一次执行过程,是系统运行程序的基本单位 系统运行一个程序,即一个进程从创建、运行到消亡的过程

      • 启动main函数则启动了一个JVM进程,main函数所在线程为进程中的一个线程,也称主线程

      • 以下为一个个的进程
        ly-20241212141934200

        • 查看java进程

          jps -l
          32 org.jetbrains.jps.cmdline.Launcher
          10084
          16244 com.Test
          17400 sun.tools.jps.Jps
          
        • 杀死进程

           taskkill /f /pid 16244
          
    • 何为线程

      • 线程,比进程更小的执行单位

      • 同类的多个线程共享进程堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈、本地方法栈,又被称为轻量级进程

      • Java天生就是多线程程序,如:

        public class MultiThread {
        	public static void main(String[] args) {
        		// 获取 Java 线程管理 MXBean
        	ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        		// 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息
        		ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        		// 遍历线程信息,仅打印线程 ID 和线程名称信息
        		for (ThreadInfo threadInfo : threadInfos) {
        			System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
        		}
        	}
        }
        //输出
        [5] Attach Listener //添加事件
        [4] Signal Dispatcher // 分发处理给 JVM 信号的线程
        [3] Finalizer //调用对象 finalize 方法的线程
        [2] Reference Handler //清除 reference 线程
        [1] main //main 线程,程序入口
        

        也就是说,一个Java程序的运行,是main线程和多个其他线程同时运行

        ...

io模型

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

https://zhuanlan.zhihu.com/p/360878783 IO多路复用讲解,这是一个与系统底层有关的知识点,需要一些操作系统调用代码才知道IO多路复用省的时间。

I/O #

何为I/O #

  • I/O(Input/Output),即输入/输出 从计算机结构的角度来解读一下I/O,根据冯诺依曼结构,计算机结构分为5大部分:运算器控制器存储器输入设备输出设备 ly-20241212141951603 其中,输入设备:键盘;输出设备:显示器 网卡、硬盘既属于输入设备也属于输出设备
  • 输入设备向计算机输入(内存)数据,输出设备接收计算机(内存)输出的数据,即I/O描述了计算机系统外部设备之间通信的过程
  • 从应用程序的角度解读I/O
    • 为了保证系统稳定性和安全性,一个进程的地址空间划分为用户空间User space内核空间Kernel space kernel 英[ˈkɜːnl]
    • 平常运行的应用程序都运行在用户空间,只有内核空间才能进行系统态级别资源有关操作—文件管理、进程通信、内存管理
    • 如果要进行IO操作,就得依赖内核空间的能力,用户空间的程序不能直接访问内核空间
    • 用户进程要想执行IO操作,必须通过系统调用来间接访问内核空间
  • 对于磁盘IO(读写文件)网络IO(网络请求和响应),从应用程序视角来看,应用程序对操作系统的内核发起IO调用(系统调用),操作系统负责的内核执行具体IO操作
    • 应用程序只是发起了IO操作调用,而具体的IO执行则由操作系统内核完成
  • 应用程序发起I/O后,经历两个步骤
    • 内核等待I/O设备准备好数据
    • 内核将数据从内核空间拷贝到用户空间

有哪些常见的IO模型 #

UNIX系统下,包括5种:同步阻塞I/O同步非阻塞I/OI/O多路复用信号驱动I/O异步I/O

Java中3中常见I/O模型 #

BIO (Blocking I/O ) #

  • 应用程序发起read调用后,会一直阻塞,直到内核把数据拷贝到用户空间 ly-20241212141951883

NIO (Non-blocking/New I/O) #

  • 对于java.nio包,提供了ChannelSelectorBuffer等抽象概念,对于高负载高并发,应使用NIO
  • NIO是I/O多路复用模型,属于同步非阻塞IO模型
    • 一般的同步非阻塞 IO 模型中,应用程序会一直发起 read 调用。
      等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的**,**直到在内核把数据拷贝到用户空间。

      相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。

      但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。

      ★★ 也就是说,【准备数据,数据就绪】是不阻塞的。而【拷贝数据】是阻塞图源:《深入拆解Tomcat & Jetty》

      ...

io设计模式

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

装饰器模式 #

​ 类图:
ly-20241212141951023

  • 装饰器,Decorator,装饰器模式可以在不改变原有对象的情况下拓展其功能

  • ★装饰器模式,通过组合替代继承来扩展原始类功能,在一些继承关系较复杂的场景(IO这一场景各种类的继承关系就比较复杂)下更加实用

  • 对于字节流,FilterInputStream(对应输入流)和FilterOutputStream(对应输出流)装饰器模式的核心,分别用于增强(继承了)InputStreamOutputStream子类对象的功能 Filter (过滤的意思),中间(Closeable)下面这两条虚线代表实现;最下面的实线代表继承 ly-20241212141951298

  • 其中BufferedInputStream(字节缓冲输入流)、DataInputStream等等都是FilterInputStream的子类,对应的BufferedOutputStream和DataOutputStream都是FilterOutputStream的子类

  • 例子,使用BufferedInputStream(字节缓冲输入流)来增强FileInputStream功能

    • BufferedInputStream源码(构造函数)

      private static int DEFAULT_BUFFER_SIZE = 8192;
      public BufferedInputStream(InputStream in) {
          this(in, DEFAULT_BUFFER_SIZE);
      }
      
      public BufferedInputStream(InputStream in, int size) {
          super(in);
          if (size <= 0) {
              throw new IllegalArgumentException("Buffer size <= 0");
          }
          buf = new byte[size];
      }
      
    • 使用

      try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("input.txt"))) {
          int content;
          long skip = bis.skip(2);
          while ((content = bis.read()) != -1) {
              System.out.print((char) content);
          }
      } catch (IOException e) {
          e.printStackTrace();
      }
      
  • ZipInputStream和ZipOutputStream还可以用来增强BufferedInputStream和BufferedOutputStream的能力

    ...

io基础

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

简介 #

  • IO,即Input/Output,输入和输出,输入就是数据输入到计算机内存;输出则是输出到外部存储(如数据库文件远程主机

  • 根据数据处理方式,又分为字节流字符流

  • 基类

    • 字节输入流 InputStream,字符输入流 Reader
    • 字节输出流 OutputStream, 字符输出流 Writer

字节流 #

  • 字节输入流 InputStream InputStream用于从源头(通常是文件)读取数据(字节信息)到内存中,java.io.InputStream抽象类是所有字节输入流的父类

    • 常用方法

      • read() :返回输入流中下一个字节的数据。返回的值介于 0 到 255 之间。如果未读取任何字节,则代码返回 -1 ,表示文件结束。
      • read(byte b[ ]) : 从输入流中读取一些字节存储到数组 b 中。如果数组 b 的长度为零,则不读取。如果没有可用字节读取,返回 -1。如果有可用字节读取,则最多读取的字节数最多等于 b.length , 返回读取的字节数。这个方法等价于 read(b, 0, b.length)
      • read(byte b[], int off, int len) :在read(byte b[ ]) 方法的基础上增加了 off 参数(偏移量)和 len 参数(要读取的最大字节数)。
      • skip(long n) :忽略输入流中的 n 个字节 ,返回实际忽略的字节数。
      • available() :返回输入流中可以读取的字节数。
      • close() :关闭输入流释放相关的系统资源。
    • Java9 新增了多个实用方法

      ...

ConcurrentHashMap源码

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

总结 #

Java7 中 ConcurrentHashMap 使用的分段锁,也就是每一个 Segment 上同时只有一个线程可以操作,每一个 Segment 都是一个类似 HashMap 数组的结构,每一个HashMap可以扩容,它的冲突会转化为链表。但是 Segment 的个数一但初始化就不能改变。

Java8 中的 ConcurrentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构也由 Java7 中的 Segment 数组 + HashEntry 数组 + 链表 进化成了 Node 数组 + 链表 / 红黑树Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。

源码 (略过) #

ConcurrentHashMap1.7 #

  • 存储结构
    • Segment数组(该数组用来加锁,每个数组元素是一个HashEntry数组(该数组可能包含链表)
    • 如图,ConcurrentHashMap由多个Segment组合,每一个Segment是一个类似HashMap的结构,每一个HashMap内部可以扩容,但是Segment个数初始化后不能改变,默认16个(即默认支持16个线程并发) ly-20241212141930985

ConcurrentHashMap1.8 #

  • 存储结构 ly-20241212141931187 可以发现 Java8 的 ConcurrentHashMap 相对于 Java7 来说变化比较大,不再是之前的 Segment 数组 + HashEntry 数组 + 链表,而是 Node 数组 + 链表 / 红黑树。当冲突链表达到一定长度时,链表会转换成红黑树。

    ...