天天看点

为什么 95% 的 Java 程序员,都是用不好 Synchronized?

作者:互联网技术学堂

引言

Synchronized 是 Java 中常用的同步机制,用于确保多个线程在访问共享资源时的互斥性。然而,许多 Java 程序员都不了解 Synchronized 的优化技巧和原理,从而导致程序性能和并发性能的下降。在本篇技术博客中,我们将详细探讨 Synchronized 的锁优化和获取锁的流程,并提供相关的 Java 代码示例。

大家好,这里是互联网技术学堂,留下你的点赞、关注、分享,支持一下吧,谢谢。

为什么 95% 的 Java 程序员,都是用不好 Synchronized?

章节一:Synchronized 的锁优化

Synchronized 的锁优化主要包括以下几个方面:

1. 细粒度锁

在实际应用中,对于一些共享资源,我们可以采用细粒度锁来提高并发性能。比如,在一个对象中,有多个方法需要访问同一个共享资源,但这些方法并不是同时执行的,我们可以将这些方法中需要访问共享资源的代码块加上 Synchronized 关键字,以达到保证共享资源的互斥性的目的。这样做的好处是,可以减小锁的粒度,提高并发性能。

示例代码:

public class Counter {
  private int count;

  public synchronized void increment() {
    count++;
  }

  public synchronized void decrement() {
    count--;
  }
}           

上述代码中,使用 Synchronized 关键字修饰了 increment 和 decrement 方法,以确保对 count 的操作是原子性的。这样做的缺点是,如果在高并发的情况下,所有的线程都需要获取 Counter 对象的锁,会导致性能下降。因此,我们可以采用下面介绍的锁优化技巧来提高并发性能。

2. 锁粗化

锁粗化是指将多个连续的加锁、解锁操作合并成一个较大的代码块,以减少加锁、解锁的次数,从而提高并发性能。在实际应用中,如果发现某个对象的多个方法都需要加锁、解锁,可以将它们合并成一个大的代码块,这样做的好处是减少了加锁、解锁的次数,从而减少了线程间的竞争,提高了并发性能。

示例代码:

public class Counter {
  private int count;

  public void increment() {
    synchronized (this) {
      count++;
    }
  }

  public void decrement() {
    synchronized (this) {
      count--;
    }
  }
}           

上述代码中,将 increment 和 decrement 方法中的 Synchronized 关键字去掉,并在方法中加上一个同步代码块,以确保对 count 的操作是原子性的。这样做的好处是,避免了在高并发情况下,所有的线程都需要获取 Counter 对象的锁,从而提高了并发。

3. 锁分离

锁分离是指将一个对象中的多个共享资源,分别使用不同的锁来保护,以减小锁的粒度,提高并发性能。在实际应用中,如果一个对象中有多个共享资源,我们可以使用不同的锁来保护它们,这样做的好处是,可以减小锁的粒度,提高并发性能。

示例代码:

public class Counter {
  private int count1;
  private int count2;
  private Object lock1 = new Object();
  private Object lock2 = new Object();

  public void incrementCount1() {
    synchronized (lock1) {
      count1++;
    }
  }

  public void decrementCount1() {
    synchronized (lock1) {
      count1--;
    }
  }

  public void incrementCount2() {
    synchronized (lock2) {
      count2++;
    }
  }

  public void decrementCount2() {
    synchronized (lock2) {
      count2--;
    }
  }
}           

上述代码中,将 Counter 对象中的 count1 和 count2 分别使用 lock1 和 lock2 两个对象来保护,以确保对它们的操作是原子性的。这样做的好处是,避免了在高并发情况下,所有的线程都需要获取 Counter 对象的锁,从而提高了并发性能。

为什么 95% 的 Java 程序员,都是用不好 Synchronized?

4. 自旋锁

自旋锁是指在获取锁失败时,线程不会被阻塞,而是会不断地尝试获取锁,直到获取成功为止。在低并发的情况下,自旋锁可以减少线程上下文切换的开销,提高并发性能。但是,在高并发的情况下,自旋锁会占用大量的 CPU 资源,从而导致性能下降。

示例代码:

public class Counter {
  private int count;
  private volatile boolean lock = false;

  public void increment() {
    while (lock) {
      // 自旋等待锁释放
    }
    lock = true;
    count++;
    lock = false;
  }

  public void decrement() {
    while (lock) {
      // 自旋等待锁释放
    }
    lock = true;
    count--;
    lock = false;
  }
}           

章节二:获取锁的流程分析

在使用 Synchronized 时,线程获取锁的流程分为以下几个步骤:

1. 尝试获取锁

当一个线程执行到 Synchronized 代码块或方法时,它会尝试获取锁。

2. 获取锁成功

轻量级锁的释放也是通过 CAS 操作来进行的,主要步骤如下:

  1. 取出在获取轻量级锁保存在 Displaced Mark Word 中的数据;
  2. 用 CAS 操作将取出的数据替换当前对象的 Mark Word 中,如果成功,则说明释放锁成功;
  3. 如果 CAS 操作替换失败,说明有其他线程尝试获取该锁,则需要在释放锁的同时需要唤醒被挂起的线程。

3. 获取锁失败

如果当前锁已经被其他线程持有,则当前线程会进入阻塞状态,直到获取到锁为止。

4. 阻塞状态

当线程进入阻塞状态时,它会进入一个等待队列,等待其他线程释放锁并通知它。在 Java 中,等待队列是通过内部锁对象的 wait() 和 notify() 方法实现的。

5. 通知等待线程

当一个线程释放锁并通知等待队列中的其他线程时,它会通过内部锁对象的 notify() 或 notifyAll() 方法来通知等待队列中的线程。

6. 竞争锁

当多个线程同时竞争同一个锁时,它们会进入锁池。在锁池中,所有等待锁的线程会处于阻塞状态,直到其中一个线程获取到锁并进入同步代码块或方法中。

7. 释放锁

当线程执行完同步代码块或方法后,会释放锁,并通知等待队列中的其他线程。

为什么 95% 的 Java 程序员,都是用不好 Synchronized?

章节三:Synchronized 锁优化

Synchronized 锁优化可以分为以下几个方面:

1. 减小锁粒度

减小锁粒度是指将一个大的同步代码块或方法,拆分成多个小的同步代码块或方法,以减小锁的粒度,提高并发性能。在实际应用中,如果一个对象中有多个共享资源,我们可以使用不同的锁来保护它们,这样做的好处是,可以减小锁的粒度,提高并发性能。

2. 选择合适的锁类型

在 Java 中,Synchronized 可以分为对象锁和类锁两种。如果多个线程需要竞争同一个对象的资源,我们可以使用对象锁;如果多个线程需要竞争同一个类的资源,我们可以使用类锁。在实际应用中,我们需要根据具体情况来选择合适的锁类型,以提高并发性能。

3. 使用可重入锁

可重入锁是指同一个线程可以重复获取同一个锁而不会死锁的锁。在 Java 中,Synchronized 就是一种可重入锁。如果一个线程已经获取了某个锁,那么它在获取该锁的过程中,可以重复获取多次,而不会出现死锁现象。

4. 避免过度同步

过度同步是指在同步代码块或方法中,不必要的代码也被同步了,从而导致性能下降。在实际应用中,我们应该避免过度同步,只同步必要的代码块或方法,以提高并发性能。

5. 减小同步代码块或方法的执行时间

同步代码块或方法的执行时间越短,线程在获取锁和释放锁的时间就会越短,从而提高并发性能。在实际应用中,我们可以通过将一些非同步的代码移到同步代码块或方法外,或者使用局部变量来减小同步代码块或方法的执行时间。

6. 使用 CAS 原子操作

CAS 原子操作是一种无锁并发机制,它能够提高并发性能,避免锁竞争。在 Java 中,AtomicInteger、AtomicLong、AtomicBoolean 等类就是使用 CAS 原子操作实现的。如果我们需要对一个共享变量进行增、减、比较等操作,并且不需要进行复杂的计算,那么使用 CAS 原子操作可以提高并发性能。

7. 使用分段锁

分段锁是指将一个共享资源分成多个段,对每个段分别加锁。在 Java 中,ConcurrentHashMap 就是使用分段锁来保护共享资源的。如果多个线程需要竞争同一个共享资源,并且这个共享资源可以分成多个独立的段,那么使用分段锁可以提高并发性能。

章节四:示例代码

下面是一个简单的示例代码,演示了 Synchronized 锁的使用流程和优化方式。

public class SynchronizedDemo {

    private int count = 0;

    public synchronized void add() {
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedDemo demo = new SynchronizedDemo();

        // 创建多个线程,同时对 count 变量进行加 1 操作
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    demo.add();
                }
            }).start();
        }

        // 等待所有线程执行完毕
        Thread.sleep(1000);

        // 输出 count 变量的值
        System.out.println(demo.count);
    }
}           

在上面的示例代码中,我们创建了多个线程,同时对 count 变量进行加 1 操作。由于 count 变量是共享资源,我们需要对 add() 方法进行同步,以避免多个线程同时对 count 变量进行操作。在 add() 方法中,我们使用了 synchronized 关键字来获取锁,确保在同一时刻只有一个线程可以执行该方法。在 main() 方法中,我们使用 Thread.sleep() 方法来等待所有线程执行完毕,并输出 count 变量的值。

在实际应用中,我们需要对 Synchronized 锁进行优化,以提高并发性能。我们可以根据具体情况,选择合适的锁方式和优化策略。下面是一个优化后的示例代码,演示了如何使用 ReentrantLock 来代替 synchronized 关键字。

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {

    private int count = 0;
    private ReentrantLock lock = new ReentrantLock();

    public void add() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockDemo demo = new ReentrantLockDemo();

        // 创建多个线程,同时对 count 变量进行加 1 操作
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    demo.add();
                }
            }).start();
        }

        // 等待所有线程执行完毕
        Thread.sleep(1000);

        // 输出 count 变量的值
        System.out.println(demo.count);
    }
}           

在上面的示例代码中,我们使用了 ReentrantLock 来代替 synchronized 关键字。在 add() 方法中,我们首先调用 lock() 方法获取锁,在 try-catch-finally 代码块中执行业务逻辑,最后调用 unlock() 方法释放锁。由于 ReentrantLock 是可重入锁,因此同一个线程可以多次获取锁,而不会造成死锁的情况。

总结

在实际应用中,我们需要根据具体情况,选择合适的同步方式和优化策略,以提高并发性能。我们需要根据业务逻辑的复杂程度、共享资源的访问频率和并发度、线程之间的竞争关系等因素,综合考虑选择合适的同步方式和优化策略。

继续阅读