1,概述
上文我们说到volatile是解决并发的可见性和有序性的问题。原子性问题的解决并不是通过volatile来解决的,下面我们看一下例子:
public class VolatileDemo1 { static volatile int flag = 0; public static void main(String[] args) { new Thread() { @Override public void run() { while (true) { flag++; System.out.println(Thread.currentThread().getName() + "线程修改变后的变量为" + flag); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); new Thread() { @Override public void run() { while (true) { flag++; System.out.println(Thread.currentThread().getName() + "线程修改变后的变量为" + flag); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); } }
运行多次出现这样的结果:
分析下原因:为什么会出现两次8,两次10这些情况。我们的本意是是顺序递增。其实原因也很简单。当第一个线程执行完flag++(等于7),就切换到第二个线程了flag++(等于8),然后因为volatile是有可见性的。所以打印出来的都是8了。解决办法就是加锁:synochronized。加锁有很多种方式,什么静态方法加synochronized,普通方法加synochronized的区别。这些基础的就不说了。自行百度。解决代码如下:
public class VolatileDemo1 { static int flag = 0; public static void main(String[] args) { new Thread() { @Override public void run() { while (true) { synchronized (VolatileDemo1.class){ flag++; System.out.println(Thread.currentThread().getName() + "线程修改变后的变量为" + flag); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); new Thread() { @Override public void run() { while (true) { synchronized (VolatileDemo1.class){ flag++; System.out.println(Thread.currentThread().getName() + "线程修改变后的变量为" + flag); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); } }
2,synchronized底层原理
2.1 monitor
使用synchronized之后,编译后的jvm指令会多出来monitorenter和monitorexit两个指令。效果如下:
monitorenter // 对应代码 monitorexit
每个java对象都有一个与之对象的monitor。当然了,类也是Class类型的java对象。每个monitor都有一个计数器。当一个线程过来获取锁时,就会将计数器+1.别的线程如果发现计数器不等于0,就说明有人持有该对象的锁,就会同步阻塞等待。
2.2 可重入锁
synchoronized(VolatileDemo1.class){ synchronized(VolatileDemo1.class){ //相应代码 } }
对同一个对象可多次加锁。这种情况是这样的:当第一个线程获取锁时,将计数器+1,此时计数器=1.再次加锁,此时计数器=2。其他线程只要发现计数器不等于0.就表明有有别的线程占有锁。而每次走出一个synchronized,就会将计数器-1,直到最后到0,彻底释放了锁。
3,wait/notifyAll
这两个方法是属于Object的,用于控制线程的。场景,我们实现一个简易队列,两个线程,一个线程负责插入,一个负责读取。队列的大小是100。代码如下:
public class MyQueue { private final static int MAX_SIZE = 100; private LinkedList<String> queue = new LinkedList<String>(); /** * 一个线程负责插入数据,不能超过100 * * @param element */ public synchronized void offer(String element) { try { if (queue.size() == MAX_SIZE) { System.out.println("都100啦。。。。。。。。。。。。"); wait(); } queue.addLast(element); notifyAll(); } catch (Exception e) { e.printStackTrace(); } } public synchronized String take() { String element = null; try { if (queue.size() == 0) { wait(); } element = queue.removeFirst(); notifyAll(); } catch (Exception e) { e.printStackTrace(); } return element; } public static void main(String[] args) { final MyQueue queue = new MyQueue(); new Thread() { @Override public void run() { for (int i = 1; i < 1000; i++) { queue.offer(i + ""); System.out.println("插入的元素是:"+i); } } }.start(); new Thread() { @Override public void run() { for (int i = 1; i < 1000; i++) { String msg = queue.take(); System.out.println("取出的元素是:" + msg); } } }.start(); } }
4,Atomic原子系列
4.1 概述问题的其他解决方案
如概述的问题。当时我们的解决方案是使用synchronized锁。但是锁是重量级的操作,有更好的方案吗?有,可以使用原子系列操作:
public class VolatileDemo1 {
static AtomicInteger flag = new AtomicInteger(1);
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "线程修改变后的变量为" + flag.getAndIncrement());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread() {
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "线程修改变后的变量为" + flag.getAndIncrement());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
4.2 Atomic系列原理
Atomic实现类有很多:AtomicLong、AtomicInteger、AtomicReference、LongAdder等等。它是采用无锁化(也叫乐观锁)。过程:每次尝试修改之前,先判断是否被修改了,没有就自己修改;如果被别人修改了,就重新查出来最新的值,再重复前面的操作。这个过程叫做CAS(compare and set)。我们来看看AtomicInteger的源码:
static {//初始化之前执行这个静态代码块。这里有个valueOffset指针,可以认为是value值的指针偏移量。 try {
//底层是unsafe来操作的,这个对象只能用于jdk内部。外部使用会报错 valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } }
//核心变量value. private volatile int value; public AtomicInteger(int initialValue) { value = initialValue; }
当我们执行getAndIncrement(),看看源码:
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do {//这里的办法都是本地方法。实现的就是上文我们说的执行过程。 var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
4.3 Atomic的弊端
4.3.1:ABA问题
比如一个线程修改一个变量时,先判断是否被修改了,没有修改就自己来修改。这里有个问题,假设第二个线程过i来修改了变量,然后第三,第四也都修改了这个变量,但是第N个线程又把变量回到初始值。这时候第一个线程来判断发现,变量没改变。所有的修改操作对线程1都没可见。
简单的说:
线程2 把变量A=0变成了1
线程3 把变量A从1又变回到了0
线程1 变量A=0,没有改变,然后自身改变。
AtomicInteger是做自增操作的,所以没有ABA问题。但是Atomic的其他类确实会出现这些问题。
解决方案:使用AtomicStampedReference解决。它的原理其实是使用版本戳version来对记录或对象标记,避免并发操作带来的问题
4.3.2:无线循环问题
当执行CAS的过程中,很多人都会发现,修改变量的时候会一直判断变量是否改变,如果改变就一直执行循环重新判断,这在高并发的场景下是很常见的。
解决方案:使用LongAdder解决。它的原理是分段CAS。
4.3.3:多变量原子问题
AtomicInteger、AtomicLong这些都是为了保证一个基本变量保持原子性操作。
解决方案:使用AtomicReference解决。它是允许你自定义对象,对象里可以封装多个变量,然后检查这个对象的引用是不是一致。