Java核心基础(3)——多线程设计与并发

线程基础

什么是线程?

线程是程序的一个载体,是调度CPU的最小资源单位

必须要有线程去调度CPU执行指令。

线程与进程

进程

运行程序后,程序指令和数据会被放入内存。
程序员开始运行后,放到内存中的内容就是进程,是资源分配的基本单位

启动一个应用程序,就会开启一个进程。

线程

线程是程序执行的基本单位,是一条进程的执行路径。

多线程

在同一个进程中开启了多条执行路径,每条执行路径不会相互影响

程序是如何运行的?

程序运行需要指令和数据。

  1. CPU读指令,指令存在CPU的PC(程序计数器)中
  2. CPU再读数据,将数据存到CPU的寄存器
  3. 计算结果回写,完成后再读下一条指令与所需数据,依次反复

谁调度线程

由线程调度器与操作系统算法进行线程的调度。

线程切换的概念

线程的执行并不一定是顺序的,多个线程可由线程调度器切换。

切换方式:如A线程正在执行,此时需要切换至另一线程B先执行,程序计数器与寄存器会保存A线程到缓存,B线程完成后,CPU再从缓存恢复线程A继续执行。

线程是不是越多越好?
不是,线程越多,线程切换越复杂,程序运行效率越低。

线程的类型

线程可分为用户线程与守护线程。

用户线程

一般由用户程序实现,应用创建管理,速度快,不依赖内核,如果用户线程阻塞则进程阻塞,JVM必须确保用户线程执行完毕才会退出

守护线程

又叫内核线程,由操作系统管理,守护线程阻塞不会导致进程阻塞,任何一个守护线程守护所有用户线程,JVM不用确保守护线程执行完毕就可以先退出,守护线程执行完再最后退出。常见守护线程有GC、内存监控线程等。


Java默认情况下创建用户线程
thread.setDaemon(true) 可设置线程为守护线程;
thread.setDaemon(false) 可设置线程为用户线程;

线程池

核心思想

可复用线程,管理线程,统一创建和销毁。
可缓存,可定长度,可定时,单例模式,内部有多个worker,通过复用worker来处理线程

何时使用线程池

  1. 单个任务处理时间短
  2. 需要处理的任务数大

线程池的5种状态

image

ThreadPoolExecutor参数

通常使用ThreadPoolExecutor类创建线程池。

  • corePoolSize:核心线程数
  • maximumPoolSize:最大线程数
  • keepAliveTime:针对非核心线程的最长闲置时间
  • TimeUnit:时间单位
  • BlockingQueue<Runnable>:阻塞队列,此队列只允许一个线程操作出队与入队
  • ThreadFactory:线程工厂
  • RejectedExecutionPolicy:拒绝策略

线程池原理与使用步骤

  1. 假设线程池corePoolSize2maximumPoolSize3,执行t1t2任务线程,这两个线程会交给线程池中的两个核心线程worker处理
  2. 假设阻塞队列长度为5,当再想执行t3~t7任务,若t1t2没执行完,则t3~t7任务放入阻塞队列
  3. 此时再想入t8至阻塞队列,超出阻塞队列最大长度5,并且此时t1t2没有执行完,线程会新建一个临时线程处理t8,本例中最大临时线程的数量为maximumPoolSize - corePoolSize = 1
    t8处理完,闲置keepAliveTime后,临时线程自动销毁;
    t8未处理完,且有t9进入,此时无多余临时线程,则执行线程池的拒绝策略:
    • AbortPolicy(默认):丢弃任务t9并抛出异常
    • DiscardPolicy:丢弃任务t9但不抛异常
    • DiscardOlderstPolicy:丢弃队列最前任务t3,并重新提交任务t9至队列
    • CallerRunsPolicy:由提交任务t9的线程(可能是主线程)处理,回退给提交者
  4. t1t2处理结束后,空出的核心线程worker处理阻塞队列头任务

线程状态

Thread.state

  1. NEW:尚未启动的线程
  2. RUNNABLE:在JVM中执行的线程
  3. BLOCKED:被阻塞等待监视器锁定的线程
  4. WAITTING:正在等待另一个线程执行特定动作的线程
  5. TIMED_WATTING:正在等待另一个线程执行动作到达指令等待时间的线程
  6. TERMINATED:已退出的线程

多线程的创建方法

  • 继承Thread类,重写run()方法
  • 实现Runnable接口,重写run()方法,无返回值
  • 实现Callable接口,重写run()方法,结合FutureTask.get()获得返回值
  • 线程池创建线程
  • 使用Spring异步注解@Asyn创建

如何停止线程

  • 建议线程正常停止,利用次数,不建议死循环
  • 建议使用标志位,设置一个标志位,while(flag),当需要停止时,设置flagfalse
  • 不要使用stop()destroy()

线程生命周期

image

sleep() 线程休眠

  • sleep(t)指定当前线程阻塞的毫秒数t
  • sleep()需抛出异常Interrupted Exception
  • t时间到达后,进入就绪状态,等待CPU调度
  • sleep不会释放锁! 区别于wait()会释放锁

运行 ————> 阻塞 ————> 就绪 ————>运行
    sleep      t时间后    CPU调度

yield() 线程礼让

  • 让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让CPU重新调度,礼让不一定成功,看CPU状态。
    如线程B在就绪状态,线程A在运行,A使用yield()礼让B线程后进入就绪状态,此时A、B都在就绪状态等待调度,下一次CPU调度不一定就让B执行,也有可能还是让A执行,所以礼让不一定成功,可以提升B线程优先级,提高CPU调度概率。
  • 礼让结束后,继续执行后续代码,不是从头开始

运行 ————> 就绪 ————>运行
    yield      CPU调度

join() 线程合并

  • join()合并线程,让线程先执行
  • 类似插队
  • 调用t.join()的线程At先运行,A进入等待池,其它线程不受影响

线程优先级

Java提供一个线程调度器,监控就绪状态的所有线程,根据优先级决定哪个线程先执行。
但是,线程优先级高未必一定先执行,还是看CPU状态,只能说优先级高的线程先执行的概率大一些。
同样的,优先级低只意味着先获得调度的概率低,并不是不会被调度。
可能出现优先级低反而先被调度的情况,导致性能倒置。

优先级用数字 1~10 表示,默认是 5,使用t.getPriority().setPriority(num)修改优先级。
优先级的设立应在start()之前。

JMM(Java内存模型)

什么是JMM

JMM是抽象概念,由JSR133规范,描述的是程序间变量的访问规则,与CPU缓存模型相似,是标准化的,用于屏蔽各种硬件与操作系统的内存访问差异

JMM规范了多线程程序允许表现出的行为
image
JMM使不同版本的JDK、操作系统、硬件运行出来的代码具有一致性。
主内存中有:堆、方法区中元素或任何被线程共享的数据(如数据库中读取的数据);
工作内存中有:栈、CPU三级缓存、CPU寄存器中数据。

JMM中三大特性

  • 可见性
  • 原子性
  • 有序性

什么是可见性问题

假设线程1、2同时使用主内存共享变量 x = 1
image
② 若此时线程1 x += 1,线程2能否嗅探到?
默认是不能的,因为线程工作内存间不可互相访问

这就是JMM的可见性问题。

JMM 8大数据原子操作

① lock:作用于主内存,把一个变量标记为一条线程独占状态。
② unlock:作用于主内存变量,把一个处于锁状态的变量释放,释放后的变量可被其它线程锁定。
③ read:把一个变量值从主内存传输到线程的工作内存中,以便随后的load使用。
④ load:把read操作从主内存中得到的变量值放入内存的变量副本中。
⑤ use:把工作内存中的一个变量值传递给执行引擎。
⑥ assign:把一个从执行引擎接收到的值赋给工作内存中的变量。
⑦ store:把工作内存中的一个变量的值传送到主内存中,以便随后的write操作。
⑧ write:把store操作从工作内存中的一个变量的值传送到主内存变量中。

详细共享变量实现

image

当线程B 进行⑥后,为何线程A不会回到主内存查看共享变量是否变化并修改变量副本?
答:因为线程A运行时,工作内存中已有initFlag副本,线程会先在工作内存中找此变量。

  • 若工作内存中有,则直接使用;
  • 若无,才会回到主内存查找read

如何让线程间可见,线程B改变变量值之后,如何让线程A立刻嗅探到?
使用volatile修饰变量,实现可见性并禁止指令重排序。

由于JMM对线程的规范(线程工作内存间不可互相访问)导致共享变量的修改不能被各线程及时嗅探,而添加volatile修饰变量,则可解决这一问题。

volatile可见性实现原理

底层实现:通过汇编lock前缀指令触发底层缓存锁定机制(缓存一致性协议或总线锁,优先使用缓存一致性协议,总线锁效率较低)

例如触发其中一种缓存一致性协议MESI,lock指令会触发锁定变量缓存行区域并写回主内存,这个操作称为“缓存锁定”。

MESI

MESI为一种缓存一致性协议,主要有四个状态:

  • M:已修改,代表变量已修改
  • E:独占,被一个CPU核独占
  • S:共享,代表变量在多个CPU核上都有副本
  • I:代表变量失效,对应副本应销毁

lock前缀触发MESI过程

总线锁

当缓存一致性协议不可用时再使用,非volatile优先,效率低,几乎不用。

CPU从主存读取数据到缓存区中,总线会加锁锁定该缓存对应的主存区域,来自其它CPU或总线代理的控制请求将被阻塞,无法读写内存直到锁定被释放。

volatile不能保证原子性

volatile可保证可见性,有序性,但不保证原子性,原子性需用sycronize满足。

如A线程、B线程都想num++
A线程num++,还没来及发出“写”消息同步回主内存,B线程拿到num,也num++,此时A线程发出“写”消息,置B内部工作内存无效,导致Bnum++无效,数据丢失,重新回主内存读num,少加了一次num,违背原子性原则。

锁与同步

线程通信的方法

  • wait():无参情况下,表示线程一直等待,直到其它线程通知,与sleep()不同,wait()会释放锁!
  • wait(long timeout):指定等待的毫秒数timeout
  • notify():唤醒一个处于等待状态的线程
  • notifyAll():唤醒同一个对象上所有调用wait()方法的线程,优先级高的优先调度

上述均是Object类的方法,不是Thread的方法,只能在同步方法或同步代码块中使用。
为什么这些方法放在Object类中?
答:因为syncronized可对任意对象加锁。

posted @ 2021-04-04 21:01  CQCx64  阅读(74)  评论(0)    收藏  举报