原子性问题

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解决。它是允许你自定义对象,对象里可以封装多个变量,然后检查这个对象的引用是不是一致。

转载于:https://www.cnblogs.com/xtz2018/p/11445961.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值