如何保证原子性
同一时刻只有一个线程执行称之为互斥.如果我们 能够保证对共享变量的修改是互斥的那么就能保证原子性了.
锁:
java提供的synchronized关键字,就是锁的实现.
synchronized是独占锁,并不能改变CPU时间切换的特点,只有当其他线程访问资源时,发现锁未被释放,只能等待.
synchronized一定能保证原子性,因为被其修饰的某段代码,只能由一个线程执行,所以一定可以保证原子操作.
JUC原子变量
juc(java.util.concurrent包)中的lock包和atomic包,他们也可以解决原子性问题.
加锁是阻塞式方式实现
原子变量是非阻塞方式实现
原子类的原子性是通过volatile和CAS实现的.
AtomicInteger类中的value被volatile修饰,这就保证了value内存的可见性,这为后续的CAS提供了基础.
CAS(比较并交换)
CAS是一种乐观锁的实现方式,采用自旋的思想.
每次判断预估值和内存值是否相同,如果不同,则重新拿最新值与预估值进行重新判断.
CAS三个操作数
1.内存值 V
2.预估值 A
3.更新值 B
当且仅当V==A时,V=B否则不做处理
这样做效率高,不会阻塞
缺点:
由于是自旋的方式,锁不断循环,会消耗CPU,导致其饱满.
ABA问题
当某个线程将A改为B,再将B改为A,当另一个线程去判断预估值时,会认为他没有被修改过
解决:
添加一个版本号,来避免这个问题
比如(A,1)改为(B,2)时,再将其改回是(A,3)当另一个线程预估值判断时,会发现其版本号发生过修改.
ConcurrentHashMap
首先concurrentHashMap是线程安全的Map.
HashTable也是线程安全的,put()方法整个加锁,粒度高,效率低
concurrentHashMap放弃了分段锁,而采用了cas原则+synchronized.
放弃分段锁的原因:
- 加入多个锁浪费内存空间
- 为了提高GC效率
- 生产环境中,map竞争同一个锁的几率小,分段锁反而会造成更新的长时间等待
jdk8放弃了分段锁,而是用了Node锁,粒度低,效率高,并使用cas操作确保其Node的原子性,取代了锁
在put()时,首先用hash找到其链表,查看是否是第一个Node,如果是直接用CAS原则插入,无需加锁.
如果不是,则直接在其第一个Node加锁,(synchronized)
java中的锁分类
乐观锁/悲观锁
乐观锁:不加锁操作,认为对同一个数据的并发操作不做修改(读写操作;不加锁带来大量的性能提升)
悲观锁:加锁操作,认为对同一个数据的并发操作一定修改(适合操作多的场景)
共享锁/独占锁
共享锁:是指该锁被多个线程所持有,并发访问共享资源(读写锁中的读操作)
独占锁(互斥锁):该锁只能被一个线程所持有(读写锁中的写操作)
公平锁/非公平锁
公平锁:优先将锁分配给排队时间长的线程
非公平锁:不考虑线程等待,可以插队,直接获取锁,获取不到时在排到队尾等待
可重入锁
同一个线程中, 在外层获得锁后,进入方法内也可以获取锁,可一定程度避免死锁
读写锁
- 多个读者可以同时进行读操作
- 只允许一个写者写,不能与读同时进行
- 写者优先于读者(一旦有写操作,后续必须等待,唤醒时优先考虑写者)
分段锁
是一种思想,将数据分段,在每个分段上加锁
偏向锁/轻量级锁/重量级锁
锁的状态:
无锁状态;偏向锁状态;轻量级锁状态;重量级锁状态
通过对象监视器在对象头的字段表明的
四种状态都不是java语言的锁,而是jvm为了提供锁的获取与释放效率而做的优化(使用synchronized)
偏向锁:
指同步代码被一个线程访问,会自动获取锁,降低获取锁的代价
轻量级锁:
当锁是偏向锁时,被另一个线程所访问,会升级成轻量级锁,其他线程会通过自旋的方式尝试获取锁,不会阻塞,提供性能.
重量级锁:
指当锁为轻量级锁时,另一个线程虽然是自旋,但是自旋一定会持续下去,当自旋一定的次数时,会进入阻塞.该锁会膨胀为重量级锁.会让其他申请锁的线程进入阻塞,降低性能
自旋锁
比较耗CPU,尽量不要阻塞
不断循环重试获取锁
Synchronized实现
可以保证方法或者代码在运行时,同一时刻只要一个方法进入临界区.还可以保证共享变量的可见性.
java中每个对象都可以作为锁:
- 普通同步方法,锁是当前对象实例
- 静态同步方法:锁是当前类的class对象
- 同步方法区:锁是括号里面的对象
java对象头:
在 Hotspot 虚拟机中,对象在内存的分布为三个区域:
对象头;
实例数据
对齐填充
synchronized使用的锁对象是存储在对象头中的,它是轻量级锁和偏向锁的关键.
Mawrk Word:
用于存储对象自身运行时的数据.
如:哈希码(hashcode),GC分代年龄**,锁状态标志**,线程持有的锁,偏向线程ID等等;
下面就是对象头的一些信息:
ReentrantLock
利用CAS和AQS队列实现.支持公平锁和非公平锁;
假设有三个线程去竞争锁,假设A的AQS操作成功了,那么B和C则设置状态失败;
由于线程A占用了锁,B和C失败,那么线程B和C会进入等待队列.如果A拿着锁死死不放,那么B和C就会被挂起.
B和C尝试获取锁,若当线程的前驱节点是head,就有资格获取锁.
unlock()
尝试释放锁,如果成功,那么查看头结点的状态,如果是唤醒头结点的下个节点关联的线程.
AQS(AbstractQueendSynchronizer)
抽象的队列式的同步器;
核心思想:
如果请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态.如果被请求的资源被占用,那么就有需要线程阻塞等待以及被唤醒时锁分配的机制.这个机制AQS是用CLH队列锁实现的.即将暂时获取不到锁的线程加入到队列中.
AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁,失败则进入等待队列,等待被唤醒.