参考
https://www.cnblogs.com/dolphin0520/p/3920373.html
Java中的非阻塞算法
https://blog.csdn.net/lifuxiangcaohui/article/details/8051687
并发中的概念
并发编程中常遇到的三个问题
原子性问题
原子性操作是指程序执行的最小单位,也就是一个线程执行该操作时不能切换到其他线程。非原子性操作在多线程中可能会造成原子性问题,比如典型的i++:
i++ 是一个语法糖,在编译阶段会编译成: t=i+1; i=t;
试想如果两个线程同时执行到了t=i+1;然后后执行的i=t就会覆盖先执行的i=t
可见性问题
学过计算机原理的都知道,由于CPU的速度跟内存读写速度不匹配,所以在CPU跟内存之间加了一层高速缓存,使得CPU不能直接读写内存,只能读写高速缓存。
JVM的内存模型:
线程只能操作工作内存,在多线程情况下,由于每个工作线程对持有同一个变量的不同备份,导致一个线程改变了该变量的值,而另外一个线程得到的变量还是原来的值。这就是所谓的可见性的问题。
有序性问题
有序性问题是由CPU的指令重排引起的。CPU会对没有依赖关系的指令进行重排,这在单线程状态下是完全没有问题的,而多线程下可能会出现问题。
典型的问题是对象的实例化:
Object o = new Object;
实际上在CPU分为三个指令:
(1)开辟内存空间
(2)初始化
(3)赋值给o
由于CPU指令重排,真正执行的时候的顺序有可能是(1)(3)(2);
这样可能导致其他线程判断o != null 而使用o对象,而实际上o对象还没完成初始化,程序会报错。
CAS(Compare and Swap)
CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。
java一些并发类就是应用了CAS代替阻塞的思想,少量的循环比短暂的阻塞效率更好(轻度和中度的争用情况),因为阻塞意味着需要进行线程的挂起和唤醒,涉及到上下文的切换。所以失败重试的非阻塞算法在某些情况下效率更高。
自旋锁和互斥锁
自旋锁是让当前线程不断地循环,当循环条件不满足(获得锁)时进入临界区。
互斥锁是指如果不能马上获得锁,就进入阻塞状态,需要被唤醒。
自旋锁的好处是不需要改变线程的状态,响应速度快,但是不断地循环会耗费CPU资源。
互斥锁的好处是阻塞等待时不占用cpu资源,但是需要耗费线程阻塞和恢复的时间。
使用情况 在资源竞争激烈的情况下使用互斥锁,否则使用自旋锁。
可重入锁的概念
可重入锁只针对单个线程而言的,如果单个线程在获得对象锁后,再次访问这个对象的其他同步方法,如果不需要等待就称该锁为可重入锁,否则称为不可冲入锁。
synchronized关键字
synchronized修饰的代码块/方法时,任何线程进入同步块时都需要获得该对象的锁,也就是多个线程不能同时进入一个或多个同步块。
举个例子:
public class Test {
synchronized void synMethod(){
System.out.println("同步方法1");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized void synMethod2(){
System.out.println("同步方法2");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
当线程一正在执行synMethod时,线程二不能马上执行synMethod2,必须要等待线程一释放Test的对象锁,同时线程二获得该锁之后,线程二才能进入synMethod2。
- synchronized提供的是非公平锁,可重入锁。
- synchronized在等待锁期间是不可以中断的。
- JDK1.6之后,对synchronized优化,根据不同情形出现了偏向锁、轻量锁、对象锁,自旋锁(或自适应自旋锁)等,因此,现在的synchronized可以说是一个几种锁过程的封装。
ReentrantLock 可重入锁
使用:
public class Test {
ReentrantLock mLock = new ReentrantLock(true);
void lockMethod() {
mLock.lock();
try {
System.out.println("同步方法1");
lockMethod2(); //在同步方法1中调用同步方法2不会出现死锁
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
mLock.unlock();
}
}
void lockMethod2() {
mLock.lock();
try {
System.out.println("同步方法2");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
mLock.unlock();
}
}
}
- ReentrantLock是可重入锁,通过其构造方法可以设定为公平锁或者非公平锁。
- 为了保证锁的正确释放,编写代码时要把互斥区(临界区)放在try块中,并且在finally块中释放锁。
- ReentrantLock的lockInteruptibly是等待可中断的,在等待锁的过程中是可以调用Thread.interupt中断线程。
- tryLock会打破“公平锁”,如果想要继续保持锁的公平,可以用tryLock(0, TimeUnit.SECONDS)
Condition
Condition提供了类似wait和notify的机制,一个lock可以产生多个Condition,await方法和signal要在lock()和unLock()之间被调用。
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
try {
lock.lock();
notFull.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
调用await后,当前线程会阻塞;直到其他线程调用了signal之后,会唤醒其中一个处于await状态的线程。
Condition跟Object提供的wait和notify的区别:
- Condition能够支持不响应中断,而通过使用Object方式不支持;
- Condition能够支持多个等待队列(new多个Condition对象),而Object方式只能支持一个;
- Condition能够支持超时时间的设置,而Object不支持
volatile关键字
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
- Java提供了volatile关键字来保证可见性
- 普通的变量不能保证可见性
volatile能保证一定的有序性
- volatile前面的语句保证在volatile操作前
- volatile后面的语句保证在volatile操作后
原理
《深入理解Java虚拟机》
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
volatile不能替代synchronized,因为volatile不能保证操作的原子性