目录
四、synchronized和ReentrantLock(重入锁)
一、synchronized
1.线程安全问题的主要原因及解决办法
2.互斥锁的特性
(互斥性和可见性)
-》synchronized 锁的不是代码,锁的都是对象
3.获取的锁的分类:
(1)获取对象锁
(2)获取类锁
(3)对象锁和类锁的总结
二 、synchronized底层实现原理
(1)实现基础
-》Java对象头
-》Monitor
(2)对象在内存中的布局
-》对象头
-》实例数据
-》对齐填充
注:实例数据和对齐填充不做展开
(3)对象头的结构
synchronized的锁对象时存储在对象头里的,主要由以下两部分组成
(4)Mark Word:被设计成一个非固定的数据结构
会根据对象本身的状态,复用自己的存储空间
重量级锁(synchonized):10,指针指向monitor的起始位置
(5)Monitor:
(源码层面:是由ObjectMonitor实现的,位于hotspot虚拟机源码,在ObjectMonitor.hpp中,是通过c++实现的)
每个Java对象天生自带了一把看不见的锁
可以将其理解为一个同步工具,也可以理解为一种同步机制,通常他被描述为一个对象
当monitor被某个线程持有后,他便处于锁定状态
ObjectMonitor中有两个队列,WaitSet和EntryList,等待池和锁池,他们是用来保存ObjectMonitor的对象列表
每个对象锁的线程都会被封装成ObjectMonitor来保存到里面,其中有个字段owner,他是指向持有ObjectMonitor对象的线程。
当多个线程同时访问同一段代码的时候,首先会进入到EntryList集合里面,当线程获取到对象的Monitor后,就进入到Object区
域,并把Monitor中的Owner变量设置为当前线程。
同时Monitor中的计数器Count就会+1,若线程调用方法,将释放当前持有的Monitor,owner就将恢复成null,count也将-1;
同时该线程,即ObjectWaiter实例,就会被记录到WaitSet中等待被唤醒,若当前线程执行完毕,他也将释放Monitor锁,并复位
对应变量的值,以便其他线程进入获取Monitor锁。
Monitor对象存在于每个对象的对象头中,synchronized便是通过这种方式去获取锁的,这也是为什么JAVA中任意对象可以作为锁的原因。
(6)重入
重入会成功,synchronized是可重入的
(7)通常不使用synchronized的原因
自适应自旋
锁消除
锁粗化
轻量级锁
偏向锁
(8)Java6之后,它的性能得到了很大的提升
三、锁优化
1.自旋锁
2.自适应自旋锁
由于每次线程需要等待的时间是不固定的,如何设计自旋
3.锁消除
JIT编译器,英文写作Just-In-Time Compiler,中文意思是即时编译器
举例:
4.锁粗化
例如以下这种情况
如果出现一连串操作都对同一个操作反复加锁和解锁,会导致不必要的性能消耗
JVM会把加锁粗化到整个循环外面
5.synchronized的四种状态
6偏向锁
在大多数情况下,锁不仅不存在竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁的代价,引入偏向锁
也就是说,当一个线程访问一个同步块,并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块的时候, 不需要进行CAS操作,从而提高程序性能。
注:偏向锁不适用于锁竞争比较激烈的多线程场合
6.轻量级锁
7.锁的内存语义
8.偏向锁、轻量级锁、重量级锁的汇总
四、synchronized和ReentrantLock(重入锁)
1.ReentrantLock
AQS:AbstractQueuedSynchronizer队列同步器,是JAVA并发用来构建同步锁,或其他同步组件的基础框架,是JUC packet的核心,一般使用AQS的核心是继承
利用AQS去实现一个同步操作,至少要实现两个基本类型的方法,分别是acquire方法,他是用来获取资源的独占权,还有release操作,主要是用来释放对某个资源的独占。
2.区别
(1)公平性
打印结果,不断的争抢锁
如果设置为非公平锁:
会一直打印
(2)ReentrantLock将锁对象化
(3)总结
五、JMM和happens-before
1.Java内存模型JMM
JAVA内存模型规定所有变量都存储在主内存中,主内存是共享数据区,所有线程都可以访问。
但是对变量的操作,即读取、赋值等必须在工作内存中执行。
首先将变量从主内存中拷贝到自己的工作内存空间中,然后对变量进行操作,操作完成后再将变量写回主内存
(1)JMM中的主内存
(2)JMM的工作内存
(3)JMM与Java内存区域划分的关系
-》他们是不同的概念层次
(4)主内存与工作内存的数据存储类型以及操作方式归纳
(5)JMM如何解决可见性问题
简单理解为:
把数据从内存加载到缓存寄存器,然后运算结束,写回主内存。
(5)指令重排序
例如以下操作不能指令重排
2.Happens-before的八大原则
(1)八大原则
(2)happens-before的概念
六、volatile
1.概念
2.volatile的可见性
3.为什么volatile可以立即可见?
4.volatile如何禁止重排优化?
5.线程安全的单例写法
(1)不安全:
(2)修改版
6.volatile和synchronized的区别
七、CAS(Compare and Swap)
1.概念
2.思想
执行CAS操作的时候,将内存位置的值与预期原值进行比较,如果相匹配,处理器会将该位置的值更新为新值。
否则处理器不做任何操作,这里的内存位置的值V,即主内存的值。
举例:
当一个线程修改变量的值,完成这个操作,先取出共享变量的值,赋给A,然后基于A的基础进行计算,得到新值B。
执行完毕之后需要更新共享变量的值时,就可以调用CAS方法去更新变量的值
查看字节码:
add方法被拆分成
getfield 加载主内存中的数据到工作内存中
iadd 进行+1的操作
putfield 将操作后的值写回主内存
通过volatile可以保证线程之间的可见性,同时也不允许JVM对它们进行重排序;
但是并不能保证这三个指令的执行,在多线程的情况下无法做到线程安全。
如何解决?
1.悲观锁
2.AtomicInteger保证原子性
3.使用
一般情况下使用JAVA提供的包即可
4.缺点
ABA问题:一个变量A被改变为B后又被改变为A
通过控制变量值的版本来保证CAS的正确性
八、线程池
在web开发中,服务器需要接收并处理请求,服务器会为一个请求分配一个。
如果并发的请求非常多,但是每个请求的时间很短,就会使用线程池。(因为需要频繁请求和销毁)
1.使用Executors的五种方法:
1.可以指定线程数量
每当一个任务去创建一个工作线程,如果工作线程达到线程池的初始数量,则将提交的任务存在池队列中。
如果有工作线程退出,则会有新的工作线程被创建,以补足nThreads的数目。
2.处理大量短时间工作任务的线程池
3.创建一个单线程化的Executor
最大的特点是可以按顺序的去执行线程,并且在任务的给定时间,不会有多个线程是活动的
4.定时或者周期性的工作调度,前者是单一线程,后者是由多个线程来组成的
5.JDK8才引入的
内部会构建ForkJoinPool,利用working-stealing算法,并行地处理任务,不保证处理顺序。
2.Fork/Join框架
为每个任务创建一个任务队列,每个任务分配一个线程
已经完成自己任务并且空闲的线程能够从其他线程窃取任务,利用的是双端队列。
被窃取线程从头部执行任务,窃取任务的线程则是从尾部
3.为什么要使用线程池
4.Executor框架
该框架是一个根据一组执行策略调用,调度、执行和控制的异步任务的框架,目的是提供一种将任务提交与任务如何运行分离开来的机制
5.JUC的三个Executor接口
JAVA标准库提供了上述三种接口的几种基础实现,这些线程池的特点在于其高度的可调节性,以尽量满足复杂多变的应用场景。
Executors则从简化使用的角度,提供了各种方便的静态工厂方法。