synchronized锁
synchronized锁用法
// synchronized锁底层原理
public class SynchronizedTest03 {
//修饰静态方法(同步方法)
//代码块0(锁定的是当前类)
public synchronized static void access0(){
try {
TimeUnit.*MINUTES*.sleep(1);
System.*out*.println(Thread.currentThread().getName()+" is running");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//修饰非静态方法(同步方法)
//代码块1(锁定的是当前对象的调用者)
public synchronized void access1(){
try {
TimeUnit.MINUTES.sleep(1);
System.out.println(Thread.currentThread().getName()+" is running");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//修饰非静态方法(同步代码块)
//代码块2(锁定的是对象)this指的是当前对象
public void access2(){
synchronized(this){
try {
synchronized (this){
TimeUnit.*MINUTES*.sleep(1);
}
System.*out*.println(Thread.currentThread().getName()+" is running");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//代码块3(锁定的是CLASS类)(同步代码块)
public void access3(){
synchronized(SynchronizedTest03.class){//ClassLoader class --> “堆”区生成一个Class对象:所有的对象
//有Class对象的所有的对象都共同使用这一个锁
try {
TimeUnit.MINUTES.sleep(1);
System.out.println(Thread.currentThread().getName()+" is running");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
SynchronizedTest03 demo = new SynchronizedTest03();
for (int i = 0; i < 5; i++) {
new Thread(demo::access2).start();
}
}
}
锁的分类:
1、对象锁
synchronized(this|object) {} 修饰非静态方法
在 Java 中,每个对象都会有一个 monitor 对象,这个对象其实就***是 Java 对象的锁***,通常会被***称为“内置锁”或“对象锁”***。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。
***monitor对象(监视器)***:
A、某一线程占有这个对象的时候,先看monitor 的计数器是不是0,如果是0还没有线程占有,这个时候线程占有这个对象,并且对这个对象的monitor+1;如果不为0,表示这个线程已经被其他线程占有,这个线程等待。当线程释放占有权的时候,monitor-1;
B、同一线程可以对同一对象进行多次加锁,加一次锁就monitor+1,可重入性。
2、类锁
synchronized(类.class) {} 修饰静态方法
在 Java 中,针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即***类的 Class 对象锁***。每个类只有一个 Class 对象,所以每个类只有一个类锁。
synchronized原理分析
1、线程堆栈分析(Jconsole、Jstack pid)
上面的程序我们启动5个线程,每个线程睡眠1分钟,我们使用Jconsole软件进行查看线程。
我们可以看到有5个线程,一个线程正在等待,其他4个线程都是阻塞状态。
使用Jstack pid查看线程:
2、JVM指令分析
对当前类进行反编译:javap -v SynchronizedTest03
找到我们写的access0、access1、access2、access3方法:其中access0、access1原理相同(锁的方法),access2、access3原理相同(锁的代码块)。
A、锁的代码块(用monitor进行加锁解锁)
【问题】为什么会有两个monitorexit?
答:第一个是正常出口,第二个是异常出口。
B、锁的方法(用标记进行加锁、ACC_SYNCHRONIZED)
3、使用synchronized注意的问题
(1)与moniter关联的对象不能为空
(2)synchronized作用域太大
(3)不同的monitor企图锁相同的方法
(4)多个锁的交叉导致死锁
Java虚拟机对synchronized的优化
对锁优化加入:偏向锁、轻量级锁、重量级锁(等待时间长)
每一个monitor存放在一个实例对象里面,每一个对象都和monitor进行关联,主要与对象头进行关联,关联主要根据锁的状态进行。
无锁状态:没有加锁
偏向锁:在对象第一次被某一线程占有的时候,是否偏向锁置1,锁表01,写入线程号,当其他的线 程访问的时候,会去竞争,竞争失败(升级到轻量级锁)、很多次被第一次占有它的线程获取次数多–竞争成功
轻量级锁:线程有交替适用,互斥性不是很强,CAS失败,00
重量级锁:强互斥,10,等待时间长
自旋锁:竞争失败的时候,不是马上转化级别,而是执行几次空循环
锁消除:JIT在编译的时候吧不必要的锁去掉
volatile关键字
机器硬件CPU与JMM模型
(1)CPU Cache模型
多核CPU的情况下:
(2)CPU缓存的一致性问题
每一个CPU到内存的cache互不干涉,容易导致数据不一致问题。
每一个CPU修改内存数据的步骤:
1、从内存中把数据读到Cache中
2、在Cache中更新数据
3、把更新的结果刷新到内存
解决方案:
A)总线加锁(粒度太大)
B)MESI(缓存一致性协议)
a.读操作:不做任何事情,把Cache中的数据读到寄存器
b.写操作:发出信号通知其他的CPU讲改变量的Cache line置为无效,其他的CPU要访问这个变量的时候,只能从内存中获取。(高速缓存其实就是一组称之为缓存行(cache line)的固定大小的数据块,其大小是以突发读或者突发写周期的大小为基础,当从内存中取单元到cache中时,会一次取一个cacheline大小的内存区域到cache中,然后存进相应的cacheline中,最小单元)
(3)Java内存模型
\1) 主存中的数据所有线程都可以访问(共享数据)
\2) 每个线程都有自己的工作空间,(本地内存、私有数据)
\3) 工作空间数据:局部变量、内存的副本
\4) 线程不能直接修改内存中的数据,只能读到工作空间来修改,修改完成后刷新到内存
JMM和CPU内存直接的关系:
Volatile关键字的语义分析
volatile作用:让其他线程能够马上感知到某一线程多某个变量的修改
(1)保证可见性
对共享变量的修改,其他的线程马上能感知到
不能保证原子性,如:读、写、(i++)
(2)保证有序性
重排序(编译阶段、指令优化阶段),输入程序的代码顺序并不是实际执行的顺序,重排序后对单线程没有影响,对多线程有影响。
Happens-before中的volatile规则:
对于volatile修饰的变量:
(1)volatile之前的代码不能调整到他的后面
(2)volatile之后的代码不能调整到他的前面(as if seria)
(3)位置不变化
(3)volatile的原理和实现机制(锁、轻量级)
HSDIS --> 反编译 --> 汇编
Volatile的使用场景
(1)状态标志(开关模式)
public class ShutDownDemo extends Thread{
private volatile boolean started=false;
@Override
public void run() {
while(started){
// 代码逻辑
}
}
public void shutdown(){
started=false;
}
}
(2)双重检查锁定(DCL:double-checked-locking)
public class SingletonDemo {
// volatile的作用是可以使内存中的数据对象线程可见
// 主内存对线程是不可见的,添加volatile关键字之后,主内存对线程可见
// 使用volatile后直接跳过工作内存复制一份的情况,进行主内存操作判断是否为空
// 如果不加volatile,可能会引起指令重排,导致空指针异常
private volatile static SingletonDemo singleton;
private SingletonDemo(){
System.out.println("创建了单例对象!");
}
// double check
public static SingletonDemo getInstance(){
// 必须是一个唯一的对象
if(singleton == null){
synchronized (SingletonDemo.class){
if(singleton == null){
singleton = new SingletonDemo();
}
}
}
return singleton;
}
}
分析:不加volatile关键字的影响
修改代码:
private Socket socket;
private static SingletonDemo singleton;
private SingletonDemo(){
socket = new Socket();
singleton = new SingletonDemo();
socket.toString();
}
在初始化的时候,根据JVM优化,可能会引起指令重排,导致socket.toString()在没有被初始化的情况下优先执行,就会导致空指针异常,所以必须加volatile关键字,这样singleton = new SingletonDemo();前面的代码就不会被改变执行顺序不会出现空指针了。
(3)需要利用顺序性
volatile与synchronized的区别
(1)使用上的区别:volatile只能修饰变量,synchronized只能修饰方法和语句块
(2)对原子性的保证:synchronized可以保证原子性,volatile不能保证原子性
(3)对可见性的保证:都可以保证可见性,但实现原理不同
volatile对变量加了lock,synchronized使用monitorEnter和monitorexit
(4)对有序性的保证
volatile能保证有序,synchronized可以保证有序性,但是代价(重量级)并发退化到串行
(5)其他
3)需要利用顺序性
volatile与synchronized的区别
(1)使用上的区别:volatile只能修饰变量,synchronized只能修饰方法和语句块
(2)对原子性的保证:synchronized可以保证原子性,volatile不能保证原子性
(3)对可见性的保证:都可以保证可见性,但实现原理不同
volatile对变量加了lock,synchronized使用monitorEnter和monitorexit
(4)对有序性的保证
volatile能保证有序,synchronized可以保证有序性,但是代价(重量级)并发退化到串行
(5)其他
synchronized引起阻塞、volatile不会引起阻塞