1 多线程基础
1.1 为什么会有多线程
本质原因是摩尔定律失效,CPU 的单核性能没有办法快速提高,从而导致多核+分布式的时代的来临。
多 CPU 核心意味着操作系统有了更多的并行计算资源可以使用。操作系统以线程作为基本的调度单元。单线程是最好处理的。线程越多,管理复杂度越高,跟我们程序员都喜欢自己单干是一个道理。《人月神话》里说加人可能干得更慢,可见多核时代的编程更有挑战。
1.2 Java 线程的创建过程
进程与线程的区别:进程是程序的一次执行过程,是操作系统分配资源的基本单位;线程是 CPU 调度的基本单位,一个进程可以拥有多个线程。
在 Java 层面,可以通过调用 Thread 类的 start() 方法来创建一个 Java 线程,一个 Java 线程对应着一个操作系统层面的线程,需要为这个线程创建线程栈,分配内存空间等。线程创建好之后,会执行 run() 方法中定义的任务,任务执行完成之后,线程被销毁。
2 Java 多线程*
2.1 守护线程
守护线程又称为后台线程,在JVM中,如果没有正在运行中的前台线程,则JVM就会自动结束运行,而不管守护线程(守护进程没有执行完就会被销毁)。 所以守护线程一般用于执行某些可以被放弃的任务或事件。
2.2 基础接口 Runnable
在 Java 中,可以通过实现一个 Runnable 接口的方式,来创建线程。(需要将 Runnable 实例传入 Thread,然后调用 Thread 的 start 方法真正地创建线程)
2.2.1 使用匿名类直接 new 接口
public static void main(String[] args) { Runnable r = new Runnable() { @Override public void run() { String name = Thread.currentThread().getName(); for (int i = 0; i < 10; i++) { System.out.println(name + ": " + i); } } }; Thread t1 = new Thread(r); Thread t2 = new Thread(r); t1.start(); t2.start(); }
接口中只有一个 run() 方法需要实现,也可以写成 Lambda 的形式:
public static void main(String[] args) { Runnable r = () -> { String name = Thread.currentThread().getName(); for (int i = 0; i < 10; i++) { System.out.println(name + ": " + i); } }; Thread t1 = new Thread(r); Thread t2 = new Thread(r); t1.start(); t2.start(); }
2.2.2 实现 Runnable 接口
public class RunnableTest implements Runnable { @Override public void run() { String name = Thread.currentThread().getName(); for (int i = 0; i < 10; i++) { System.out.println(name + ": " + i); } } public static void main(String[] args) { RunnableTest r = new RunnableTest(); Thread t1 = new Thread(r); Thread t2 = new Thread(r); t1.start(); t2.start(); } }
调用 Thread.run() 和 Thread.start() 的区别:
- Thread.run():在当前线程中执行 run() 方法中的代码。
- Thread.start():创建一个新的线程来执行 run() 方法中的代码。
2.3 线程状态
通过调用 Thread.start() 方法创建出一个线程之后,线程进入可运行的状态。经系统调度获得 CPU 时间片之后进入运行状态,当线程等待锁的时候进入阻塞状态,获取锁之后又进入可运行的状态。线程执行结束之后,进入终止状态。
2.4 Thread 类
2.5 wait() & notify()
注:空参的 wait() 相当于 wait(0)
wait() 与 sleep() 的区别:sleep() 方法释放 CPU 资源,不释放锁;wait() 要释放对象锁。
2.6 Thread 的状态改变操作
- Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING 状态,但不释放对象锁,millis 后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。
- Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU 时间片,但不释放锁资源,由运行状态变为就绪状态,让 OS 再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际上无法保证 yield() 达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield() 不会导致阻塞。该方法与 sleep() 类似,只是不能由用户指定暂停多长时间。
- t.join() / t.join(long millis),当前线程里调用其它线程 t 的 join 方法,当前线程进入 WAITING / TIMED_WAITING 状态,当前线程不会释放已经持有的对象锁,因为内部调用了 t.wait(),所以会释放t这个对象上的同步锁。线程t 执行完毕或者 millis 时间到,当前线程进入就绪状态。其中,wait 操作对应的 notify 是由 jvm 底层的线程执行结束前触发的。
- obj.wait(),当前线程调用对象的 wait() 方法,当前线程释放obj 对象锁,进入等待队列。依 notify() / notifyAll() 唤醒或者 wait(long timeout) timeout 时间到自动唤醒。唤醒会,线程恢复到wait 时的状态。
- obj.notify() 唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll() 唤醒在此对象监视器上等待的所有线程。
2.7 Thread 状态
3 线程安全*
3.1 多线程执行会遇到什么问题?
多个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。不进行恰当的控制,会导致线程安全问题。
3.2 并发相关的性质
1) 原子性:原子操作,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
2)可见性:对于可见性,Java 提供了 volatile 关键字来保证可见性。
当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
此外,通过 synchronized 和 Lock 也能够保证可见性,synchronized 和Lock 能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。
volatile 并不能保证原子性。
3)有序性:Java 允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影
响到多线程并发执行的正确性。可以通过 volatile 关键字来保证一定的“有序性”(synchronized 和Lock也以)。
happens-before 原则(先行发生原则):
- 程序次序规则:一个线程内,按照代码先后顺序;
- 锁定规则:一个 unLock 操作先行发生于后面对同一个锁的 lock 操作;
- volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
- 传递规则:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出 A 先于 C;
- 线程启动规则:Thread 对象的 start() 方法先行发生于此线程的每个一个动作;
- 线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行;
- 对象终结规则:一个对象的初始化完成先行发生于他的 finalize() 方法的开始。
3.3 synchronized 的实现
- 使用对象头标记字(Object monitor)中的标志位来标记锁的状态
- Synchronized 方法优化
- 偏向锁: BiaseLock
3.4 volatile
- 每次读取都强制从主内存刷数据。
- 适用场景: 单个线程写;多个线程读。
- 原则: 能不用就不用,不确定的时候也不用。
- 替代方案: Atomic 原子操作类。
3.5 final
- 使用 final 修饰的类不可以被继承;
- 使用 final 修饰的方法不可以被重写;
- 使用 final 修饰的变量不允许被修改(java 中没有常量)。
4 线程池原理与应用*
4.1 Executor – 执行者
线程池从功能上看,就是一个任务执行器。
ExcutorService 的 submit 方法: 有返回值,用Future 封装
execute 方法: 无返回值
submit 方法抛出异常可以在主线程中 get 捕获到。
execute 方法执行任务是捕捉不到异常的。
4.2 ExecutorService
shutdown():停止接收新任务,原来的任务继续执行
shutdownNow():停止接收新任务,原来的任务停止执行
boolean awaitTermination(timeOut, unit):阻塞当前线程,返回是否所有线程都执行完
4.3 ThreadPoolExecutor
ThreadPoolExecutor 提交任务逻辑 :
- 判断corePoolSize
- 加入workQueue
- 判断maximumPoolSize
- 执行拒绝策略处理器
4.4 线程池参数
缓冲队列
BlockingQueue 是双缓冲队列。BlockingQueue 允许两个线程同时向一个向队列中存储,一个从队列中取出。在保证并发安全的同时,提高了队列的存取效率。
- ArrayBlockingQueue:规定大小的BlockingQueue,其构造必须指定大小。其所含的对象是FIFO 顺序排的。
- LinkedBlockingQueue:大小不固定的BlockingQueue,若其构造时指定大小,生成的BlockingQueue 有大小限制,不指定大小,其大小有Integer.MAX_VALUE 来决定。其所含的对象是FIFO 顺序排序的。
- PriorityBlockingQueue:类似于LinkedBlockingQueue,但是其所含对象的排序不是FIFO,而是依据对象的自然顺序或者构造函数的Comparator 决定。
- SynchronizedQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成。
拒绝策略
- ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
4.5 使用 Executors 工具类创建线程池的方法
newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
newFixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
newCachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
newScheduledThreadPool
创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。
4.6 创建固定线程池的经验
不是越大越好,太小肯定也不好:
假设核心数为N
1、如果是 CPU 密集型(计算密集型)应用,则线程池大小设置为 N 或 N+1
2、如果是 IO 密集型应用,则线程池大小设置为 2N 或 2N+2
4.7 Callable – 基础接口
public class RandomSleepTask implements Callable<Integer> { @Override public Integer call() throws Exception { Integer sleep = new Random().nextInt(10000); TimeUnit.MILLISECONDS.sleep(sleep); return sleep; } }
对比 Runnable 接口和 Callable 接口:Runnable.run() 方法没有返回值,Callable.call() 方法有返回值。
4.8 Future – 基础接口
public static void main(String[] args) throws Exception { Callable<Integer> task = new RandomSleepTask(); ExecutorService executorService = initThreadPoolExecutor(); Future<Integer> future1 = executorService.submit(task); Future<Integer> future2 = executorService.submit(task); // 等待执行结果 Integer result1 = future1.get(1, TimeUnit.SECONDS); Integer result2 = future2.get(1, TimeUnit.SECONDS); System.out.println("result1=" + result1); System.out.println("result2=" + result2); }
- 本文固定链接: https://weiguangli.com/archives/612
- 转载请注明: lwg0452 于 Weiguang的博客 发表