参考连接:https://angela.blog.csdn.net/article/details/105141484?spm=1001.2014.3001.5502
1、为什么会需要synchronized?什么场景下使用synchronized?
线程访问共享资源了,当一个资源有可能被多个线程同时访问并修改的话,需要用到锁,还是画个图给您看一下,
如上图所示,比如在王者荣耀程序中,我们队有二个线程分别统计后裔和安琪拉的经济,A线程从内存中read 当前队伍总经济加载到线程的本地栈,进行 +100 操作之后,这时候B线程也从内存中取出经济值 + 200,将200写回内存,B线程前脚刚写完,后脚A线程将100 写回到内存中,就出问题了,我们队的经济应该是300, 但是内存中存的却是100,你说糟不糟心。
那你跟我讲讲用 synchronized 怎么解决这个问题的?
在访问竞态资源时加锁,因为多个线程会修改经济值,因此经济值就是静态资源,给您show 一下吧?下图是不加锁的代码和控制台的输出,请您过目:
public class ThreadTestOne {
private static int money = 0;
private static void increseMoney(int n){
money+=1;
}
public static void main(String[] args) {
Thread a = new Thread(()->{
for (int i = 0; i < 1000; i++) {
increseMoney(1);
}
});
Thread b = new Thread(()->{
for (int i = 0; i < 1000; i++) {
increseMoney(2);
}
});
a.start();
b.start();
try {
a.join();
b.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前总经济是:"+money);
}
}
查看控制台输出:
当前总经济是:1796
不加锁,结果就不对。
下面看下加锁之后的代码和控制台的输出
public class ThreadTestOne {
private static int money = 0;
private static final Object obj = new Object();
private static void increseMoney(int n){
money+=1;
}
public static void main(String[] args) {
Thread a = new Thread(()->{
synchronized (obj){
for (int i = 0; i < 1000; i++) {
increseMoney(1);
}
}
});
Thread b = new Thread(()->{
synchronized (obj){
for (int i = 0; i < 1000; i++) {
increseMoney(2);
}
}
});
a.start();
b.start();
try {
a.join();
b.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前总经济是:"+money);
}
}
2、synchronized 作用范围
- 在静态方法上加锁;
- 在非静态方法上加锁;
- 在代码块上加锁;
public class SynchronizedSample {
private final Object lock = new Object();
private static int money = 0;
//非静态方法
public synchronized void noStaticMethod(){
money++;
}
//静态方法
public static synchronized void staticMethod(){
money++;
}
public void codeBlock(){
//代码块
synchronized (lock){
money++;
}
}
}
3.1、三种作用范围加锁的区别
synchronized 这三种作用范围的加锁方式的区别:首先要明确一点:锁是加在对象上面的,我们是在对象上加锁。
重要事情说三遍:在对象上加锁 ✖️ 3 (这也是为什么wait / notify 需要在锁定对象后执行,只有先拿到锁才能释放锁)
这三种作用范围的区别实际是被加锁的对象的区别,请看下表:
作用范围 | 锁对象 |
---|---|
非静态方法 | 当前对象 => this |
静态方法 | 类对象 => SynchronizedSample.class (一切皆对象,这个是类对象) |
代码块 | 指定对象 => lock (以上面的代码为例) |
3.2、 JVM 是怎么通过synchronized 在对象上实现加锁,保证多线程访问竞态资源安全的
先说在JDK6 以前,synchronized 那时还属于重量级锁,相当于关二爷手中的青龙偃月刀,每次加锁都依赖操作系统Mutex Lock实现,涉及到操作系统让线程从用户态切换到内核态,切换成本很高;
到了JDK6,研究人员引入了偏向锁和轻量级锁,因为Sun 程序员发现大部分程序大多数时间都不会发生多个线程同时访问竞态资源的情况,每次线程都加锁解锁,每次这么搞都要操作系统在用户态和内核态之间来回切,太耗性能了。
JDK 6 以前 synchronized为什么这么重? JDK6 之后的偏向锁和轻量级锁是怎么回事?
3.3、对象头的理解
首先要了解 synchronized 的实现原理,需要理解二个预备知识:
-
第一个预备知识:需要知道 Java 对象头,锁的类型和状态和对象头的Mark Word息息相关;
synchronized 锁 和 对象头息息相关。我们来看下对象的结构:
对象存储在堆中,主要分为三部分内容,对象头、对象实例数据和对齐填充(数组对象多一个区域:记录数组长度),下面简单说一下三部分内容,虽然 synchronized 只与对象头中的 Mard Word相关。
1、对象头:
对象头分为二个部分,Mard Word 和 Klass Word,下面出了详细说明:
对象头结构 | 存储信息-说明 |
---|---|
Mard Word | 存储对象的hashCode、锁信息或分代年龄或GC标志等信息 |
Klass Word | 存储指向对象所属类(元数据)的指针,JVM通过这个确定这个对象属于哪个类 |
2、对象实例数据:
如上图所示,类中的 成员变量data 就属于对象实例数据;
3、对齐填充:
JVM要求对象占用的空间必须是8 的倍数,方便内存分配(以字节为最小单位分配),因此这部分就是用于填满不够的空间凑数用的。
2、第二个预备知识点
需要了解 Monitor ,每个对象都有一个与之关联的Monitor 对象;Monitor对象属性如下所示( Hospot 1.7 代码)
//