2022年11月21日 10:57 周一引用自https://github.com/Snailclimb/JavaGuide
从CPU缓存模型说起
#
redis是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题,CPU缓存则是为了解决CPU处理速度和内存处理速度不对等的问题
我们把内存看作外存的高速缓存,程序运行时把外存的数据复制到内存,由于内存的处理速度远高于外存,这样提高了处理速度
总结,CPU Cache缓存的是内存数据,用于解决CPU处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题
CPU Cache示意图:
CPU Cache通常分为三层,分别叫L1,L2,L3 Cache
工作方式: 先复制一份数据到CPUCache中,当CPU需要用的时候就可以从CPUCache中读取数据,运算完成后,将运算得到的数据,写回MainMemory中,此时,会出现内存缓存不一致的问题,例子:执行了i++,如果两个线程同时执行,假设两个线程从CPUCach中读取的i=1,两个线程做了1++运算完之后再写回MainMemory,此时i=2 而正确结果为3
CPU为了解决内存缓存不一致问题,可以通过制定缓存一致协议(比如MESI协议)或其他手段。这个缓存一致协议,指的是在 CPU 高速缓存与主内存交互的时候需要遵守的原则和规范
操作系统,通过内存模型MemoryModel定义一系列规范来解决这个问题
Java内存模型
#

指令重排序
#
另外,内存系统也会有“重排序”,但又不是真正意义上的重排序。在 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 开发者说,你不需要了解底层原理,直接使用并发相关的一些关键字和类(比如 volatile、synchronized、各种 Lock)即可开发出并发安全的程序。
...
2022年11月7日 16:04 周一转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
线程池
#
2022年11月6日 12:31 周日以下内容均转自
https://www.cnblogs.com/wuqinglong/p/9945618.html,部分疑惑参考自另一作者
https://github.com/farmerjohngit/myblog/issues/12 ,感谢原作者。
【目前还是存有部分疑虑(轻量级锁那块),可能需要详细看源码才能释疑】
概述
#
传统的synchronized为重量级锁(使用操作系统互斥量(mutex)来实现的传统锁),但是随着JavaSE1.6对synchronized优化后,部分情况下他就没有那么重了。本文介绍了JavaSE1.6为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁结构、及锁升级过程
实现同步的基础
#
Java中每个对象都可以作为锁,具体变现形式
- 对于普通同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前类的Class对象
- 对于同步方法块,锁是synchronized括号里配置的对象
一个线程试图访问同步代码块时,必须获取锁;在退出或者抛出异常时,必须释放锁
实现方式
#
JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步,但是两者的实现细节不一样
- 代码块同步:通过使用 monitorenter 和 monitorexit 指令实现的
- 同步方法: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 是保证同步代码块抛出异常时锁能得到正确的释放而存在的,这就理解了。
...
2022年11月3日 11:08 周四本文主要讲解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 字节文件查看
...
2022年10月31日 11:08 周一简介
#
无锁 => 偏向锁 => 轻量锁 => 重量锁
复习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
...
2022年10月28日 14:15 周五转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
JMM(JavaMemoryModel)
#
详见-知识点

volatile关键字
#
2022年10月26日 16:46 周三转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
2022年10月26日 14:17 周三转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
https://zhuanlan.zhihu.com/p/360878783 IO多路复用讲解,这是一个与系统底层有关的知识点,需要一些操作系统调用代码才知道IO多路复用省的时间。
I/O
#
何为I/O
#
- I/O(Input/Output),即输入/输出
从计算机结构的角度来解读一下I/O,根据冯诺依曼结构,计算机结构分为5大部分:运算器、控制器、存储器、输入设备、输出设备
其中,输入设备:键盘;输出设备:显示器
网卡、硬盘既属于输入设备也属于输出设备 - 输入设备向计算机输入(内存)数据,输出设备接收计算机(内存)输出的数据,即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/O,I/O多路复用、信号驱动I/O和异步I/O
Java中3中常见I/O模型
#
BIO (Blocking I/O )
#
- 应用程序发起read调用后,会一直阻塞,直到内核把数据拷贝到用户空间

NIO (Non-blocking/New I/O)
#
- 对于java.nio包,提供了Channel、Selector、Buffer等抽象概念,对于高负载高并发,应使用NIO
- NIO是I/O多路复用模型,属于同步非阻塞IO模型
一般的同步非阻塞 IO 模型中,应用程序会一直发起 read 调用。
等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的**,**直到在内核把数据拷贝到用户空间。
相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。
但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。
★★ 也就是说,【准备数据,数据就绪】是不阻塞的。而【拷贝数据】是阻塞的

...