深入理解synchronized
一:基本特性:
1.1:锁升级(自适应)(重点)
针对程序:首先未使用synchronized ,就是未加锁的状态,代码中开始调用执行synchronized后,synchronized锁变成偏向锁,如果遇到锁冲突,synchronized 锁进一步升级成轻量级锁,当冲突进一步升级,synchronized锁就由轻量级锁,转变成重量级锁.
(1)未加锁的状态(无锁)
(2)偏向锁
(3)轻量级锁
(4)重量级锁
上述锁升级过程,针对一个锁对象来说,是不可逆的,只能升级,不能降级,一旦升级到了重量级锁,就不会回退到轻量级锁(当前JVM的做法).
1.1.1 偏向锁
偏向锁:首次使用synchronizd对对象进行加锁的时候,并不是真的加锁,而只是做一个"标记"(非常轻量,非常快,几乎没有开销),如果后面没有其他线程尝试对这个对象加锁,就可以保持这个状态,一直到解锁,(解锁也就是修改一下上述标记,也几乎没有开销),但是,如果在偏向锁的状态下,有某个线程也尝试对这个锁对象进行加锁,立刻就把偏向锁升级成轻量锁(真的加锁了,真的有互斥了).
本质上,偏向锁策略就是"懒"字的具体体现:能不加锁,就不加锁,能晚加锁,就晚加锁.
二:锁消除
实际上是一种编译器优化策略.
编译器优化前和优化后逻辑是等价的.
当代码中写了加锁操作,编译器&JVM会对当前的代码做出判定,看当前代码是不是真的要加锁,如果这里不需要加锁,就会自动的把加锁操作给优化掉.
最典型的就是,在一个线程里,使用synchronized.
三:锁粗化
实际上也是编译器的一种优化策略
锁的粒度:加锁的范围内,包含多少代码,包含的代码越多,就认为锁的粒度就越粗,反之,锁的粒度就越细.
在有些逻辑中,需要频繁的加锁解锁,编译器就会自动的把多次细粒度的锁,合并成一次粗粒度的锁,
二:synchronized的使用
2.1:什么时候使用synchronized???
多线程中,出现了线程不安全.
2.2:锁的操作:
(1)加锁:t1加上锁之后,t2也尝试加锁,就会阻塞等待(都是系统内核控制)(在Java中可以看到BLOCKED状态)
(2)解锁:直到t1解锁了之后,t2才有可能拿到锁(加锁成功).
2.3:编写代码
首先创建一个对象,使用这个对象作为锁:
在Java中可以使用任何对象作为加锁对象.
创建锁对象的意义:
锁对象的用途:有且只有一个,那就是用来区分,多个线程是否针对同一个对象(count)加锁,如果是,就会出现锁竞争/锁冲突/互斥,就会引起阻塞等待;如果不是,就不会出现锁竞争,也就不会阻塞等待.
锁对象还会记录当前那个线程拿到了锁对象,在摸个线程中,进行加锁的时候,首先判定该线程是否获得了锁对象,如果获得了,就加锁,没获得,就阻塞等待.
通过设定不同的锁对象,来确定竞争关系.
public class Demo2 {
public static int count=0;
public static void main(String[] args) throws InterruptedException {
//首先创建一个对象,使用这个对象作为锁
Object locker = new Object();
Thread t1=new Thread(()->{
for (int i = 0; i < 50000; i++) {
synchronized (locker){
count++;
}
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
synchronized (locker){
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " +count);
}
}
上述代码就相当于两个线程,针对同一个锁对象加锁,就会产生互斥.
t1,t2做的事情就是:判断循环条件,加锁,load ,add, save ,解锁,i++;
假设:由于是线程是并发执行的,这里假设t1线程先执行到了加锁操作,并且t1还没解锁,那么t2线程就不能获得锁对象,就不能进行加锁操作,
假设t1刚解锁,t2 就加锁了,但t1线程并不是什么代码也不执行,而是继续执行i++,循环判断条件,当t1执行到了synchronized,发现不能获得锁对象,那么t1线程只好阻塞等待了.
因此:在t1,t2两个线程中,每次count++是存在锁竞争的,会变成"串行"执行,但是执行for 循环中的条件以及i++仍然是并发执行的.
```java
public class Demo2 {
public static int count=0;
public static void main(String[] args) throws InterruptedException {
//首先创建一个对象,使用这个对象作为锁
Object locker = new Object();
Object locker2=new Object();
Thread t1=new Thread(()->{
for (int i = 0; i < 50000; i++) {
synchronized (locker){
count++;
}
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
synchronized (locker2){
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " +count);
}
}
上述代码就是两个线程针对不同对象加锁,就不会产生互斥,也就不会发生阻塞等待现象了,但线程确是不安全的.
操作系统中的加锁,解锁功能,核心还是CPU提供的指令(硬件提供了这样的能力,软件上才有对应的功能)
三:synchronized的锁机制
synchronized 既是悲观锁,又是乐观锁,既是轻量级锁,又是重量级锁,轻量级锁是自旋锁实现,重量级锁是挂起等待锁实现,synchronized是可重入锁,不是读写锁,是非公平锁.
四:synchronized 和ReentrantLock 的区别
共同点:都是可重入锁.
(1)synchronized 进入"{“就是加锁,出”}"就是解锁,而RenntrantLock则提供了加锁解锁方法
Object locker1 =new Object();
synchronized (locker1){
}
ReentrantLock locker=new ReentrantLock(true);
//参数写true就是创建一个公平锁
//不写或写false就是创建一个非公平锁
try{
locker.lock();//加锁
}finally {
locker.unlock();//解锁
//因为可以会忘记写解锁方法,或者因其他方法返回,没执行了解锁操作,
//所以经常把解锁操作放到finally中,该方法就一定会被执行
}
(2)synchronized 只是非公平锁,但RenntrantLock既提供了公平锁的实现,又提供了非公平锁的实现.
(3)ReentrantLock提供了tryLock操作,给加锁提供了更多的操作空间,尝试加锁,如果锁已经被其他线程占有,直接返回失败,而不会继续阻塞等待(也可以通过tryLock指定等待超时时间),但synchronized 遇到锁竞争,就阻塞等待(死等).
(4)synchronized 是搭配wait ,notify等待通知机制,RenntrantLock 是搭配Condition类完成等待通知.
Condition要比wait ,notify更强一点,(多个线程wait,但notify是随机唤醒一个,Condition可以指定线程唤醒)
(5)用法不同:synchronized 可以用来修饰普通方法、静态方法和代码块;ReentrantLock 只能用于代码块;
(6)响应中断不同:synchronized 不能响应中断;ReentrantLock 可以响应中断,可用于解决死锁的问题;
(7)底层实现不同:synchronized 是 JVM 层面通过监视器实现的;ReentrantLock 是基于 AQS 实现的。