目录
基础概念
Java代码在javac编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令。
现代操作系统采用时分的方式调度运行的程序,系统为每个线程分配时间片,当时间片用完了就发生线程的调度。多核处理器当然可以同时处理多个任务,但总内存还是只有那么一块。因此在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中数据读到处理器缓存里。
在多处理器环境中,LOCK#信号确保在声言该信号期间,处理器可以独占任何共享内存 ,LOCK#信号一般不锁总线,而是锁缓存。即关键字上锁->指令信号锁缓存。同时,一个处理器的缓存回写到内存会导致其他处理器的缓存无效。毕竟指令还是按照顺序执行的,因此java依据基本底层指令提供上锁的关键字用于保证内存可见性/锁住资源,使我们能在更高层次上进行并发编程。
关键字volatile
volatile用于声明变量(字段)使其成为共享变量。用关键字volatile声明的字段每次被使用时系统都会将该字段写回到内存,同时使缓存中的该字段失效(即该字段可能保存在缓存中,系统将该字段写回到内存并使缓存失效),这样,每当线程需要使用该字段时都需要从内存中读取该字段,保证了该字段的唯一可见性。注意,volatile只保证了内存可见性,并不保证原子性,volatile适用于get/set,并不适用于getAndOperate。我猜可能有这样一种情况,如果两个核同时拿到了字段,对字段同时进行写回,相当于只执行一次操作而不是两次。
关键字synchronized
synchronized可用于声明方法,同时可以锁住对象。
- 对于普通同步方法,锁住当前实例对象。
- 对于静态同步方法,锁住当前类的Class对象。
- 对于同步方法块,锁住Synchonized括号里配置的对象。
synchronized基于指令monitorenter和monitorexit实现,是java提供的基本上锁机制。
每一个对象都有自己的监视器(monitor),获取锁就相当于拿到该对象的监视器进入同步方法/方法块,否则进入同步队列,陷入BLOCKED(阻塞)状态,当监视器退出后重新竞争锁。
cas:不加锁实现原子操作(乐观锁)
cas即compare and swap,一种乐观锁策略,对于变量x,它的实现过程是,有3个操作数,内存值V,旧的预期值E,要修改的新值U,当且仅当预期值E和内存值V相同时,才将内存值V修改为U,否则什么都不做。
原子操作是并发基本操作,保证字段的唯一修改性。cas常用于并发包中,对于共享字段x,假如要执行x++这一操作,x++必须保证原子性。一种办法是,x++写入方法中,对该方法使用synchronized上锁,肯定能保证x++的原子性,显而易见这种方式会比较耗时,另一种办法就是使用cas。
cas同样基于指令实现,依靠内存改变的唯一顺序性保证原子操作,抽象出来就是cas(旧值,新值)。拿上面那个例子说明一下,有这样一种情况,如果两个核同时拿到了字段x=56,对字段同时进行x++写回,相当于只执行一次操作(57)而不是两次(58)。现在使用cas,两个核同时访问字段x=56,核1使用cas(56,57),意为旧值为56,新值为57,核2同样像这样干,执行cas(56,57),现在,因为指令执行仍是顺序的,指令级别上必定有先后顺序,且字段x的内存值的修改是瞬间的有前后关系的(无法实现同时改变内存值吧,那就太魔幻了),因此,假设核1先执行到了,x的内存值变为了57,核2的cas(56,57)也到了,但显然旧值56和57不同啊,因此操作失败,什么也不干,下面有一张图(来源:https://www.cnblogs.com/Mainz/p/3546347.html)
在程序层面上,因为核2的cas(56,57)操作失败,那么核2继续拿新值(可以使用volatile保证能从内存拿),继续执行cas(57,58),因此,x的操作就变为了原子操作,显而易见,必须对x进行包装,内部提供cas(旧值,新值)操作,返回一个boolean判断是否执行成功,如果不成功,一直循环执行。
AtomicInteger atomicI = new AtomicInteger(0);//包装原子类
for (;;) {
int i = atomicI.get();//获取当前内存值
boolean success = atomicI.compareAndSet(i, ++i);//尝试cas
if (success) {
break;
}
}
cas中的ABA问题
ABA问题比较抽象,线程A,B同时拿到了字段x,A执行cas(x,x+1),右执行cas(x,x-1),此时B以为x没变化,但实际上x变化了,如果单纯的是换值,这么做当然没问题,但是如果操控的是带有状态的结构,就可能引发问题。
下面这篇文章写得非常好,引用于:https://cloud.tencent.com/developer/article/1098132
先描述ABA。假设两个线程T1和T2访问同一个变量V,当T1访问