Java锁相关知识点

synchronized

synchronized 有三种使用方式:

  • 作用于普通方法:锁的是当前类实例对象
  • 作用于静态方法:锁的是当前类的 class 对象
  • 作用于代码块:锁的是括号里面的对象

当一个线程访问同步代码块时,需要获得锁才能执行里面的代码,反过来,当退出同步代码块或者发生异常的时候要释放锁,通过 javap -v 进行代码的反编译可以看到是通过 monitorexter 和 monitorexit 指令实现的,当 monitor 被线程持有后,就处于锁定状态,也就是上锁了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String Test
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
LineNumberTable:
line 4: 0
line 5: 4
line 6: 12
line 7: 22
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 17
locals = [ class Test, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4

当一个线程进入了一个对象的 synchronized 方法后,其他线程是否可以进入这个对象的其他方法?

需要分情况讨论:

  • 如果这个方法是 synchronized 修饰的,不能,除非上面释放锁,如果没有加 synchronized,则可以进入
  • 如果这个方法内部有调用 wait() 方法,wait() 会释放锁(sleep() 不会),则可以进入其他 synchronized 方法
  • 如果其他方法是静态方法,可以进入,因为静态方法锁的是当前类的 class 对象,非静态的方法锁的是当前类建出来的对象,不是用一个东西

synchronized 和 ReentrantLock 的区别

synchronizedReentrantLock
是关键字,是 JVM 层面的实现,通过操作对象头中 mark word 来加锁是类,是 JDK 层面的实现,通过 Unsafe 类的 park 方法来加锁,这是两者的本质区别
自动释放锁要手动释放
是非公平锁可以设置
不能获取锁状态可以获取锁的状态,并且可以设置等待时间来避免死锁
可以锁方法或者代码快只能锁代码块

volatile

具有三个特性:

  • 保证可见性:程序中定义的共享变量存在主内存,每个线程中的变量是在工作内存中操作的,当一个线程 A 修改了主内存中的一个共享变量,这个时候线程 B 是不知道这个变量已经被修改了,因为线程之间的工作内存是互相不可见的,那么这个时候 volatile 的作用就是让线程 A 和 B 都可以感知到对方对共享变量的修改,当线程 A 更新了共享变量,会将数据写回主内存中,而线程 B 每次去读共享变量时去主内存中读取,这样就保证了线程之间的可见性。这种保证可见性的机制是内存屏障(memory barrier):

    • Load Barrier:读屏障

    • Store Barrier:写屏障

      内存屏障有两个作用:

      • 禁止屏障两侧的指令重排序优化
      • 强制把缓存中的脏数据写回主内存,并让缓存中的数据失效(这不就是 MySQL 和 Redis 那一套)
  • 不保证复合操作的原子性:所谓原子性,就是说一个操作不可被分割,要么全部执行,要么全部不执行。Java 只保证了基本数据类型的变量和赋值操作是原子性的(在 32 位的 JDK 环境下,对 64 位数据的读取不是原子性操作)

  • 禁止指令重排序(有序性):有序性指程序执行的顺序按照代码的先后顺序执行。一般来说,处理器为了提高程序运行效率,可能会对输入的代码进行优化,它不保证程序中每个语句的执行先后顺序同代码中的数据一直,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。要进行重排序,需要满足两个条件:

    • 在单线程环境下,程序运行的结果不能改变
    • 如果数据存在依赖关系,不允许重排序

volatile 的使用场景:

  • 双重检测单例
  • 立 flag

synchronized 和 volatile 的区别

volatilesynchronized
只能修饰变量可以修饰类、方法、代码快
仅能实现变量的修改可见性,不能保证原子性可以保证变量的修改可见性和原子性
不会造成线程的阻塞可能会造成线程的阻塞