【Java笔记】synchronized
目录
- 1. 代码案例
- 2. 出现线程安全问题的原因
- 3. synchronized
- 3.1 锁的概念
- 3.2 为方法加 synchronized
- 3.3 指令执行过程
- 3.4 小结(关于synchronized)
- 3.5 只给一个线程加锁也会出现线程安全问题
- 3.6 锁信息记录在对象的哪个地方?
- 4. Java 中如何判断多个线程竞争的是不是同一把锁?
- 4.1 执行不同对象的synchronized方法,修改全局变量
- 4.2 使用单独的锁对象
- 4.3 在多个实例中使用单独的锁对象
- 4.4 单个实例中,创建两个方法,使用同一个锁对象
- 4.5 使用静态全局对象做为锁对象
- 4.6 用类对象做为锁对象 (推荐)
- 4.7 使用String.class做为锁对象
- 4.8 小结
- 5. synchronized的特性
1. 代码案例
案例:两个线程,分别对 count 变量++50000次,查看运行结果
public class Demo401 {public static void main(String[] args) throws InterruptedException {Counter401 counter = new Counter401();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + counter.count);}
}class Counter401 {public int count = 0;public void increase() {count++;}
}
第一反应的运行结果应该是 100000,但真是的运行结果:
可以看到运行结果并不是 100000, 且每次运行结果都不一样!
当多个线程修改同一个变量,就会出现线程安全问题!
2. 出现线程安全问题的原因
-
- 线程是抢占式执行的
线程的执行顺序无法人为控制,线程的抢占式执行是造成线程安全问题的主要原因,完全是由CPU自己调度,无法人为干预。
- 线程是抢占式执行的
-
- 多个线程修改同一个变量
多个线程修改同一个变量会出现线程安全问题;
多个线程修改不同变量不会出现线程安全问题;
一个线程修改一个变量,也不会出现线程安全问题。
- 多个线程修改同一个变量
-
- 指令执行的过程中不能保证原子性
指令要么全都执行,要么全都不执行
- 指令执行的过程中不能保证原子性
上述代码中的 count++ 对应的是多条CPU指令:
1 . LOAD(从内存或寄存器中读取count的值);
2 . ADD(自增);
3 . STORE(把计算结果写回主内存)
两个线程指令的执行顺序可能为:
出现线程安全问题的指令执行过程演示:
假设CPU调度如图:
指令执行详细过程:
可以看出,虽然ADD两次,但是刷回主内存中的值还是 1,这就发生了线程安全问题。
-
- 线程修改共享变量时内存不可见 (内存可见性)
Java 内存模型 :JMM
- 线程修改共享变量时内存不可见 (内存可见性)
- Java线程首先从主内存读取变量的值到自己的工作内存(工作内存是JAVA层面对物理层面的关于程序所使用到的寄存器的抽象);
- 每个线程都有自己的工作内存,且线程工作内存之间是隔离的;
- 线程在自己的工作内存中把值修改完成之后再把修改后的值写回主内存;
以上执行的count++操作,由于是两个线程在执行,每个线程都有自己的工作内存,且相互之间不可见,最终导致了线程安全问题。
-
- 程序在编译执行的时候可能会出现指令重排序(有序性)
我们写的代码在编译之后可能会与代码对应的指令顺序不同,这个过程就是指令重排序,代码在JVM可能会重排,CPU执行指令是也会重排,目的就是提高代码执行效率。
3. synchronized
3.1 锁的概念
线程A拿到了锁,别的线程如果要执行被锁住的代码,必须要等到线程A释放锁,如果线程A没有释放锁,那么别的线程只能阻塞等待,这个状态就是BLOCK
指令执行过程就是 : 先拿锁 --> 执行代码 --> 释放锁 --> 下一个线程再拿锁…
举个例子: 第一个人去卫生间,把门锁上了,下一个人来卫生间就要看看卫生间上没上锁,如果上锁了就必须要等到前一个人打开锁出来才能进去使用卫生间。
3.2 为方法加 synchronized
synchronized 可以去修饰方法,也可以修饰代码块
class Counter401 {public int count = 0;public synchronized void increase() {count++;}
}
执行结果:
执行结果符合预期,线程安全问题解决了。
3.3 指令执行过程
从图中可以看出,t1 获取了锁资源,那么只能等到 t1 的指令执行完成释放锁资源之后,t2 才有机会获取锁资源。
因为 t1 释放锁之后,也有可能第二次循环时 t1 先于 t2 拿到锁,因为线程是抢占式执行的。
3.4 小结(关于synchronized)
- synchronized解决了原子性的问题,它所修饰的代码块变成了串行执行!把多线程转成了单线程的问题,解决了线程安全问题。
注意: 不要把锁定和CPU调度搞混了,保证原子性说的是所有指令不执行完不释放锁,但中途可能会被调出CPU,这时其他线程在获取锁的时候会阻塞。
-
synchronized 实现了内存可见性,但是并没有对内存可见性做技术上的处理,仅仅是实现了。
-
synchronized不保证有序性(不会禁止指令重排序)
3.5 只给一个线程加锁也会出现线程安全问题
public class Demo402 {public static void main(String[] args) throws InterruptedException {Counter402 counter = new Counter402();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase1();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + counter.count);}
}class Counter402 {public int count = 0;public synchronized void increase() { // 只给一个线程加 synchronizedcount++;}public void increase1() {count++;}
}
执行结果:显然存在线程安全问题。
线程获取锁:
1.如果只有一个线程A,那么直接可以获取锁,没有锁竞争
2.线程A和线程B共同抢一把锁的时候,存在锁竞争,谁先拿到就先执行自己的逻辑,另外一个
线程阻塞等待,等到持有锁的线程释放锁之后,再参与锁竞争
3.线程A与线程B竞争的不是同一把锁?他们之间没有竞争关系
3.6 锁信息记录在对象的哪个地方?
在Java虚拟机中,对象在内存中的结构可以划分为4个区域:
- markword:对象头:记录锁信息GC(垃圾回收)次数,程序计数器 (大小为8BYTE)
- 类型指针:当前的对象是哪个类 (大小为4BYTE)
- 实例数据:成员变量 (大小不定)
- 对齐填充:一个对象所的占的内存必须是8byte的整数倍 (大小不定)
4. Java 中如何判断多个线程竞争的是不是同一把锁?
如何描述一把锁,锁与线程之间如何关联?
锁对象本身就是一个简单的对象,任何对象都可以作为锁对象。
锁对象中记录了获取到锁的线程信息(记录的是线程的地址)
4.1 执行不同对象的synchronized方法,修改全局变量
代码:
public class Demo403 {public static void main(String[] args) throws InterruptedException {Counter403 counter = new Counter403();Counter403 counter1 = new Counter403();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase(); // 锁对象是 counter}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter1.increase(); // 锁对象是 counter1}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + Counter403.count);}
}class Counter403 {public static int count = 0;public synchronized void increase() {count++;}}
执行结果:不及预期
原因:两个锁对象是不同的实例,也就是两个线程的锁对象不同,没有锁竞争关系,因此存在线程安全问题。
4.2 使用单独的锁对象
代码:
public class Demo404 {public static void main(String[] args) throws InterruptedException {Counter404 counter = new Counter404();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + counter.count);}
}class Counter404 {public static int count = 0;Object locker = new Object(); // 锁对象public void increase() {synchronized (locker) { // 使用 lockercount++;}}}
执行结果:符合预期
线程在锁竞争的时候通过 locker 这个对象记录线程信息
Counter 中有一个 locker ,每创建一个 counter 都会初始化一个对象内部的成员变量 locker , counter 和 locker 是一一对应的。
4.3 在多个实例中使用单独的锁对象
代码:
public class Demo405 {public static void main(String[] args) throws InterruptedException {Counter405 counter = new Counter405();Counter405 counter1 = new Counter405();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase(); // counter}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter1.increase(); // counter1}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + counter.count);}
}class Counter405 {public static int count = 0;Object locker = new Object();public void increase() {synchronized (locker) {count++;}}}
执行结果:
原因:
每一个Counter中走有一个locker,使用的不是同一个锁对象,不存在锁竞争关系,存在线程安全问题。
4.4 单个实例中,创建两个方法,使用同一个锁对象
代码:
public class Demo406 {public static void main(String[] args) throws InterruptedException {Counter406 counter = new Counter406();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase1();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + counter.count);}
}class Counter406 {public static int count = 0;Object locker = new Object();public void increase() {synchronized (locker) { // 使用的都是同一个锁对象count++;}}public void increase1() {synchronized (locker) { // 使用的都是同一个锁对象count++;}}}
执行结果:
原因:虽然方法不同,但是使用的是同一个实例中的同一个锁对象,存在锁竞争关系
4.5 使用静态全局对象做为锁对象
代码:
public class Demo407 {public static void main(String[] args) throws InterruptedException {Counter407 counter = new Counter407();Counter407 counter1 = new Counter407();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter1.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + counter.count);}
}class Counter407 {public static int count = 0;static Object locker = new Object(); // 属于类的全局对象public void increase() {synchronized (locker) {count++;}}}
执行结果:
原因:static 修饰的对象是属于类的,全局唯一,在所有实例对象之间共享,因此两个线程使用的是同一个锁对象,存在锁竞争关系
4.6 用类对象做为锁对象 (推荐)
代码:
public class Demo408 {public static void main(String[] args) throws InterruptedException {Counter407 counter = new Counter407();Counter407 counter1 = new Counter407();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter1.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + counter.count);}
}class Counter408 {public static int count = 0;public void increase() {synchronized (Counter408.class) { // 使用类对象count++;}}}
执行结果:
原因:类对象全局唯一,作为锁对象存在锁竞争
4.7 使用String.class做为锁对象
代码:
public class Demo409 {public static void main(String[] args) throws InterruptedException {Counter407 counter = new Counter407();Counter407 counter1 = new Counter407();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter1.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + counter.count);}
}class Counter409 {public static int count = 0;public void increase() {synchronized (String.class) { // 使用 String 类对象count++;}}}
执行结果:
原因:使用的也是一个类对象,且全局唯一
4.8 小结
任何一个对象都可以做为锁对象,只要多个线程访问的锁对象是同一个,那么他们就存在竞争关系,否则没有竞争关系
5. synchronized的特性
1)互斥
一个线程获取了锁之后,其他线程必须要阻塞等待,只有当持有锁的线程把锁释放了之后,所有线程再去竞争锁
2)可重入
对于同一个锁对象和同一个线程,如果可以重复加锁,称之为不互斥,称之为可重入;
对于同一个锁对象和同一个线程,如果不可以重复加锁,称之为互斥,就会形成死锁。
3)可见性
从结果上看是达到内存可见性的目的,但是是通过原子性来实现的