1.关于多线程
什么是多线程?

进程:代表运行中的一个程序(一个进程往往可以包含多个线程,至少包含一个!)
线程:进程中可独立执行子任务,是进程的最小单元
线程:指程序在执行过程中,能够执行程序代码的一个执行单元
进程:指一段正在执行的程序,线程有时候也被称为轻量级进程
线程和进程:**
一个进程可以拥有多个线程,各个线程之间共享程序的内存空间(代码段、数据段和堆空间)及一些进程级的资源(例如打开的文件),
但是各个线程拥有自己的栈空间和寄存器副本
进程间运行都是基于CPU时间片去完成的,所以在任意一个时刻,对于单核CPU来说,只能有一个任务去执行。只是我们通过切换的方式去完成一个并行执行。由于CPU执行太快,所以切换的时候我们看不出来,以为是同时在进行。
对于Java而言:Thread、Runnable、Callable

并发:(多线程操作同一个资源)CPU 一核 ,模拟出来多条线程,天下武功,唯快不破,快速交替
并行:(多个人一起行走)CPU 多核 ,多个线程可以同时执行; 线程池

并行与并发的理解

并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事
多线程分享 - 图1
一个Java应用程序java.exe,其实至少三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。

线程五种状态

生产 就绪 运行 阻塞 死亡

线程的生命周期

多线程分享 - 图2
2.为什么要使用多线程

  • 使用多线程可以减少程序的响应时间
  • 与进程相比,线程的创建和切换开销更小
  • 多CPU或多核计算机本事就具有执行多线程的能力(充分利用计算机资源)
  • 使用多线程能简化程序的结构,使程序便于理解和维护

既然提高效率,可以开很多的线程嘛
Java线程数过多会造成什么异常?
1)线程的生命周期开销非常高
2)消耗过多的CPU资源
如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU资源时还将产生其他性能的开销。
3)降低稳定性
JVM在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括JVM的启动参数、Thread构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError异常。
合理设置线程
int nCPU = Runtime.getRuntime().avaliableProcessors();
对于CPU密集型线程,可以将线程数设置为N+ 1;
对于I/O密集型线程,优先考虑将线程数设置为1, 仅在一个线程不够用的情况下将线程数向2 * N靠近

3.多线程的同步机制

为啥要同步?

java允许多线程并发控制,当多个线程同时操作同一个可共享的资源变量时,将导致数据不准确,相互之间产生冲突
https://www.cnblogs.com/myseries/p/10881340.html

通过例子引出JMM内存模型

JMM内存模型:Java Memory Mode 规定了线程和内存之间的一些关系

多线程分享 - 图3
Java Memory Model :Java内存模型
happen-before原则是JMM的核心
重排序和内存可见性,

重排序需要遵守happens-before规则**

  1. 在单线程中不改变运行结果
  2. 操作不具备数据依赖性

    as-if-serial 语义 as-if-serial语义 单线程内部重排序不会改变程序运行结果

如何实现的
大家知道synchronized是通过加互斥锁来实现原子性的,JMM关于synchronized的两条规定:
  (1) 线程解锁前,必须把共享变量的最新之刷新到主内存中
  (2) 线程加锁前,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁与解锁需要时同一把锁)

  我来简单描叙一下线程执行互斥代码的过程:
    1、获得互斥锁
    2、清空工作内存
    3、从主内存拷贝变量的最新副本到工作内存
    4、执行代码
    5、将更改后的共享变量的值刷新到主内存
    6、释放互斥锁
  synchronized从而实现类原子性,也具备内存可见性。
  这里多说一下Lock,其实原理跟synchronized类似,但是比synchronized更加灵活,我们会在下一篇博客中详细探讨synchronized的缺陷以及Lock的基本用法。
  volatile是如何实现内存可见性的呢?
  深入来说:是通过加入内存屏障和禁止重排序优化来实现的。(重排序指单线程中在保证执行结果不变的前提下java虚拟机为了提升处理速度可能会将指令重排,达到最合理化)
  对volatile变量执行写操作时,会在写操作后加入一条store屏障指令
  改变线程工作内存中的volatile变量副本的值
  将改变后的副本的值从工作内存刷新到主内存

  对volatile变量执行读操作时,会在读操作前加入一条load屏障指令
  从主内存中读取volatile变量的最新值到线程的工作内存中
  从工作内存中读取volatile变量的副本
  简单来说:volatile变量在每次被线程访问时,都强迫从sy主内存中重读变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样在任何时刻,不同的线程总能看到该变量的最新值。从而保证了变量的内存可见性。

 synchronized和volatile的比较
  volatile不需要加锁,比synchronized更加轻量级,不会阻塞线程
  从内存可见性讲,volatile读相当于加锁,volatile写相当于解锁
  synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性

https://www.cnblogs.com/chengxiao/p/6528109.html
原子操作,01 原子性
首先是我们彼此都要保持一致的观点:原子(Atomic)操作指相应的操作是单一不可分割的操作
(1)首先是代码例子
对int型变量conut执行counter++的操作不是原子操作
这可以分为3个操作
读取变量counter的当前值
拿counter当前值和1做加法运算
将counter的当前值增加1后赋值给counter变量
上面的步骤2,很有可能在执行的时候就已经被其他线程修改了,其所为的“当前值”已经是过期的

内存屏障(Memory Barrier)

CPU中,每个CPU又有多级缓存【上图统一定义为高速缓存】,一般分为L1,L2,L3,因为这些缓存的出现,提高了数据访问性能,避免每次都向内存索取,但是弊端也很明显,不能实时的和内存发生信息交换,分在不同CPU执行的不同线程对同一个变量的缓存值不同。

  • 硬件层的内存屏障分为两种:Load BarrierStore Barrier即读屏障和写屏障。【内存屏障是硬件层的】
    为什么需要内存屏障
  1. 由于现代操作系统都是多处理器操作系统,每个处理器都会有自己的缓存,可能存再不同处理器缓存不一致的问题,而且由于操作系统可能存在重排序,导致读取到错误的数据,因此,操作系统提供了一些内存屏障以解决这种问题.
  2. 简单来说:
  3. 1.在不同CPU执行的不同线程对同一个变量的缓存值不同,为了解决这个问题。
  4. 2.volatile可以解决上面的问题,不同硬件对内存屏障的实现方式不一样。java屏蔽掉这些差异,通过jvm生成内存屏障的指令。
  5. 对于读屏障:在指令前插入读屏障,可以让高速缓存中的数据失效,强制从主内存取。

内存屏障的作用
  1. cpu执行指令可能是无序的,它有两个比较重要的作用
  2. 1.阻止屏障两侧指令重排序
  3. 2.强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。

JMM 是一种规范,是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题

synchronized

synchronized修饰代码块或方法,可以看作是一个原子操作;
即有synchronized关键字修饰的语句块
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步

volatile特殊域变量

原理是每次要线程访问volatile修饰的变量时都是从内存中读取,而不是从缓存中读取,因此每个线程访问到的变量值都是一样的。这样就保证了同步。
volatile修饰的成员变量
volatile修饰的成员变量

a. volatile关键字为域变量的访问提供了一种免锁机制 b. 使用volatile修饰符相当于告诉虚拟机该域可能会被其他线程更新 c. 因此每次使用该域就要重新计算,而不是使用寄存器中的值 d. volatile不会提供任何原子操作,它也不能用来修饰fianl类型的变量

使用重入锁

在javaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入(重复进入)、互斥、实现了Lock接口的锁,它与使用synchronized方法和块具有相同的基本行为和语义,并且扩展了其能力。 ReentrantLock():创建一个ReentrantLock实例
lock():获得锁
unlock():释放锁
注:ReetrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用。

使用局部变量ThreadLocal

ThreadLocal适用的场景是,多个线程都需要使用一个变量,但这个变量的值不需要在各个线程间共享,各个线程都只使用自己的这个变量的值。

使用原子变量

需要使用线程同步的根本原因在于对普通变量的操作不是原子的。
那么什么是原子操作呢?
原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作
即这几种行为要么同时完成,要么都不完成.
在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类
锁的问题:
死锁:
上下文切换,性能开销很大 :自旋锁
上下文切换
多线程
即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。
什么是上下文切换
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。
所以任务从保存到再加载的过程就是一次上下文切换。
上下文切换也会影响多线程的执行速度
因为线程有创建和上下文切换的开销,所以有时候并发不一定比串行快。
减少上下文切换的办法
减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。
无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一
些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这
样会造成大量线程都处于等待状态。
协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

4.互动小问题~

5.如何使用?
我们使用哪种同步机制?
乐观与悲观
CAS Compare and Swap 机制 比较并替换
sychornized和volatile还有lock 精准唤醒

6.项目中的多线程 :世界线程模型
为什么要应用多线程
为什么用这种方式实现,多线程易出现的问题

对比 多进程 多线程
数据共享,同步 数据共享复杂,需要IPC;数据分离,同步简单 共享进程数据,共享简单,同步复杂
内存,CPU 占用内存多,切换复杂,CPU利用率低 占用内存少,切换简单,CPU利用率高
创建销毁,切换 创建,销毁和切换复杂,速度慢 创建,销毁和切换简单,速度快