JMM(Java内存模型),主要描述了一组规则,主要定义了程序执行过程中变量的访问方式来保证单线程、多线程下的正常执行。JVM运行的实体是线程,每个线程运行时,都会创建一个工作内存【栈空间(栈帧)】来保存所有的私有变量。
JMM内存模型规定所有遍历都存储在主内存中,主内存的变量所有线程都可以共享,对主内存中的变量进行操作时,不同线程要copy主内存的内容,然后在自己的工作内存中进行,完成后刷回主内存,所有线程通过主内存来通信。
JMM围绕着原子性、有序性、可见性三点展开
主内存:所有线程创建的实例对象都放在主内存,不管该实例对象是成员变量,还是局部变量,类信息、常量、静态变量都是放在主内存中,属于所有线程共享区域,所以存在线程之间安全问题。
工作内存:主要存储方法内部的变量和主内存中变量的副本,每个线程只操作工作内存中的数据,对其他线程不可见,线程安全。
JMM数据同步的原子操作 8个:(保证多线程安全)
lock-unlock、read-load、use-assign、store-write
JMM如何解决原子性、可见性、有序性问题
原子性:通过synchronized和Lock实现原子性
可见性:volatile关键字保证了可见性,变量被volatile关键字修饰后没回保证此变量修改的值立即刷新到主内存,被其他线程可见。synchronized和Lock同一时刻只有一个线程能访问同步代码块,所以是也支持了可见性。
有序性:volatile保证了有序性、synchronized和Lock也保证了有序性
happens-before原则:
只通过volatile和synchronized来保证原子性、可见性、有序性,并发程序会比较麻烦。所以jdk1.5以后提供了happens-before原则:
1、程序顺序原则:一个线程内,程序顺序执行
2、锁规则:unlock操作必须发生在下一个加锁之前,也就是lock和unlock一一对应。
3、volatile规则:volatile变量被线程访问时,强迫从主内存读取最新变量值,发生变化时会强迫刷新到主内存,任何时候,不同线程总能看到改变量最新值。
4、线程启动规则:start()方法先于每个动作
5、传递性:A优先于B,B优先于C,A优先于C
6、线程终止规则:线程所有操作优先于线程终结。
volatile关键字可以保证可见性和有序性
volatile关键字是Java虚拟机提供的轻量级的同步机制,保证了Java内存模型的两个特性,可见性、有序性。
可见性:对非volatile修饰的变量,不同的线程从主内存拷贝到CPU缓存中,多线程的情况下就会拷贝到多个不同的CPU的cache中,volatile修饰的变量会强制每次去主内存拷贝,并且发生改变后立即刷新回主内存。
有序性:volatile关键字会禁止指令重排。
问题:volatile保证不了原子性,可以使用原子类【java.util.concurrent.atomic】解决原子性问题
原子类的实现主要利用CAS(compare and swap)+ volatile 和本地方法实现
CAS:(compare and swap比较并交换)
CAS指令有3个操作数,内存值V,旧的预期值A,要修改的更新值B,当且仅当A==B会执行,其他情况自旋重新执行。【CAS同样需要volatile来保证可见性】
cas是一种系统原语,执行过程不允许中断,保证了原子性,具体是通过Unsafe类的本地方法实现
执行过程:【方法:获取变量+1操作】
1线程A获取变量值为3之后未执行+1时,被挂起,此时期望更改后的值为4;
2线程B获取变量值为3之后执行+1并修改,由于变量被volatile修饰,所以立即刷回主内存;
3线程A执行,执行compareAndSwapInt(),发现内存获取的值和之前的值不同,则修改失败,只能重新获取
4线程A重新获取变量进行+1
CAS的缺点:
(1)在线程执行不成功的情况下会一直循环等待重新执行,长时间不成功,消耗太大
(2)只能保证一个共享变量的原子操作,多变量操作时无法保证
(3)会导致ABA问题
由于CAS不能保证多变量操作的原子性,所以通过锁来保证原子性
AQS(AbstractQueuedSynchronizer 抽象队列同步器):
AQS的核心思想是,如果被请求的共享资源是空闲的,则设置当前请求此资源的线程为有效的工作线程,并将共享资源上锁。但如果被请求的资源被占用,那么久需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的。
【CLH队列时虚拟的双向队列,AQS将等待的线程封装成一个CLH队列的Node节点实现锁的分配,通过CAS、自旋、LockSupportpark的方式,维护state变量状态,使并发达到同步效果】
锁和同步器的关系:锁,面向使用者,调用即可、同步器,面向锁的实现者
AQS对资源的共享方式有两种:
- 独占:只有一个线程执行,如ReentrantLock,可以分为公平锁和非公平锁
- 公平锁:按照线程在队列中的排队顺序
- 非公平锁:当线程要获取锁时,无视队列顺序,直接抢
- 共享:多个线程可同时执行,如Semaphore/CountDownLatch
AQS底层使用模板方法模式:
模板方法模式的使用,自定义一个同步器只需要重写如下方法,其他方法均为final。
isHeldExclusively()//该线程是否正在独占资源,用的condition需要实现
//独占
tryAcquire(int)//独占方式,尝试获取资源
tryRelease(int)//独占方式,尝试释放资源
//共享
tryAcquireShared(int)//共享方式,尝试获取资源
tryReleaseShared(int)//共享方式,尝试释放资源
ReentrantLock为例,state初始化为0,A线程lock()时,调用tryAcquire()独占资源并将state+1,此后其他线程就会请求失败,直到A线程unlock()到state=0。在释放锁之前,A线城内部可以重复获取此锁(state累加),也就是可重入,但是获取的次数等于释放的次数,state为0才可以释放锁。
CountDownLatch为例,任务有N个子线程执行,State初始化为N,每个线程执行完之后countDown一次,state-1,这时要用CAS执行,所有线程执行完,state变为0。
AQS中独占锁和共享锁的操作流程:
独占锁是持有锁的线程释放之后才会唤醒下一个
共享锁是线程获取锁之后就会唤醒下一个线程,所以共享锁在获取锁和释放锁时都会调用doReleaseShared方法唤醒下一个线程,这个过程会受共享线程数量的限制。