文章目录
1. 悲观锁(阻塞)
1.1. 临界区与竞态条件
1.1.1. 临界区
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块就称为临界区(Critical Section)。易发生指令交错,就会出现前面的问题。
private static int count = 0; // 共享资源
private static void increment()
// 临界区(整个代码块)
{
count++; }
private static void decrement()
// 临界区(整个代码块)
{
count--; }
1.1.2. 竞态条件
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件(Race Condition)。
⭐ 避免竞态条件的解决方案:
- 阻塞式:synchronized,lock。
- 非阻塞式:原子变量。
1.1.3. 原子性
public class ThreadTest {
private static int count = 0;
public static void main(String[] args) {
// 线程1对count自增5000次
Thread thread1 = new Thread(() -> {
// 临界区,发生了竞态条件
for (int i = 0; i < 5000; i++) count++;
});
// 线程2对count自减5000次
Thread thread2 = new Thread(() -> {
// 临界区,发生了竞态条件
for (int i = 0; i < 5000; i++) count--;
});
thread1.start();
thread2.start();
}
}
- 理想情况下,两个线程运行结束后
count == 0
。 - 实际情况下,两个线程运行结束后
count != 0
。
i++
和 i--
在 java 中不是原子操作。对于 i++
而言(i
为静态变量),实际会产生如下的 JVM 字节码指令:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
而对应 i--
也是类似:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
如果在执行指令的同时,发生了上下文切换,则可能一次自增和自减后 i!=0
。
1.2. synchronized 概念
用来给某个目标(对象,方法等)加锁,相当于不管哪一个线程运行到这个行时,都必须先检查有没有其它线程正在用这个目标,如果有就要等待正在使用的线程运行完后释放该锁,没有的话则对该目标先加锁后再运行。
public class ThreadTest {
private static int count = 0;
// 锁对象
private static Object lock = new Object;
public static void main(String[] args) {
// 线程1对count自增5000次
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) count++;
}
});
// 线程2对count自减5000次
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) count--;
}
});
thread1.start();
thread2.start();
}
}
对关键操作加上 synchronized
后结果就会正确 count = 0
。
⭐️ 对 synchronized 的理解: synchronized
实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,也不会被线程切换所打断。
1.2.1. synchronized 修饰方法
【修饰成员方法】
class Test {
public synchronized void test() {
// 临界区
}
}
// 两者在效果上等价
class Test {
public void test() {
// 对this加锁,相当于把该类对象给锁住(Test对象)
synchronized (this) {
// 临界区
}
}
}
由于是对本类对象加锁,因此当这个类有多个 synchronized
方法时,则多线程调用同一个对象的不同同步方法,会产生锁竞争。
class Test {
public synchronized void test1() {
System.out.println("test1");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void test2() {
System.out.println("test2");
}
}
// 调用代码
Test test = new Test();
// 效果:立即输出"test1"。执行test1(),会对test对象加锁,5s后释放锁
new Thread(() -> test.test1()).start();
// 效果:5s后输出"test2"。线程同步阻塞,等待test对象释放锁后才能执行
new Thread(() -> test.test2()).start();
// 效果:立即输出"test2"。新建的test对象还没有被加锁,可以立即执行
new Thread(() -> new Test().test2()).start();
【修饰静态方法】
class Test {
public synchronized static void test() {
// 临界区
}
}
// 两者在效果上等价
class Test {
public static void test() {
// 静态方法,没有实例对象,只能对类对象加锁(Test.class)
synchronized (Test.class) {
// 临界区
}
}
}
由于是对 class 类对象加锁,因此当这个类有多个 synchronized static
方法时,则多线程调用会产生锁竞争。
class Test {
public synchronized static void test1() {
System.out.println("test1");
try {
Thread.sleep(5000);
} catch (