Java多线程并发笔记

多线程并发带来的问题:

1、缓存不一致
变量存储在主内存中,每个线程有自己的工作内存,线程读取变量,会先将变量从主内存读取到工作内存,然后再工作内存中操作变量,然后再将工作内存中,更改后的变量回写到主内存。如果一个线程再另外一个线程将变量回写到主内存之前读取了变量,则2个线程中的变量会存在不一致情况。
2、编译器优化,导致代码乱序执行(Java虚拟机指令重排序优化)
编译器会在不影响代码执行结果的情况下,打乱代码的执行顺序

Java内存模型:

Java内存模型主要定义程序中各个变量的访问规则。Java内存模型规定,所有变量存储在主内存,每个线程有自己的工作内存,线程的工作内存中存储着线程所用到的变量的副本。线程对变量的操纵,必须在工作内存中进行,而不能直接读写主内存中的变量。线程之间,无法直接访问对方的工作内存中的变量,线程直接内存传递,必须通过主内存来完成。

线程的工作内存跟主内存直接的交互操作:

1、lock(锁定) 作用于主内存的变量,把一个变量标识未一条线程独占状态
2、unlock(解锁) 作用于主内存的变量,把一个变量从锁定状态释放,只有释放后的变量,才能够被其他线程锁定
3、read(读取) 作用于主内存的变量,从主内存中读取一个变量的值传输到线程的工作内存中,便于下一步load使用
4、load(加载) 作用工作内存的变量,将上一步read的变量值,放入工作内存的变量副本中
5、use(使用) 作用于工作内存的变量,把工作内存的变量,传递给执行引擎
6、assign(赋值) 作用于工作内存的变量,把更改后的值,赋值给变量副本
7、store(存储) 作用于工作内存的变量,把工作内存中的变量副本的值,回传到主内存,便于下一步write使用
8、write(回写) 作用于主内存中的变量,把上一步store操作从工作内存回传的值,放入主内存变量中

上面8个操作有如下限定:

1、不允许read和load、store和write操作之一单独出现,即不允许从主内存读取了变量,但是工作内存不接受,或者工作内存发起了回写,但是主内存不接受的情况
2、不允许一个线程丢弃他的最近的assign操作,即工作内存的变量值更改后,必须把该变量同步回主内存
3、不允许一个线程无来由的同步变量,即工作内存中的变量,不能在没有发生assign操作的情况下,同步回主内存
4、一个变量不允许诞生在工作内存中,工作内存中使用的变量,必须要经过read、load操作,从主内存中读取
5、一个变量在同一时刻,只允许一个线程对其lock操作,但可以被同一线程lock多次,被lock多次后,只有执行同样次数的unlock操作,才能解锁
6、一个变量没有被lock的情况下,不允许对它执行unlock操作,同样也不允许去unlock一个被其他线程lock的变量
7、对一个变量执行lock之后,会清空工作内存中此变量的副本
8、对一个变量执行unlock之前,必须先把变量同步回主内存

原子性、可见性和有序性

1、原子性:原子性是指指令的最小单位,指令要么执行,要么不执行,不存在执行到一半,去执行其他指令的情况。上述的8个操作中,每一个操作都可以看作是具备原子性的
2、可见性:当一个变量被多个线程读取的时候,一个线程修改了变量之后,其他线程能立即感知到变量的改变,Java通过volatile保证变量的可见性,synchronized和final也能实现变量的可见性
volatile:被volatile修饰的变量,在被改变后会立即被同步到主内存,线程在读取之前,从主内存刷新变量来保证变量的可见性
synchronized:synchronized是通过对变量加锁,保证同一时刻,只有一个线程访问,并且锁释放前必须同步回主内存保证了变量的可见性
final:被final修饰的字段,在构造器中一旦初始化完成,在其他线程中就能够看架final字段的值。
3、有序性:程序执行的顺序按照代码的顺序执行,volatile和synchronized可以保证线程之间操作的有序性

线程的状态:

新建(New):线程创建之后,处于待运行状态
运行状态(Runnable):运行状态的线程,可能已经正在执行,也可能在等待着CPU分配执行时间。
等待状态(Waitting):等待分为无限期等待和有限期等待:
无限期等待状态:处于这种状态的线程不会被分配CPU执行时间,需要等待其他线程显示唤醒。没有设置TimeOut的Object.wait()、Thread.join()和LockSupport.park()方法会导致线程处于无限期等待状态。
有限等待状态:处于改状态的线程也不会被分配CPU执行时间,但是会在一定时间后被系统自动唤醒。设置了TimeOut的Object.wait()、Thread.join()和LockSupport.park()方法会使线程处于有限期等待状态。
阻塞状态(Blocked):阻塞状态在等待着获取到一个排他锁
结束(Terminated):已终止线程的线程状态,线程已经结束执行。

线程安全:

当多个线程访问一个对象时,如果不考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象是线程安全的。

线程安全的实现方法:

1、互斥同步(阻塞同步):多个线程并发访问共享数据时,共享数据保证在同一时刻,只被一个线程使用。
2、非阻塞同步:先对共享数据进行操作,如果共享数据不存在其他线程竞争的情况,则操作成功;如果存在竞争情况,再采取其他补偿措施,比如不断的重试
3、无同步方案:涉及共享数据的才存在线程安全问题,如果一个方法不涉及共享数据,则天然的线程安全,不需要去做任何同步措施

锁优化:

1、自旋锁和自适应自旋:共享数据的锁定状态只会持续很短的时间,如果为了这段时间去挂起和恢复线程,会得不偿失,所以可以让后面请求锁的线程,等待一会,但不放弃CPU执行时间,看看等待的锁是否会很快释放。为了让线程等待,通常时让线程执行一个空循环,这就是所谓的自旋锁。自适应自旋意味着自旋时间不固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
2、锁消除:虚拟机即使编译器在运行时,对代码上要求同步,但是检测到不可能存在共享数据竞争的锁进行消除。
3、锁粗化:在开发的时候,推荐将同步块的作用范围限制的尽可能小--只在共享数据的实际作用域中才进行同步,这样是为了使需要同步的操作数量尽可能小,但是如果一系列操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中,那即使没有线程竞争,频繁的加锁解锁也会导致不必要的性能损耗。虚拟机如果探测到上述操作的情况,会将锁的范围扩大(粗化)到整个操作序列的外部。
4、轻量级锁
5、偏向锁:锁对象第一次被线程获取后,会进入偏向模式,将获取锁的线程ID记录在对象的Mark Word之中,之后持有偏向锁的线程每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作。

你可能感兴趣的:(Java多线程并发笔记)