知识脉络
并发问题的根源:保证线程安全的三要素
- 可见性:CPU缓存引起
一个线程对共享变量的修改,另一个线程能立即看见。
现象:一个线程1修改一个变量,会先把变量放到cpu缓存中,但这时,另一个线程(对应的cpu不相同!so 缓存不同)需要读取这个变量,从内存中读取出来的结果不一样。
解决方案:1 JMM(java内存模型)提供来volatile 2 synchronized,lock
volatile: 当一个共享变量被volatile修饰时,保证修改的值立即被更新到主存。
synchronized, lock的方法:锁释放前,进程会将对变量的修改更新到主存。
- 有序性:重排序引起
程序执行的顺序按照代码的先后顺序执行。(允许jvm对指令重新排序)
解决方案:as-if-seria(线程内):指令重排序,但不会对存在数据以来的操作重新排序,保证排序后结果不变。
happends-before(多线程)
volatile, synchronized, lock都可以用来保证有序性。
- 原子性:分时复用引起
一个操作要么都完成,要么都不完成。现在只有基本的读取和赋值是原子操作,因此需要结合锁。
方案:悲观锁:synchronized关键字,jvm级别锁。
乐观锁:自旋锁+CAS。无法保证可见性,一般配合volatile使用。
JMM java内存模型
本质:规范了JVM如何按需禁用缓存 和 如何编译优化
方法:volatile, synchronized, final ; happends-before规则。
volatile, synchronized, final
volatile
使用条件:
- 只有状态独立与程序内其他内容才能使用volatile.
- 变量没有包含在具有其他变量的不变式中。
- 状态独立于程序内其他内容
- volatile实现可见性
基于内存屏障实现的
添加了vilatile关键字的变量,在修改时,jvm产生一个lock前缀的指令发送给该cpu1,这个指令会将当前处理器缓存行的数据写回到内存。内存被写会后,其他的处理器中的缓存也会失效,因此会从内存中重新读取新的数据。(缓存一致性协议:处理器在总线上传播的数据检查自己的缓存是否过期,过期则回重新从内存读取新的数据),
lock前缀的指令:锁定的是缓存。
- volatile实现有序
基于 happens-before实现
happens-before规则:对volatile变量的写必须 发生在 任意后续中对volatile变量的读。
(提供一个内存屏障)
volatile禁止重排的方法: StoreStore, StoreLoad, LoadLoad, LoadStore
-
volatile写操作:前面有一个StoreStore防止上面的写操作和下面的volatile写重新排序。后面有一个StoreLoad:防止后面的写操作和上面的volatile写重排序
-
volatile读操作:后面有一个LoadLoad防止后面的读和上面的volatile读重排序。后面还有一个LoadStore防止后面的写和上面的volatile读重排序
- volatile不能实现原子性
synchronized
tips: 方法正常执行or抛出异常,都会释放锁
1 使用方法
- this方法 或者 手动指定锁
// this方法
public void run(){
// 代码块同步 this, 两个线程运行run,使用的同一个锁,线程1等待0完成后再执行
synchronized(this){
try{
Thread.sleep(3000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
Thread t1 = new Thread(instence); // instence是上文中声明的构造器
Thread t2 = new Thread(instence);
t1.start();
t2.start();
// 手动设置锁
Object block1 = new Object();
Object block2 = new Object();
public void run(){
synchronized(block1){
}
synchronized(block2){
}
}
public static void main(String[] args){
Thread t1 = new Thread(instence);
Thread t2 = new Thread(instence);
t1.start();
t2.start();
}
当两个thread分别传入不同的instence时候:
Thread t1 = new Thread(instence1);
以及Thread t1 = new Thread(instence2);
锁用在普通方法,默认锁就是this,无法实现锁的功能(因为是两个锁);修饰静态方法,默认的锁就是当前在的Class类,这就是一个锁,才能实现不同进程中这个方法的同步。
public void run(){
method();
}
public synchronized void method(){
}
public void run(){
method();
}
2 synchronized原理
保持monitor计数器。当计数器为0,说明没有人获得锁,线程就会申请得到锁,然后将计数器+1。每当有新进程想要获得锁,计数器就加1。释放则-1
- synchronized实现可见性的原理:
public class MonitorDemo{
public synchronized void writer(){
}
public synchronized void reader(){
}
}
3 synchronized的优化
之前是基于mutex lock实现的,涉及了用户态和内核态的切换,代价很高。因此需要被优化。
- 锁粗化:减少不必要的连在一起的锁。
- 锁消除:
- 轻量级锁:CAS原子命令代替悲观锁
- 适应性自旋:CAS失败后进入忙则等待,之后再次尝试,尝试失败的次数超过阈值则调用阻塞。避免阻塞, 减少了从用户态切换到内核台的消费。
一些问题
举例说明线程和进程
在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,main函数再的线程就是进程的主线程
多线程带来的问题
内存泄露、死锁、线程不安全
死锁:
条件:互斥、资源不可剥夺、请求并保持(一次性申请完所有资源)、循环等待(按照什么顺序申请资源,就按照什么顺序释放资源)
sleep(), wait()方法的区别
sleep(): 没有释放锁
wait():释放了锁
wait经常用于线程之间的交互、通信。调用之后,线程不会自动苏醒,需要别的线程调用notify()
sleep通常用于暂停执行。调用之后,线程会自动苏醒。
通信方式
线程共享进程的堆和方法区资源。
但每个线程有自己的程序计数器、虚拟机栈和本地方法栈
-
堆:存放新创建的对象
-
方法区:已被加载的类信息,常量、静态变量、即时编译器编译后的代码
-
虚拟机栈:为虚拟机执行java方法而服务,存放局部变量表、操作数栈、常量池等信息
-
本地方法栈:为虚拟机使用到的native方法服务。
进程通信:
- socket通信(基于tcp/udp的通信方式,不同主机之间通过网络进行通信)
- 信号量:用在解决进程之间的同步。
- 消息队列:存放在内核里,只有内核重启(操作系统重启)或者显示地删除消息队列时,消息队列才被真正的删除
- 共享内存:依赖互斥锁和信号量等等。
- 管道(具有亲缘关系的父子进程间or兄弟进程之间的通信)
线程间通信:
-互斥锁,读写锁:mutex, 比如java中的synchronized关键词和各种lock
-信号量:
-事件Event: Wait/Notify: 通过通知操作的方式保持多线程同步。方便的实现多线程优先级的比较操作。