为了保证同一数据在不同线程中被访问的正确性,Java 提供了访问时的锁机制 synchronized,当一个线程获得对象的排他锁,该线程就可以独占该资源,其他线程必须等待,直至有用锁的线程释放锁。但是存在以下问题:

  • 一个线程持有锁会导致其他所有需要锁的线程挂起
  • 在多线程竞争下,加锁和释放锁会导致比较多的上下文切换和调度延时,降低程序性能
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引发性能问题

1 synchronized 关键字

private关键字帮助我们保证了数据只能被对象的方法访问。synchronized关键字是为了保护对象属性的。保护synchronized 关键字有两种用法:

  1. synchronized 方法 public synchronized void method(int args) {}
  2. synchronized 代码块 synchronized (Obj) { ... }

1.1 synchronized 方法

synchronized 方法 :public synchronized void method(int args) {}

  • 该方法控制对“对象”的访问,每个对象对应一把锁
  • 每个 synchronized 方法都必须获得调用该方法的对象的锁才能执行,否则就会阻塞
  • 若将一个大方法声明为synchronized将会影响效率。所以明确代码临界区后,将临界区代码抽象出来作为**synchronized**方法是较好的做法。

    1. public class TestSync implements Runnable {
    2. private int numTickets = 10;
    3. private boolean flag = true;
    4. public static void main(String[] args) {
    5. TestSync testSync = new TestSync();
    6. new Thread(testSync, "lrt").start();
    7. new Thread(testSync, "dhr").start();
    8. new Thread(testSync, "yyds").start();
    9. }
    10. @Override
    11. public void run() {
    12. while (flag) {
    13. try {
    14. buy();
    15. } catch (InterruptedException e) {
    16. e.printStackTrace();
    17. }
    18. }
    19. }
    20. private void buy() throws InterruptedException {
    21. if (numTickets <= 0) {
    22. flag = false;
    23. return;
    24. }
    25. Thread.sleep(100);
    26. // Buy a ticket.
    27. System.out.println(Thread.currentThread().getName() + " 拿到了第 " + numTickets-- + " 张票");
    28. }
    29. }

执行结果:

  1. dhr 拿到了第 9 张票
  2. yyds 拿到了第 8 张票
  3. lrt 拿到了第 10 张票
  4. lrt 拿到了第 6 张票 // 重复了
  5. yyds 拿到了第 7 张票
  6. dhr 拿到了第 6 张票 // 重复了
  7. yyds 拿到了第 5 张票
  8. dhr 拿到了第 3 张票
  9. lrt 拿到了第 4 张票
  10. yyds 拿到了第 2 张票 // 重复了
  11. lrt 拿到了第 2 张票 // 重复了
  12. dhr 拿到了第 1 张票

可以发现出现了线程不安全的情况。

使用synchronized关键字,只需要将临界区方法用synchronized关键字修饰就可以,即将上面的buy()方法改成private synchronized void buy() ...即可。
使用 synchronized 方法的执行结果:

  1. lrt 拿到了第 10 张票
  2. lrt 拿到了第 9 张票
  3. lrt 拿到了第 8 张票
  4. lrt 拿到了第 7 张票
  5. lrt 拿到了第 6 张票
  6. lrt 拿到了第 5 张票
  7. lrt 拿到了第 4 张票
  8. lrt 拿到了第 3 张票
  9. yyds 拿到了第 2 张票
  10. yyds 拿到了第 1 张票

这里发现了一个问题,线程之间基本不会交替运行,而我最开始的几次测试时只有“lrt”线程执行了,其他的线程一直没有得到执行。这其实跟 CPU 的性能有关,如果 CPU 性能比较好,则需要将numTickets变量值增加才能看到线程的交替。

1.2 synchronized 代码块

synchronized 代码块:synchronized (Obj) { ... }
Obj被称为同步监视器

  • 可以是任何对象,但推荐使用共享资源作为同步监视器
  • 同步方法中无需指定同步监视器,因为其监视器就是this,或者是class(static 方法)

2 Lock 锁

image.png