JMM
java虚拟机规范中试图定义一种java内存模型(JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都可能达到一致的内存访问效果。在此之前,C语言/C++直接使用物理硬件和操作系统的内存模型,所以就会出现在一套平台上并发访问正常,但是在另一套平台上却有问题,平台兼容性相对较差。
主内存和工作内存之间的交互
Java内存模型定义了8种操作来完成关于主内存和工作内存之间具体的交互。除此之外,java内存模型还规定了在执行上述8中基本操作时必须满足以下规则。通过这8中内存访问操作及其相关的规定,再加上volatile的一些特殊规定,就完全可以确定哪些内存访问操作在并发下是安全的。
由于这种定义相当严谨但又十分的繁琐,实践起来很是麻烦,所以java虚拟机提供了一个等效判断原则—Happens-Before(先行发现原则)。
Happens-Before
Happens-Before规则-阐述内存可见性。无需任何同步手段就可以保证的。
- 程序顺序规则:在一个线程内,程序的执行规则跟程序的书写规则是一致的,从上往下执行。
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
- 线程启动规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
- 线程中止规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
- 线程中断规则: 对线程interrupt方法的调用happens-before于被中断线程的代码检测到中断事件的发生。
volatile
- 可见性
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
- 有volatile变量修饰的共享变量进行写操作的时候会使用CPU提供的Lock前缀指令:
- 将当前处理器缓存行的数据写回到系统内存
- 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
- 顺序性
- 禁止指令的重排序,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。
- 在每个volatile写操作的前面插入一个StoreStore屏障。在每个volatile写操作的后面插入一个StoreLoad屏障。
- 在每个volatile读操作的后面插入一个LoadLoad屏障。在每个volatile读操作的后面插入一个LoadStore屏障。
JVM锁
- 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
- 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界
- synchronized是无法禁止指令重排和处理器优化的。
synchronized锁的实现原理
在使用 synchronized 来同步代码块的时候,经编译后,会在代码块的起始位置插入 monitorenter指令,在结束或异常处插入 monitorexit指令。当执行到 monitorenter 指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。而 synchronized 用的锁是存放在 Java对象头中的。
Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为重量级锁。
synchronized锁的4种状态升级
无锁、偏向锁,轻量级锁(自旋锁),重量级锁,锁状态只能升级,不能降级。
在 synchronized 最初的实现方式是 “阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,这种方式就是 synchronized实现同步最初的方式,这也是当初开发者诟病的地方,这也是在JDK6以前 synchronized效率低下的原因,JDK6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。
偏向锁
初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
轻量级锁
轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。
长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。
重量级锁
重量级锁显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。
重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
volatile与synchronized区别
- 仅靠volatile不能保证线程的安全性。(原子性)
- volatile轻量级,只能修饰变量。synchronized重量级,可修饰代码块和方法
- volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。
- synchronized不仅保证可见性,而且还保证原子性,因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。
- synchronized有性能损耗,加锁、解锁的过程是要有性能损耗的,volatile在大多数场景下也比锁的开销要低。
产生阻塞
java线程安全的三个特性
- 可见性:volatile,synchronized
- 原子性:synchronized,final,volatile
- 顺序性:volatile
也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
java解决多线程并发安全问题的方式
数据共享
同步锁
单例模式
1.对象的发布与逸出
发布(Publish)一个对象的意思是指,使对象在当前作用域之外的代码中使用。
逸出:如果再对象构造完全之前就发布该对象,就会破坏线程安全性。当某个不应该发布的对象被发布时,这种情况就被成为逸出(Escape)
2.饿汉模式/懒汉模式:volatie+(synchronized/Lock)
/**
* 饿汉模式
*/
public class HungrySingleton {
private static HungrySingleton instance=new HungrySingleton();
/**
* 禁止外部构建
*/
private HungrySingleton(){}
/**
* 由外部调用
* @return
*/
public static HungrySingleton getInstance(){
return instance;
}
/**
* 测试
* @param args
*/
public static void main(String[] args) {
for(int i=0;i<20;i++){
new Thread(()->{
System.out.println(HungrySingleton.getInstance());
}).start();
}
}
}
public class SingletonExample {
// 私有构造函数
private SingletonExample() {
}
// 单例对象
private static SingletonExample instance = null;
// 静态的工厂方法
public static SingletonExample getInstance() {
if (instance == null) { // 双重检测机制
synchronized (SingletonExample.class) { // 同步锁
if (instance == null) {
instance = new SingletonExample();
}
}
}
return instance;
}
}
这里有一个知识点:CPU指令相关
在上述代码中,执行new操作的时候,CPU一共进行了三次指令
(1)memory = allocate() 分配对象的内存空间
(2)ctorInstance() 初始化对象
(3)instance = memory 设置instance指向刚分配的内存
private volatile static SingletonExample instance = null;
线程安全的集合
https://my.oschina.net/zhdkn/blog/116901
JDK1.5之前,可以使用Venctor和Hashtable,也可以由java.util.Collections来创建线程安全的集合,
如:
Connections.synchronizedSet(Set<T>);
Connections.synchronizedList(List<T>);
Connections.synchronizedMap(Map<K, V>)等,
其简单的原理是每个方法都增加了synchronized来保证线程安全。
JDK1.5之后,提供了java.util.concurrent并发包,它提供的新集合类允许通过在语义中的少量更改来获得更高的并发。
CopyOnWriteArrayList
其中的set、add、remove等方法,都使用了ReentrantLock的lock()来加锁,unlock()来解锁。当增加元素的时候使用Arrays.copyOf()来拷贝副本,在副本上增加元素,然后改变原引用指向副本。
CopyOnWriteArraySet
使用了CopyOnWriteArrayList来存储数据,remove方法调用CopyOnWriteArrayList的remove方法。add方法调用了CopyOnWriteArrayList的addIfAbsent方法,addIfAbsent同样使用了ReentrantLock的lock()来加锁,unlock()来解锁。
ConcurrentHashMap
允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hash table,它们有自己的锁(由ReentrantLock来实现的)。只要多个修改操作发生在不同的段上,它们就可以并发进行。
数据隔离
ThreadLocal<br /> InheritableThreadLocal<br /> TransmittableThreadLocal<br /> immutable对象 [https://blog.csdn.net/z_z_zZZ/article/details/80769540](https://blog.csdn.net/z_z_zZZ/article/details/80769540)
阻塞队列
ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
并发工具类
- CountDownLatch闭锁
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
static CountDownLatch c = new CountDownLatch(2);
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
public void run() {
try {
System.out.println("线程1开始执行!");
Thread.sleep(3000);
System.out.println("线程1结束执行!");
c.countDown();
} catch (InterruptedException e) {
}
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
try {
System.out.println("线程2开始执行!");
Thread.sleep(5000);
System.out.println("线程2结束执行!");
c.countDown();
} catch (InterruptedException e) {
}
}
});
t1.start();
t2.start();
try {
System.out.println("开始等待线程1、2结束");
c.await();
System.out.println("线程1、2结束");
} catch (InterruptedException e) {
}
}
}
开始等待线程1、2结束
线程2开始执行!
线程1开始执行!
线程1结束执行!
线程2结束执行!
线程1、2结束
- CycliBarrier同步屏障
public class CycliBarrierDemo {
static CyclicBarrier c = new CyclicBarrier(3);
public static void main(String[] args) throws Exception{
Thread t1 = new Thread(new Runnable() {
public void run() {
try {
System.out.println("线程1开始执行!");
Thread.sleep(3000);
System.out.println("线程1结束执行!");
c.await();
} catch (Exception e) {
}
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
try {
System.out.println("线程2开始执行!");
Thread.sleep(5000);
System.out.println("线程2结束执行!");
c.await();
} catch (Exception e) {
}
}
});
t1.start();
t2.start();
System.out.println("开始等待线程1、2结束");
c.await();
System.out.println("线程1、2结束");
}
}
开始等待线程1、2结束
线程2开始执行!
线程1开始执行!
线程1结束执行!
线程2结束执行!
线程1、2结束
- Semaphore信号量
是用来控制同时访问特定资源的线程数量动态代理
jedis:https://asia.feishu.cn/docs/doccnyto9mkvY45oKZ5jq5WE4Sb
mybatis:https://zhuanlan.zhihu.com/p/343374447
线程间的通信:共享内存和消息传递
https://blog.csdn.net/jisuanji12306/article/details/86363390