Java锁 synchronized

Synchronized 关键字
喜欢底层源码的朋友可以来交流探讨。交流群:818491202 验证:88

在java中,相信大家都用过 synchronized 来解决多线程安全问题,下面简单描述一下 synchronized 的相关特性.
被 synchronized 包含的代码块具有以下特性

原子性 同步的代码块操作不可中断
可见性 同步代码块里的数据都是最新的,也就是主存数据,并且操作完会立即刷新主存

这两个特性将在下面的代码示例中展示(更底层的留到更后面说)

synchronized 原子性的实现就是: 加锁

java中的对象锁有四种状态: 无锁、偏向锁、轻量级锁、重量级锁(monitor).从左往右逐渐升级.

根据对象锁的竞争,锁会逐渐升级,最后可能 升级成重量级锁也就是 monitor.并且锁只能升级,不能降级
原子性
synchronized 保证了同步代码块里面的操作的原子性
代码示例
public class SyncTest2 {
// 计数
static CountDownLatch cdl = new CountDownLatch(2);

public static void main(String[] args) throws Exception{
    LockObject lockObject = new LockObject();
    new Thread(new TestSyncRunnable(lockObject)).start();
    new Thread(new TestSyncRunnable(lockObject)).start();
    cdl.await();
}

static class LockObject{
    private int cnt;
}

static class TestSyncRunnable implements Runnable{

    private LockObject lockObject;

    TestSyncRunnable(LockObject lockObject){
        this.lockObject = lockObject;
    }

    @Override
    public void run() {
        synchronized(this.lockObject) {
            System.out.println(String.format("线程%s开始执行任务",Thread.currentThread()));
            for(int i = 0;i<10000000;i++) {
                this.lockObject.cnt++;

// System.out.println(String.format("线程%s执行了第%s次",Thread.currentThread(),i));
}
System.out.println(String.format("线程%s执行完毕了",Thread.currentThread()));
}
cdl.countDown();
}
}
上面这个结果,不管运行几次,结果都是一个执行完毕了,另一个才开始执行,因为 synchronized 是包括了整个循环操作.

线程Thread[Thread-0,5,main]开始执行任务

线程Thread[Thread-0,5,main]执行完毕了

线程Thread[Thread-1,5,main]开始执行任务

线程Thread[Thread-1,5,main]执行完毕了
所有线程执行完毕了,结果: cnt = 20000000
此时我们 将 synchronized 关键字去掉 ,run方法变成了如下
@Override
public void run() {
// synchronized(this.lockObject) {
System.out.println(String.format("线程%s开始执行任务",Thread.currentThread()));
for(int i = 0;i<10000000;i++) {
this.lockObject.cnt++;
}
System.out.println(String.format("线程%s执行完毕了",Thread.currentThread()));
// }
cdl.countDown();
}
下面是输出的结果,不管运行了多少次,因为循环次数比较多,所以 能很明显的能看出来,在第一个线程执行完毕前,都会被第二个线程打断
线程Thread[Thread-0,5,main]开始执行任务

线程Thread[Thread-1,5,main]开始执行任务

线程Thread[Thread-1,5,main]执行完毕了

线程Thread[Thread-0,5,main]执行完毕了

可见性

可见性表明在 synchronized 代码块中的对象,都会从主存中获取最新数据,并且在同步代码块结束后,会将最新数据写入主存.

本来想了一个例子,但是这个例子似乎不太恰当,由于i++这个操作并不是原子性并且java中似乎没有 有原子性但是没有可见性的东西.
在验证东西的时候,都是单一变量原则,我觉得无法完全证明,所以例子就不贴出来了.(如果各位有什么好的想法或者建议可以偷偷告诉我,真的可行的话我会偷偷在这加上个例子~~)

JVM 对象模型
这篇是讲 synchronized 的,如果连模型都讲了会不会太多(会的)?但是 synchronized 跟对象模型根本离不开,so就一起放在这里介绍吧~
当我们申请一个java对象的时候,jvm将会构造一个 包含三部分数据的对象

对象头 header - 包含对象的各种标识

实例数据 - java对象中拥有的具体字段

对齐填充 - 对象字段4字节对齐,对象地址必须是8字节的倍数,不满则补.

Header
JVM 对象的 Header 由三部分组成

Mark Word 用于存储对象的各种 标志信息 (thread ID、 epoch、age、biasable、lock、hashCode 等)

Class Metadata Address 指向class地址的指针(class要加载到内存中,既然是内存,那就肯定有个地址),用来标记该对象的类型-class

Array length 如果该对象是数组的话,则会多4个字节(32bit)来存储数组的长度

image.png

Mark Word


image.png

可以看到有几个属性值,从左往右看:

thread ID (线程ID,记录持有偏向锁的线程ID最开始是0,这个判断偏向锁是否要升级成轻量级锁)

epoch (纪元,可以理解为用来记录偏向锁的 identifier )

age (对象的年龄,存活过了多少次gc)

biasable (是否开启偏向锁,默认开启,在某些已经确定会发生竞争的场景,关闭偏向锁能提高效率 .开启的话值为1,否则为0,如下图
就是开启的)

pointer to lock record (指向栈中锁记录的指针)

pointer to heavyweight monitor (指向监视器的指针)

锁标识位 (这个东西是用来标记当前对象锁状态,下面列出几种状态值)

01 无锁、偏向锁
00 轻量级锁
10 重量级锁
11 表明对象要被回收了, 标记GC

锁升级流程
1.开始的时候对象是无锁的, 如果开启了偏向锁,此时的 ThreadID 为0 ,表示没有线程持有锁.(禁用了偏向锁的话,则从轻量级锁开始)
2.当一个线程第一次给对象加锁对时候,此时 thread ID 会 从默认的0改为该加锁线程的ID,并且同一个线程可以多次获取该锁,也就是可重入.
3.重入流程: 当还是偏向锁并且获取锁的时候,根据 尝试加锁的线程ID和对象中存储的 thread ID 比较 ,如果是同一个线程,则允许重入,即获取锁.
4.当尝试加锁的线程ID跟当前对象存储的 thread ID不同 , 则表明有第二个线程来争抢锁(在变成轻量级锁之前, threadID 是会一直保存着). 一旦(划重点: 一旦)有第二个线程来争抢锁的时候,就会转变成轻量级锁的结构,不管当前对象是不是正被锁着.(升级成轻量级锁的时候 thread ID 已经不存在)
5.当变成轻量级锁后,会进行一定次数的自旋(实际上就是 循环CAS操作 ),自旋一定次数锁都失败后,锁最后会升级成 重量级锁(monitor) .
一些参数
禁用偏向锁
偏向锁的话,可以用以下参数关闭
// 禁用偏向锁
-XX:-UseBiasedLocking
自旋次数
// 设置自旋次数
-XX:PreBlockSpin
// 禁用偏向锁
-XX:-UseBiasedLocking
重量级锁 - monitor
锁竞争严重的话,最后对象锁会升级为重量级锁-monitor
关于 monitor的结构 ,先来一张图吧~

image.png

属性说明
_owner: 当前锁的持有者
EntryList: 正在阻塞争抢锁锁的线程集合,没有争抢到锁就会进入该队列
WaitSet: 调用 wait() 被挂起的线程集合
流程
(咳咳,这里简单的描述一下流程)

文章最后的参考链接有一个较详细的 synchronized 源码解读链接

当线程争抢锁失败的时候,会进入 EntryList 进行阻塞.
当持有锁的线程调用 wait() 方法的时候,实际上是执行了 monitorexit 放弃了锁, 然后挂起线程,线程进入 waitSet.
当持有锁的线程调用 notify()/notifyAll() 并且在同步代码块结束的时候 ,也就是调用了 monitorexit 的时候在 waitSet 中的线程才能获取到锁.

来个🌰
public class SyncDemo {
int i;

public void test1() {
    synchronized (this) {
        i++;
    }
}

public static void main(String[] args) throws InterruptedException {
    SyncDemo syncDemo = new SyncDemo();
    for (int i = 0; i < 100; i++) {
        syncDemo.test1();
    }
}

}
使用javap查看字节码
// 地址太长就省略前面的,知道是class就好了
javap -v -l -c /xxxxx/SyncDemo.class
java字节码
public void test1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter // 这个就是 synchronized 的 monitor, 先获取锁
4: aload_0
5: dup
6: getfield #2 // Field i:I , 获取i的数值并入栈
9: iconst_1 // 将1(这里是int)入栈
10: iadd // 将栈顶2个int数值相加,结果入栈
11: putfield #2 // Field i:I , 从栈顶弹出并赋值给i
14: aload_1
15: monitorexit // 操作结束后释放锁
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return

通过字节码可以看到,使用的 monitorenter 进行加锁 , 操作结束后 monitorexit 释放锁.
那如果有多个线程进行锁的争抢呢?
像下面这样的代码
public static void main(String[] args) throws InterruptedException {
SyncDemo syncDemo = new SyncDemo();
// 启动两个线程进行争抢
new Thread(new SyncDemoRunnable(syncDemo)).start();
new Thread(new SyncDemoRunnable(syncDemo)).start();
}
}
public class SyncDemoRunnable implements Runnable{
SyncDemo syncDemo;

SyncDemoRunnable(SyncDemo syncDemo){
    this.syncDemo = syncDemo;
}

@Override
public void run() {
    for (int i = 0; i < 10000000; i++) {
        synchronized (this.syncDemo) {
            this.syncDemo.i++;
        }
    }
}

}
这时候看javap的代码甚至会发现 monitorentry 都不见了( 可能是使用的姿势不对? ).
最后通过查看汇编代码会发现,实际上都有lock 前缀的指令,证明其是原子操作.

喜欢底层源码的朋友可以来交流探讨。交流群:818491202 验证:88

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容