首先,其实我们在看博客或者看视频都可以学到这个知识点,但是还是强烈建议大家看书,然后针对某一个不理解的细节去针对性的看博客或者视频。
一、Synchronized作用范围
谷歌翻译是已同步的意思。是Java为了处理并发编程的一个关键字。代表多个线程需要争抢同一把锁,抢到了才能进行自己的工作。在Java中,任何对象都可以用作线程竞争的锁,这也能解释,为什么Object类中有wait,notify等方法了。
可以作用在三个地方:
- 作用在代码块,需要传入一个锁对象;此时锁住的是传入的对象;
- 作用在非静态方法上,锁住的是当前类的实例对象;
- 作用在静态方法上,锁住的是当前类的Class对象。如果自己写工具类,慎重加锁,否则会造成性能问题;
二、jdk1.6之前Synchronized实现
这里为什么要说是jdk1.6之前呢,因为我在学习的时候很容易将实现原理和后边要说的锁升级弄混了,所以我认为分开说比较好。
网上很多写的命令还有啥的都不对,还是自己写吧:编写一个测试类 ,里边有一个synchronized代码块。
package com.lifeisftc.magic.string;
public class LockTest {
private static Object LOCK = new Object();
public static int main(String[] args) {
synchronized (LOCK){
System.out.println("Hello World");
}
return 1;
}
}
进入到文件目录,先后使用以下命令处理文件,观察命令行输出:
javac LockTest.java
javap -c LockTest
结果如下:注意看红色框,其他的可以不了解。
这这俩字节码是啥意思呢?我把JVM书上的解释贴一下:
synchronized关键字经过编译之后,会在同步块的前后分别形成一个monitorenter和monitorexit两个 字节码指令。这两个字节码指令都需要一个reference类型的参数来 指明要锁定和解锁的对象。
- 修饰代码块:取传入对象的reference;
- 修饰实例方法:取当前实例的reference;
- 修饰静态方法:取该类Class对象的reference。
在执行monitorenter指令时:
- 尝试获取对象锁,如果对象没被锁定,或者当前线程已经拥有了该对象的锁(这种情况是重入),就把锁的计数器+1,特殊情况下重入一次就+1,退出一次就-1。所以最后这个线程释放锁的时候,锁的计数器肯定是0。
- 这里提一下ReentrantLock,底层源码也有一个volicate修饰的value,用来计数。不过里边有一个判断,最多只能重入Integer.MAX_VALUE次,超过以后就报错了。感兴趣可以看一下。
- 那么当有线程抢到锁之后,如何保证其他线程处于阻塞状态,线程执行完后又该怎么唤醒其他线程呢?
- 有一点是Java线程是映射到操作系统的原生线程上去的,阻塞和唤醒都需要从用户态切换到内核态,这个时间开销非常大;
Synchronized实现原理讲完了,就这么简单。
——————————————————jdk1.6怎么优化Synchronized———————————————
三、锁的优化和升级
刚才说了,实现线程阻塞和唤醒需要从用户态切换到内核态,开销非常大,那到底有多大呢?贴一张JVM书里边的图(1.6之前)
看到了吧,差距就是这么明显,一个关键字被一个API干蒙了,就这?
为了找回场子肯定要优化呀:
附:这部分只是在《深入理解Java虚拟机》第十三章都有写,可以对比着看,我就不复制粘贴了。
既然切换状态花费时间那么长,那我们不切换行不行,用别的方式去实现呢,比如用CAS?
- 这样吧,我也不去通知你了,你没抢到锁就自己等一会儿吧,可以原地转个圈(自旋)啥的,转完圈你再来问问我,看看我给不给你;这就是下边要说的自旋锁。
- 另外还有的人,都不可能会产生线程安全问题的,你也加锁,你加任你加,我编译时候给你去掉,哼!这就是锁消除;
- 锁粗化:我们都知道StringBuffer是线程安全的,我要是连着执行三次.append(),那不相当于加了三个锁么,那么在编译时就在开头和结尾加一个不就完了么;
- 轻量级锁和偏向锁:撤销偏向锁的时候会导致stop the word。
偏向锁—>轻量级锁(CAS)—>重量级锁