八、并发编程

170 阅读26分钟

为什么线程不安全?

线程不安全通常是指在多线程环境下,对于共享的数据(比如同一个对象的属性或者静态变量等)进行读写操作时,由于线程之间的竞争和交错执行,导致数据出现异常的情况,即数据不一致、丢失、重复等问题。

线程不安全的原因有很多,主要包括以下几点:

  1. 竞态条件:当多个线程同时对共享的数据进行修改时,由于线程之间执行顺序的不确定性,可能会导致结果出现异常。
  2. 数据依赖:当多个线程对共享数据进行读写时,读线程可能会读取到未被写线程修改的数据,导致结果不正确。
  3. 缓存一致性:当多个线程对共享数据进行读写时,由于CPU缓存的存在,可能会导致不同线程之间的数据不一致。
  4. 访问共享资源的方式不正确:比如在多线程环境下,如果对同一个对象的属性进行并发读写操作,而没有采用同步措施,则会导致线程不安全的问题。

为了解决线程不安全的问题,需要采用线程同步的机制来保证多线程访问共享数据时的互斥性、可见性和有序性。常见的线程同步机制包括 synchronized 关键字、Lock 接口、volatile 关键字等。

并发编程有哪些特性?

当多个线程同时访问共享资源时,会出现原子性、可见性和有序性问题,并发编程三个重要特性:

  • 原子性:一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。

    使用原子类,例如AtomicInteger,AtomicLong,AtomicBoolean等(种原子类是利用 CAS 操作,也可能也会用到 volatile或者final关键字,来保证原子操作),或者使用synchronized关键字或Lock锁来保证代码块的原子性。

  • 可见性:当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。

    使用volatile关键字来保证变量的可见性,当一个变量被volatile修饰后,线程每次访问该变量都会从内存中读取,而不是从缓存中读取,从而保证了变量的可见性。另外,使用synchronized关键字或Lock锁也可以保证变量的可见性。如果我们将变量声明为volatile,这就指示JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。

  • 有序性:由于指令重排序问题,代码的执行顺序未必就是编写代码时候的顺序。我

    使用volatile关键字来保证指令的有序性,当一个变量被volatile修饰后,它前面的所有指令都会在它之前执行,而它后面的所有指令都会在它之后执行,从而保证了指令的有序性。此外,使用synchronized关键字或Lock锁也可以保证指令的有序性。volatile 关键字可以禁止指令进行重排序优化。

1、线程、进程

(1)进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。

(2)线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。

1.1 创建线程的方式

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口:如果需要线程执行完得到一个返回值,选择此方式
  • ThreadPoolExecutor线程池

1.1.1 继承Thread类

public class MyThread extends Thread{
    @Override
    public void run() {
//        super.run();
        System.out.println("这是一个线程++++++++++++");
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }

}

1.1.2 实现Runnable接口

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("这是一个线程++++++++++++");
    }

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

1.1.3 实现Callable接口

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("这是一个线程:" + System.currentTimeMillis());
        Thread.sleep(5000);
        return "这是一个线程++++++++++++";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable();
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<String> future = executorService.submit(myCallable);
        future.get();// Future.get()是一个阻塞方法,会阻塞主线程
        System.out.println("执行结束:" + System.currentTimeMillis());
    }


}
这是一个线程:1677682413493
执行结束:1677682418499

1.1.4 线程池

1、通过 Executors 创建

newFixedThreadPool
// 1、Executors.newFixedThreadPool()	
// 创建一个大小固定的线程池,可控制并发的线程数,超出的线程会在队列中等待
public static void main(String[] args) {

    // 创建 2 个线程的线程池
    ExecutorService threadPool = Executors.newFixedThreadPool(2);
    // 创建任务
    Runnable runnable = () -> System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
    // 线程池执行任务(一次添加 8 个任务)
    threadPool.execute(runnable);
    threadPool.execute(runnable);
    threadPool.execute(runnable);
    threadPool.execute(runnable);
    threadPool.execute(runnable);
    threadPool.execute(runnable);
    threadPool.execute(runnable);
    threadPool.execute(runnable);
}

    
任务被执行,线程:pool-1-thread-1
任务被执行,线程:pool-1-thread-2
任务被执行,线程:pool-1-thread-1
任务被执行,线程:pool-1-thread-2
任务被执行,线程:pool-1-thread-2
任务被执行,线程:pool-1-thread-1
任务被执行,线程:pool-1-thread-2
任务被执行,线程:pool-1-thread-1
newCachedThreadPool

CachedThreadPool 是根据短时间的任务量来决定创建的线程数量的,所以它适合短时间内有突发大量任务的处理场景。

// 2、Executors.newCachedThreadPool()	
// 创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程
public static void main(String[] args) {
    // 创建线程池
    ExecutorService threadPool = Executors.newCachedThreadPool();
    // 执行任务
    for (int i = 0; i < 5; i++) {
        threadPool.execute(() -> {
            System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
        });
    }
}

任务被执行,线程:pool-1-thread-1
任务被执行,线程:pool-1-thread-3
任务被执行,线程:pool-1-thread-2
任务被执行,线程:pool-1-thread-5
任务被执行,线程:pool-1-thread-4
newSingleThreadExecutor
// 3、Executors.newSingleThreadExecutor()  
// 创建单个线程的线程池,可以保证先进先出的执行顺序
public static void main(String[] args) {

    // 创建线程池
    ExecutorService threadPool = Executors.newSingleThreadExecutor();
    // 执行任务
    for (int i = 0; i < 10; i++) {
        int index = i;
        threadPool.execute(() -> {
            System.out.println(index + ": 任务被执行: " + Thread.currentThread().getName());
        });
    }
}
0: 任务被执行: pool-1-thread-1
1: 任务被执行: pool-1-thread-1
2: 任务被执行: pool-1-thread-1
3: 任务被执行: pool-1-thread-1
4: 任务被执行: pool-1-thread-1
5: 任务被执行: pool-1-thread-1
6: 任务被执行: pool-1-thread-1
7: 任务被执行: pool-1-thread-1
8: 任务被执行: pool-1-thread-1
9: 任务被执行: pool-1-thread-1
newScheduledThreadPool
// 4、Executors.newScheduledThreadPool()
// 创建一个可以执行延迟任务的线程池
public static void main(String[] args) {
    // 创建线程池
    ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(5);
    // 添加定时执行任务(1s 后执行)
    System.out.println("添加任务,时间:" + new Date());
    threadPool.schedule(() -> {
        System.out.println("任务被执行,时间:" + new Date());
    }, 2, TimeUnit.SECONDS);
}

添加任务,时间:Wed Mar 01 23:40:08 CST 2023
任务被执行,时间:Wed Mar 01 23:40:10 CST 2023
newSingleThreadScheduledExecutor
// 5、Executors.newSingleThreadScheduledExecutor()
// 创建一个单线程的可以执行延迟任务的线程池
public static void main(String[] args) {
    // 创建线程池
    ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor();
    // 添加定时执行任务(2s 后执行)
    System.out.println("添加任务,时间:" + new Date());
    threadPool.schedule(() -> {
        System.out.println("任务被执行,时间:" + new Date());
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
        }
    }, 2, TimeUnit.SECONDS);
}

添加任务,时间:Wed Mar 01 23:41:05 CST 2023
任务被执行,时间:Wed Mar 01 23:41:07 CST 2023
newWorkStealingPool
// 6、Executors.newWorkStealingPool()
// 创建一个抢占式执行的线程池
// 需要注意的是此方法是 JDK 1.8 版本新增的,所以 1.8 版本之前的程序中不能使用。
public static void main(String[] args) {
    // 创建线程池
    ExecutorService threadPool = Executors.newWorkStealingPool();
    // 执行任务
    for (int i = 0; i < 10; i++) {
        final int index = i;
        threadPool.execute(() -> {
            System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName());
        });
    }
    // 确保任务执行完成
    while (!threadPool.isTerminated()) {
    }
}


 0被执行,线程名:ForkJoinPool-1-worker-1
 5被执行,线程名:ForkJoinPool-1-worker-6
 2被执行,线程名:ForkJoinPool-1-worker-3
 8被执行,线程名:ForkJoinPool-1-worker-3
 9被执行,线程名:ForkJoinPool-1-worker-3
 1被执行,线程名:ForkJoinPool-1-worker-2 
 4被执行,线程名:ForkJoinPool-1-worker-1
 3被执行,线程名:ForkJoinPool-1-worker-4
 7被执行,线程名:ForkJoinPool-1-worker-6
 6被执行,线程名:ForkJoinPool-1-worker-5

2、通过 ThreadPoolExecutor 创建

public static void main(String[] args) {

    // 创建线程池
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10,
            100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
    // 执行任务
    for (int i = 0; i < 10; i++) {
        final int index = i;
        threadPool.execute(() -> {
            System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName());
        });
    }
}

0 被执行,线程名:pool-1-thread-1
1 被执行,线程名:pool-1-thread-2
2 被执行,线程名:pool-1-thread-3
3 被执行,线程名:pool-1-thread-4
4 被执行,线程名:pool-1-thread-5
5 被执行,线程名:pool-1-thread-6
6 被执行,线程名:pool-1-thread-7
7 被执行,线程名:pool-1-thread-8
8 被执行,线程名:pool-1-thread-9
9 被执行,线程名:pool-1-thread-10

2、为什么要使用多线程呢?

多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。 利用好多线程机制可以大大提高系统整体的并发能力以及性能。 在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。

3、线程的生命周期和状态?

NEW: 初始状态,线程被创建出来但没有被调用 start() 。

RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。

BLOCKED :阻塞状态,需要等待锁释放。

WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。

TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。

TERMINATED:终止状态,表示该线程已经运行完毕。

(1)线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。

在操作系统层面,线程有 READY 和 RUNNING 状态;而在 JVM 层面,只能看到 RUNNABLE 状态,所以 Java系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。

为什么 JVM 没有区分这两种状态呢

现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是用所谓的“时间分片(time quantum or time slice)”方式进行抢占式(preemptive)轮转调度(round-robin 式)。这个时间分片通常是很小的,一个线程一次最多只能在 CPU 上运行比如 10-20ms 的时间(此时处于 running 状态),也即大概只有 0.01 秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到 ready 状态)。线程切换的如此之快,区分这两种状态就没什么意义了。

(2)当线程执行wait()方法之后,线程进入WAITING(等待)状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。

(3)TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。

(4)当线程进入synchronized 方法/块或者调用wait后(被notify)重新进入synchronized 方法/块,但是锁被其它线程占有,这个时候线程就会进入BLOCKED(阻塞)状态。

(5)线程在执行完了run()方法之后将会进入到 TERMINATED(终止) 状态。

4、什么是上下文切换?

线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。

(1)主动让出 CPU,比如调用了 sleep(), wait() 等。

(2)时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。

(3)调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。

(4)被终止或结束运行

这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场* *。并加载下一个将要占用 CPU 的线程上下文。**这就是所谓的上下文切换。上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。

5、什么是线程死锁?如何避免死锁?

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

产生死锁的四个必要条件

(1)互斥条件:该资源任意一个时刻只由一个线程占用。

(2)请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。

(3)不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。

(4)循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

破坏死锁的产生的必要条件

(1)破坏请求与保持条件 :一次性申请所有的资源。

(2)破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

(3)破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

6、sleep() 方法和 wait() 方法对比

(1)sleep() 方法没有释放锁,而 wait() 方法释放了锁 。

(2)wait() 通常被用于线程间交互/通信,sleep()通常被用于暂停执行。

(3)wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。

(4)sleep() 是 Thread 类的静态本地方法,wait() 则是 Object 类的本地方法。

为什么 wait() 方法不定义在 Thread 中?sleep() 方法定义在 Thread 中?

(1)wait()是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(Object)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object)而非当前的线程(Thread)。

(2)sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。

7、可以直接调用 Thread 类的 run 方法吗?

new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

8、volatile

volatile 关键字其实并非是 Java 语言特有的,在 C 语言里也有,它最原始的意义就是禁用 CPU 缓存。如果我们将一个变量使用 volatile 修饰,这就指示 编译器,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。 ​ 在 Java 中,volatile 关键字除了可以保证变量的可见性,还有一个重要的作用就是防止 JVM 的指令重排序。 如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。

参考 【第七章 一个变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存之间的实现细节

9、synchronized

synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁; synchronized 关键字加到实例方法上是给对象实例上锁; 尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能。

构造方法不能使用 synchronized 关键字修饰。 构造方法本身就属于线程安全的,不存在同步的构造方法一说。

synchronized 加锁过程?或者说synchronized怎么实现加锁的?

synchronized 是 Java 中常用的一种同步机制,用于实现线程之间的同步。在 Java 中,每个对象都有一个内部锁,也称为监视器锁,通过使用 synchronized 可以获得该对象的内部锁。

加锁过程如下:

  1. 线程尝试获得锁,如果锁未被其他线程占用,则成功获取锁,进入临界区。如果锁已经被占用,则进入阻塞状态等待锁释放。
  2. 如果获取锁的线程在临界区内执行时间过长或者发生了异常,会导致其他线程一直处于等待状态,这时候可以使用 try...finally 块确保锁一定能够被释放。
  3. 当线程执行完临界区的代码后,会释放锁,其他线程便可以尝试获取锁,进入临界区。

需要注意的是,synchronized 在锁的获取和释放过程中都会涉及到一定的性能开销,因此在使用时需要避免出现不必要的竞争和死锁等问题。

synchronized 底层原理了解吗?

synchronized 底层原理了解吗?

synchronized 关键字底层原理属于 JVM 层面的东西。

(1)synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行monitorenter指令时,线程试图获取锁也就是获取对象监视器monitor的持有权。在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。 对象锁的的拥有者线程才可以执行monitorexit指令来释放锁。在执行monitorexit指令后,将锁计数器设为0,表明锁被释放,其他线程可以尝试获取锁。

(2)synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。

9.1 synchronized 和 volatile 有什么区别?

synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!

(1)volatile 关键字是线程同步的轻量级实现,所以 volatile性能肯定比synchronized关键字要好 。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。

(2)volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。

(3)volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

9.2 JVM对synchronized的锁优化

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。锁的状态总共有四种,级别从低到高依次是:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以升级但不能降级

9.2.1 偏向锁

偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提升程序的性能。

对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的

9.2.2 轻量级锁

偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

9.2.3 自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

9.2.4 锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

public class StringBufferRemoveSync {
​
    public void add(String str1, String str2) {
        //StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
        //因此sb属于不可能共享的资源,JVM会自动消除内部的锁
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }
​
    public static void main(String[] args) {
        StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
        for (int i = 0; i < 10000000; i++) {
            rmsync.add("abc", "123");
        }
    }
​
}

10、synchronized 和 ReentrantLock ?

  1. 底层实现方式区别:synchronized 是 JVM 实现的,而 Lock 是基于 Java 语言实现的。
  2. 语法层面区别:synchronized 是 Java 关键字,直接加在方法或代码块上,进入同步代码块或同步方法时自动获取锁,并在退出同步代码块或同步方法时释放锁,而 Lock 是一个 Java 接口,需要显式地定义锁的获取和释放。
  3. 锁的可重入性:两者都是可重入锁。
  4. 可中断性:Lock 提供了可中断性,即在等待锁的过程中可以响应中断;而 synchronized 则不支持。
  5. 公平性:synchronized 不保证线程获得锁的公平性,即不保证等待时间最长的线程优先获得锁;而 Lock 可以通过构造函数选择公平锁或非公平锁。
  6. 可实现选择性通知(锁可以绑定多个条件): synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。
  7. 锁的粒度:synchronized 的粒度比较粗,只能对整个方法或代码块进行加锁,而 Lock 可以细粒度地控制锁的获取和释放,可以对不同的资源进行加锁。
  8. 性能表现:在低竞争情况下,synchronized 的性能表现比 Lock 更好,因为 synchronized 是 JVM 原生实现的,而 Lock 需要调用 Java 方法,存在一定的性能开销。但在高竞争情况下,Lock 的性能表现会更好,因为它可以提供更细粒度的锁控制,减少线程的阻塞时间,从而提高并发效率。

总的来说,synchronized 使用起来比较方便,但是锁的粒度比较粗,可中断性和公平性也不够灵活;而 Lock 则需要手动获取和释放锁,使用起来相对复杂,但是锁的粒度比较细,可中断性和公平性也更加灵活。

项目中有用过synchronized吗?有什么需要注意的?

作为一种Java中的同步机制,synchronized在多线程并发的环境下经常会被用到。如果在项目中使用synchronized需要注意以下几点:

  1. 确定加锁范围:synchronized的作用是加锁,防止多个线程同时访问临界区,所以需要明确加锁的范围,避免不必要的锁竞争。
  2. 避免死锁:当多个线程都在等待某个对象的锁时,就会出现死锁,导致程序无法继续执行。因此需要谨慎地使用synchronized,尽可能减少锁竞争的情况。
  3. 注意性能问题:synchronized在加锁和释放锁的过程中,会对性能产生影响,因此需要在代码实现上尽可能地减少加锁的频率,避免影响程序的性能。
  4. 避免过度同步:过度的同步会导致线程阻塞,降低程序的效率,因此需要在实现时根据具体情况,选择适当的同步机制。
  5. 线程安全问题:在多线程环境下,需要保证共享资源的安全,避免出现线程安全问题,例如线程间的数据竞争、重排序等问题。

总之,在使用synchronized时需要注意以上几个方面,避免出现不必要的问题。同时,为了更好地管理和控制线程,可以考虑使用更加高级的并发机制,例如Lock、Semaphore、CountDownLatch等。

公平锁和非公平锁

公平锁 : 锁被释放之后,先申请的线程/进程先得到锁。

非公平锁 :锁被释放之后,后申请的线程/进程可能会先获取到锁,是随机或者按照其他优先级排序的。

关于 Condition接口的补充

Condition是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是 Condition 接口默认提供的。而synchronized关键字就相当于整个 Lock 对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程,这样会造成很大的效率问题。而Condition实例的signalAll()方法,只会唤醒注册在该Condition实例中的所有等待线程。