1 CAS
CAS的ABA“问题如何解决:对原来的数据加上一个版本号,或者增加一个boolean标志位记录是否更改过。
AtomicXXX使用volatile和CAS实现,保证了原子性和可见性
锁升级(synchronize)
synchronized:锁的是对象不是代码,只有拿到该对象锁才能执行锁定代码;锁定方法和非锁定方法可以同时执行。
注意:Synchronized(Object) 不能使用String常量、Interger、Long基础类型
下图表示了对象头中运行时元数据的内存状态
对象的锁的升级过程(synchronize(o)的升级过程):new的时候没有锁=>偏向锁=>轻量级锁(CAS)=>重量级锁。
①刚开始创建对象的时候,该对象没有上锁;②当有一个线程占用该对象为该对象上偏向锁,既对象头的运行时元数据中增加一个54位指向当前线程的指针,申明该对象被该线程占用(这种情况是没有其他线程竞争的时候);③当有其他线程竞争该对象的时候,该对象的锁升级为轻量级锁(CAS 自旋锁)=》去掉该对象上的指向原来线程指针,竞争该对象的每个线程在线程栈中生产一个LockRecord对象(对象的hashcode等头信息被记录进去),这些线程使用CAS的方式竞争锁(将对象的对象头中增加自己LockRecord的指针——62位);④当竞争比较激烈,或者有线程自旋大于10次,升级成重量级锁=》对象头的运行时元数据中的锁信息改为指向互斥量(重量级锁)的指针。
synchronize底层实现使用指令lock comxchg
轻量级锁和重量级锁(互斥量)的区别:轻量级锁(CAS)不是真正意义上的锁,不需要向操作系统内核申请,更加的轻量(但线程要一直访问对象,会消耗cpu资源)。重量级锁是操作系统的锁,需要从操作系统用户态向内核态申请,重量级锁有一个队列,不需要像CAS那样一直自旋。
synchronized实现过程:
1.java代码:synchronized
2.字节码:monitorenter monitorexit
3.执行过程自动升级
4.底层指令 lock comxchg
Volatile (可见性,有序性)
可见性:某一个线程对变量的修改会及时更改这个值,并且其他线程要使用这个值的时候会重新读取。保证缓存行(64字节)中的数据一致性
有序性:volatile底层使用Memory Barrier(内存屏障)来做指令排序,防止了指令重排序
volatile不能保证原子性,volatile修饰的变量可以多个线程同时访问,对于i++这类的操作就不能保证操作的原子性。
CPU的构成:ALU逻辑计算单元、PC程序计数器、Register寄存器、L1、L2、L3三级缓存。
ALU负责计算,PC计数器指定正在执行的线程当前执行的指令位置,Register寄存计算用的数据。L1,L2,L3提供cpu和内存之间的缓存。
线程:程序执行的基本单位
进程:计算机分配资源的基本单位
线程切换:当前正在运行线程A,当切换线程到线程B时,CPU将当前线程A的PC和Register的结果缓存起来,在来执行线程B(一个CPU只有一个PC和寄存器的时候)。
超线程:如果一个CPU中有一个ALU和多个PC、多个寄存器的话,那么CPU在做线程切换的时候不用再缓存执行上一个线程时PC和寄存器中的值。直接将ALU切换到另一组PC和寄存器运行。
由于ALU的速度很快,所以切换线程时缓存PC和寄存器中数据的时候显得很低效,而使用所谓的四核八线程的CPU切换线程的时候不用缓存,直接切换ALU,更加的高效。
缓存行:计算机读取数据按块进行读取,读取到内存和cpu中的一块数据就称为缓存行,一个缓存行64字节。
cpu中的数据一致性是数据块的层面
对于上图,如果想x、y两个使用volatile修饰,并且两个之和小于一个缓存行的时候(x和y在一个缓存行中被读到内存中),这时候两个线程分别更改x和y,则每一次更改都会通知对方线程重新读取内存(cpu层面的数据一致性是缓存行级别的)。
缓存行对齐:(补齐到缓存行大小)对于上面这个例子,我们可以增加x,y对象的大小,使他们总共的大小大于64字节(一个缓存行的大小),这样就单独放到一个缓存行中,每一次对x,y值的修改时,对方线程
就不用在重新读取数据。
cpu的乱序执行:当上下两条指令互不影响时(上一条指令是读取内存数据、下一条时进行一个计算,cpu在执行读指令的时候会执行下一条计算指令),跳过上一条先执行下一跳。
读指令的同时可以同时执行不影响的其他指令而写指令的同时可以合并写。
因此,在多线程的情况下,cpu的乱序执行(指令重排序)就造成问题,必须使用Memory Barrier(内存屏障)来做指令排序(volatile的底层实现)
volatile实现可见性
1)汇编层面:在写操作的时候加了一个lock指令。该指令会把写缓冲区中的数据刷新到主内存中去,并且无效化所有cpu中缓存了该地址的数据。
2)硬件层面:通过底层cpu的缓存一致性协议(MESI)实现,当一个线程修改变量通过总线传递到主存的时候,会被其他cpu嗅探到,然后无效化其工作内存中的变量,重新读取主存中的值。
volatile如何实现禁止指令重排序
1.jvm通过内存屏障,在屏障两端的指令不能重排序,hotspot底层通过操作系统lock前缀指令实现(读屏障,写屏障)jvm的内存屏障,防止指令重排。在volatile的读写前后加入了四个内存屏障。
内存屏障
JMM模型里有八个指令完成数据的读写,通过其中的load和store指令相互组合成的4个内存屏障实现禁止指令重排(在写入和读取之前添加内存屏障实现禁止指令重排序)
主要由两个作用:1)阻止屏障两侧的指令重排序;2)强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
懒汉式的单例模式(使用双层检查锁)就是要使用volatile修饰实例变量,防止new对象时指令重排。
系统底层如何保证数据一致性
1.通过MESI协议保证cpu中数据的一致性,数据(数据大小小于缓存行)在多个cpu中被多个线程修改,每次修改后都会通知另一个cpu重新读取数据(volatile的系统底层实现)
2.锁总线(互斥量):如果MESI协议无法保证数据一致性(比如数据大小大于缓存行),通过采用向操作系统内核申请锁,锁住数据总线(一个线程操作的时候其他线程不能操作)。
总线风暴
volatile通过总线嗅探机制保证共享变量在多线程之间的一致性。
因为总线是固定的,所有相应可以接受的通信能力也就是固定的了,如果缓存一致性流量突然激增,必然会使总线的处理能力受到影响。而恰好CAS和volatile 会导致缓存一致性流量增大。如果很多线程都共享一个变量,当共享变量进行CAS等数据变更时,就有可能产生总线风暴。
可以通过Synchronized加锁代替
锁的使用时机
加锁代码执行时间比较长,线程多=》使用重量锁(Synchronized)
执行时间长,线程少=》使用自旋锁(Volatile、Atmo…)
JAVA内存模型JMM
Java内存分为主存和工作内存,所有的变量都存储在主存(物理内存)中,每条线程有自己的工作内存(类似于物理上的高速缓存)。线程的工作内存保存了该线程使用的变量在主内存的副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接在主内存中操作;不同线程之间无法直接访问对方工作内存中的变量,线程间变量之间的传递需要通过主内存完成
jvm中的原子操作
可重入锁(Synchronized、ReentrantLock)
多个方法是同一个锁,则可以相互调用
ReentrantLock实现原理
简单来说,ReentrantLock的实现是一种自旋锁(CAS+等待队列),通过循环的调用CAS来实现加锁,性能较好,因为避免了使线程进入内核态的同步阻塞状态。底层通过LockSpuuort和AQS实现
ReentrantLock与Synchronized的区别
1)等待可中断:持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,改为去做其他的事,这相当于Synchronized来说可以避免出现死锁的情况。通过lock.lockInterruptibly()来实现这个机制。
2)公平锁:可以实现公平锁。
3)锁绑定多个条件:提供了一个Condition条件类,可以绑定多个条件,方便的实现等待通知机制。每次唤醒指定队列(阻塞队列)中的线程,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
CountDownLatch
用来线程计数,当某几个线程都完成了之后,再进行之后的操作。比如说A线程需要等待B、C线程执行到某一步之后再执行。A线程调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行。B、C线程中使用countDown()进行计数
CyclicBarrier
使多个线程等待至某一状态后再全部统一执行,CyclicBarrier可以循环使用,调用await()后,线程就处于barrier了。
public class Test {
public static void main(String[] args) {
int N =4;
CyclicBarrier barrier =new CyclicBarrier(N);
for(int i=0;i<N;i++)
new Writer(barrier).start();
}
static class Writerextends Thread{
private CyclicBarrier cyclicBarrier;
public Writer(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");
try {
Thread.sleep(5000); //以睡眠来模拟写入数据操作
System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");
cyclicBarrier.await();
}catch (InterruptedException e) {
e.printStackTrace();
}catch(BrokenBarrierException e){
e.printStackTrace();
}
System.out.println("所有线程写入完毕,继续处理其他任务...");
}
}
}
读写锁(ReadWriteLock)
读锁(共享锁-可以多个线程持有这个锁,同时运行)、写锁(排它锁-只能有一个线程持有这个锁)
读锁可以多个线程共享,不会阻塞线程
写锁是排它锁,当写锁被获取的时候,会阻塞其他线程获取写锁和读锁,当写锁释放的时候,才能获取读锁和写锁
Semaphore(信号量,线程同步)
可以用来限流(控制同时执行某段代码时的线程数)
wait¬ify¬ifyAll
wait
如果一个线程调用了对象的wait()方法后,该线程就会进入到对象的等待池中,进入等待池中的线程处于wait状态,不会去争抢锁了。
notify
如果一个线程调用了对象的notify方法,就会在该对象的等待池中随机唤醒一个等待的线程,被唤醒的线程就会进入该对象的锁池(阻塞队列)中,锁池中的线程会去竞争该对象锁。
notifyAll
线程调用一个对象的notifyAll方法,会唤醒该对象的等待池中的全部等待的线程,被唤醒的线程都会进入到该对象的锁池(阻塞队列)中,等待竞争锁。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池(阻塞队列)中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
综上,所谓唤醒线程,另一种解释可以说是将线程由等待池(等待队列)移动到锁池(阻塞队列),notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify只会唤醒一个线程。
线程状态
Synchronize会使没有抢到锁的线程进入blocked状态(唤醒block的线程会切换到操作系统内核),Synchronize阻塞的线程进入的是同步阻塞。ReentrantLock底层是CAS+AQS,没抢占到锁的线程进入AQS,线程进入等待队列(等待阻塞)
线程状态:new,Runnable(Ready,Running),Blocked(等待阻塞、同步阻塞、其他阻塞),Terminated
单例的实现方式
1、懒汉式
通过双重检查锁保证,实现懒加载。但是单例可以被反射和序列化破坏
2、饿汉式
通过static关键字修饰,通过jvm保证单例。优点就是实现简单,而且安全可靠。
3、通过单元素的枚举类实现单例
懒汉式都可以被反射、序列化的方式破坏,通过枚举类实现的单例,通过jvm进行保证。
public enum Resource {
INSTANCE;
private Resource instance;
Resource() {
instance = new Resource();
}
public Resource getInstance() {
return instance;
}
}
上面的类Resource是我们要应用单例模式的资源,具体可以表现为网络连接,数据库连接,线程池等等。
获取资源的方式很简单,只要 Resource.INSTANCE.getInstance() 即可获得所要实例。下面我们来看看单例是如何被保证的:
首先,在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法,同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。
也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。
public enum Resource {
INSTANCE;
public void doSomething() {
System.out.println("doSomething");
}
}
//调用方法
public class Main {
public static void main(String[] args) {
Singleton.INSTANCE.doSomething();
}
}
直接通过Singleton.INSTANCE.doSomething()的方式调用即可。方便、简洁又安全。