一文搞懂JUC并发编程(6w字)。持续更新,欢迎补充~

创作不易,如果觉得写的不错就投币支持一下吧~

文章目录

一、线程和进程

1.1 进程

进程是计算机中正在执行的程序的实例。每个进程都有自己的内存空间、代码、数据和执行状态。进程是操作系统中最基本的概念之一,它负责管理和分配计算机资源,同时也是实现多任务处理的基本单位。在多道程序设计中,操作系统将 CPU 时间分配给多个进程,使得它们可以并发地运行,从而提高系统的效率。每个进程都是独立的,它们之间相互隔离,互不干扰,可以执行不同的任务。

1.2 线程

线程是进程中的一条执行路径,也是计算机执行指令的基本单位之一。一个进程可以拥有多个线程,每个线程都是独立运行的,同时可以与其他线程共享进程资源。线程拥有自己的堆栈、程序计数器和寄存器等执行状态,但共享进程的代码、数据和文件等内存资源。线程能够并发执行,从而提高了程序的效率和响应速度。线程通常可以分为用户线程和内核线程,在不同的操作系统中有不同的实现方式。

1.3 进程和线程的区别

进程和线程是计算机中不同的执行单位。

  1. 调度:操作系统将进程分配给不同的CPU核心,从而使得不同的进程可以并行执行。而每个进程又可以包含多个线程,这些线程共享进程的内存空间和资源,由操作系统调度执行,实现线程之间的并发执行。
  2. 资源管理:每个进程有自己独立的内存空间、打开的文件、I/O设备等资源。而线程只有一些基本的运行时状态,如程序计数器、寄存器、堆栈等,在共享进程内存的同时还可以共享进程的资源。因此,创建和撤销线程的开销通常比进程小得多,并且线程之间的通信也更加高效。
  3. 安全性:由于各个进程拥有独立的内存空间,因此彼此之间不会互相干扰。但线程之间共享相同的内存区域,因此如果一个线程写入了某个内存位置,另外一个线程也可能会读取到这个位置的值,所以线程之间在访问共享资源时需要进行同步和互斥。
  4. 多核利用:由于操作系统可以将多个进程分配给多个CPU核心同时执行,因此可以充分利用多核CPU提高系统的性能。而线程也可以在多核CPU上并发执行,但是需要避免线程之间的竞争条件和死锁等问题,否则可能会降低系统的性能。

1.4 线程分为哪些

  1. 用户线程(User Thread):是指需要在用户程序中自己创建的线程,它们只有在得到CPU时间片之后才能够执行。用户线程是依赖于进程而存在的线程,通过线程库实现。
  2. 内核线程(Kernel Thread):是操作系统内部维护的一种线程,与用户进程无关,是操作系统调度器直接管理和调度的线程,可以访问内核地址空间,因此具有较高的特权级别。
  3. 守护线程(Daemon Thread):是为其他线程提供服务的线程,只有当进程中存在非守护线程时,守护线程才会继续执行;当所有非守护线程执行完毕后,守护线程也随之自动结束。
  4. 僵死线程(Zombie Thread):创建线程的主线程必须通过pthread_join等待子线程结束并回收资源,不然,主线程马上结束,从而使创建的线程没有机会开始执行就结束了,这种被称为“僵线程”,已经结束的线程占用系统资源,导致系统不稳定。

1.5 线程之间的通信和进程之间的通信

1.5.1 线程之间的通信方式

  1. 共享内存(Shared Memory):线程之间可以通过共享内存来交换数据和信息,这是一种高效的通信方式。
  2. 信号量(Semaphore):信号量是一种用于控制进程间同步和互斥的机制,也可以用于在线程之间进行通信。
  3. 条件变量(Condition Variable):条件变量是一种用于线程之间进行协调和同步的机制,它可以让线程在满足特定条件时进行等待,从而避免了忙等待的情况发生。
  4. 管道流(Pipe):线程之间可以使用管道流来进行通信,这是一种比较低层次的通信方式。

1.5.2 进程之间的通信方式

  1. 管道(Pipe):管道是一种进程间通信方式,可以将两个进程连接在一起,使得它们之间可以进行通信和数据传输。
  2. 信号(Signal):信号是一种异步通信方式,可以向目标进程发送一个信号来通知它发生了某个事件。
  3. 共享内存(Shared Memory):共享内存是一种高效的进程间通信方式,它允许不同的进程直接访问同一个逻辑地址空间,从而实现数据的共享。
  4. 消息队列(Message Queue):消息队列是一种进程间通信方式,它提供了一种消息传递的机制,使得不同的进程之间可以进行异步通信。

1.6 并行与并发

并发: 指的是同一时间段内,处理多个事情的能力。它强调的是同时性,并不关注任务的执行顺序。比如,在一个 CPU 上运行多个线程时,由于 CPU 时间的分配和抢占式调度,多个任务会交替执行,看起来就像是在同时进行一样。

并行: 是指在同一时刻可以处理多个任务,这些任务可以通过多线程、多进程和分布式计算等方式实现同时运行的效果。并行强调的是同时性和并发性,即多个任务在同一时刻同时执行。比如,使用多核 CPU 同时运行多个独立的线程或者进程,可以加速计算过程,提高系统的处理性能。

二、Java线程

2.1 创建线程的方式

2.1.1 继承Thread类

talk is cheap, show me the code!

class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行的代码
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread(); // 创建线程对象
        thread.start(); // 启动线程
    }
}

2.1.2 实现Runnable接口

public class Main {
    public static void main(String[] args) {
        // 创建一个实现Runnable接口的类的对象
        MyRunnable myRunnable = new MyRunnable();
        // 将myRunnable对象作为参数创建Thread对象
        Thread thread = new Thread(myRunnable);
        // 启动线程
        thread.start();
    }
}

// 实现Runnable接口的类
class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行的代码
        System.out.println("线程已启动");
    }
}

2.1.3 FutureTask和Callable创建


import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建Callable对象
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1; i <= 100; i++) {
                    sum += i;
                }
                return sum;
            }
        };

        // 创建FutureTask对象
        FutureTask<Integer> futureTask = new FutureTask<>(callable);

        // 创建线程并启动
        Thread thread = new Thread(futureTask);
        thread.start();

        // 获取线程返回值
        int result = futureTask.get();
        System.out.println("线程返回值为:" + result);
    }
}

2.1.4 使用线程池创建

线程池创建线程部分,下面会详细的讲,这里就不多赘述了

2.1.5 Thread和Runnable创建线程的区别

  1. 继承Thread类创建线程:通过继承Thread类并重写run()方法来创建新的线程。这种方式比较简单,但是由于Java是单继承的,因此如果线程需要继承其他类,就无法使用这种方式。

  2. 实现Runnable接口创建线程:通过实现Runnable接口,并将它传递给Thread类的构造方法来创建新的线程。这种方式比较灵活,可以避免单继承的限制,同时也更容易对代码进行复用。

2.2 线程常见方法

方法名static功能说明注意
start()启动一个新线程,在新的线程运行 run 方法中的代码 start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException
run()新线程启动后会调用的方法 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为
join()等待线程运行结束
join(long n)等待线程运行结束,最多等待 n毫秒
getId()获取线程长整型的 id
getName()获取线程名
setName(String)修改线程名
getPriority()获取线程优先级
setPriority(int)修改线程优先级java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率
getState()获取线程状态Java 中线程状态是用 6 个 enum 表示,分别为:NEW,RUNNABLE, BLOCKED, WAITING,TIMED_WAITING, TERMINATED
isInterrupted()判断是否被打断不会清除打断标记
isAlive()线程是否存活(还没有运行完毕)
interrupt()打断线程如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记 ;如果打断的正在运行的线程,则会设置打断标记 ;park 的线程被打断,也会设置打断标记
interrupted()static判断当前线程是否被打断会清除打断标记
currentThread()static获取当前正在执行的线程
sleep(long n)static让当前执行的线程休眠n毫秒,休眠时让出 cpu的时间片给其它线程
yield()static提示线程调度器让出当前线程对CPU的使用主要是为了测试和调试

2.2.1 start和run的区别

  1. start方法用于启动一个新的线程,它会在新的线程中执行run方法。start方法调用后,就会立即返回,并且不会等待run方法执行完毕。
  2. run方法是线程的一个普通方法,可以直接调用。如果将run方法当做普通的方法来调用,那么该方法会在当前线程中执行,并且不会启动新的线程。因此,如果需要创建新的线程并启动它,那么就必须使用start方法。

2.2.2 sleep和wait的区别

sleep和wait在Java中都可以用来控制线程的等待时间,但它们有以下不同点:

  1. sleep方法是线程类(Thread)的静态方法,而wait方法是Object类的实例方法,也就是说只能在同步代码块(synchronized)或同步方法中使用wait方法。
  2. sleep方法暂停当前线程,让出CPU资源,让线程进入阻塞状态,但不会释放对象的锁,也就是说,当一个线程执行synchronized同步块时,另一个线程执行synchronized同步块是需要等待的。而wait方法会释放对象的锁,使得其他线程可以获取该对象锁,从而对对象进行操作。
  3. sleep方法指定了等待的时间,当时间到达后,线程自动恢复运行状态;而wait方法被唤醒后,必须要等待获取锁之后才能继续执行。

总的来说,sleep适合控制线程的程序执行周期,wait适合进行线程间的通信和协作,其目的也是不同的。

2.2.3 sleep和yield的区别

  1. sleep()方法:是线程的一种阻塞状态,它会让当前线程进入休眠状态,暂停执行一段时间。在sleep()期间,线程不会释放它所持有的锁资源。sleep()方法通常用于暂停线程一段时间,以等待一些事件的发生。
  2. yield()方法:是线程的一种主动让出CPU的状态。调用yield()方法后,当前线程会停止执行,并将CPU资源让给其他优先级相同或更高的线程。如果没有更高优先级的线程等待,则当前线程继续执行。需要注意的是,yield()方法只是释放了CPU资源,让当前线程从 Running 进入 Runnable 就绪状态,并不会释放锁。

2.2.4 join()方法

join()方法:是Thread类提供的一个方法,它可以让当前线程等待另一个线程执行完毕后再继续执行。
在不使用同步锁的情况下,使用join()方法实现三个线程的顺序执行,一起来看一下下面的代码。

public static void main() {
	Thread t1 = new Thread(new Runnable() {
	    public void run() {
	        // 第一个线程的任务
	    }
	});

	Thread t2 = new Thread(new Runnable() {
	    public void run() {
	        // 第二个线程的任务
	    }
	});

	Thread t3 = new Thread(new Runnable() {
	    public void run() {
	        // 第三个线程的任务
	    }
	});
	t1.start();
	t1.join(); // 等待t1执行完毕
	
	t2.start();
	t2.join(); // 等待t2执行完毕
	
	t3.start();
	t3.join(); // 等待t3执行完毕
}

//注意:这里是main方法的主线程在等待子线程t1执行完,然后执行t2,最后执行t3。不是t1、t2、t3在等待对方执行完

2.2.5 interrupt()方法

对于打断 sleep,wait,join 的线程,都会让线程进入阻塞状态,并会将中断状态清除。
以下是三个具体的demo,分别演示了打断 sleep,wait,join 的线程时中断状态的情况.

2.2.5.1 打断sleep的线程
public class SleepDemo implements Runnable {
    @Override
    public void run() {
        try {
            System.out.println("线程开始休眠");
            Thread.sleep(5000); // 线程休眠5秒
        } catch (InterruptedException e) {
            System.out.println("线程被打断");
            System.out.println("中断状态:" + Thread.currentThread().isInterrupted()); //中断状态被清除
            Thread.currentThread().interrupt(); // 重新设置中断状态
            System.out.println("重新设置中断状态后,中断状态为:" + Thread.currentThread().isInterrupted());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new SleepDemo());
        thread.start();
        Thread.sleep(2000); // 主线程等待2秒
        thread.interrupt(); // 打断线程
    }
}

在上述代码中,我们启动了一个线程并让它休眠5秒钟。主线程等待了两秒后,通过调用线程的interrupt()方法打断该线程。当线程被打断时,会进入catch语句块并输出相应的信息。在捕获到InterruptedException异常后,我们重新设置了线程中断状态,并通过isInterrupted()方法验证了重新设置中断状态的结果。

运行该程序,输出结果如下:

线程开始休眠
线程被打断
中断状态:false
重新设置中断状态后,中断状态为:true

我们可以看到,在打断 sleep 的线程时,中断状态会被清除,并需要调用线程的interrupt()方法重新设置中断状态。

2.2.5.2 打断wait的线程
public class WaitDemo implements Runnable {
    @Override
    public void run() {
        synchronized (this) {
            try {
                System.out.println("线程开始等待");
                this.wait(); // 线程等待
            } catch (InterruptedException e) {
                System.out.println("线程被打断");
                System.out.println("中断状态:" + Thread.currentThread().isInterrupted()); //中断状态被清除
                Thread.currentThread().interrupt(); // 重新设置中断状态
                System.out.println("重新设置中断状态后,中断状态为:" + Thread.currentThread().isInterrupted());
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new WaitDemo());
        thread.start();
        Thread.sleep(2000); // 主线程等待2秒
        thread.interrupt(); // 打断线程
    }
}

在上述代码中,我们启动了一个线程并让它进入wait状态。主线程等待了两秒后,通过调用线程的interrupt()方法打断该线程。当线程被打断时,会进入catch语句块并输出相应的信息。在捕获到InterruptedException异常后,我们重新设置了线程中断状态,并通过isInterrupted()方法验证了重新设置中断状态的结果。

运行该程序,输出结果如下:

线程开始等待
线程被打断
中断状态:false
重新设置中断状态后,中断状态为:true

我们可以看到,在打断 wait 的线程时,中断状态会被清除,并需要调用线程的interrupt()方法重新设置中断状态。

2.2.5.3 打断join的线程
public class JoinDemo implements Runnable {
    @Override
    public void run() {
        try {
            System.out.println("线程开始执行");
            Thread.sleep(3000); // 线程休眠3秒
        } catch (InterruptedException e) {
            System.out.println("线程被打断");
            System.out.println("中断状态:" + Thread.currentThread().isInterrupted());//中断状态被清除
            Thread.currentThread().interrupt(); // 重新设置中断状态
            System.out.println("重新设置中断状态后,中断状态为:" + Thread.currentThread().isInterrupted());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new JoinDemo());
        thread.start();
        thread.join(); // 等待线程执行完毕
        thread.interrupt(); // 打断线程
    }
}

在上述代码中,我们启动了一个线程并让它休眠3秒。在主线程中,我们使用线程的join()方法等待该线程执行完毕,然后通过调用线程的interrupt()方法打断该线程。当线程被打断时,会进入catch语句块并输出相应的信息。在捕获到InterruptedException异常后,我们重新设置了线程中断状态,并通过isInterrupted()方法验证了重新设置中断状态的结果。

运行该程序,输出结果如下:

线程开始执行
线程被打断
中断状态:false
重新设置中断状态后,中断状态为:true

我们可以看到,在打断 join 的线程时,中断状态会被清除,并需要调用线程的interrupt()方法重新设置中断状态。

2.2.5.4 打断正常执行的线程
private static void test2() throws InterruptedException {
	Thread t2 = new Thread(()->{
		while(true) {
			Thread current = Thread.currentThread();
			boolean interrupted = current.isInterrupted();
			if(interrupted) {
				log.debug(" 打断状态: {}", interrupted);
				break;
			}
		}
	}, "t2");
	t2.start();
	sleep(0.5);
	t2.interrupt();
}

输出结果为:

打断状态: true

上面的代码我们可以看到,打断正常运行的线程,打断状态并不会被清除。

2.2.5.5 打断park线程
public class ParkDemo implements Runnable {
    @Override
    public void run() {
        System.out.println("线程开始执行");
        LockSupport.park(); // 线程阻塞
        System.out.println("线程继续执行");
        System.out.println("线程打断状态:" + Thread.currentThread().isInterrupted());
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new ParkDemo());
        thread.start();

        Thread.sleep(2000); // 主线程等待2秒
        thread.interrupt(); // 打断线程
        System.out.println("打断线程后,线程打断状态为:" + thread.isInterrupted());

        LockSupport.unpark(thread); // 解除线程的阻塞
    }
}

输出结果:

线程开始执行
打断线程后,线程打断状态为:true
线程继续执行
线程打断状态:false

我们可以看到,在打断park()方法的线程时,打断状态会被保留。当线程被解除阻塞后,打断状态会清除。因此,在本示例中,我们可以看到线程打断状态在阻塞结束后变为了false。

2.2.6 notify()和notifyAll()

notify() 和 notifyAll() 是Java中用于线程通信的方法,都是 Object类中的实例方法,并且只能在同步块(synchronized)或同步方法中使用。

  1. notify()方法用于唤醒一个正在等待该对象锁的线程,使其从wait()方法返回,继续执行后续操作。如果多个线程都在等待该对象锁,则将随机选择其中一个线程唤醒,而其他线程则仍然处于等待状态。
  2. notifyAll() 方法与 notify()方法类似,但不同之处在于它将唤醒所有正在等待该对象锁的线程,而不是随机选择其中一个线程进行唤醒。因此,notifyAll()
    方法更适用于多个线程需要同时被唤醒的场景。

需要注意的是,notify() 和 notifyAll() 方法并不能保证等待线程直接继续运行,可能还需要再次等待获取对象锁,因此,在使用这些方法时,需要合理设计线程之间的协调或者条件判断,以避免死锁或者其他不良后果。同时,为了避免虚假唤醒(spurious wakeup),应该在循环中使用wait()方法,而不是在条件分支中使用。

2.2.7 stop()、suspend()、resume()方法(已过时,不推荐)

  1. stop()方法用于停止线程,即直接结束当前线程的执行。它不会释放线程所占用的资源,也不会保证线程的安全终止,因此已被标记为“deprecated”,不推荐使用。
  2. suspend()方法用于暂停线程,即使线程暂停在临界区内,也不会释放持有的锁,从而可能导致死锁等问题。也因此,suspend()方法已被标记为“deprecated”,不推荐使用。
  3. resume()方法用于恢复线程的执行。它只能恢复被suspend()方法暂停的线程,但如果调用时没有先获取锁,则有可能导致线程永久阻塞等问题。因此,resume()方法也已被标记为“deprecated”,不推荐使用

以上三个方法都已被废弃是因为它们容易引起程序出现各种未知的异常,从而导致程序的不稳定性和运行错误。

相比之下,Java提供了更加安全和可靠的线程控制方法,譬如wait()、notify()和notifyAll()等方法[1]。这些方法可以保证线程的安全性和正确性,从而更加适合在Java中进行线程编程。

2.3 主线程与守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

log.debug("开始运行...");
Thread t1 = new Thread(() -> {
	log.debug("开始运行...");
	sleep(2);
	log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
sleep(1);
log.debug("运行结束...");

输出:

开始运行...
开始运行...
运行结束...

2.4 线程的状态

2.4.1 操作系统层面

操作系统层面线程的状态通常被分为五种,包括:

  1. 新建状态(New): 表示线程被创建,但是还没有分配到系统资源;
  2. 就绪状态(Runnable):表示线程已经分配到了所有必要的资源,正在等待分配CPU时间片来运行;
  3. 运行状态(Running): 表示线程正在执行其任务代码;
  4. 阻塞状态(Blocked): 表示线程在等待某个事件(如 IO 操作、锁等)发生而暂停执行,进入阻塞状态;
  5. 终止状态(Terminated): 表示线程已经结束执行。

其中,就绪状态和阻塞状态也被统称为“阻塞态”,源于它们都不处于运行态。而创建状态通常也被认为是一种特殊的新建状态。

状态转换关系如图所示:
在这里插入图片描述

2.4.2 Java层面

Java层面的线程状态有6种,分别是:

  1. NEW(新建): 表示线程已经被创建,但是还没有调用start()方法。
  2. RUNNABLE(可运行/运行中):可运行状态包括运行中状态(RUNNING)和就绪状态(READY)。
  3. BLOCKED(阻塞): 表示线程被阻塞了,处于等待某个监视器锁的持有者释放锁以便可以获得该锁的状态。
  4. WAITING(等待): 表示线程在等待另一个线程指示一个条件的状态。因此,线程会一直等待,直到被其它线程通过 notify() 或 notifyAll() 方法唤醒。
  5. TIMED_WAITING(超时等待):表示线程在等待另一个线程指示一个条件的状态,并且会在指定的时间内等待,如果超时了还没有被唤醒,那么线程会自行唤醒。
  6. TERMINATED(终止): 表示线程已经执行完毕,退出了run()方法,线程对象不再可用。

需要注意的是,Java线程的状态与操作系统层面线程状态并不完全一致,Java线程细化了等待另一个线程的两种状态,并增加了超时等待状态,使用起来更加灵活。

状态转换关系如图所示:
在这里插入图片描述

三、线程池

线程池是一种利用池化技术思想来实现的线程管理技术。线程池可以将多个任务提交到一个线程队列中,而不是为每个任务都创建一个新线程。优点:使用线程池能够避免频繁创建和销毁线程带来的开销,复用已经创建的线程能够快速响应请求,提高处理请求的效率。而且,线程池可以控制系统中线程的最大数量,避免线程数量过多引起的内存溢出、系统资源耗尽等问题

线程池通常由三部分组成:

  1. 任务队列:用于存储待执行的任务。
  2. 线程池管理器:管理线程池的创建、销毁和线程调度等任务。
  3. 工作线程:实际执行任务的线程。

3.1 线程池核心参数

public ThreadPoolExecutor(int corePoolSize,
						int maximumPoolSize,
						long keepAliveTime,
						TimeUnit unit,
						BlockingQueue<Runnable> workQueue,
						ThreadFactory threadFactory,
						RejectedExecutionHandler handler)
  1. corePoolSize:核心线程池大小,即线程池中会维护的最小线程数量,即使这些线程处于空闲状态,它们也不会被销毁,除非设置了 allowCoreThreadTimeOut。
  2. maximumPoolSize:线程池能容纳的最大线程数,当队列满了,就会继续创建线程去消费任务,直到达到最大线程数。
  3. keepAliveTime:线程池中多余的空闲线程存活时间。
  4. unit:空闲线程存活时间的单位,例如 TimeUnit.SECONDS。
  5. workQueue:等待队列,用于存储还未执行的任务。
  6. threadFactory:创建新线程的工厂。
  7. handler:饱和策略,当线程池中的线程数达到最大值,如何处理新提交的任务。

问题:核心线程数和最大线程数该设置成多少?
核心线程数和最大线程数的设置需要根据具体的需求和硬件资源进行考量。以下提供一些参考:

  1. 核心线程数

核心线程数是线程池中一直保持存活的线程数。在任务很少的情况下,核心线程可以保证任务能够及时处理。但如果任务量和任务复杂度增加,核心线程数太少会导致任务等待队列过长,影响系统的性能。
通常建议将核心线程数设置为 CPU 核心数的 2 倍或 4 倍,以充分利用 CPU 资源,提高系统的吞吐量。

  1. 最大线程数

最大线程数是线程池中最多可以创建的线程数,当任务量过大时可以创建额外的线程处理任务。但是最大线程数不能无限增加,否则会占用过多的内存资源。
最大线程数的设置需要根据系统负载、CPU 核心数、内存大小等因素进行考虑。一般建议将最大线程数设置为核心线程数的 2 倍,可以根据具体情况进行适当调整。

需要特别注意的是,线程池的参数配置不正确可能会导致线程饥饿、线程阻塞、任务堆积等问题,影响系统的性能。因此,在设置线程池参数之前,需要对具体的业务场景、系统负载等进行分析,以确保线程池的最大效率和可靠性。

3.2 线程池工作流程

线程池流程图
线程池刚创建的时候,线程数是0。当任务被提交到线程池,会先判断当前线程数量是否小于corePoolSize,如果小于则创建线程来执行提交的任务,否则将任务放入workQueue队列,如果workQueue满了,则判断当前线程数量是否小于maximumPoolSize,如果小于则创建线程执行任务,否则就会调用RejectedExecutionHandler,来处理任务,默认的拒绝策略是丢弃。

3.3 线程池源码实现

//核心代码
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    //如果当前活跃线程小于核心池大小,就尝试创建新的线程
    if (workerCountOf(c) < corePoolSize) {
        //如果成功创建新线程并且启动成功,直接返回
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    //线程池处于运行状态,并且成功将任务加入阻塞队列时,会执行下面的代码
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        //如果重复检查时,线程池已经不是运行状态,则将刚添加的任务从阻塞队列中移除,并执行拒绝策略
        if (! isRunning(recheck) && remove(command))
            reject(command);
        //如果活跃线程为0,则创建一个非核心线程,并将firstTask设置为null
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    //如果添加非核心线程失败,则执行拒绝策略
    else if (!addWorker(command, false))
        reject(command);
}
//获取活跃的线程数
private static int workerCountOf(int c)  { return c & CAPACITY; }
//获取线程池运行状态
private static int runStateOf(int c)     { return c & ~CAPACITY; }

3.4 线程池状态

状态名高 3位接收新任务处理阻塞队列任务说明
RUNNING111YY
SHUTDOWN000NY不会接收新任务,但会处理阻塞队列剩余任务
STOP001NN会中断正在执行的任务,并抛弃阻塞队列任务
TIDYING010--
TERMINATED011--终结状态

3.5 线程池拒绝策略

  1. AbortPolicy: 让调用者抛出 RejectedExecutionException 异常,这是默认策略
  2. CallerRunsPolicy: 让调用者运行任务
  3. DiscardPolicy: 放弃本次任务
  4. DiscardOldestPolicy: 放弃队列中最早的任务,本任务取而代之
  5. Dubbo 的实现: 在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题
  6. Netty 的实现: 是创建一个新线程来执行任务
  7. ActiveMQ 的实现: 带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
  8. PinPoint 的实现: 它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略

3.6 常见的线程池

  1. FixedThreadPool:固定大小的线程池,同时只能执行指定数量的任务。通过 ThreadPoolExecutor 的静态方法 newFixedThreadPool 来创建该线程池。
public static ExecutorService newFixedThreadPool(int nThreads) {
	return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,
									new LinkedBlockingQueue<Runnable>());
}

在实现上,该线程池具有固定的线程数,超出的任务会在等待队列中排队。

适用场景:适用于需要限制线程并发数量、处理大量短期任务的场景。

  1. CachedThreadPool:根据需要创建新线程的线程池。通过 ThreadPoolExecutor 的静态方法 newCachedThreadPool 来创建该线程池。
public static ExecutorService newCachedThreadPool() {
	return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
			60L, TimeUnit.SECONDS,
			new SynchronousQueue<Runnable>());
}

在实现上,该线程池没有固定线程数,根据任务量动态创建线程,空闲线程会在 60 秒后被回收。

适用场景:适用于处理大量短期任务,以及任务之间需求相互独立的场景。

  1. SingleThreadExecutor:单个后台线程的线程池。通过 ThreadPoolExecutor 的静态方法 newSingleThreadExecutor 来创建该线程池。
public static ExecutorService newSingleThreadExecutor() {
	return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,
				0L, TimeUnit.MILLISECONDS,
				new LinkedBlockingQueue<Runnable>()));
}

在实现上,该线程池只有一个线程,所有任务按顺序执行。

适用场景:适用于需要顺序执行一些任务的场景,例如定时任务。

  1. ScheduledThreadPool:定时任务线程池。通过 ScheduledExecutorService 的静态方法 newScheduledThreadPool 来创建该线程池。
public ScheduledThreadPoolExecutor(int corePoolSize) {
        return new ThreadPoolExecutor(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

在实现上,该线程池可以在指定的延迟后执行 Runnable 或 Callable 对象,并按指定的时间间隔循环执行。

适用场景:适用于需要定时执行任务的场景,例如心跳检测、定时任务等。

  1. WorkStealingPool:工作窃取线程池。通过 ForkJoinPool 的静态方法 newWorkStealingPool 来创建该线程池。
ExecutorService executor = Executors.newWorkStealingPool();

在实现上,该线程池使用一组工作线程来执行任务,每个工作线程都有自己的任务队列。当一个工作线程完成了自己的任务队列中所有的任务,它可以从其他工作线程的队列中窃取任务来执行。

适用场景:适用于需要处理大量独立任务的场景,例如图像处理、数据分析等。

3.7 线程池常用的队列

3.7.1 SynchronousQueue

SynchronousQueue是Java并发包中最为特殊的阻塞队列。它的特点是不存储元素,插入一个元素之后必须等待其他线程取走后才能再插入。因此,对于SynchronousQueue而言,其最大容量是0。
CachedThreadPool线程池使用的队列就是这这个。

实例代码:

import java.util.concurrent.SynchronousQueue;

public class SynchronousQueueDemo {
    public static void main(String[] args) {
        SynchronousQueue<Integer> queue = new SynchronousQueue<>();
        Thread sender = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Sending data: " + i);
                try {
                    queue.put(i);
                    System.out.println("Data sent: " + i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread receiver = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Receiving data: " + i);
                try {
                    int data = queue.take();
                    System.out.println("Data received: " + data);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        sender.start();
        receiver.start();
        try {
            sender.join();
            receiver.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出:

Sending data: 0
Data sent: 0
Receiving data: 0
Data received: 0
Sending data: 1
Data sent: 1
Receiving data: 1
Data received: 1
Sending data: 2
Data sent: 2
Receiving data: 2
Data received: 2
Sending data: 3
Data sent: 3
Receiving data: 3
Data received: 3
Sending data: 4
Data sent: 4
Receiving data: 4
Data received: 4

3.7.2 LinkedBlockingQueue

LinkedBlockingQueue是一个基于链表结构的阻塞队列,容量可以选择限制或不限制。如果指定了容量大小,那么它就是一个有界队列,如果不指定,默认容量大小为Integer.MAX_VALUE,即无界队列。

示例代码:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class LinkedBlockingQueueDemo {
    public static void main(String[] args) {
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(2);
        Thread sender = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Sending data: " + i);
                try {
                    queue.put(i);
                    System.out.println("Data sent: " + i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread peeker = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Peeking data: " + i);
                try {
                    int data = queue.peek();
                    System.out.println("Data peeked: " + data);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread receiver = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Receiving data: " + i);
                try {
                    int data = queue.take();
                    System.out.println("Data received: " + data);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        sender.start();
        peeker.start();
        receiver.start();
        try {
            sender.join();
            peeker.join();
            receiver.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出:

Sending data: 0
Data sent: 0
Peeking data: 0
Data peeked: 0
Sending data: 1
Data sent: 1
Peeking data: 1
Data peeked: 1
Sending data: 2
Receiving data: 0
Data sent: 2
Data received: 0
Peeking data: 2
Data peeked: 2
Sending data: 3
Receiving data: 1
Data sent: 3
Data received: 1
Peeking data: 3
Data peeked: 3
Sending data: 4
Receiving data: 2
Data sent: 4
Data received: 2
Peeking data: 4
Data peeked: 4
Receiving data: 3
Receiving data: 4
Data received: 3
Data received: 4

3.7.3 ArrayBlockingQueue

ArrayBlockingQueue是一个数组结构的阻塞队列,其容量固定,创建时需要指定容量大小。ArrayBlockingQueue的优点是对于有界队列性能比较稳定,但缺点是不能动态扩展队列大小。
在线程池中,ArrayBlockingQueue常被用作有限任务队列。当任务队列达到容量上限时,新提交的任务将无法加入队列,此时可以根据策略进行阻塞、丢弃等处理。

示例代码:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class ArrayBlockingQueueDemo {
    public static void main(String[] args) {
        BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(2);
        Thread sender = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Sending data: " + i);
                try {
                    queue.put(i);
                    System.out.println("Data sent: " + i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread receiver = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Receiving data: " + i);
                try {
                    int data = queue.take();
                    System.out.println("Data received: " + data);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        sender.start();
        receiver.start();
        try {
            sender.join();
            receiver.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出:

Sending data: 0
Data sent: 0
Sending data: 1
Data sent: 1
Receiving data: 0
Data received: 0
Receiving data: 1
Data received: 1
Sending data: 2
Data sent: 2
Sending data: 3
Data sent: 3
Receiving data: 2
Data received: 2
Receiving data: 3
Data received: 3
Sending data: 4
Data sent: 4
Receiving data: 4
Data received: 4

3.7.4 PriorityBlockingQueue

PriorityBlockingQueue 是 Java 中的一个阻塞队列实现,它可以自动按照元素的优先级进行排序。与 PriorityQueue 不同的是,PriorityBlockingQueue 是基于并发包中的锁和条件变量实现的,因此可以在多线程环境下安全地使用。

示例代码:

import java.util.concurrent.PriorityBlockingQueue;

public class PriorityBlockingQueueDemo {
    public static void main(String[] args) throws InterruptedException {
        PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
        queue.put(new MyTask("Task-1", 3));
        queue.put(new MyTask("Task-2", 2));
        queue.put(new MyTask("Task-3", 1));

        while (!queue.isEmpty()) {
            System.out.println(queue.take());
        }
    }

    static class MyTask implements Comparable<MyTask> {
        private String name;
        private int priority;

        public MyTask(String name, int priority) {
            this.name = name;
            this.priority = priority;
        }

        @Override
        public int compareTo(MyTask o) {
            return Integer.compare(this.priority, o.priority);
        }

        @Override
        public String toString() {
            return "Task(name=" + name + ", priority=" + priority + ")";
        }
    }
}

输出:

Task(name=Task-3, priority=1)
Task(name=Task-2, priority=2)
Task(name=Task-1, priority=3)

3.7.5 DelayQueue

DelayQueue 是 Java 中的一个阻塞队列实现,其中的元素只有在其指定的延迟时间到期后才能被获取。这个延迟时间可以通过实现 Delayed 接口来自定义。

代码示例:

import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class DelayQueueDemo {
    public static void main(String[] args) throws InterruptedException {
        DelayQueue<MyTask> queue = new DelayQueue<>();
        queue.put(new MyTask("Task-1", 2, TimeUnit.SECONDS));
        queue.put(new MyTask("Task-2", 5, TimeUnit.SECONDS));
        queue.put(new MyTask("Task-3", 3, TimeUnit.SECONDS));

        while (!queue.isEmpty()) {
            System.out.println(queue.take());
        }
    }

    static class MyTask implements Delayed {
        private String name;
        private long duration;
        private long expireTime;

        public MyTask(String name, long duration, TimeUnit unit) {
            this.name = name;
            this.duration = TimeUnit.MILLISECONDS.convert(duration, unit);
            this.expireTime = System.currentTimeMillis() + this.duration;
        }

        @Override
        public long getDelay(TimeUnit unit) {
            return unit.convert(expireTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
        }

        @Override
        public int compareTo(Delayed o) {
            return Long.compare(this.getDelay(TimeUnit.MILLISECONDS), o.getDelay(TimeUnit.MILLISECONDS));
        }

        @Override
        public String toString() {
            return "Task(name=" + name + ", duration=" + duration + ")";
        }
    }
}

输出:

Task(name=Task-1, duration=2000)
Task(name=Task-3, duration=3000)
Task(name=Task-2, duration=5000)

在上面的示例中,我们创建了一个 DelayQueue 对象,并向队列中放入了三个延迟任务。其中,每个任务的延迟时间都不同,分别是 2 秒、5 秒、3 秒。

接着我们使用一个循环不断地从队列中获取元素并输出。由于 DelayQueue 是一个阻塞队列,当队列为空时,获取队首元素的操作将会被阻塞,直到队列中有元素被添加。

四、线程安全问题(扫盲+巩固)

4.1 线程安全

线程安全指的是在多个线程同时对共享资源进行访问,能够保证程序按照我们预期的行为执行,并且不会出现数据不一致、重复执行、死锁等问题。

给大家简单举个线程安全的例子。比如说:多线程下的i++是不是线程安全的问题?

相信这个问题已经是老生常谈了吧。结果大家可能都知道,多线程情况下i++操作其实不是线程安全的,i++相当于是i = i + 1。这其实是三个操作,一个是读取i的值,然后再对i进行加一,最后是对i进行赋值。

为什么会不安全呢?

根据之前讲的并发的概念,线程是会交替执行的,就是一个线程的时间片用完之后,会进行上下文切换,切换到下一个线程继续执行。 假设两个线程,线程a和线程b,变量i=0。现在线程a先拿到i=0,刚好时间片用完了,切换到了线程b,现在线程b也拿到了i=0,并对它进行了加一,现在线程b中i的值等于1。之后又切换到了线程a,a的值还是没有加一之前的值,也就是i=0,然后加一,i变成了1。
这就出现了线程安全的问题,预期结果是,两个线程都执行了加一的操作,i应该等于2,但是线程a和b都拿到的i=0的初始值,并对i进行加一,导致最终的结果是i=1。具体原因可以看JMM和volatile章节

4.2 线程安全产生的原因

在多线程情况下,当多个线程同时访问共享资源时,由于读写操作不具有原子性,可能会出现数据竞争(data race)的问题,导致数据不一致、重复执行或者其他异常。线程安全的实现需要使用同步机制(如synchronized、lock等)来保证代码块或方法的原子性和独占性。

具体产生的原因需要从JVM层面去分析,这方面会在第七点:JMM和volatile 详细讲解。

多线程情况下出现的数据不一致代码演示:

public class UnsafeThread implements Runnable {
    private int count = 0;

    // 多个线程调用该方法,可能会出现线程安全问题
    public void run() {
        for (int i = 0; i < 100000; i++) {
            count++;
        }
        System.out.println(Thread.currentThread().getName() + " : " + count);
    }

    public static void main(String[] args) {
        UnsafeThread unsafeThread = new UnsafeThread();

        // 启动两个线程调用同一个实例的 run 方法
        Thread thread1 = new Thread(unsafeThread, "Thread-1");
        Thread thread2 = new Thread(unsafeThread, "Thread-2");

        thread1.start();
        thread2.start();
    }
}

在上述代码中,两个线程在调用 UnsafeThread 实例的 run 方法时,可能会同时访问 count 变量并进行更新操作,由于该操作不具有原子性,可能导致数据竞争问题。运行多次后,输出结果可能为:

Thread-1 : 187724
Thread-2 : 194410

4.3 临界区

临界区指的是一段代码,是多个线程对共享变量进行同时进行读写的那段代码,被称为是临界区。

4.4 竞态条件

竞态条件(Race Condition) 是指当多个线程并发执行时,由于执行顺序不确定或者执行时间差异导致程序的输出结果不一致,因此产生的一种随机、非确定性的错误行为。竞态条件常常是由于共享资源的读写操作不具有原子性和独占性,因此需要使用同步机制来保证程序的正确性。

需要注意的是,竞态条件可能对程序的性能和正确性都产生不良影响,因此在多线程编程中需要仔细处理以避免出现这类问题。示例代码中的 UnsafeThread 就是一个典型的竞态条件问题的例子。

4.4 线程死锁

4.4.1 线程死锁是什么?

线程死锁指的是在多线程编程中,若干个线程因为互相竞争共享资源而陷入等待状态,导致无法继续执行的一种情况。当多个线程之间的请求和释放资源的顺序出现了环路时就会形成死锁,从而导致程序无法正常执行。

下面展示一个死锁的代码:

public class ThreadDeadlockDemo {
    static Object resourceA = new Object();
    static Object resourceB = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (resourceA) {
                System.out.println(Thread.currentThread().getName() + " 获取到了 resourceA");
                try {
                    // 为了让两个线程都能获取到资源,
                    // 这里等待一段时间
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " 尝试获取 resourceB");
                synchronized (resourceB) {
                    System.out.println(Thread.currentThread().getName() + " 获取到了 resourceB");
                }
            }
        }, "Thread-1");

        Thread thread2 = new Thread(() -> {
            synchronized (resourceB) {
                System.out.println(Thread.currentThread().getName() + " 获取到了 resourceB");
                try {
                    // 为了让两个线程都能获取到资源,
                    // 这里等待一段时间
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " 尝试获取 resourceA");
                synchronized (resourceA) {
                    System.out.println(Thread.currentThread().getName() + " 获取到了 resourceA");
                }
            }
        }, "Thread-2");

        // 启动两个线程,模拟死锁的发生
        thread1.start();
        thread2.start();
    }
}

在上述代码中, Thread-1 线程尝试获取 resourceB 资源时,由于该资源已被 Thread-2 线程占用,因此 Thread-1 线程会被阻塞。同时,Thread-2 线程尝试获取 resourceA 资源时,由于该资源已被 Thread-1 线程占用,因此 Thread-2 线程也会被阻塞,从而导致两个线程陷入等待状态,无法正常执行,形成了死锁。

4.4.2 线程死锁产生的条件?

线程死锁产生的必要条件有四个,它们是:

  1. 互斥条件:线程对于所分配到的资源具有排它性,即一个资源只能被一个线程占用,直到被该线程释放。
  2. 请求与保持条件:一个线程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在未使用完之前,不能被其他线程强行剥夺,只能由该线程自己释放。
  4. 环路等待条件:存在一种等待环路,即若干个线程之间形成一种头尾相接的循环等待资源的关系。

4.4.3 线程死锁如何避免和解决?

常见的解决线程死锁的方法有以下几种:

  1. 避免死锁产生:避免死锁产生是最可靠的方式,其核心思想就是破坏死锁的四个产生条件之一,例如,尽量保持资源请求的顺序一致、使用定时或者随机算法来释放资源等。
  2. 检测并解除死锁:检测到死锁后,需要对死锁进行解除。可以使用“资源分配图”或“银行家算法”等技术进行死锁检测,并通过撤销进程或者资源剥夺等方式解除死锁。
  3. 忽略死锁: 在某些情况下,忽略死锁可能比其他方法更具有可取性,例如,当死锁出现概率较小且易于通过重启应用程序来恢复时。

五、悲观锁

悲观锁是一种传统的并发控制方式,它假设数据会被其他事务所修改,因此在访问数据之前就会对其上锁,以防止其他事务的干扰。而在对数据进行更新操作之前,需要先获取对数据的独占访问权限。

悲观锁适用于写多读少的场景,可以有效避免读写冲突而导致的脏数据和数据不一致的问题,但在高并发情况下,加锁和解锁会带来较大的性能损耗,降低系统的吞吐量。

Java 实现悲观锁的方式有两种,分别是是synchronized和ReentrantLock,他们底层的实现原理都是通过AQS实现的。如果不了解AQS的是底层实现原理的,可以去看《一文搞懂AQS底层实现原理》这篇博客。

3.1 synchronized(非公平锁)

3.1.1 修饰的地方与锁的对象

synchronized 可以作用在以下三个地方:

  1. 普通方法:可以用 synchronized 修饰一般的成员方法,这时它默认使用当前对象 this 作为锁。
  2. 静态方法:可以用synchronized 修饰静态方法,这时它默认使用当前类的 Class 对象作为锁。
  3. 代码块:可以用 synchronized修饰方法内部的代码块,这时需要指定锁对象。

3.1.2 修饰普通方法

public class SynchronizedDemo1 {
    private int count = 0;

    public synchronized void increment() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++;
        System.out.println(Thread.currentThread().getName() + " count: " + count);
    }

    public static void main(String[] args) {
        SynchronizedDemo1 demo = new SynchronizedDemo1();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10; j++) {
                    demo.increment();
                }
            }, "Thread-" + i).start();
        }
    }
}

在上面的示例中,使用 synchronized 修饰了 increment() 方法,这时它默认的锁对象是当前对象 this。当多个线程同时访问该方法时,只有一个线程能够获得锁,其他线程需要等待。
输出如下:

Thread-1 count: 1
Thread-4 count: 2
Thread-4 count: 3
Thread-4 count: 4
Thread-4 count: 5
Thread-4 count: 6
Thread-4 count: 7
Thread-4 count: 8
Thread-4 count: 9
Thread-4 count: 10
Thread-2 count: 11
Thread-3 count: 12
Thread-0 count: 13
Thread-1 count: 14
Thread-1 count: 15
Thread-1 count: 16
Thread-1 count: 17
Thread-1 count: 18
Thread-1 count: 19
Thread-1 count: 20
Thread-3 count: 21
Thread-0 count: 22
Thread-2 count: 23
Thread-3 count: 24
Thread-0 count: 25
Thread-1 count: 26
Thread-2 count: 27
Thread-3 count: 28
Thread-0 count: 29
Thread-2 count: 30

3.1.3 修饰静态方法

public class SynchronizedDemo2 {
    private static int count = 0;

    public static synchronized void increment() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++;
        System.out.println(Thread.currentThread().getName() + " count: " + count);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10; j++) {
                    SynchronizedDemo2.increment();
                }
            }, "Thread-" + i).start();
        }
    }
}

在上面的示例中,使用 synchronized 修饰了静态方法 increment(),这时它默认的锁对象是当前类的 Class 对象。当多个线程同时访问该方法时,只有一个线程能够获得锁,其他线程需要等待。
输出如下:

Thread-1 count: 1
Thread-2 count: 2
Thread-3 count: 3
Thread-4 count: 4
Thread-0 count: 5
Thread-2 count: 6
Thread-3 count: 7
Thread-1 count: 8
Thread-4 count: 9
Thread-0 count: 10
Thread-4 count: 11
Thread-1 count: 12
Thread-3 count: 13
Thread-2 count: 14
Thread-0 count: 15
Thread-2 count: 16
Thread-3 count: 17
Thread-1 count: 18
Thread-4 count: 19
Thread-0 count: 20
Thread-4 count: 21
Thread-1 count: 22
Thread-3 count: 23
Thread-2 count: 24
Thread-0 count: 25

3.1.4 修饰代码块

public class SynchronizedDemo3 {
    private final Object lock = new Object();
    private int count = 0;

    public void increment() {
        synchronized (lock) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
            System.out.println(Thread.currentThread().getName() + " count: " + count);
        }
    }

    public static void main(String[] args) {
        SynchronizedDemo3 demo = new SynchronizedDemo3();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10; j++) {
                    demo.increment();
                }
            }, "Thread-" + i).start();
        }
    }
}

在上面的示例中,使用 synchronized 修饰了方法内部的代码块,这时需要指定锁对象。这里创建了一个 lock 对象作为锁。当多个线程同时访问 increment() 方法时,只有一个线程能够获得 lock 对象的锁,其他线程需要等待。
输出如下:

Thread-1 count: 1
Thread-2 count: 2
Thread-3 count: 3
Thread-4 count: 4
Thread-0 count: 5
Thread-2 count: 6
Thread-3 count: 7
Thread-1 count: 8
Thread-4 count: 9
Thread-0 count: 10
Thread-4 count: 11
Thread-1 count: 12
Thread-3 count: 13
Thread-2 count: 14
Thread-0 count: 15
Thread-2 count: 16
Thread-3 count: 17
Thread-1 count: 18
Thread-4 count: 19
Thread-0 count: 20
Thread-4 count: 21
Thread-1 count: 22
Thread-3 count: 23
Thread-2 count: 24
Thread-0 count: 25

3.1.5 实现原理

synchronized的实现原理是基于monitorenter和monitorexit这两个字节码指令来实现的,它们分别位于方法的开始和结束位置。

当一个线程需要访问某个对象的方法时,它会先去检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置(JVM会根据这个标志来判断该方法是否是同步方法),如果设置了,执行线程将先持有monitor, 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。

从指令的角度上看,当前线程先会执行monitorenter指令,试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。这里需要注意的是,同一个线程是可重入锁的

当一个线程获得了该对象的monitor锁后,其他试图访问该对象的synchronized代码块或synchronized方法的线程将被阻塞,直到拥有该对象的monitor锁的线程释放该锁。(这只是最原始重量级锁的设计,后面会讲解JDK6之后锁升级的过程)

3.1.6 synchronized锁升级

锁升级会经历以下四个过程:

  1. 无锁状态 :当没有多个线程对共享资源同时进行竞争的时候,是无锁状态
  2. 偏向锁状态 :第一个获取到锁的线程会在mark word中设置线程id。如果下次有线程尝试获取锁,则会先去判断是否是mark word中设置的线程id。如果是,则允许重入,如果不是,则升级为下一级锁。
  3. 轻量级锁状态 :当有多个线程尝试获取同步锁的时候,先会去通过cas尝试设置mark word中的线程id以获取锁,如果cas设置失败,则会通过自旋的方式不断的cas尝试获取到锁。当自旋了一定的次数之后,还没有获取到锁,则会升级为重量级锁。对cas不了解的同学,后面乐观锁部分会详细讲解。
  4. 重量级锁状态:重量级锁就是monitor锁,没有获取到锁的线程会被阻塞,直到执行monitorexit指令释放锁,并且锁计数器为0的时候,才会停止阻塞,进入下一轮竞争。
    注意:synchronized锁是非公平锁

锁升级的好处:

  1. 如果一直使用重量级锁,那每次都会让操作系统从用户态切换成内核态,非常的消耗性能。
  2. synchronized 锁的升级机制可以根据不同的场景和状态动态地选择适合的锁级别,以提高并发性能和降低锁竞争的代价。

3.1.7 对象结构

对象结构
Java对象结构可以分为以下部分:

  1. 对象头(Object Header): 存储对象的标记信息(如哈希码、GC分代年龄等)和锁信息(如偏向锁、轻量级锁、重量级锁等)。
  2. 实例数据(Instance Data): 存储对象的实际数据,也就是我们在代码中定义的成员变量。
  3. 对齐填充(Padding):因为虚拟机要求对象大小必须是8字节的倍数,所以有时需要填充一些无用的空间来使得对象大小符合要求。

Java对象头通常包括两部分:klass pointer 和 Mark Word。

  1. Klass Pointer:指向该对象的类元数据信息的指针。在Hotspot VM中,由于存在运行期根据类元数据来确定对象类型的情况,因此对象头需要存放一个指向类元数据信息的指针,即Klass Pointer。
  2. Mark Word:对象头中除了Klass Pointer以外的部分都被称为Mark Word,也可以简单理解为对象名片。Mark Word 中存储了一些用于实现 Java 线程安全及 GC相关的信息。这些信息包括对象的哈希码、对象是否被锁定、对象标记信息(在GC过程中,用于判断对象是否可达等)等。
  3. 数组长度:如果是数组对象,则会另外存储数据长度

3.2 ReentrantLock

ReentrantLock 是一种可重入锁,与 synchronized 类似,但是更加灵活和强大,主要体现在可打断、锁超时、可指定公平锁和非公平锁以及支持条件变量。其基本原理是利用AQS(AbstractQueuedSynchronizer)实现线程同步和互斥,将需要互斥访问的代码段包含在 lock 和 unlock方法中,在 lock 方法中获取锁,同时将当前线程添加到等待队列中,若锁被占用则线程进入阻塞状态;在 unlock方法中释放锁,并将等待队列的第一个线程唤醒,使其可以继续执行。与 synchronized 不同,ReentrantLock支持公平锁和非公平锁两种模式,并且可以通过锁升级实现多种级别的锁。另外,ReentrantLock 还支持 Condition类型,即条件变量,可以实现线程在满足某个条件时进入等待状态,以及唤醒等待该条件的线程。

以下代码是ReentrantLock的使用示例:

// 获取锁
reentrantLock.lock();
try {
	// 临界区
} finally {
	// 释放锁
	reentrantLock.unlock();
}

3.2.1 可重入

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantDemo {
    static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        lock.lock(); // 第一次获取锁
        try {
            System.out.println("第一次获取锁成功");
            lock.lock(); // 第二次获取锁
            try {
                System.out.println("第二次获取锁成功");
            } finally {
                lock.unlock(); // 释放锁
                System.out.println("第二次释放锁成功");
            }
        } finally {
            lock.unlock(); // 释放锁
            System.out.println("第一次释放锁成功");
        }
    }
}

在该示例中,线程首先调用 lock() 方法获取锁,在获取锁后,又再次调用 lock() 方法获取同一把锁。由于 ReentrantLock 支持可重入,同一线程可以多次获取同一个锁,所以程序不会被阻塞,而是顺利执行,并且可以正确释放掉锁。

3.2.2 可打断

import java.util.concurrent.locks.ReentrantLock;

public class InterruptibleDemo {
    static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                lock.lockInterruptibly(); // 获取可打断的锁
                System.out.println("线程 t1 获取可打断的锁成功");
                Thread.sleep(5000); // 模拟线程 t1 正在执行任务
            } catch (InterruptedException e) {
                System.out.println("线程 t1 被中断了!");
            } finally {
                lock.unlock(); // 释放锁
                System.out.println("线程 t1 执行完毕,释放可打断的锁");
            }
        });

        Thread t2 = new Thread(() -> {
            try {
                lock.lock(); // 获取普通的不可打断的锁
                System.out.println("线程 t2 获取不可打断的锁成功");
            } finally {
                lock.unlock(); // 释放锁
                System.out.println("线程 t2 执行完毕,释放普通的不可打断的锁");
            }
        });

        t1.start();
        Thread.sleep(1000); // 让 t1 先获取锁
        t2.start();
        Thread.sleep(1000); // 让 t2 等待,确保 t1 进入阻塞状态

        // 主线程中断 t1 的锁获取过程
        t1.interrupt();
    }
}

在该示例中,线程 t1 通过 lockInterruptibly() 方法获取可打断的锁,并在获取锁后休眠 5 秒钟,模拟正在执行任务。同时,线程 t2 也通过 lock() 方法获取普通的不可打断的锁。

由于 ReentrantLock 支持可打断的锁获取,当一个线程正在尝试获取可打断的锁时,可以被其他线程中断,从而避免因为死锁等原因导致程序一直无法正常退出。

在主线程中,等待 1 秒后启动线程 t2 获取普通的不可打断的锁,之后再等待 1 秒钟,确保线程 t1 已经获取到锁并进入阻塞状态。接着,主线程中断线程 t1 的锁获取过程,这会抛出 InterruptedException 异常。在线程 t1 中,捕获异常并释放锁。

由于线程 t1 在获取锁的过程中被中断了,它没有执行完整个任务,而是在 try-catch 块中捕获 InterruptedException 异常后,直接释放了锁并退出了。因此,在输出日志中,可以看到线程 t1 执行完毕、释放锁的语句没有被输出,而线程 t2 执行完毕、释放锁的语句被正常输出。

3.2.3 锁超时

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class TimeoutDemo {
    static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                if (lock.tryLock(5, TimeUnit.SECONDS)) { // 等待 5 秒超时获取锁
                    System.out.println("线程 t1 获取锁成功");
                    Thread.sleep(10000); // 模拟线程 t1 正在执行任务,执行 10 秒钟
                } else {
                    System.out.println("线程 t1 等待获取锁超时"); // 如果超时,则输出等待超时的日志
                }
            } catch (InterruptedException e) {
                System.out.println("线程 t1 被中断了!");
            } finally {
                if (lock.isHeldByCurrentThread()) { // 如果当前线程持有锁,则释放锁
                    lock.unlock();
                }
                System.out.println("线程 t1 执行完毕,释放锁");
            }
        });

        Thread t2 = new Thread(() -> {
            try {
                lock.lock(); // 获取普通的不可打断的锁
                System.out.println("线程 t2 获取锁成功");
            } finally {
                lock.unlock(); // 释放锁
                System.out.println("线程 t2 执行完毕,释放锁");
            }
        });

        t1.start();
        Thread.sleep(3000); // 让 t1 先启动并开始获取锁
        t2.start(); // 启动 t2 获取锁
    }
}

在该示例中,线程 t1 通过 tryLock() 方法尝试获取锁,等待 5 秒超时。如果在 5 秒钟内获取到了锁,则输出获取锁成功的日志,并模拟执行任务,执行 10 秒钟。否则,在 5 秒钟后输出获取锁超时的日志。

在主线程中,等待 3 秒钟后启动线程 t2 获取普通的不可打断的锁。

由于 ReentrantLock 支持超时的锁获取,当一个线程正在尝试获取超时的锁时,在指定的时间内未获取到锁,则放弃等待并退出。这种方式可以避免因为死锁等原因导致线程一直等待,从而影响程序的正常运行。

在以上示例中,线程 t1 在等待 5 秒钟后依然没有获取到锁,因此输出获取锁超时的日志。同时,线程 t2 则通过 lock() 方法成功获取了锁,执行完任务后释放锁。在输出日志中可以看到,线程 t2 执行完毕、释放锁的语句被正常输出,而线程 t1 输出了等待超时的日志。

3.2.4 公平锁和非公平锁

import java.util.concurrent.locks.ReentrantLock;

public class LockDemo {
    static ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
    static ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁

    public static void main(String[] args) {
        new Thread(() -> testLock(fairLock), "Thread1").start();
        new Thread(() -> testLock(fairLock), "Thread2").start();
        new Thread(() -> testLock(unfairLock), "Thread3").start();
        new Thread(() -> testLock(unfairLock), "Thread4").start();
    }

    private static void testLock(ReentrantLock lock) {
        for (int i = 0; i < 2; i++) {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "获取锁成功");
                Thread.sleep(1000); // 模拟线程在执行任务
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock(); // 释放锁
            }
        }
    }
}

输出日志如下:
公平锁:

Thread1获取锁成功
Thread1获取锁成功
Thread2获取锁成功
Thread2获取锁成功

非公平锁:

Thread3获取锁成功
Thread4获取锁成功
Thread3获取锁成功
Thread4获取锁成功

在该示例中,我们创建了两个 ReentrantLock 对象,分别表示公平锁和非公平锁。然后分别创建了四个线程,其中前两个线程使用公平锁,后两个线程使用非公平锁。每个线程都会尝试获取锁、睡眠一秒钟模拟执行任务,然后释放锁。

在输出日志中可以看到,使用公平锁的两个线程依次获取到了锁并执行任务,而使用非公平锁的两个线程则可以同时获取到锁。这是因为非公平锁允许新来的线程插队获取锁,从而导致之前等待的线程会饥饿,无法及时获得锁。相比之下,公平锁保证了锁的获取顺序,虽然效率不如非公平锁,但能够避免线程饥饿的问题。

需要注意的是,在实际开发中,应该根据具体情况选择公平锁或非公平锁。如果希望保证锁获取的公平性,那么应该使用公平锁;如果对锁的获取顺序没有特殊要求,且希望获得更好的性能,可以选择非公平锁。

3.2.5 Condition条件变量

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionDemo {
    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();
    static int count = 0;

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            lock.lock();
            try {
                while (count < 5) {
                    System.out.println(Thread.currentThread().getName() + ":等待信号");
                    condition.await(); // 等待信号
                }
                System.out.println(Thread.currentThread().getName() + ":收到信号,继续执行");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "Thread1");
        thread1.start();

        Thread thread2 = new Thread(() -> {
            lock.lock();
            try {
                while (count < 5) {
                    Thread.sleep(1000); // 模拟耗时操作
                    count++;
                    System.out.println(Thread.currentThread().getName() + ":发送信号,已完成" + count + "次");
                    condition.signal(); // 发送信号
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "Thread2");
        thread2.start();
    }
}

输出:

Thread1:等待信号
Thread2:发送信号,已完成1Thread1:等待信号
Thread2:发送信号,已完成2Thread1:等待信号
Thread2:发送信号,已完成3Thread1:等待信号
Thread2:发送信号,已完成4Thread1:等待信号
Thread2:发送信号,已完成5Thread1:收到信号,继续执行

在该示例中,我们创建了一个计数器 count 和一个 ReentrantLock 对象 lock,以及一个与 lock 相关联的 Condition 条件变量 condition。然后创建了两个线程,其中线程1等待接收线程2发送的信号,线程2执行完一定次数的任务后,会发送信号给线程1。

在输出日志中可以看到,线程1开始等待信号,线程2执行任务并发送信号,线程1收到信号后继续执行。

3.2.6 和synchronized的区别

  1. 锁的获取和释放方式不同:synchronized 关键字会自动释放锁,而 ReentrantLock 则需要手动调用 unlock 方法来释放锁。
  2. 支持公平锁和非公平锁机制:synchronized 关键字只支持非公平锁机制,而 ReentrantLock 可以通过构造函数设置为公平锁或者非公平锁。
  3. 粒度不同:synchronized 关键字粒度比较粗,只能锁住整个方法或者代码块,而 ReentrantLock 可以灵活地控制需要锁住的代码范围。
  4. 可中断性:ReentrantLock 支持 lockInterruptibly 方法,可以在获取锁时响应中断;而 synchronized 关键字则无法被中断。
  5. 等待可中断:ReentrantLock 支持通过 tryLock 方法尝试获取锁,并且可以设置等待时间,在等待时间到达后自动放弃锁的获取,以避免死锁问题;而 synchronized 关键字则无法做到这一点。
  6. 性能差异:在低并发情况下,synchronized 的性能优于 ReentrantLock,但是在高并发情况下,ReentrantLock 的性能就比 synchronized 好了很多。

六、乐观锁

乐观锁是一种并发控制的策略,它假设在大部分情况下操作不会发生冲突,因此在进行数据更新时,不会对共享数据进行加锁,而是直接进行数据更新,如果更新过程中发现其他线程已经更新了该数据,则认为发生了冲突,根据具体情况选择回滚操作或延迟重试。乐观锁的特点是不阻塞其他线程的读操作,可以提高系统的吞吐量和并发性能。
适用场景:读多写少,并发冲突少

乐观锁的实现方式有多种,如CAS(Compare and Swap)机制、版本/时间戳机制、数据库乐观锁、缓存乐观锁、分布式锁等。其中,通过CAS机制实现的乐观锁最为常见,其基本原理是先读取共享数据的版本号,然后更新数据时比较版本号是否发生变化,如果没有变化则更新数据,否则认为发生了冲突,需要进行回滚或者重试。

6.1 CAS

6.1.1 CAS的定义

CAS (Compare and Swap) 是一种原子性操作,用于实现多线程环境下的同步和互斥。在 CAS 操作中,会比较内存中某个位置的值和预期值是否相等,如果相等就将该位置值更新为新的值,如果不相等,则不做任何操作。这一系列操作是原子性的,任何时刻只有一个线程能够执行操作,并且保证线程安全。

6.1.2 CAS的问题及解决方案

  1. 循环时间长开销大:如果 CAS 失败,会一直尝试,这会耗费很多的 CPU 时间。
  2. 只能保证一个共享变量的原子操作:CAS 只能保证对单个共享变量的原子性操作,无法处理多个共享变量之间的同步问题。
  3. ABA 问题:由于 CAS 算法不使用锁,因此在一定程度上避免了死锁、饥饿等问题。但是,在某些特殊情况下,会出现 ABA 问题。

ABA问题描述:

ABA 问题的本质是在某个线程执行 CAS 操作之前,另一个线程修改了共享变量两次,并且第二次修改和 CAS 操作之间没有其他线程的干扰,这样就可能造成 CAS 操作成功,但是实际上共享变量已经被其他线程改变过了。

解决方案:

解决 ABA 问题的一种方法是为共享变量增加版本号或者时间戳,每次修改时更新版本号或时间戳,这样即使共享变量被修改了多次,版本号或时间戳也会随之改变,避免了 ABA 问题的出现。

6.1 原子类(JUC包下)

Java 原子类的底层实现主要是基于 sun.misc.Unsafe 类提供的原子操作方法来实现的。由于 Unsafe 类提供了硬件级别的原子操作支持,因此可以在不使用锁的情况下实现线程安全的操作,具有很高的执行效率。

在使用原子类时,通过 Unsafe 类的方法更新变量值,并且确保在多个线程同时对同一变量进行访问时保持数据的一致性和正确性。同时,Java 原子类的实现还考虑到了各种内存模型、CPU架构和操作系统等因素对数据的影响,从而对不同平台提供了最优化的实现方式。

需要注意的是,虽然 Java 原子类提供了一种方便的线程安全的解决方案,但是在某些极端情况下,仍然无法完全避免并发问题。特别是在复杂的多线程程序中,应该根据实际情况选择合适的并发解决方案,如加锁、分段锁、CAS 等。

Unsafe使用CAS来保证原子操作,代码如下:

@Data
class Student {
	volatile int id;
	volatile String name;
}

Unsafe unsafe = UnsafeAccessor.getUnsafe();
Field id = Student.class.getDeclaredField("id");
Field name = Student.class.getDeclaredField("name");
// 获得成员变量的偏移量
long idOffset = UnsafeAccessor.unsafe.objectFieldOffset(id);
long nameOffset = UnsafeAccessor.unsafe.objectFieldOffset(name);
Student student = new Student();
// 使用 cas 方法替换成员变量的值
UnsafeAccessor.unsafe.compareAndSwapInt(student, idOffset, 0, 20); // 返回 true
UnsafeAccessor.unsafe.compareAndSwapObject(student, nameOffset, null, "张三"); // 返回 true
System.out.println(student);

6.1.1 原子类有哪些

  1. AtomicBoolean:提供原子更新布尔类型的操作。
  2. AtomicInteger:提供原子更新整型的操作。
  3. AtomicLong:提供原子更新长整型的操作。
  4. AtomicReference:提供原子更新引用类型的操作。
  5. AtomicStampedReference:提供原子更新带有版本号或时间戳的引用类型的操作,ABA问题解决方案。
  6. AtomicMarkableReference:提供原子更新带有标记位的引用类型的操作,ABA问题解决方案。
  7. AtomicIntegerFieldUpdater:提供对指定类的指定字段进行原子更新操作的支持。
  8. AtomicLongFieldUpdater:提供对指定类的指定字段进行原子更新操作的支持。
  9. AtomicReferenceArray:提供原子更新数组类型的操作。
  10. AtomicIntegerArray:提供原子更新整型数组类型的操作。
  11. AtomicLongArray:提供原子更新长整型数组类型的操作。
  12. DoubleAccumulator:提供带有两个操作数(基于 double 值的累加器)的原子更新操作。
  13. LongAccumulator:提供带有两个操作数(基于 long 值的累加器)的原子更新操作。

6.1.2 原子类的CAS使用

class DecimalAccountSafeCas implements DecimalAccount {
	AtomicReference<BigDecimal> ref;
	public DecimalAccountSafeCas(BigDecimal balance) {
		ref = new AtomicReference<>(balance);
	}
	@Override
	public BigDecimal getBalance() {
		return ref.get();
	}
	@Override
	public void withdraw(BigDecimal amount) {
		while (true) {
			BigDecimal prev = ref.get();
			BigDecimal next = prev.subtract(amount);
			if (ref.compareAndSet(prev, next)) {
				break;
			}
		}
	}
}

看到这段代码可能有同学会有疑问了,原子类不是线程安全的吗?为什么还要多余的用CAS去保证线程安全?
原子类确实能保证单个操作的原子性,但是上面这段代码有两个操作,一个是取出之前的余额,然后再减。在多线程场景下,withdraw()这个方法里的代码其实是会产生静态条件的,并不是线程安全的。所以我们这里使用原子类自带的CAS方法来进行并发控制。

6.1.3 AtomicStampedReference(ABA问题解决方案)

import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicStampedReferenceDemo {
    public static void main(String[] args) {
        String str1 = "Hello";
        String str2 = "World";
        AtomicStampedReference<String> stampedRef = new AtomicStampedReference<>(str1, 0);
        // 输出当前的引用和版本号
        System.out.println("Current reference: " + stampedRef.getReference() + ", stamp: " + stampedRef.getStamp());
        // 尝试以原子方式更新引用和版本号
        boolean success = stampedRef.compareAndSet(str1, str2, 0, 1);
        // 输出更新后的引用和版本号
        System.out.println("New reference: " + stampedRef.getReference() + ", stamp: " + stampedRef.getStamp());
        System.out.println("Update success? " + success);
        // 尝试以原子方式更新引用和版本号,但是使用错误的版本号
        success = stampedRef.compareAndSet(str1, str2, 0, 2);
        // 输出修改失败后的引用和版本号
        System.out.println("New reference: " + stampedRef.getReference() + ", stamp: " + stampedRef.getStamp());
        System.out.println("Update success? " + success);
    }
}

输出:

Current reference: Hello, stamp: 0
New reference: World, stamp: 1
Update success? true
New reference: World, stamp: 1
Update success? false

6.1.4 AtomicMarkableReference(ABA问题解决方案)

import java.util.concurrent.atomic.AtomicMarkableReference;

public class AtomicMarkableReferenceDemo {

    public static void main(String[] args) {
        String str1 = "Hello";
        String str2 = "World";
        AtomicMarkableReference<String> markRef = new AtomicMarkableReference<>(str1, false);
        // 输出当前的引用和标记位
        System.out.println("Current reference: " + markRef.getReference() + ", mark: " + markRef.isMarked());
        // 尝试以原子方式更新引用和标记位
        boolean success = markRef.compareAndSet(str1, str2, false, true);
        // 输出更新后的引用和标记位
        System.out.println("New reference: " + markRef.getReference() + ", mark: " + markRef.isMarked());
        System.out.println("Update success? " + success);
        // 尝试以原子方式更新引用和标记位,但是使用错误的标记位
        success = markRef.compareAndSet(str1, str2, true, false);
        // 输出修改失败后的引用和标记位
        System.out.println("New reference: " + markRef.getReference() + ", mark: " + markRef.isMarked());
        System.out.println("Update success? " + success);
    }
}

输出:

Current reference: Hello, mark: false
New reference: World, mark: true
Update success? true
New reference: World, mark: true
Update success? false

七、JMM

7.1 JMM是什么?

JMM(Java Memory Model)是用于规范 Java 程序中多线程并发访问共享变量的一种抽象规范,它定义了一种 happens-before 关系,用于确保不同线程中执行的操作之间建立一定的顺序关系。

7.2 JMM工作内存和主内存

  1. 主内存:是一个共享内存区域,是所有线程共同访问的内存区域。
  2. 工作内存:每个线程都有自己的工作内存,是线程私有的内存区域。

JMM 规定了线程之间的通信必须通过主内存来完成。当各个线程访问共享变量时,它们会将变量从主内存复制到自己的工作内存中进行操作。每个线程在自己的工作内存中对变量的操作结果,不一定会立即同步到主内存中,因此在一个线程中修改了变量的值后,其它线程可能无法立即看到这个修改结果。
为了解决这个问题,JMM 定义了一套规则,即在某些情况下,JVM 必须把工作内存中的数据同步回主内存,以保证线程之间对共享变量所做的修改能够及时被其他线程看到。具体来说,当一个线程执行 lock、unlock、volatile 读写操作时,JVM 都会强制把工作内存中的内容刷新到主内存中。
工作内存与主内存

7.3 JMM和JVM的区别

  1. JMM 是一个规范,它定义了 Java 并发编程中的内存模型,而 JVM 是实现这个规范的具体平台。
  2. JMM 主要规定了多线程编程中共享变量的可见性、有序性和原子性等问题,而 JVM 必须按照 JMM 规范来实现相应的内存模型,以确保程序的正确性和可靠性。
  3. JMM 规范了 Java 中多线程程序在共享内存环境中的行为,而 JVM 实现了 JMM 中定义的 synchronized、volatile 等关键字和机制,并提供了一些额外的机制和 API 来支持多线程编程,例如线程池、锁、原子类等。
  4. JMM 中的主内存和工作内存是抽象的概念,而 JVM 中的内存模型则是具体实现的,包括了堆、栈、方法区、程序计数器等内存区域。

7.4 JMM与硬件内存架构关系

  1. 硬件内存架构是指计算机硬件中的内存组织和管理方式,包括 CPU 中的寄存器、高速缓存、主存和内存屏障等。它并没有JMM中工作内存的划分,所有的变量都会存到主存中,当然也有可能存储到 CPU 缓存或者寄存器中。
  2. JMM是用于规范 Java 程序中多线程并发访问共享变量的一种抽象模型,并不实际存在。它只是将硬件的主存根据一定的规则来进行划分,实际上操作的还是硬件的内存。

JMM和硬件内存之间存在着相互交叉的关系,具体关系如下图所示:
JMM与硬件内存架构关系

7.5 happens-before规则

  1. 程序顺序规则(Program Order Rule,简称 POR):在一个线程内,按照程序代码的顺序,前面的操作 happens-before 后面的操作。
  2. 锁定规则(Lock Rule):对于一个锁的解锁操作,必须 happens-before 于随后对于同一个锁的加锁操作。
  3. volatile 变量规则(Volatile Variable Rule):对于一个 volatile 变量的写操作,必须 happens-before 于随后对于同一个变量的读操作。
  4. 传递性规则(Transitivity Rule):如果 A happens-before B,B happens-before C,则可以得出 A happens-before C。
  5. 线程启动规则(Thread Start Rule):主线程 A 启动并等待子线程 B,那么 B 中的操作 happens-before 于 A 中的 join 操作返回。
  6. 线程终止规则(Thread Termination Rule):子线程 B 执行结束,那么 B 中的操作 happens-before 于主线程 A 中通过 join 观察到 B 已经执行结束。
  7. 中断规则(Interruption Rule):线程 A 调用线程 B 的 interrupt 方法,那么 A 中的操作 happens-before 于 B 检测到中断事件的操作。

八、volatile

volatile 是一个关键字,用于告诉编译器和 CPU,该变量是易变的,不能被缓存、优化或重排序,每次访问都能从内存中读取最新的值。

volatile 可以保证其修饰的变量的可见性和有序性,禁止指令重排序。但是无法保证原子性(不能保证完全的原子性,只能保证单次读/写操作具有原子性,即无法保证复合操作的原子性)。

8.1 指令重排序

指令重排序是现代处理器为了提高执行效率而采取的一种策略,在不影响最后结果的情况下,允许CPU对无依赖的指令乱序执行,这样可以提高流水线的运行效率。

以下是可能会发生指令重排序的原因:

  1. 编译器优化:编译器在将代码翻译成机器指令时,可能会对指令进行优化,以提高程序的运行效率。在优化过程中,编译器可以重排指令,来达到更好的优化效果。
  2. 处理器优化:现代处理器通常都具有指令流水线功能,即可以同时执行多条指令。为了充分利用这种特性,处理器可能会对指令进行重新排序,从而降低指令之间的相关性,提高处理器的吞吐量。
  3. 内存系统重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

8.2 可见性

可见性是指一个线程对共享变量的修改,可以被其他线程及时地感知到
在多线程并发编程中,由于每个线程都有自己的工作内存,因此线程之间存在着数据不一致的问题。如果一个线程对某个共享变量进行了修改,而其他线程无法看到这个修改,就会导致程序出现意想不到的行为。

不可见的示例代码如下:

public class VisibilityDemo {

    // 构造共享变量
    public static boolean flag = true;
//    public static volatile boolean flag = true;   // 如果使用volatile修饰则可以终止循环

    public static void main(String[] args) {
        // 线程1更改flag
        new Thread(() -> {
            // 睡眠3秒确保线程2启动
            try { TimeUnit.SECONDS.sleep(3);  } catch (InterruptedException e) {e.printStackTrace();}
            // 修改共享变量
            flag = false;
            System.out.println("修改成功,当前flag为true");
        }, "one").start();

        // 线程2获取更新后的flag终止循环
        new Thread(() -> {
            while (flag) {

            }
            System.out.println("获取到修改后的flag,终止循环");
        }, "two").start();
    }
}

上面的代码flag是共享变量,一个线程会对flag进行修改,另一个线程则会使用flag作为while循环判断。结果是线程2会进入死循环,flag的修改没有被线程2感知到。

8.3 有序性

有序性指禁止指令重排序,即保证程序执行代码的顺序与编写程序的顺序一致(程序执行顺序按照代码的先后顺序执行)。

8.4 volatile是怎么保证可见性的

以上面举的不可见的代码为例,被 volatile 修饰的共享变量 flag 被一个线程修改后,JMM(Java内存模型)会把该线程的CPU内存中的共享变量 flag 立即强制刷新回主存中,并且让其他线程的CPU内存中的共享变量 flag 缓存失效,这样当其他线程需要访问该共享变量 flag 时,就会从主存获取最新的数据。
volatile保证可见性
两点疑问及解答:

为什么会有CPU内存?

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1/L2/其他)后再进行操作,但是操作完后的数据不知道何时才会写回主存。所以如果是普通变量(未被修饰的),什么时候被写入主存是不确定的,所以读取的可能还是旧值,因此无法保证可见性。

各个线程的CPU内存是怎么保持一致性的?

实现了缓存一致性协议(MESI),MESI在硬件上约定了:每个处理器通过嗅探在总线上传播的数据来检查自己的CPU内存的值是否过期,当处理器发现自己的缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置为无效状态。当处理器对该数据进行修改操作时,会重新从系统内存(主存)中把数据读到处理器缓存(CPU内存)里。

8.5 volatile是保证可见性的原理

8.5.1 Lock指令(汇编指令)

通过上面的例子的Class文件查看汇编指令时,会发现变量有无被 volatile 修饰的区别在于被 volatile 修饰的变量会多一个lock前缀的指令。

lock前缀的指令会触发两个事件:

  1. 将当前线程的处理器缓存行(CPU内存的最小存储单元,这里可以大致理解为CPU内存)的数据写回到主存(系统内存)中
  2. 写回主存的操作会使其他线程的CPU内存中该内存地址的数据无效(缓存失效)

所以使用 volatile 修饰的变量在汇编指令中会有lock前缀的指令,所以会将处理器缓存的数据写回主存中,同时使其他线程的处理器缓存的数据失效,这样其他线程需要使用数据时,会从主存中读取最新的数据,从而实现可见性。

8.5.2 内存屏障(CPU指令)

为了性能优化,JMM 在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序。JMM 提供了内存屏障阻止这种重排序

这里介绍的是内存屏障中的一类:读写屏障(用于强制读取或刷新主存的数据,保证数据一致性)

  1. Store屏障:当一个线程修改了volatile变量的值,它会在修改后插入一个写屏障,告诉处理器在写屏障之前将所有存储在缓存中的数据同步到主内存。
  2. Load屏障:当另一个线程读取volatile变量的值,它会在读取前插入一个读屏障,告诉处理器在读屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果。

对上面的例子使用javap查看JVM指令时,如果被 volatile 修饰时多一个 ACC_VOLATILE,JVM把字节码生成机器码时会在相应位置插入内存屏障指令,因此可以通过读写屏障实现 volatile 修饰变量的可见性。

注意读写屏障的特点:可以将所有变量(包括不被 volatile 修饰的变量)一起全部刷入主存,尽管这个特性可以使未被 volatile 修饰的变量也具备所谓的可见性,但是不应该过于依赖这个特性,在编程时,对需要要求可见性的变量应当明确的用 volatile 修饰(当然除了volatile,synchronized、final以及各种锁都可以实现可见性,这里不过多说明)。

8.6 volatile是怎么保证有序性的

volatile通过禁止指令的重排序来保证代码的执行顺序。

为了禁止指令重排序,volatile主要会用到四种内存屏障,它们分别是:
内存屏障
volatile的插入屏障策略

  1. 在每个 volatile 写操作的前面插入一个 StoreStore 屏障
  2. 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障
  3. 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障
  4. 在每个 volatile 读操作的后面插入一个 LoadStore 屏障

内存屏障的执行策略
以双重检查的单例demo为例:

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
             	//插入LoadLoad屏障
                if (instance == null) {
                	//插入StoreStore屏障
                    instance = new Singleton();
                    //插入LoadLoad屏障
                }
            }
        }
        return instance;
    }
}

如果这里不加volatile可能会出现问题,instance = new Singleton();这句代码字节码层面会解析成两个操作,一个是赋值,一个是new对象。如果不加volatile,这两个操作会指令重排,导致返回的instance是没有初始化的。
DCL不加volatile

8.7 volatile为什么不能保证原子性?

原子性指一个操作或一系列操作是不可分割的,要么全部执行成功,要么全部不执行(中途不可被中断)。

代码示例:

public class VolatileDemo {
    private static volatile int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("count: " + count);
    }
}

输出:

count: 16720

可能有人会问,为什么上面代码会有问题?volatile不是可以保证线程的可见性和禁止指令重排序吗?

volatile的线程可见性确实可以做到共享变量读 / 写操作的原子性,但是i++其实并不是一个操作,而是由三个操作组成。

  1. 读取 i 的值
  2. 将 i 自增1(i + 1)
  3. 写回 i 的新值(i = i + 1)

这三个步骤在执行时可能会被打断,具体来说就是,在某线程读取 i 的值后,还没有进行加 1 操作时,其他线程也可以读取 i 的值并开始对其进行修改,从而导致线程之间的竞争条件,破坏 i++ 操作的原子性。

此时,volatile 可以保证多线程之间对 i 变量的可见性,但它并不能保证 i++ 操作的原子性。因为 volatile 只能保证对变量的读写操作具有原子性,但是 i++ 操作包含了读、写两个操作,volatile 只保证了其中一部分的原子性,无法保证整个操作的原子性。

如果要保证几个操作的原子性,就必须使用synchronize或者lock去保证。

8.8 较低开销的读写锁

/**
 * 读写锁实现多线程下的计数器
 */
public class VolatileSynchronizedCounter {
    // volatile变量
    private volatile int count = 0;
    // synchronized方法
    public synchronized void increment() { 
        count++; // 原子操作
    }
    public int getCount() {
        return count;
    }
}

synchronized保证写操作count++的原子性,volatile修饰让变量具有可见性,count的修改会让其它线程立刻的感知到。所有能getCount()返回的值都是从主存获取到的最新值。

九、JUC

9.1 AQS

AQS是很多并发工具类的底层实现,所以这个我会在 《一文搞懂AQS》 (待更新)文章中详细去讲解。

9.2 读写锁ReentrantReadWriteLock

读写锁是一种特殊的锁,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。在多线程并发访问时,读写锁可以提高系统的性能和吞吐量。

注意:

  1. 读锁不支持条件变量
  2. 重入时升级不支持:ReentrantReadWriteLock 支持重入,即同一个线程可以获取多次读锁或写锁,但是不支持从读锁升级为写锁。实际上,在一个线程持有读锁的情况下,如果直接尝试获取写锁,则会导致线程永久阻塞。

示例代码如下:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockDemo {
    private static int value = 0;
    private static ReadWriteLock lock = new ReentrantReadWriteLock();
    private static Lock readLock = lock.readLock();
    private static Lock writeLock = lock.writeLock();

    public static void main(String[] args) {
        // 启动 3 个读线程和 2 个写线程
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                read();
            }).start();
        }
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                write();
            }).start();
        }
    }

    private static void read() {
        readLock.lock(); // 获取读锁
        try {
            System.out.println(Thread.currentThread().getName() + " start reading, value is " + value);
            Thread.sleep(1000); // 模拟读操作执行耗时
            System.out.println(Thread.currentThread().getName() + " end reading, value is " + value);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readLock.unlock(); // 释放读锁
        }
    }

    private static void write() {
        writeLock.lock(); // 获取写锁
        try {
            System.out.println(Thread.currentThread().getName() + " start writing");
            Thread.sleep(2000); // 模拟写操作执行耗时
            value++; // 对共享变量进行写操作
            System.out.println(Thread.currentThread().getName() + " end writing, value is " + value);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock(); // 释放写锁
        }
    }
}

输出:

Thread-0 start reading, value is 0
Thread-1 start reading, value is 0
Thread-2 start reading, value is 0
Thread-4 start writing
Thread-4 end writing, value is 1
Thread-3 start writing
Thread-3 end writing, value is 2
Thread-1 end reading, value is 0
Thread-0 end reading, value is 0
Thread-2 end reading, value is 0

当有线程获取到写锁的时候,读的线程会阻塞,等待写线程完成之后,才会读操作。

9.3 StampedLock

StampedLock是JDK8新增的一种读写锁实现,与ReentrantReadWriteLock相比,StampedLock具有更好的并发性能。StampedLock支持三种模式的访问:写、悲观读和乐观读。它的特点是在使用读锁、写锁时都必须配合使用加解读锁。

注意:

  1. StampedLock 不支持条件变量
  2. StampedLock 不支持可重入

代码示例:

import java.util.concurrent.locks.StampedLock;

public class StampedLockDemo {
    private static final StampedLock lock = new StampedLock();
    private static int data = 0;

    public static void main(String[] args) {
        Thread readerThread = new Thread(() -> {
            long stamp = lock.tryOptimisticRead();
            int value = data;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (lock.validate(stamp)) {
                System.out.println("Optimistic read success, value: " + value);
            } else {
                System.out.println("Optimistic read failed, value may be modified");
                stamp = lock.readLock();
                try {
                    value = data;
                } finally {
                    lock.unlockRead(stamp);
                }
                System.out.println("Pessimistic read obtained, value: " + value);
            }
        });

        Thread writerThread = new Thread(() -> {
            long stamp = lock.writeLock();
            try {
                Thread.sleep(5000);
                data = 1;
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlockWrite(stamp);
            }
            System.out.println("Data has been modified to 1");
        });

        readerThread.start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        writerThread.start();

        try {
            readerThread.join();
            writerThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出:

Optimistic read failed, value may be modified
Data has been modified to 1
Pessimistic read obtained, value: 1

在这个 demo 中,我们创建了一个 StampedLock 对象和一个共享变量 data。在启动两个线程之后,readerThread 会先获取一个乐观读锁,然后进行一段逻辑操作,最后尝试验证锁状态并输出相应的信息。如果验证成功,说明锁未被占用或者仅被读锁占用,此时获取锁的线程可以直接使用共享资源;否则说明锁被其他线程独占或者有写锁存在,这时候需要升级为悲观读锁,获取到锁之后再次读取共享资源。

long stamp = lock.tryOptimisticRead();
// 验戳
if(!lock.validate(stamp)){
	// 锁升级
}

这段代码就是加乐观读的操作,它会进行戳校验,保证读取到的是共享资源最新值。

9.4 Semaphore

Semaphore是一个并发控制工具,用于控制对共享资源的访问。Semaphore可以看作是一个计数器,初始化时设定一个计数值,每当一个线程访问共享资源时,计数值减一;当计数值为0时,所有试图访问共享资源的线程都将被阻塞,直到有一个线程释放了计数值。

代码示例:

import java.util.concurrent.Semaphore;

public class SemaphoreDemo {
    private static final Semaphore semaphore = new Semaphore(1);

    public static void main(String[] args) {
        MyThread t1 = new MyThread("A");
        MyThread t2 = new MyThread("B");
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static class MyThread extends Thread {
        private final String name;

        public MyThread(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            try {
                System.out.println(name + " is waiting for a permit.");
                semaphore.acquire();
                System.out.println(name + " gets a permit.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(name + " releases the permit.");
                semaphore.release();
            }
        }
    }
}

输出:

A is waiting for a permit.
B is waiting for a permit.
A gets a permit.
A releases the permit.
B gets a permit.
B releases the permit.

在这个 demo 中,我们创建了一个 Semaphore 对象并初始化为1。在启动两个线程之后,每个线程都会请求获取一个许可证(即调用 semaphore.acquire() 方法),如果此时有其他线程已经占用了许可证,则当前线程将被阻塞;否则,当前线程将得到许可证,并执行一段逻辑操作。最后,线程释放许可证(即调用 semaphore.release() 方法),让其他线程可以获取许可证进行操作。

9.5 CountdownLatch

CountdownLatch是Java中的一个多线程同步工具,可以用来控制一个或多个线程等待一组事件的发生。
它的核心思想是让一个或多个线程持续等待,直到其他线程完成某些操作后再一起执行。该工具是通过一个计数器来实现的,计数器的初始值为指定的数目,每当一个线程完成了一定的任务,计数器的值就会减1,在某个时刻,计数器的值将会减为0,此时所有被阻塞的线程都会被唤醒继续执行。
其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一。

代码示例:

import java.util.concurrent.CountDownLatch;

public class CountdownLatchDemo {
    public static void main(String[] args) {
        CountDownLatch latch = new CountDownLatch(2);

        WorkerThread t1 = new WorkerThread("Thread1", 2000, latch);
        WorkerThread t2 = new WorkerThread("Thread2", 3000, latch);

        t1.start();
        t2.start();

        try {
            latch.await();
            System.out.println("All threads have finished their work.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static class WorkerThread extends Thread {
        private final String name;
        private final long time;
        private final CountDownLatch latch;

        public WorkerThread(String name, long time, CountDownLatch latch) {
            this.name = name;
            this.time = time;
            this.latch = latch;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(time);
                System.out.println(name + " has finished its work.");
                latch.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

输出:

Thread1 has finished its work.
Thread2 has finished its work.
All threads have finished their work.

9.6 CyclicBarrier

CyclicBarrier是Java中的一个多线程同步工具,用于控制多个线程之间的同步。
和CountDownLatch一样,它也可以阻塞线程直到满足某个条件,不同的是CyclicBarrier的操作是可循环的,即被阻塞的线程可以反复循环地等待某些操作的完成。当所有线程都达到障碍点后,障碍将打开并允许所有线程继续向前执行。

示例代码:

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        int threadCount = 3;
        CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> System.out.println("All threads have finished their work."));

        for (int i = 1; i <= threadCount; i++) {
            new WorkerThread("Thread" + i, barrier).start();
        }
    }

    static class WorkerThread extends Thread {
        private final String name;
        private final CyclicBarrier barrier;

        public WorkerThread(String name, CyclicBarrier barrier) {
            this.name = name;
            this.barrier = barrier;
        }

        @Override
        public void run() {
            try {
                System.out.println(name + " has started its work.");
                Thread.sleep((long) (Math.random() * 3000));
                System.out.println(name + " has finished its work and is waiting for others.");

                barrier.await();

                System.out.println(name + " continues to execute after all threads have met the barrier.");
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }
}

输出:

Thread1 has started its work.
Thread2 has started its work.
Thread3 has started its work.
Thread3 has finished its work and is waiting for others.
Thread1 has finished its work and is waiting for others.
Thread2 has finished its work and is waiting for others.
All threads have finished their work.
Thread1 continues to execute after all threads have met the barrier.
Thread2 continues to execute after all threads have met the barrier.
Thread3 continues to execute after all threads have met the barrier.

在这个例子中,我们创建了一个计数器为3的CyclicBarrier对象。三个WorkerThread线程分别进行一些任务,并在执行完毕后调用 barrier.await() 方法来等待其他线程。在所有三个线程都执行到 await 方法处时,所有线程会被释放并继续执行。最后,主线程结束,并输出 All threads have finished their work.。

9.7 线程安全集合类

  1. 遗留的线程安全集合如 Hashtable , Vector
  2. 使用 Collections 装饰的线程安全集合,如:
  • Collections.synchronizedCollection
  • Collections.synchronizedList
  • Collections.synchronizedMap
  • Collections.synchronizedSet
  • Collections.synchronizedNavigableMap
  • Collections.synchronizedNavigableSet
  • Collections.synchronizedSortedMap
  • Collections.synchronizedSortedSet
  1. java.util.concurrent.*

重点介绍 java.util.concurrent.* 下的线程安全集合类,可以发现它们有规律,里面包含三类关键词:Blocking、CopyOnWrite、Concurrent

  • Blocking 大部分实现基于锁,并提供用来阻塞的方法
  • CopyOnWrite 之类容器修改开销相对较重
  • Concurrent 类型的容器

内部很多操作使用 cas 优化,一般可以提供较高吞吐量
弱一致性。

  • 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的
  • 求大小弱一致性,size 操作未必是 100% 准确
  • 读取弱一致性

9.8 ConcurrentHashMap

ConcurrentHashMap是Java中的一个线程安全的哈希表,它可以在多线程并发的情况下更高效地进行读写操作。
如果想要了解ConcurrentHashMap的底层实现原理,可以去看 《一文搞懂HashMap和ConcurrentHashMap》 (待更新)这篇文章。

示例代码:

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapDemo {
    public static void main(String[] args) throws InterruptedException {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        Thread putThread = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                map.put("key" + i, i);
                System.out.println("Put " + i + " in map.");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread getThread = new Thread(() -> {
            while (!map.isEmpty()) {
                for (String key : map.keySet()) {
                    System.out.println("Get " + map.get(key) + " from map.");
                    map.remove(key);
                }
            }
        });

        putThread.start();
        getThread.start();

        putThread.join();
        getThread.join();
    }
}

输出:

Put 1 in map.
Put 2 in map.
Get 1 from map.
Get 2 from map.
Put 3 in map.
Put 4 in map.
Get 3 from map.
Get 4 from map.
Put 5 in map.
Get 5 from map.

在这个示例中,我们创建了一个ConcurrentHashMap对象,并启动了两个线程。putThread线程向ConcurrentHashMap中插入五个键值对,然后休眠1秒钟;getThread线程不断从ConcurrentHashMap中取出元素,直到ConcurrentHashMap为空为止。

9.9 ConcurrentLinkedQueue

ConcurrentLinkedQueue是Java中的一个线程安全的队列,它基于链表实现,支持并发的入队和出队操作,并且不需要使用显式的锁来保证线程安全。ConcurrentLinkedQueue的元素按照FIFO(先进先出)的顺序排列。

示例代码:

import java.util.concurrent.ConcurrentLinkedQueue;

public class ConcurrentLinkedQueueDemo {
    public static void main(String[] args) throws InterruptedException {
        ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();

        Thread offerThread = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                queue.offer("item" + i);
                System.out.println("Offer " + "item" + i + " to queue.");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread pollThread = new Thread(() -> {
            while (!queue.isEmpty()) {
                String item = queue.poll();
                System.out.println("Poll " + item + " from queue.");
            }
        });

        offerThread.start();
        pollThread.start();

        offerThread.join();
        pollThread.join();
    }
}

输出:

Offer item1 to queue.
Poll item1 from queue.
Offer item2 to queue.
Poll item2 from queue.
Offer item3 to queue.
Poll item3 from queue.
Offer item4 to queue.
Poll item4 from queue.
Offer item5 to queue.
Poll item5 from queue.

在这个示例中,我们创建了一个ConcurrentLinkedQueue对象,并启动了两个线程。offerThread线程向ConcurrentLinkedQueue中插入五个元素,然后休眠1秒钟;pollThread线程不断从ConcurrentLinkedQueue中取出元素,直到ConcurrentLinkedQueue为空为止。
可以看到,offerThread和pollThread是并行执行的,它们对ConcurrentLinkedQueue的操作不会相互影响,并且ConcurrentLinkedQueue能够正确地处理线程间的冲突和竞争。

9.10 CopyOnWriteArrayList

CopyOnWriteArrayList是Java中的一个线程安全的ArrayList,它的实现方式是在进行add、set等写操作时,不直接修改原有的数组,而是先复制一份新的数组,然后进行修改,最后再将新数组赋值给原来的数组。这种写时复制的方式可以保证对原有数组不会产生影响,因此可以支持高并发的写操作。

代码示例:

import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListDemo {
    public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();

        Thread addThread = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                list.add(i);
                System.out.println("Add " + i + " to list.");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread getThread = new Thread(() -> {
            while (!list.isEmpty()) {
                Integer item = list.get(0);
                System.out.println("Get " + item + " from list.");
                list.remove(item);
            }
        });

        addThread.start();
        getThread.start();

        addThread.join();
        getThread.join();
    }
}

输出:

Add 1 to list.
Get 1 from list.
Add 2 to list.
Get 2 from list.
Add 3 to list.
Get 3 from list.
Add 4 to list.
Get 4 from list.
Add 5 to list.
Get 5 from list.

可以看到,addThread和getThread是并行执行的,它们对CopyOnWriteArrayList的操作不会相互影响,并且CopyOnWriteArrayList能够正确地处理线程间的冲突和竞争

9.11 FutureTask

FutureTask 是 Java 并发编程提供的一个实现了 RunnableFuture 接口的类,该类实现了 Future 和 Runnable 接口,因此可以被用来异步执行任务并获得任务的执行结果。FutureTask 对象通常是由 ExecutorService 执行任务的结果。

示例代码:

import java.util.concurrent.*;

public class FutureTaskDemo {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // 创建一个 FutureTask 对象,它会计算从1加到100的和
        FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1; i <= 100; i++) {
                    sum += i;
                }
                return sum;
            }
        });

        // 将 FutureTask 提交给 ExecutorService 执行
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.submit(task);

        // 等待任务执行完成并获取结果
        int result = task.get();

        // 输出结果
        System.out.println("1 + 2 + ... + 100 = " + result);

        // 关闭线程池
        executor.shutdown();
    }
}

输出:

1 + 2 + ... + 100 = 5050

9.12 CompletableFuture

CompletableFuture 是 Java 并发编程提供的一个实现了 CompletionStage 接口的类,该类实现了异步执行任务和处理任务结果的能力,支持链式操作,可以更加方便地处理异步任务的结果。CompletableFuture 对象可以通过多种方法创建,并且可以通过一些静态方法,如 thenApply()、thenAccept()、thenRun() 等来实现任务完成后的处理,并支持多个 CompletableFuture 对象的组合操作。

CompletableFuture 相比于 FutureTask有什么优势?

  1. 功能更强大:CompletableFuture 是 FutureTask 的升级版,提供了更加丰富的异步编程功能,支持更加方便的任务组合、转换以及异常处理等。在 Java 8 中,CompletableFuture 还提供了一些新的方法,如 thenCombine()、thenAcceptBoth()、runAfterBoth() 等,支持更加灵活的链式操作。
  2. 更加灵活的回调方式:CompletableFuture 支持回调方式的处理结果,可以通过 thenApply()、thenAccept()、thenRun() 等方法注册回调函数,并且具有返回值的回调函数还可以继续执行异步任务。
  3. 状态可手动设置:CompletableFuture 对象的状态可以手动设置,即可以手动将状态设置为已完成、抛出异常或者被取消,而不需要等待异步任务执行完毕。这种方式可以被用于实现一些特定的场景,例如超时控制、异常处理等。
  4. 多线程竞争问题:当多个线程同时尝试对同一个 CompletableFuture 对象执行 complete() 或 completeExceptionally() 方法时,只有其中一个线程可以成功地操作,并且其他的操作将会失败,这意味着 CompletableFuture 对象不存在多线程竞争的问题。但是,FutureTask 并没有提供这种机制,需要使用锁等方式进行控制。

代码示例:

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class CompletableFutureDemo {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // 创建一个 CompletableFuture 对象,它会计算从1加到100的和
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            int sum = 0;
            for (int i = 1; i <= 100; i++) {
                sum += i;
            }
            return sum;
        });

        // 链式调用 thenAccept() 方法处理计算结果
        future.thenAccept(result -> System.out.println("1 + 2 + ... + 100 = " + result));
    }
}

输出:

1 + 2 + ... + 100 = 5050

与 CompletableFuture 不同的是,FutureTask 只能表示一个异步任务,而且其执行结果只能通过 get() 方法等待获取。同时 CompletableFuture 支持异常处理、链式操作、超时等功能,因此更加灵活和方便。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

java干货仓库

觉得写的不错,就给博主投币吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值