【Java进阶笔记】synchronized原理(悲观锁和乐观锁)



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 (
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值