基本概念
串行:等待用户输入,系统空闲
批处理:预先写下指令执行,如果按顺序执行任务A、B,但A处理时间较长,此时系统空闲
进程:独立占用内存空间,相互不干扰并可以相互切换,切换时会保存状态,重新切换即恢复状态。
线程:执行进程的子任务,实现进程的内部并发,实现粒度更细的任务控制。
进程和线程区别
进程是资源分配的最小单位,线程是CPU调度的最小单位
线程不能看作独立应用,进程可以看作独立应用。
进程有独立的地址空间,互不影响。
进程切换比线程切换消耗大。
线程生命周期
NEW-RUNNABLE-RUNNING-BLOCKED-TERMINATED
1.新建状态
New一个Thread对象。
2.可运行状态
线程启动start方法
3.运行状态
Cpu选中线程进入
可转换的状态
死亡 | 阻塞 | 可运行状态 |
不推荐的stop方法、意外死亡 | 第四点 | Yield、cpu轮询调度 |
4.线程阻塞:
Sleep、wait、获取锁而等待、阻塞的IO操作
可转换的状态:
死亡 | 可运行状态 |
Stop方法、意外死亡 | 1.线程阻塞结束,读取到想要的数据 2.完成指定的休眠时间 3.被其他线程notify 4.获取到锁的资源 5.阻塞过程被打断,interupt |
5.死亡:
正常运行结束
Jvm关闭
线程出错意外结束
线程常用api
sleep
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),不会释放锁,可以通过interrupt打破
wait
方法则会在线程休眠的同时释放掉机锁,也可以通过interrupt打破
notify
随机唤醒在此对象监视器上等待的单个线程,然后回到该对象锁的在锁池,等待选中
start
启动线程
interrupt
interrupt()方法中断线程,该方法会把中断标识置为true(isInterrupt方法),wait, sleep,join方法,会不断的轮询监听 interrupted 标志位,发现其设置为true后,会停止阻塞并抛出 InterruptedException异常,该异常抛出后,会把中断标志复位
yield
yield()方法会使得当前线程让出cpu的调度权,让其他线程得以执行,但不能保证其它线程一定会执行。
join
join()方法使当前线程停下来等待,直至另一个调用join方法的线程终止,也可以通过interrupt打破
线程关闭
Stop方法:可能不会释放掉monitor锁,不建议使用
线程生命周期的正常结束
捕获终断信号关闭线程。
使用volatile(保证不同线程之间对共享变量操作时的可见性的一个修饰符)开关控制
异常退出,可以把异常封装成runtimeexception抛出而结束
run方法传参
构造函数传参
成员变量传参
处理线程返回值
主线程等待
使用Thread类的join方法组阻塞当前线程
通过Callable接口实现:通过Futuretask或线程池(提交一个实现callable接口的任务类,然后结果用future接口接收)
futuretask实现了runnable、future接口,只要把实现callable的类放进futuretask即可
具体例子
public static void test() throws ExecutionException, InterruptedException {
FutureTask t=new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
TimeUnit.SECONDS.sleep(5);
return "ok";
}
});
new Thread(t).start();
System.out.println("线程开启");
System.out.println(t.get());
ExecutorService s=Executors.newSingleThreadExecutor();
Future f= s.submit(new Callable() {
@Override
public Object call() throws Exception {
TimeUnit.SECONDS.sleep(5);
return "ok";
}
});
System.out.println(f.get());
s.shutdown();
}
死锁:
是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进
死锁条件:
互斥条件:一个资源在某个时间段只允许一个线程占用
请求和保持条件:一个线程在占用一个资源的同时发起一个新的获取另外的资源请求
不剥夺条件:未完成任务时无法释放资源,完成后自行释放
环路等待条件:指在发生死锁时,必然存在一个进程资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,,Pn正在等待已被P0占用的资源
多线程并发的最佳实践
使用本地变量
使用不可变量
最小化锁的作用域范围:S=1/(1-a+a/n),阿姆达尔定律
使用线程池
宁可使用同步也不要使用线程的wait和notify
使用blockingqueue实现生产-消费模式
使用并发集合而不是加了锁的同步集合
使用semaphore创建有界访问
宁可使用同步代码块也不使用同步方法
尽量避免使用静态变量
对象存储
hotpspot虚拟机中对象存储分为三块区域
实例数据:实例化对象里的数据
对齐填充:虚拟机要求java对象的开始地址必须是8字节的整数倍,对象头是8字节的,如果实例数据部分不满足8字节的整数倍,那么对其填充的作用就是补齐,形成整数倍。
对象头
主要结构
头对象结构 | 说明 |
Mark Work | 非固定的数据结构,默认存储对象运行时数据比如hashCode,分代年龄,锁类型,锁标志位等 |
Class Metadata Address | 类型指针指向对象的类元数据,jvm通过该指针确定该对象是哪个类实例,对于java数组还有额外用于记录数组大小的数据 |
32位的mark work示例
monitor
每个java对象自带一个内部锁,也称监视器锁,作为同步工具。
当一个线程获取到了monitor以后,monitor的count会加一,会成为该锁的owner,其他线程想要获取时,会被分配到锁池,当执行wait方法时,释放monitor,count会减一,owner置为空,并且并且进入到等待池里面等待再次被唤醒,如果执行完毕,则释放monitor并复位monitor对象的数据
自旋锁
共享数据的锁定状态持续时间较短,切换线程消耗相对较大
通过让线程执行忙循环等待锁的释放,不让出cpu
如果锁被其他线程长时间占用,会带来性能问题
自适应自旋锁
自旋的次数不固定
由前一次在同一个锁上的自选时间以及锁的拥有者的状态来决定,比如在同一个锁自旋获取的几率较大,会加大自旋的次数
反之减少或放弃自旋
锁消除
JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁
锁粗化
通过扩大加锁范围,避免重复加锁和解锁。
悲观锁
synchronized
四种状态
无锁、偏向锁、轻量级锁、重量级锁
升级方向:无锁->偏向锁->轻量级所->重量级锁
锁 | 优点 | 缺点 | 使用场景 |
偏向锁 | 加锁和解锁不需要CAS操作,没有额外的性能消耗,和执行非同步方法相比仅存纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 只有一个线程访问同步块或者同步方法的场景 |
轻量级锁 | 竞争的线程不会阻塞,提高响应速度 | 若线程长时间抢不到锁,自旋会消耗CPU性能 | 线程交替执行同步块或者同步方法的场景 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢,在多线程下,频繁的获取释放锁,会带来巨大的性能消耗 | 追求吞吐量,同步块或同步方法执行时间较长的场景 |
偏向锁
减少同一线程获取锁的代价
大多数情况下,锁不存在多线程竞争、总是由同一线程多次获得
如果一个线程获得了锁,那么锁进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需检查Mark Work的锁标记位为偏向锁以及当前线程id等于Mark Word的ThreadID即可,省去了大量有关锁申请的操作
不适合锁竞争比较激烈的多线程场合
轻量级锁
轻量级锁是由偏向锁升级而来,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用时候,偏向锁会升级为轻量级锁。
适用场景:线程交替执行同步块
若存在同一时间访问同一锁的情况,就会导致轻量级锁升级为重量级锁
上锁
在代码进入同步块时候,如果同步对象锁状态为无锁状态(锁标志为"01"状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的Mark Word拷贝
拷贝对象头的Mark Work复制到锁记录中
拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Work更新为指向锁记录的指针,并将锁记录里的owner指针向object markword。
如果这个更新工作成功了,那么线程就拥有该对象的锁,并且对象Mark word的锁标志位设为00,即处于轻量级锁定状态;
如果更新失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明了当前线程已经拥有了这个对象的锁,那就可以直接进行同步块继续执行。否则说明多个线程竞争锁,轻量级锁升级为重量级锁,标志位变为10,Mark Word存储的就是指向重量级锁的指针,后面等待锁的线程也要进入阻塞状态。而当前线程(即该线程栈帧中的锁记录空间的owner指针指向object makword的线程)便尝试使用自选来获取锁
解锁
通过CAS操作尝试把线程中复制的mark work对象替换当前Mark Word
如果替换成功,同步完成,否则在释放锁的同时,唤醒被挂起的线程
由于线程释放锁时,java内存模型会把该线程对应的本地内存中的共享变量刷新到主内存中;
而当线程获取锁时,java内存模型会把该线程对应得本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
ReentrantLock(可重入锁)
特点:基于AQS(AQS是AbstractQueuedSynchronizer的简称。AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架)实现
能够实现synchronzied更细粒度的控制
低竞争场景下性能未必比synchronized高,并且也是可重入
可以设置公平性(按调用lock方法先后获取)
将锁对象化
判断是否有线程,或者某个特性线程,在排队等锁
带超时的获取锁的尝试
感知是否成功获取锁
与synchorize区别
synchronized是关键字,ReentrantLock是类
ReentrantLock可以对获取锁的等待时间进行设置,避免死锁
ReentrantLock可以获取各种锁的信息
ReentrantLock可以灵活地实现多路通知
机制:sync操作Mark Word,lock调用Unsafe类的park()方法
乐观锁
CAS(Compare and Swap)
一种高效实现线程安全性的方法
支持原子更新操作,适用于计数器,序列发生器等场景
属于乐观锁机制
CAS操作失败时由开发者决定是继续尝试还是执行别的操作
思想
包含三个操作数-内存地址(V)、预期原值(A)、新值(B)
将内存位置的值与预期原值A相比较,匹配则把新值B更新到内存地址V里面,这里的内存是指主内存,不是线程私有部分
缺点
若循环时间长,则开销很大
只能保证一个共享变量的原子问题
ABA问题,如果内存地址V初次读取的值是A,虽然在准备赋值时发现是A,但有可能期间发生了改变为B而后再变回A,就会认为从来没变更,最后加入版本机制AtomicStampedReference来解决,即比较条件变为版本号+预期值
JMM
java内存模型本身一种抽象的概念,并不真实存在,描述的是一组规范或规则,该规则围绕原子性、有序性、可见性展开,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式
线程本地内存需要把主内存中要操作的数据拷贝到本地内存中,进行处理后再更新到主内存
主内存
存储java实例对象
包括成员变量、类信息、常量、静态变量等
属于数据共享区域(对应java内存区域的堆、方法区)、多线程并发操作时会引发线程安全问题
工作内存
存储当前方法所有本地变量信息,本地变量对其他线程不可见
字节码行号指示器、相关的native方法信息
属于线程私有数据区域(对应java内存区域的程序计算器、虚拟机栈、本地方法栈),不存在线程安全问题
方法里的基本数据类型本地变量将直接存储在工作内存的栈帧结构中
引用类型的本地变量:引用存储在工作内存中,实例存储在主内存中
成员变量、static变量、类信息存储在主内存中
主内存共享的方式是线程各拷贝一份数据到工作内存,操作完成后刷回主内存
同步操作
- lock:作用于主内存的变量,把一个变量标识为一个线程的独占状态
- unlock:作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read:作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load:作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
- use:作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
- assign:作用于工作内存的变量,它把一个从执行引擎接受的值赋值给工作内存的变量
- store:作用于工作内存的变量,把工作内存中的一个变量值传递到主内存中,以便随后的write操作
- write:作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存
- lock用于主内存,把变量标识为线程独占状态,然后read操作把主内存传输到线程的工作线程中,load把在工作内存中的变量放到变量副本中,通过use传给执行引擎,执行引擎执行好后通过assign传回给工作内存,store到工作内存里,最后通过write操作把store里的值传到主内存中
同步规则
- 如果要把一个变量从主内存中复制到工作内存中,就需要按顺序地执行read和load操作,如果把变量从工作内存中同步回主内存,就要按顺序地执行store和write操作,但jmm只要求上述操作必须按顺序执行,没有保证必须连续执行
- 不允许read和load、store和write操作之一单独出现
- 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中
- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了load和assign操作
- 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock以后,只有执行相同的unlock操作,变量才会被解锁。lock和unlokc必须成对出现
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
- 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许unlock一个被其他线程锁定的变量
- 对一个变量执行unlock操作之前,必须先把此变量同步到主内存(执行store和write操作)
jmm解决可见性问题
指令重排序需要满足的条件
在单线程环境下不能改变程序运行的结果
存在数据依赖关系的不允许重排序
无法通过happens-before原则推导出来的,才能进行指令的重排序
A操作的结果必须对B操作可见,则A与B存在happen-before关系
happen-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()方法的开始
如果两个操作不满足上述任意一个happens-before规则,那么这两个操作就没有顺序的保障,jvm可以对这两个操作进行重排序
如果操作A happens-before操作B,那么操作A在内存上所做的操作对B都是可见的
volatile
jvm提供的轻量级同步机制
保证被volatile修饰的共享变量对所有线程可见
禁止指令重排序优化
当写一个volatile变量时,jmm会把该线程对应的工作内存的共享变量值刷新到主内存中
当读取一个volatile变量时,jmm会把该线程对应的工作内存置为无效,从主内存中读取共享变量
如何禁止重排优化?
首先要了解内存屏障(Memory Barrier)的概念
1.保证特定操作的执行顺序
2.保证某些变量的内存可见性
通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化
强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本
volatile与synchronized区别
volatile本质是告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞知道该线程完成变量操作为止
volatile仅能用于变量级别;synchronizedd则可以使用在变量、方法和类级别
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量修改的可见性和原子性
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
线程安全:
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确行为,那么称这个类为线程安全
原子性:
提供了互斥访问,同一个时刻只能有一个线程对它进行操作
可见性:
一个线程对主内存的修改可以及时被其他线程观察到
有序性:
一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序
发布对象:使一个对象能够被当前范围之外的代码所使用
对象逸出:一种错误的发布。当一个对象还没有构造完成时,被其他线程所见
安全发布对象:
在静态初始化函数中初始化一个对象引用
将对象的引用保存到volatile类型域或者AtomicReference对象中
将对象的引用保存到某个正确构造对象的final类型域中
将对象的引用保存到一个由锁保护的域中
线程封闭:把对象封装到一个线程,只能在某一个线程访问
Ad-hoc 线程封闭:程序控制实现,最糟糕,忽略
堆栈封闭:局部变量,无并发问题
ThreadLocal 线程封闭,内部维护一个map
安全对象策略:
线程限制:一个被线程限制的对象,由线程独占,并且只能被占有它的线程修改
共享只读:一个共享只读的对象,在没有额外同步的情况下,可以被多个线程并发访问,但是任何线程都不能修改它
线程安全对象:一个线程安全的对象或者容器,在内部通过同步机制保证线程安全,所以其他线程无需额外的同步就可以通过公共接口随意访问它
被守护对象:被守护对象只能通过获取特定的锁访问