synchronized
synchronized 有三种使用方式:
- 作用于普通方法:锁的是当前类实例对象
- 作用于静态方法:锁的是当前类的 class 对象
- 作用于代码块:锁的是括号里面的对象
当一个线程访问同步代码块时,需要获得锁才能执行里面的代码,反过来,当退出同步代码块或者发生异常的时候要释放锁,通过 javap -v 进行代码的反编译可以看到是通过 monitorexter 和 monitorexit 指令实现的,当 monitor 被线程持有后,就处于锁定状态,也就是上锁了
1 | public void test(); |
当一个线程进入了一个对象的 synchronized 方法后,其他线程是否可以进入这个对象的其他方法?
需要分情况讨论:
- 如果这个方法是 synchronized 修饰的,不能,除非上面释放锁,如果没有加 synchronized,则可以进入
- 如果这个方法内部有调用 wait() 方法,wait() 会释放锁(sleep() 不会),则可以进入其他 synchronized 方法
- 如果其他方法是静态方法,可以进入,因为静态方法锁的是当前类的 class 对象,非静态的方法锁的是当前类建出来的对象,不是用一个东西
synchronized 和 ReentrantLock 的区别
synchronized | ReentrantLock |
---|---|
是关键字,是 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 的区别
volatile | synchronized |
---|---|
只能修饰变量 | 可以修饰类、方法、代码快 |
仅能实现变量的修改可见性,不能保证原子性 | 可以保证变量的修改可见性和原子性 |
不会造成线程的阻塞 | 可能会造成线程的阻塞 |