Java并发详解(万字图文总结)

Java并发详解(万字图文总结)-mikechen

Java并发是Java面试经常考察的内容,下面给大家总结了非常全面的Java并发@mikechen

Java并发基础

Java并发编程是一种处理多个线程同时执行的编程方式。以下是Java并发的一些基础概念和机制:

1. 并发与并行性

并发性(Concurrency): 多个任务交替执行的能力。在单处理器系统中通过时间片轮转实现。

如下图所示:

Java并发详解(万字图文总结)-mikechen

通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。

并行性(Parallelism): 多个任务同时执行的能力,在多处理器系统中实现。

如下图所示:

Java并发详解(万字图文总结)-mikechen

多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。

2. 线程与进程

线程(Thread)

是程序执行的最小单元,一个进程中可以包含多个线程。

线程共享进程的资源,但拥有独立的执行路径。

 

进程(Process)

是程序的一次执行,每个进程拥有独立的内存空间,互不干扰。

 

3.线程生命周期

线程具有不同的状态,包括:

  • 新建状态(New): 线程创建但未启动。
  • 就绪状态(Runnable): 线程可以被调度执行。
  • 运行状态(Running): 线程正在执行。
  • 阻塞状态(Blocked): 线程被阻塞,等待某个条件。
  • 终止状态(Terminated): 线程执行完毕。

如下图所示:

Java并发详解(万字图文总结)-mikechen

4.线程通信

wait()、notify()、notifyAll(): 在synchronized块内实现线程间通信。

如下所示:

synchronized (sharedObject) {
    while (condition) {
        sharedObject.wait();
    }
    // 执行线程的任务
    sharedObject.notify();
}

 

Java并发线程

1.Java线程的创建

在Java中,有两种主要的方式来创建线程:继承Thread类和实现Runnable接口。

1)继承Thread

继承Thread类是一种创建线程的简单方式,需要重写run()方法,并在该方法中定义线程要执行的任务。

如下所示:

class MyThread extends Thread {
    public void run() {
        // 线程执行的任务
        System.out.println("MyThread is running.");
    }
}

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

 

2)实现 Runnable 接口

实现Runnable接口是另一种创建线程的方式,这种方式更灵活。

如下所示:

class MyRunnable implements Runnable {
    public void run() {
        // 线程执行的任务
        System.out.println("MyRunnable is running.");
    }
}

public class Main {
    public static void main(String[] args) {
        // 创建线程对象,传入实现了 Runnable 接口的实例
        Thread myThread = new Thread(new MyRunnable());

        // 启动线程
        myThread.start();
    }
}

 

3)使用 Lambda 表达式

从Java 8开始,可以使用Lambda表达式更简洁地创建线程,特别适用于Runnable接口。

如下所示:

public class Main {
    public static void main(String[] args) {
        // 使用 Lambda 表达式创建线程
        Thread myThread = new Thread(() -> {
            // 线程执行的任务
            System.out.println("Thread using Lambda is running.");
        });

        // 启动线程
        myThread.start();
    }
}

 

2.查看进程和线程的方法

在命令行中,可以使用操作系统提供的工具,如ps(Unix/Linux)来查看运行中的Java进程。

ps -ef | grep java

可以通过操作系统提供的工具,如top(Unix/Linux)来查看Java应用程序中的线程信息。

top -H -p <pid>

 

3.线程的常见方法

创建和启动线程

构造方法:

Thread(): 创建一个新的线程对象。

Thread(String name): 创建一个带有指定名称的线程对象。

Thread(Runnable target): 创建一个带有指定Runnable对象的线程对象。

Thread(Runnable target, String name): 创建一个带有指定Runnable对象和名称的线程对象。

启动线程:

start(): 启动线程,使线程进入就绪状态,等待调度执行。

线程状态查询

getState(): 获取线程的当前状态。

getName(): 获取线程的名称。

getId(): 获取线程的唯一标识符。

getPriority(): 获取线程的优先级。

线程控制

中断线程:

interrupt(): 中断线程。

等待线程终止:

join(): 等待该线程终止。

join(long millis): 最多等待millis毫秒。

让出CPU执行权

yield(): 暂停当前正在执行的线程对象,让其他具有相同优先级的线程执行。

线程休眠:

sleep(long millis): 在指定的毫秒数内让当前正在执行的线程休眠。

等待和唤醒:

wait(): 使当前线程等待,直到其他线程调用notify()或notifyAll()唤醒它。

notify(): 唤醒在等待池中等待的单个线程。

notifyAll(): 唤醒在等待池中等待的所有线程。

 

Java并发线程池

Java线程池是一种用于管理和复用线程的机制,它可以在应用程序中有效地管理并发任务的执行。

线程池可以提高程序的性能、稳定性,并且更好地控制资源的使用。

1.线程池的创建

Java提供了Executor框架用于线程池的创建,其中主要的实现类是ThreadPoolExecutor。

ThreadPoolExecutor 创建线程池时所设置的 7 个参数,如下所示:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
}

详细的参数详解,如下所示:

Java并发详解(万字图文总结)-mikechen

2.线程池的类型

Java中提供了几种常见的线程池,这些线程池分别适用于不同的场景。

// 创建一个固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);

// 创建一个单线程的线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

// 创建一个可以根据需要创建新线程的线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();


//支持定时任务和周期性任务的线程池
ScheduledThreadPoolExecutor scheduledExecutor = new ScheduledThreadPoolExecutor(corePoolSize);

//支持工作窃取算法,用于提高并行任务处理的效率。
ExecutorService workStealingPool = Executors.newWorkStealingPool();


 

3.线程池的执行流程

线程池的执行流程,如下图所示:

Java并发详解(万字图文总结)-mikechen
主要包含3点:

首先:判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。

其次:线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。

再次:判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

Java并发线程锁

在Java中线程锁是一种用于控制多个线程对共享资源访问的机制,它可以防止多个线程同时修改共享资源,从而确保线程安全。

1.Synchronized

synchronized关键字是Java中最基本的锁机制,它可以用于方法或代码块,确保同一时刻只有一个线程访问被锁定的资源。

如下所示:

Object lock = new Object();

synchronized (lock) {
    // 线程安全的代码块
}

 

2.ReentrantLock

ReentrantLock是java.util.concurrent.locks包中提供的锁机制,它支持重入,允许同一线程多次获取锁。

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

Lock lock = new ReentrantLock();

lock.lock();
try {
    // 线程安全的代码块
} finally {
    lock.unlock();
}

 

3.ReentrantReadWriteLock

ReadWriteLock接口提供了读锁和写锁两种类型的锁,如下所示:

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

ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock readLock = readWriteLock.readLock();
Lock writeLock = readWriteLock.writeLock();

// 读操作
readLock.lock();
try {
    // 读取共享资源
} finally {
    readLock.unlock();
}

// 写操作
writeLock.lock();
try {
    // 写入共享资源
} finally {
    writeLock.unlock();
}

允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。

 

Java并发工具类

Java并发工具类提供了一系列用于多线程编程的实用工具,用于简化和优化并发程序的开发。

如下图所示:

Java并发详解(万字图文总结)-mikechen

包含了:

1.并发工具类

提供:CountDownLatchCyclicBarrierSemaphore等,可以实现更加丰富的多线程操作。

2.并发容器

提供各种线程安全的容器:最常见的ConcurrentHashMap、有序的ConcurrentSkipListMap,实现线程安全的动态数组CopyOnWriteArrayList等。

ConcurrentHashMap是线程安全的哈希表实现,用于在多线程环境中进行高效的并发访问。

相对于传统的HashMapConcurrentHashMap通过分段锁机制实现更好的并发性能。

3.并发队列

各种BlockingQueue的实现:常用的ArrayBlockingQueue、SynchorousQueue或针对特定场景的PriorityBlockingQueue。

4.Executor框架

可以创建各种不同类型的线程池,调度任务运行等,绝大部分情况下,不再需要自己从头实现线程池和任务调度器。

以上是一些常见的Java并发工具类,它们提供了各种机制来简化多线程编程中的同步和协调工作。

 

Java并发内存模型

Java并发内存模型,英文全称是Java Memory Model,简称为JMM。

JMM规定了各种情况下线程间的可见性、有序性和原子性等问题。

1. 可见性(Visibility)

可见性指的是一个线程对共享变量值的修改能够被其他线程立即看到。

在多线程环境中,一个线程对共享变量的修改并不一定会立即反映到其他线程中,这是因为每个线程都有自己的工作内存,线程之间通过主内存进行通信。

可见性问题可能导致一个线程对共享变量的修改无法被其他线程及时感知,从而引发错误。

解决可见性问题的方法:

  • 使用volatile关键字: volatile关键字保证了变量的可见性,一个线程对volatile变量的修改对其他线程是可见的。
  • 使用synchronized关键字: synchronized关键字不仅保证了原子性,还保证了可见性。

2. 有序性(Ordering)

有序性指的是程序的执行顺序与代码的书写顺序一致。

在多线程环境中,由于指令重排序的存在,线程执行的顺序可能与代码书写的顺序不一致,从而引发意外的结果。

解决有序性问题的方法:

  • 使用synchronized关键字: synchronized关键字保证了临界区内的代码是串行执行的,避免了指令重排序问题。
  • 使用volatile关键字: volatile关键字禁止了指令重排序。
  • 使用java.util.concurrent工具类: java.util.concurrent包中提供的工具类(例如CountDownLatchCyclicBarrier等)也能够保证一定的有序性。

3. 原子性(Atomicity)

原子性指的是一个操作是不可中断的,要么全部执行成功,要么全部执行失败。

在多线程环境中,如果一个操作包含多个步骤,那么其中任何一步出现问题都可能导致整个操作的失败,这就涉及到原子性的问题。

 

4.主内存和工作内存

主内存(Main Memory)

所有线程共享的内存区域,存储着实例字段、静态字段和代码等。主内存是多个线程共同访问的,是线程之间数据的真实存储位置。

工作内存(Working Memory)

每个线程独有的内存区域,存储了该线程使用的变量副本。线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的数据。

 

5. 内存间的交互操作

读操作(Read): 从主内存拷贝变量的值到工作内存。

写操作(Write): 将工作内存中的变量的值写回主内存。

 

6. Happens-Before关系

JMM通过Happens-Before关系来规定操作的执行顺序。

如果一个操作A Happens-Before操作B,那么操作A的执行结果对于操作B是可见的。

  • 程序次序规则: 在一个线程内,按照程序代码的顺序,前面的操作Happens-Before于后续的任意操作。
  • 锁定规则: 释放锁Happens-Before于后续对同一锁的获取操作。
  • volatile变量规则: 对一个volatile变量的写操作Happens-Before于后续对这个变量的读操作。
  • 传递性: 如果A Happens-Before B,且B Happens-Before C,则A Happens-Before C。

Java并发内存模型规定了多线程程序中共享数据的访问规则,通过Happens-Before关系、volatile关键字、synchronized关键字等机制确保了多线程程序的可见性、有序性和一致性。

作者简介

陈睿|mikechen,10年+大厂架构经验,就职于阿里巴巴、淘宝、百度等一线互联网大厂。

关注作者「mikechen」公众号,获取更多技术干货!

评论交流
    说说你的看法