概念
1、乐观锁
乐观锁是一种概念,去解决数据同步的思想。即再进行数据更新时会先去判断该数据是否被修改过,如果无保存成功,如果有则更新失败
2、CAS (Compare And Swap 比较并且替换)
CAS是乐观锁的一种实现,是一种轻量级锁,其工作原理为:当需要修改某条数据是,先去对比该条数据是否与读的时候相同,如果不同则放弃修改,若相同则修改成功
典型的CAS的运用在JUC的AtomicInteger、AtomicLong
拿AtomicInteger举例,他的自增函数incrementAndGet()就是这样实现的,其中就有大量循环判断的过程,直到符合条件才成功。
大概意思就是循环判断给定偏移量是否等于内存中的偏移量,直到成功才退出,看到do while的循环没。
ps: 这里考虑到一个问题,在平常业务中我们使用数据库将数据存在数据库中,当然内存去修数据的值与数据库的值进行对比,从而去实现CAS,但是这段源码中java运行在虚拟机上,并没有内存去磁盘的交互,他是如何做到比较数据被修改的,又是如何去比较的。这里引入应该JMM(JAVA Memory Mode)java内存模型的概念,这里与jvm虚拟机的内存模型说的不是一个东西。由于现代计算机CPU的飞速发展,内存的读写与cpu的处理性能不在一个数量级上为了解决木桶效应,cpu中加入缓存机制。可以经常听到CPU双核16线程3级缓存等描述cpu性能的名词这里就是指CPU可以支持多线程执行去提高执行效率。每个线程都有各自的缓存空间【如下图1-1】,
【图1-1】
MSI协议
于是CAS多线程中实现数据同步即各个线程中的缓存数据与主内存数据的比较与同步。既然有这么多个线程同时取处理数据 必然会导致每个线程中缓存的数据不同步,为了解决这个问题就有了缓存一致性协议,目前的协议有MSI、MESI等。先来聊一下MSI协议MSI这三个字母代表了cache line可能的三种状态,分别是Modified(修改), Shared(共享)和Invalid(实效)
1、当一些CPU从内存读取了数据到自己的cache line,此时这些CPU中的这些cache line中的数据都是一样的,和内存对应位置的数据也是一样的,cache line都处于shared状态。
2、接下来P2将自己cache line的数据更改为13,P2的这条cache line就变为modified状态(S–>M),其他CPU的cache line就变为invalid状态(S–>I)。
3、然后如果P1试图读取这条cache line中的数据,由于是invalid状态,于是将触发cache miss(细分的话叫read miss),那么P2将会把自己cache line的数据写回(writeback)到内存,供P1从内存读取,之后P1和P2的cache line都将回到shared状态(I–>S, M–>S)。
4、这里包含着一个前提,就是某个CPU在自己的cache中,对内存某个位置对应的invalid cache line访问触发的cache miss,其他拥有这个内存位置对应的cache line的CPU都能识别,这个前提自然也属于MSI协议实现的一部分。
在上面图2的状态中,如果P1不是读取,而是写入这条cache line,那么也将触发cache miss(不过这次是write miss),P1的cache line将变为modified状态(I–>M),而P2的cache line将变为invalid状态(M–>I)。
MESI协议
该协议即是在MSI协议上加上的Exclusive (这行数据有效,数据和内存中的数据一致,数据只存在于本Cache中。)只有一个线程独占该资源的时候不会考虑有其他线程并行的情况。这个协议会加快CPU的处理效率。
3、ABA
ABA是在CAS的时候为引发ABA,ABA是指在对比修改数据时会因为改数据被重复修改,但是最终仍然与最开始读的数据一致,此时CAS误认为该数据与读时相同,使得修改成功。原理图如下:
4、避免出现ABA情况
解决ABA的方法通常是加上版本号,比如在修改一个字段时,同时有另外一个字段去控制它的版本号,一次修改操作就加一个版本号,这样就就可以避免ABA问题的产生。
实现
一、synchronized的实现
synchronized有三种实现方式
1、通过使用synchronized去修饰实例方法,通过锁实例对象去实现线程同步
public class AccountingSync implements Runnable{
//共享资源(临界资源)
static int i=0;
/**
* synchronized 修饰实例方法
*/
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
AccountingSync instance=new AccountingSync();
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
/**
* 输出结果:
* 2000000
*/
}
上述代码中将synchronized作用于increase这个实例方法中,并且从始至终只创建了一个AccountingSync实例对象,固即使有两个线程去操作同一个对象去改变静态变量时,由于该对象实例已经被锁,不会触发多线程引起的脏数据问题
2、通过使用synchronized作用于 静态方法
当synchronized作用于静态方法时,其锁就是当前类Class对象锁。由于静态成员不属于任何一个实例对象,是类成员。
public class AccountingSyncClass implements Runnable{
static int i=0;
/**
* 作用于静态方法,锁是当前class对象,也就是
* AccountingSyncClass类对应的class对象
*/
public static synchronized void increase(){
i++;
}
/**
* 非静态,访问时锁不一样不会发生互斥
*/
public synchronized void increase4Obj(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
//new新实例
Thread t1=new Thread(new AccountingSyncClass());
//new心事了
Thread t2=new Thread(new AccountingSyncClass());
//启动线程
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
由于synchronized关键字修饰的是静态increase方法,与修饰实例方法不同的是,其锁对象是当前类的class对象。注意代码中的increase4Obj方法是实例方法,其对象锁是当前实例对象,如果别的线程调用该方法,将不会产生互斥现象,毕竟锁对象不同,但我们应该意识到这种情况下可能会发现线程安全问题(操作了共享静态变量i)。
3、synchronized同步代码块
除了使用关键字修饰实例方法和静态方法外,还可以使用同步代码块,在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了,同步代码块的使用示例如下:
public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
@Override
public void run() {
//省略其他耗时操作....
//使用同步代码块对变量i进行同步操作,锁对象为instance
synchronized(instance){
for(int j=0;j<1000000;j++){
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
从代码看出,将synchronized作用于一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行i++;操作。当然除了instance作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁,如下代码:
//this,当前实例对象锁
synchronized(this){
for(int j=0;j<1000000;j++){
i++;
}
}
//class对象锁
synchronized(AccountingSync.class){
for(int j=0;j<1000000;j++){
i++;
}
}
一、Volatile的实现
1、volatile去修饰变量去保证变量的可见性与防止指令重排
(1)、保证变量的可见性,不保证原子性
a.当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去;
b.这个写会操作会导致其他线程中的缓存无效。
(2)、禁止指令重排
//TODO
二volatile的实效场景
1、volatile不适合复合场景
(1)、例如,inc++不是一个原子性操作,可以由读取、加、赋值3步组成,所以结果并不能达到100000。
在进行加的时候是在cpu中的寄存器进行运算的,运算结果再进行写的操作,如果有多个线程进行写的操作,volatile无法保证某个线程的读取、加、赋值是处于一个原子性
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j=0;j<10000;j++){
test.increase();
}
}).start();
}
//等待线程处理完
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(test.inc);
}
}
(2)、解决方法
1、采用synchronized
下面展示一些 内联代码片
。
public class Test {
public volatile int inc = 0;
public synchronized void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j=0;j<10000;j++){
test.increase();
}
}).start();
}
//等待线程处理完
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(test.inc);
}
}
2、采用lock
public class Test {
public int inc = 0;
Lock lock =new ReentrantLock();
public void increase() {
lock.lock();
try {
inc++;
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
final Test test = new Test();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j=0;j<1000;j++){
test.increase();
}
}).start();
}
//等待线程处理完
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(test.inc);
}
}
3、.采用java并发包中的原子操作类,原子操作类是通过CAS循环的方式来保证其原子性的
public class Test {
public AtomicInteger inc = new AtomicInteger(0);
public void increase() {
inc.incrementAndGet();
}
public static void main(String[] args) {
final Test test = new Test();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j=0;j<1000;j++){
test.increase();
}
}).start();
}
//等待线程处理完
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(test.inc);
}
}
总结
1、volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如booleanflag;或者作为触发器,实现轻量级同步。
2、volatile属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁_上,所以说它是低成本的。
3、volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见,volatile属性不会被线程缓存,始终从主 存中读取。
4、计算机的交互过程 寄存器(cpu)—>缓存—>内存—>磁盘