1.synchronized的特性
1.1互斥性
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待,当一个线程在执行加锁的代码块时,另一个线程再来加锁就无法加上,这就是互斥性。
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁
举例说明一下互斥性,相当于我们上厕所,一个坑位只能有人使用。当有人进入的时候,另一个人就无法进入,
加锁:就相当于厕所里面有人,并且锁着门了。
解锁: 就相当于厕所里面人出来了,它开始叫队伍中的一个人。叫人就算是唤醒等待队列中的一个线程。
阻塞等待定义:针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁。
注意:
上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 "唤醒". 这
也就是操作系统线程调度的一部分工作.
假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B
和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能
获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.
1.2可重入性
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。
要想了解可重入性,必须先了解“死锁”,死锁就是按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无法进行解锁操作. 这时候就会死锁。
Java 中的 synchronized 是 可重入锁, 因此没有上面的问题
2.synchronized 使用示例
synchronized 本质上要修改指定对象的 "对象头". 从使用角度来看, synchronized 也势必要搭配一个具体的对象来使用
2.1直接修饰普通方法: 锁的 SynchronizedDemo 对象
public class SynchronizedDemo { public synchronized void methond() { } }
2.2修饰静态方法: 锁的 SynchronizedDemo 类的对象
public class SynchronizedDemo { public synchronized static void method() { } }
2.3修饰代码块: 明确指定锁哪个对象
public class SynchronizedDemo { public void method() { synchronized (this) { } } }
我们重点要理解,synchronized 锁的是什么. 两个线程竞争同一把锁, 才会产生阻塞等待,两个线程分别尝试获取两把不同的锁, 不会产生竞争.
3.volatile关键字
3.1volatile 能保证内存可见性
代码在写入 volatile 修饰的变量的时候
- 改变线程工作内存中volatile变量副本的值
- 将改变后的副本的值从工作内存刷新到主内存
代码在读取 volatile 修饰的变量的时候
- 从主内存中读取volatile变量的最新值到线程的工作内存中
- 从工作内存中读取volatile变量的副本
volatile的作用:前面我们讨论内存可见性时说了, 直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度非常快, 但是可能出现数据不一致的情况.加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了
4.synchronized和volatile的区别
volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.