深入浅出synchronized(一)

133 阅读4分钟

「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战

介绍

多个线程对共享资源的进行读写操作的时候,由于cpu指令执行的顺序不同,导致每次的结果可能不一样。为了解决这一问题,可以用加锁的方式解决。

临界区和竞态条件

临界区: 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。

竞态条件: 多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量

synchronized加锁

synchronized让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换,保证了临界区代码的原子性。

可以做如下类比:

  • synchronized(对象)中的对象好比是房间,每个房间只有一把钥匙,多个线程相当于人,人需要有钥匙(持有锁)才能进入房间。
  • 当线程t1执行临界区时,相当于第一个人拿到了钥匙进入房间做事情。
  • 当线程t2也运行到了临界区时,相当于第二个人来到房间门口,由于没有钥匙,只能阻塞住。
  • 如果在这中间线程t1的cpu时间片不幸用完,它会被踢出门外(不要以为持有了锁,它就会一直执行下去),t1仍然拿着钥匙,下次t1再次被cpu分配到时间片时,它继续开门进入工作。
  • 当t1执行完sychronized的代码,它会开门,交出钥匙,同时唤醒其他阻塞的线程,让他们竞争钥匙,进门工作。

synchronized实践

synchronized加在方法上,相当于对当前对象加锁,synchronized加在静态方法上,相当于对类对象加锁。进行synchronized的加锁分析时,主要看不同的线程是否持有同一把锁,或者拿上面的比方来说,是不是进入同一个房间。如果是类对象,那么一个类就是一个房间,对象的话,一个对象是同一个房间。

synchronized加在方法上

public class SychronizedTest {
    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(()->{ n1.a(); }).start();
        new Thread(()->{ n1.b(); }).start();
    }
}

@Slf4j
class Number{
    @SneakyThrows
    public synchronized void a() {
        log.debug("a start......");
        Thread.sleep(1000);
        log.debug("a end......");
    }
    @SneakyThrows
    public synchronized void b() {
        log.debug("b start......");
        Thread.sleep(1000);
        log.debug("b end......");
    }
}

结果:

synchronized加在方法上相当于对当前这个对象加锁,他们想要进的是同一个房间(同一把锁)。


synchronized加在其中一个方法上

public class SychronizedTest2 {
    public static void main(String[] args) {
        Number2 n1 = new Number2();
        new Thread(() -> {
            n1.a();
        }).start();
        new Thread(() -> {
            n1.b();
        }).start();
    }
}

@Slf4j
class Number2 {
    @SneakyThrows
    public synchronized void a() {
        log.debug("a start......");
        Thread.sleep(1000);
        log.debug("a end......");
    }

    @SneakyThrows
    public void b() {
        log.debug("b start......");
        Thread.sleep(1000);
        log.debug("b end......");
    }
}

结果:

方法a加了锁,方法b未加锁,所以两个线程没有持有同一个锁,所以不会阻塞。

synchronized加在不同锁上

public class SychronizedTest3 {
    public static void main(String[] args) {
        Number3 n1 = new Number3();
        Number3 n2 = new Number3();
        new Thread(() -> {
            n1.a();
        }).start();
        new Thread(() -> {
            n2.b();
        }).start();
    }
}

@Slf4j
class Number3 {
    @SneakyThrows
    public synchronized void a() {
        log.debug("a start......");
        Thread.sleep(1000);
        log.debug("a end......");
    }

    @SneakyThrows
    public synchronized void b() {
        log.debug("b start......");
        Thread.sleep(1000);
        log.debug("b end......");
    }
}

结果:

本质上,两个线程是在不同的两个锁,也就是说进入的两个不同的房间,所以不会阻塞。

synchronized加在静态方法上

public class SychronizedTest4 {
    public static void main(String[] args) {
        Number4 n1 = new Number4();
        new Thread(() -> {
            n1.a();
        }).start();
        new Thread(() -> {
            n1.b();
        }).start();
    }
}

@Slf4j
class Number4 {
    @SneakyThrows
    public synchronized static void a() {
        log.debug("a start......");
        Thread.sleep(1000);
        log.debug("a end......");
    }

    @SneakyThrows
    public synchronized void b() {
        log.debug("b start......");
        Thread.sleep(1000);
        log.debug("b end......");
    }
}

结果:

synchronized加在静态方法上,相当于对这个类对象加锁,和加在方法上不是同一个锁,他们进入的不是同一个房间,所以不会阻塞。

synchronized全加在静态方法上

public class SychronizedTest5 {
    public static void main(String[] args) {
        Number5 n1 = new Number5();
        new Thread(() -> {
            n1.a();
        }).start();
        new Thread(() -> {
            n1.b();
        }).start();
    }
}

@Slf4j
class Number5 {
    @SneakyThrows
    public synchronized static void a() {
        log.debug("a start......");
        Thread.sleep(1000);
        log.debug("a end......");
    }

    @SneakyThrows
    public synchronized static void b() {
        log.debug("b start......");
        Thread.sleep(1000);
        log.debug("b end......");
    }
}

结果:

两个线程加的是同一个锁,这个Numer4的这个类对象的锁,所以会阻塞。