目录
5、synchronized 关键字 - 监视器锁 monitor lock
一、多线程带来的风险-线程安全
1、观察线程不安全
public class ThreadDemo2 {
private static long count = 0;
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(()->{
for (int i = 1;i <= 500000;i++) {
count++;
}
});
Thread t2 = new Thread(()->{
for (long i = 0;i < 500000;i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
//存在线程安全问题,输出的结果可能不准确
System.out.println("count= "+count);
}
}
其运行结果:
明显这个结果和我们的预期是不一样的,这是因为存在线程安全问题。若把count++的操作在一个单线程环境下运行 ,便不会出现这样的问题。下面我们来说一下线程安全问题。
2、线程安全的概念
想给出⼀个线程安全的确切定义是复杂的,但我们可以这样认为:
3、线程不安全的原因
- 线程调度是随机的
- 修改共享数据
上面的线程不安全的代码中,涉及到多个线程针对 count 变量进行修改, 此时这个 count 是⼀个多个线程都能访问到的 "共享数据" 。
- 原子性
什么是原子性:
⼀条 java 语句不⼀定是原子的,也不一定只是一条指令。
比如,刚才我们看到的 count++,其实是由三步操作组成的:
- 从内存把数据读到 CPU 寄存器中
- 进行数据更新
- 把数据写回到内存
那么不保证原子性会给多线程带来什么问题呢?
- 可见性
可见性指,⼀个线程对共享变量值的修改,能够及时地被其他线程看到。这里先不过多介绍。
- 指令重排序
什么是代码重排序?
假设有⼀段代码是这样的:
- 去前台取下 U 盘
- 去教室写 10 分钟作业
- 去前台取下快递
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑⼀次前台。这种叫做指令重排序。
重排序是⼀个比较复杂的话题, 涉及到 CPU 以及编译器的⼀些底层⼯作原理, 此处不做过多讨论。
4、解决之前的线程不安全问题
解决之后的代码:
public class ThreadDemo2 {
private static long count = 0;
public static void main(String[] args) throws InterruptedException{
Object locker = new Object();
Thread t1 = new Thread(()->{
for (int i = 1;i <= 500000;i++) {
synchronized (locker) {
count++;
}
}
});
Thread t2 = new Thread(()->{
for (long i = 0;i < 500000;i++) {
synchronized (locker) {
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count= "+count);
}
}
这时的结果就一定是1000000,如图:
下面就给大家解释一下,这个线程不安全的问题是如何解决的。
5、synchronized 关键字 - 监视器锁 monitor lock
5.1 synchronized 的特性
1) 互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到 同⼀个对象 synchronized 就会 阻塞等待。
-
进入 synchronized 修饰的代码块,相当于 加锁
-
退出 synchronized 修饰的代码块,相当于 解锁
synchronized用的“锁”是存在Java对象“头”里面的。
synchronized的底层是使用操作系统的mutex lock实现的。
2) 可重入
synchronized 同步块对同⼀条线程来说是可重入的,不会出现自己把自己锁死的问题;
Java 中的 synchronized 是 可重入锁, 因此没有上面的问题。
for (int i = 0; i < 50000; i++) {
synchronized (locker) {
synchronized (locker) {
count++;
}
}
}
在可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息.
- 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
- 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)
5.2 synchronized 使用示例
synchronized 本质上要修改指定对象的 "对象头",从使用度来看,synchronized 也势必要搭配⼀个具体的对象来使用。
1) 修饰代码块: 明确指定锁哪个对象.
锁任意对象:
public class SynchronizedDemo {
private Object locker = new Object();
public void method() {
synchronized (locker) {
}
}
}
锁当前对象:
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
2) 直接修饰普通方法: 锁的 SynchronizedDemo 对象
public class SynchronizedDemo {
public synchronized void methond() {
}
}
3) 修饰静态方法: 锁的 SynchronizedDemo 类的对象
public class SynchronizedDemo {
public synchronized static void method() {
}
}
我们重点要理解,synchronized 锁的是什么. 两个线程竞争同⼀把锁, 才会产生阻塞等待.
两个线程分别尝试获取两把不同的锁, 不会产⽣竞争.
5.3 Java 标准库中的线程安全类
Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施:
但是还有⼀些是线程安全的. 使用了⼀些锁机制来控制:
StringBuffer 的核心方法都带有 synchronized .
还有的虽然没有加锁, 但是不涉及 "修改", 仍然是线程安全的: String