F-1.多线程的基本概念


1.Java线程的创建过程

image.png

2.线程和进程的区别:

进程:是操作系统启动一个程序的运行单元,一个进程里有一个或多个线程,这些线程共享这个进程在操作系统中的资源,比如内存。不同的进程之间使用的内存资源一般是被隔离的,特殊情况下可以做共享。
线程:操作系统调度运行任务执行方法的基本单元。
现在的计算机发展状态下,这两个概念越来越相似越来越模糊了。

3.线程状态

image.png

4.线程改变状态的操作

  • Thead.sleep(millis毫秒):当前线程调用此方法,当前线程进入TIMED_WAITING 状态,但不释放对象锁,指定参数时间后,线程自动苏醒进入Ready就绪状态。作用:给其它线程执行机会的最佳方式。
  • Thead.yield:当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由Running运行状态变为Ready就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。
  • thead.join/thead.join(millis毫秒):当前线程里调用其它线程thread的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁,因为join底层内部调用了thread.wait,所以会释放tthread这个对象上的同步锁。线程thread执行完毕或者millis时间到,当前线程进入就绪状态。其中,wait操作对应的notify是由jvm底层的线程执行结束前触发的。
  • obj.wait:当前线程调用指定对象的wait()方法,当前线程释放obj对象锁,进入等待队列。恢复执行时会再次去尝试拿到锁。依靠notify()/notifyAll()唤醒或者wait(long timeout)的timeout时间到自动唤醒。唤醒后线程恢复到Runnable的状态。
  • obj.notify()/obj.nofityAll():唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。唤醒后线程恢复到Runnable的状态。
  • Thead.start():创建一个操作系统层面的对象,真正进入整个线程的生命周期。

5.线程的中断与异常处理

  • 线程内部自己处理异常,不溢出到外层(Future可以封装)
  • 如果线程被obj.wait,thread.join和Thread.sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。如果线程没有被阻塞,这时调用interrupt()将不起作用,直到执行到wait/sleep/join时,才马上会抛出InterruptedException。
  • 非以上obj.wait,thread.join和Thread.sleep三种方法的阻塞,需要打断时,则在业务设计层面做处理,比如设置一个外部全局状态,在处理逻辑中判断此状态来决定是否中断。

F-2.线程安全

并发相关的性质

  • 原子性:原子操作(对基本数据类型变量的读取和赋值操作是原子性操作),这些操作是不可被中断被打扰,要么执行,要么不执行。或者说这个操作本身不能够再拆解成多步不同的操作。
  • 可见性:JVM内部两个线程修改同一变量时各自拥有该变量的一个副本,副本修改完毕后再同步到主内存。1.volatile:修饰共享变量,保证线程在副本中修改的值立即被更新到主内存,同时更新到其他线程的副本,但不能保证原子性。2.synchronized/lock:修饰的代码块能保证对其他线程可见,
  • 有序性:Java允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。可以通过volatile/synchronized/Lock关键字来保证一定的“有序性”。参照下方happens-before原则。

happens-before原则(先行发生原则):感觉并没有什么卵用

  1. 程序次序规则:一个线程内,按照代码先后顺序
  2. 锁定规则:一个 unLock 操作先行发生于后面对同一个锁的 lock 操作
  3. Volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  4. 传递规则:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出 A 先于 C
  5. 线程启动规则:Thread 对象的 start() 方法先行发生于此线程的每个一个动作
  6. 线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join() 方法结束、 Thread.isAlive() 的返回值手段检测到线程已经终止执行
  8. 对象终结规则:一个对象的初始化完成先行发生于他的 finalize() 方法的开始

Synchronized(同步块关键字)实现

  1. 不管是变量,代码块,方法,类,都是针对某个对象加锁
  2. Synchronized方法优化:尽量减少加锁粒度,能加方法就不加类,能加代码块就不加方法。
  3. 锁状态

image.png

volatile

  1. 每次读取都强制从主内存刷数据
  2. 适用场景: 单个线程写;多个线程读
  3. 原则: 能不用就不用,不确定的时候也不用
  4. 替代方案: Atomic 原子操作类

F-3.线程池

JDK提供的线程池设计和实现

  1. Excutor: 执行者,顶层接口
  2. ExcutorService: 接口 API
  3. ThreadFactory: 线程工厂
  4. ThreadPoolExecutor
  5. Excutors: 工具类,创建线程

ExecutorService主要接口

  • execute(Runnable command):执行可运行的任务
  • shutdown:关闭线程池,停止接收新的线程任务,但执行中的线程任务不中断。优雅停止
  • shutdownNow:关闭线程池,停止接收新的线程任务,执行中的线程任务也强制中断。
  • submit:提交任务去执行,有三个重载方法
  • awaitTermination(timeout,unit):阻塞当前线程,返回线程是否全部执行完毕的boolean

案例:
重启服务器时需要先用shutdown关闭线程池,然后使用awaitTermination阻塞当前主线程一段时间,比如3分钟。3分钟之后该主线程不再阻塞,返回是否执行完毕的结果。如果为true,则正常退出执行重启服务器。如果为false,则认为业务线程已超时,执行shutdownNow强制关闭。

execute和submit的区别:
返回值
execute:没有返回值,只能简单提交Runnable给线程池去运行。
submit:有返回值,可以获得一个Future
异常
execute:和普通线程的异常机制一样,必须用try/catch捕获。如果没有捕获一些运行时异常,也会打印出堆栈信息。
submit:异常会被吃掉不打印堆栈信息。但可以调用返回值future的get方法,可以打印堆栈。但future.get是阻塞的,需要等待线程执行完毕才返回,所以可以获得Callable.call()的返回值。

  1. Future<Integer> future = Executors.newCachedThreadPool().submit(
  2. new Callable<Integer>() {
  3. @Override
  4. public Integer call() throws Exception {
  5. return i = 1 / 0;
  6. }
  7. }
  8. );
  9. System.out.println(future.get());

ThreadPoolExecutor线程池实现类

缓冲队列BlockingQueue

  1. ArrayBlockingQueue:规定大小的 BlockingQueue,其构造必须指定大小。其所含的对象是 FIFO 顺序排序的。
  2. LinkedBlockingQueue:大小不固定的 BlockingQueue,若其构造时指定大小,生成的 BlockingQueue 有大小限制,不指定大小,其大小有 Integer.MAX_VALUE 来决定。其所含的对象是 FIFO 顺序排序的。
  3. PriorityBlockingQueue:类似于 LinkedBlockingQueue,但是其所含对象的排序不是FIFO, 而是依据对象的自然顺序或者构造函数的 Comparator 决定。
  4. SynchronizedQueue:特殊的 BlockingQueue,对其的操作必须是放和取交替完成。

拒绝策略reject

  1. ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出 RejectedExecutionException异常(默认拒绝策略)
  2. ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常
  3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
  4. ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务(使用较多的拒绝策略)

工具类创建线程池的常用方法

  1. newSingleThreadExecutor
    创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。多余提交的任务会被保存在队伍队列,待线程空闲按照FIFO的顺序执行。
  2. newFixedThreadPool
    创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
  3. newCachedThreadPool
    创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程, 那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。
    此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
  4. newScheduledThreadPool
    创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。

schedule()方法:在给定时间,对方法进行一次调度
scheduleAtFixedRate()方法:它是以上一个任务开始执行时间为起点,之后的period时间,调度下一次任务。任务调度的频率是一样的。
scheduleWithFixedDelay()方法:在上一个任务结束后,再经过delay时间进行任务调度。

创建固定线程池的经验
不是越大越好,太小肯定也不好。假设CPU核心数为N
1、如果是CPU密集型应用,则线程池大小设置为 N 或 N+1
2、如果是IO密集型应用,则线程池大小设置为 2N 或 2N+2。因为I/O线程在等待IO处理时,CPU资源是闲置的,可以被新的线程拿来使用。

Callable – 基础接口
• Runnable#run()没有返回值
• Callable#call()方法有返回值