[TOC]
本文记录对 Java多线程相关技术的理解,读完将了解以下知识点:
- 线程是什么?与进程有什么关系?
- Java 多线程怎么实现?
- 多线程并行为什么要做同步以及怎么同步?
- 多线程间如何通信
线程 & 进程
-
定义
通俗一点理解,线程是操作系统中执行代码的一条路径。一个线程从创建到结束期间的任务就是在指定的代码路径上执行一次。
线程是运行在进程中的一个实体,一个进程可以有多个线程。
进程是软件中应用程序一次运行的一个实例,一个软件中可以有多个进程。
-
线程和进程的不同
- 线程是CPU调度的基本单位,多个线程共享进程的资源,线程不单独拥有操作系统资源(除了线程栈空间)。
- 进程是操作系统分配系统资源的基本单位,进程拥有独立的运行环境。
- 进程的创建开销大,线程更轻量级。
- 进程和线程都是并发技术的一种载体
Java 多线程实现
Java 实现多线程有多种方法。
Thread & Runnable
通过创建 Thread 对象,实现其 run() 方法,并通过 Thread.start() 方法启动线程。
Thread t = new Thread() {
@Override
public void run(){
...
}
};
t.start();
还可以通过 Runnable 对象创建 Thread 对象,并通过 Thread.start() 方法启动线程。
Runnable r = new Runnable() {
@Override
public void run() {
...
}
};
Thread t = new Thread(r);
t.start();
FutureTask & Callable
FutureTask + Callable 实现多线程与 Thread + Runnable 的区别是前者有返回值。
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
return "Done";
}
};
FutureTask<String> task = new FutureTask<>(callable);
new Thread(task).start();
try {
Thread.sleep(2000);
System.out.println(task.get());
} catch (Exception e) {
}
ThreadFactory
线程工厂,通过传入 Runnable 对象创建线程对象。
ThreadFactory f = new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r);
}
};
Thread t= f.newThread(() -> System.out.println("New Thread"));
t.start();
Excutors 线程池
Excutors 是 Java 提供的线程池工具类。
-
创建指定大小的线程池
public static ExecutorService newFixedThreadPool(int nThreads)
-
创建无限大小的线程池,初始大小为0
public static ExecutorService newCachedThreadPool()
-
创建无限大小的线程池,初始大小指定
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
-
创建只有一个线程的线程池
public static ExecutorService newSingleThreadExecutor()
Excutors 创建线程池的方法最终都是通过 ThreadPoolExcutor 创建出来的。
ThreadPoolExcutor 构造函数的五个参数决定了创建的线程池是哪个类型。
- corePoolSize 线程池始终活跃的线程数。
- maximumPoolSize 线程池最大能创建的线程数。
- keepAliveTime 始终活跃线程之外的线程空闲之后存活的最长时间。
- unit 存活时间单位。
- workQueue 当线程池工作饱和时,新来的任务存放的队列。
线程同步
多线程环境下为什么要做同步
多线程运行环境下,多个线程共享进程的资源。在多个线程访问同一个资源时,某一个线程对资源进行写操作的过程中,其他线程对这个写了一半的资源进行了读操作,或者对这个写了一半的资源进行了写操作,则会导致数据不一致,即数据错误。
多线程同步则是对共享资源的访问进行控制,让同一时间只有一个线程可以访问资源,保证数据的一致性。
Java 多线程同步机制
Java 实现多线程同步有三种常用的方式。
synchronized
synchronized 可以修饰代码块、实例方法、静态方法,对应同步的粒度不一样。
synchronized 本质是通过控制同步代码在同一时间内只有一个线程能访问,这样就保证了同步代码涉及的资源在同一时间内只有一个线程能访问,Java 中将这个控制线程的单元是称为 monitor,monitor 其实也是一段具体的代码,实现了互斥访问和访问缓存队列。
任何线程在获得 monitor 对象的第一时间,会将共享内存中的资源复制到自己的缓存中;在释放 monitor 的第一时间,会将缓存中的资源复制到共享内存中。这样下一个请求获得 monitor 的线程就可以在共享内存中取到正确的数据。
volatile
- volatile 能保证基本类型的赋值操作和对象的引用赋值操作的同步性,但不能保证对象内容的修改同步。
- volatile 不能解决 ++ 的原子性问题。
- volatile 基于禁止指令重排序、写后缓存失效、写后立即刷新内存机制实现了数据修改对多线程的可见性。
- Java 在 java.util.concurrent.atomic 包下封装了基本类型的 volatile 同步实现。
ReentrantReadWriteLock
ReentrantReadWriteLock 是 Java 中封装的读写锁,通过 ReentrantReadWriteLock.readLock() 拿到读锁,ReentrantReadWriteLock.writeLock() 拿到写锁,读锁和写锁分别用于对共享资源的读同步和写同步。
读写锁有配套的 lock() 和 unlock() 方法,注意在异常分支调用 unlock() 释放锁对象,否则可能出现死锁的情况。
锁的优化
锁升级
Java 中锁有四种状态,分别是无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。
- 偏向锁
- 由于大多数情况下,多线程间锁竞争是不发生的,往往都是同一个线程多次请求同一个锁,这样每次竞争锁都会增加不必要的资源消耗,为了降低竞争锁的资源消耗,引入了偏向锁。
- 偏向锁不会主动释放锁,当线程 A 首次访问同步代码块并获取锁对象时,会在 java 对象头和栈帧中记录偏向锁的线程 id。下一次线程 A 再次获取锁的时候,只需要比较当前线程 id 和 java 对象头中的线程 id 是否一致,如果一致,则不用使用 CAS 来加锁、解锁;如果不一致(非线程A),则会去查询记录的线程 id 对应的线程是否还存活,如果没有存活,那么锁对象被标记为无锁状态,其他线程可以获取并将其重新设置为偏向锁;如果存活,则会去该线程的栈帧信息中查询此线程是否还需要继续持有这个锁对象,如果不需要则和线程不存活处理一致;如果仍然需要,则会暂停此线程,撤销偏向锁,升级为轻量级锁。
- 轻量级锁
- 在大多数情况下,除了锁竞争的现象不发生,还有线程持有锁的时间一般也不长。阻塞线程需要将 CPU 从用户态切换到内核态,会消耗资源,如果阻塞不久立即被唤醒,阻塞线程带来的资源消耗就有点得不偿失,因此这种状况下,还不如让线程自旋着等待锁释放,这就是轻量级锁。
- 当线程 A 持有的偏向锁时,线程 B 尝试竞争锁,这时候线程 B 就会自旋在那等待线程 A 释放锁,如果自旋次数达到了设定的阈值线程 A 还没有释放锁,或者是线程 B 在自旋的过程中,又有线程 C 尝试竞争这个锁对象,这个时候轻量级锁就会升级为重量级锁。
- 重量级锁
- 重量级锁会把除了拥有锁的线程之外的其他线程全部阻塞,防止 CPU 空旋。
锁粗化
- 一般来说,锁的粒度应该尽可能小,这样就能缩短其他线程的阻塞时间,等待的线程能尽快竞争到锁资源。但是加锁和解锁需要额外的资源消耗,如果锁粒度过小,则会导致一系列的加锁解锁操作,可能会导致不必要的资源消耗。
- 锁粗化就是将多个连续的加锁、解锁操作连接到一起,扩展成一个更大的锁,避免频繁的加锁和解锁操作。
锁消除
- Java 虚拟机在 JIT 编译时,通过对上下文进行扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁。## 多线程间的通信
线程间的通信指的是两个线程之间的交互,如启动、终止、等待/唤醒。
-
启动
启动一个线程的方法有多种,参考 Java 多线程实现章节,这里每一种实现方法都是由一个线程启动另外一个线程的方法。
-
终止
-
Thread.stop()
暴力停止一个线程,线程的执行会立即停止掉。
-
Thread.interrupt()
标记线程为终止状态,需要配合Thread.interrupt() 或 isInterrupted() 使用来终止线程。
-
-
等待/ 唤醒
-
wait()
一个线程需要等待另外一个线程执行完成,调用 wait() 可以使得当前线程进入到当前同步块 monitor 对象的等待唤醒队列。
-
notify() / notifyAll()
notify() 用于唤醒 monitor 等待唤醒队列中的一个线程;notifyAll() 用于唤醒 monitor 等待唤醒队列中的所有线程。
-
join()
等待调用线程执行完成,再继续往下执行。
-
yeild()
暂时释放自身资源给同优先级线程使用。
-
总结
读完应该理解:
- 线程、进程是什么,有什么特点,为什么会将这两个联系到一起。
- Java 实现多线程的几种方式。
- 多线程执行环境下会有什么问题,Java 实现同步的几种方法。
- 线程间通信的几种手段和原理。