看《java并发编程的艺术》这本书,想着看的时候做个简单的总结,方便以后直接看重点。
一.并发编程的挑战
1.上下文切换
Cpu时间片通过给每个线程分配CPU时间片来实现多线程机制,时间片一般是几十毫秒。任务从保存到再加载的过程就是一次上下文切换。
如何减少上下文切换?
- 无锁并发编程:多线程处理数据时,避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
- CAS算法:Compare and Swap,即比较再交换。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。容易看出 CAS 操作是基于共享数据不会被修改的假设,采用了类似于数据库的commit-retry 的模式。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
- 使用最少线程:避免创建不需要的线程
- 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
2.死锁
避免死锁的几个常见方法:
- 避免一个线程同时获取多个锁
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
- 尝试使用定时锁,使用lock.trylock(timeout)来替代使用内部锁机制
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
二.java并发机制的底层实现原理
java使用的并发机制依赖于JVM的实现和CPU的指令
1.volatile的应用
volatile是轻量级的synchronized,它保证了共享变量的“可见性”,就是说当一个线程修改一个共享变量的时候,另外一个线程能读到这个修改的值。它不会引起线程上下文的切换和调度。
那么volatile如何保证可见性的呢?
volatile变量修饰的共享变量在进行写操作的时候会多出第二行汇编代码,有一个lock前缀指令:
- 将当前处理器缓存行的数据写回到系统内存
- 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效
2.synchronized的实现原理与应用
synchronized实现同步的基础:java中的每一个对象都可以作为锁
- 对于普通同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前类的class对象
- 对于同步方法块,锁是Synchronized括号里配置的对象
当一个线程视图访问同步代码块时,首先必须得到锁,退出或抛出异常时必须释放锁。
synchronized在jvm里的实现原理:JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,通过两个指令,monitorenter和monitorexit
monitorenter指令是在编译后插入到同步代码块的开始位置,monitorexit是插入到方法结束处和异常处,jvm要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。
synchronized用的锁是存在java对象头里的
3.原子操作的实现原理
处理器如何实现原子操作?
- 使用总线锁保证原子性:总线锁就是使用处理器提供的一个Lock#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞,那么该处理器就可以独占共享内存。
- 使用缓存锁保证原子性:同一时刻,只需保证对某个内存地址的操作是原子性即可,但总线锁把CPU和内存之间的通信锁住了,使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大。采用缓存锁定的方式来实现复杂的原子性。
java如何实现原子操作?
java中可以通过锁和循环CAS的方式来实现原子操作
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* java使用循环CAS实现原子操作
*/
public class Counter {
private AtomicInteger atomic1 = new AtomicInteger(1);
private int i=1;
public static void main(String[] args) {
final Counter cas = new Counter();
List<Thread> ts = new ArrayList<>(600);
long start = System.currentTimeMillis();
for(int j=0;j<100;j++){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i< 10000;i++){
cas.count();
cas.safeCount();
}
}
});
ts.add(t);
}
for (Thread t:ts){
t.start();
}
for (Thread t:ts){
try {
t.join();
}catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.println(cas.i);
System.out.println(cas.atomic1.get());
System.out.println(System.currentTimeMillis()-start);
}
/**
* 使用CAS实现线程安全计数器
*/
private void safeCount(){
for(;;){
int i = atomic1.get();
boolean suc = atomic1.compareAndSet(i,++i);
if(suc){
break;
}
}
}
/**
* 非线程安全计数器
*/
private void count(){
i++;
}
}
运行结果如下:
可见线程安全计数器正确显示了结果。
CAS实现原子操作的三大问题:
- ABA问题:CAS需要在操作值的时候检查值有没有发生变化,如果没有发生变化就更新。但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时就会发现它的值没有发生变化,但是实际上变化了。ABA问题的解决思路就是使用版本号,A->B->A变成了1A->2B->3A, java中提供了一个类来解决这个ABA问题:AtomicStampedReference
/**
* Atomically sets the value of both the reference and stamp
* to the given update values if the
* current reference is {@code ==} to the expected reference
* and the current stamp is equal to the expected stamp.
*
* @param expectedReference the expected value of the reference
* @param newReference the new value for the reference
* @param expectedStamp the expected value of the stamp
* @param newStamp the new value for the stamp
* @return {@code true} if successful
*/
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
这个方法的作用就是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
- 循环时间长开销大。
- 只能保证一个共享变量的原子操作。解决办法是把多个共享变量合并成一个共享变量来操作·,比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。java中提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
使用锁机制实现原子操作:锁机制保证了只有获得锁的线程才能操作锁定的内存区域,JVM内部实现了很多种锁机制,有偏向锁,轻量级锁和互斥锁,但是除了偏向锁,JVM实现锁的方式都用了循环CAS