上篇提到的竞态条件的发生,那么怎么解决它了?一般有两种解决方式,阻塞式和非阻塞式。

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

    synchronized关键字

    synchronized关键字是 Java 内置的一个关键字,synchronized其实是一种锁,俗称对象锁,它采用的互斥的方式是同一时刻最多只有一个线程能够获得【对象锁】,其它线程如果还想要获得这个对象锁就会进入【阻塞】。这样就可以保证持有对象锁的线程可以安全的执行临界区内的代码了,不用担心线程上下文的切换而导致竞态条件的出现。

    注意: 虽然 java 中互斥和同步都可以采用 synchronized 关键字来实现,但它们还是有区别的:

    1. 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
    2. 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

image.png
synchronized可以修饰代码块,也可以修饰方法,其修饰代码块的语法如下:

  1. // 线程1, 线程2(blocked)
  2. synchronized(对象) {
  3. // 临界区
  4. }

synchronized修饰代码块

解决事故

被 synchronized 关键字修饰的代码块就是前面提到的临界区。我们将上篇中的对共享变量操作的代码使用 synchronized 关键字修饰,使得它成为临界区,但是,我们还需要创建一个对象,让执行临界区的线程持有这个对象的锁,另一个线程就只能阻塞,这样就可以解决同步问题了。如:

  1. @Slf4j(topic = "c.ShareTest")
  2. public class ShareTest {
  3. static int counter = 0;
  4. static Object obj = new Object();
  5. public static void main(String[] args) throws InterruptedException {
  6. Thread t1 = new Thread(() -> {
  7. for (int i = 0; i < 5000; i++) {
  8. synchronized (obj) {
  9. counter++;
  10. }
  11. }
  12. }, "t1");
  13. Thread t2 = new Thread(() -> {
  14. for (int i = 0; i < 5000; i++) {
  15. synchronized (obj) {
  16. counter--;
  17. }
  18. }
  19. }, "t2");
  20. t1.start();
  21. t2.start();
  22. //main线程等待t1、t2的执行完毕才去打印counter
  23. t1.join();
  24. t2.join();
  25. log.debug("{}", counter);
  26. }
  27. }

image.png
可以看到,测试结果符合预期值。
image.png
上图的room就是这里的obj了。
image.png
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。

改进解决思想

上面的解决方案可以优化,我们可以运用面向对象的思想,将对象锁封装成一个对象,如:

  1. @Slf4j(topic = "c.ShareTest")
  2. public class ShareTest {
  3. static Room room = new Room();
  4. public static void main(String[] args) throws InterruptedException {
  5. Thread t1 = new Thread(() -> {
  6. for (int i = 0; i < 5000; i++) {
  7. room.add();
  8. }
  9. }, "t1");
  10. Thread t2 = new Thread(() -> {
  11. for (int i = 0; i < 5000; i++) {
  12. room.sub();
  13. }
  14. }, "t2");
  15. t1.start();
  16. t2.start();
  17. //main线程等待t1、t2的执行完毕才去打印counter
  18. t1.join();
  19. t2.join();
  20. log.debug("{}", room.getCounter());
  21. }
  22. }
  23. class Room {
  24. private int counter = 0;
  25. public void add() {
  26. synchronized (this) {
  27. counter++;
  28. }
  29. }
  30. public void sub() {
  31. synchronized (this) {
  32. counter--;
  33. }
  34. }
  35. public int getCounter() {
  36. return counter;
  37. }
  38. }

其中,this就是指的调用这个方法的当前对象,也就是上面的 room 对象,因此 room 就是一个对象锁对象,线程可以获取 room 的对象锁。测试结果如下:
image.png

synchronized修饰方法

synchronized关键字修饰方法其实完全可以和修饰代码块等价,如:
image.png
image.png
因此下面这样其实是一样的:
image.png
image.png