【并发】保证共享变量在多线程并发时的线程安全

Code:

public class AdderTest {

    static int i;
    static CountDownLatch latch = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {

        Runnable task = new Runnable() {
            @Override
            public void run() {
                int x = 0;
                while (x++ < 100000){
                        i++;
                }
                latch.countDown();
            }
        };

        Thread adder1 = new Thread(task, "Adder-1");
        Thread adder2 = new Thread(task,"Adder-2");

        //开启两个加法器线程
        adder1.start();
        adder2.start();

        //主线程阻塞,等待两个加法器线程完成操作
        latch.await();

        System.out.println(i);
    }
}

        两个Adder线程执行同样的操作:累加共享变量i,执行这个操作十万次;使用CountDownLatch是为了让主线程等待两个Adder线程执行完累加操作,最后打印共享变量i,看共享变量i的最终值是否是我们预期的二十万。

        共享变量i的最终值

【并发】保证共享变量在多线程并发时的线程安全_第1张图片

        每次执行的结果都不相同,但这些结果有一个共同点:都小于二十万。为什么会出现这种情况呢?因为在字节码中,i++可不仅仅是一步操作

0: getstatic #2     // 获取静态变量i的值
3: iconst_1         // 将常量1推送到栈顶
4: iadd             // 将栈顶两个int型数值相加
5: putstatic #2    // 将相加后的值存储到静态变量i
8: return           // 返回

        可以看到,一个简单的i++代码在字节码中包含四步,这四步即包含读又包含写,而多线程场景下,对共享变量进行读、写操作无疑会产生线程安全问题。因为线程对共享变量执行读、写操作时无法保证这两个操作的原子性

【并发】保证共享变量在多线程并发时的线程安全_第2张图片

        依照上图,假设静态变量i的值一开始是1,线程Adder-1先获取了静态变量i的值,执行+1操作,准备赋值给静态变量i时,线程Adder-1的CPU时间片用完了,线程被挂起;随后线程Adder-2也来执行相同的操作,由于它的CPU时间片充足,它能完整的执行所有操作。

        假设线程Adder-2在线程Adder-1挂起的这段时间,执行了三次完整操作,那么静态变量i的值就是4。此时线程Adder-1又获得了CPU的时间片,能够继续执行赋值操作,将静态变量i的值修改成2,这就相当于将线程Adder-2之前三次操作的结果给覆盖掉了,这也是为什么最终结果总是小于预期值的原因。

解决方案:

        (1)悲观锁

        synchronized

public class AdderTest {

    static int i;
    static CountDownLatch latch = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {

        Object lock = new Object();
        Runnable task = new Runnable() {
            @Override
            public void run() {
                int x = 0;
                synchronized (lock){
                    while (x++ < 100000) {
                        i++;
                    }
                }
                latch.countDown();
            }
        };

        Thread adder1 = new Thread(task, "Adder-1");
        Thread adder2 = new Thread(task,"Adder-2");

        //开启两个加法器线程
        adder1.start();
        adder2.start();

        //主线程阻塞,等待两个加法器线程完成操作
        latch.await();

        System.out.println(i);
    }
}

        为了减少锁带来的性能影响,我们希望锁粒度尽可能小,一般只会对存在共享变量读、写的临界区代码块上锁(这里临界区代码块是i++),但这里为了减少多次获得锁、释放锁带来的性能影响,我们应该把锁放在while循环外。

        ReentrantLock:

public class AdderTest {

    static int i;
    static CountDownLatch latch = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        Runnable task = new Runnable() {
            @Override
            public void run() {
                int x = 0;
                try {
                    lock.lock();
                    while (x++ < 100000){
                        i++;
                    }
                }finally {
                    lock.unlock();
                }
                latch.countDown();
            }
        };

        Thread adder1 = new Thread(task, "Adder-1");
        Thread adder2 = new Thread(task,"Adder-2");

        //开启两个加法器线程
        adder1.start();
        adder2.start();

        //主线程阻塞,等待两个加法器线程完成操作
        latch.await();

        System.out.println(i);
    }
}

        使用ReentrantLock实现线程互斥时,一般搭配try、finally一块使用,避免死锁现象产生。对finally机制感兴趣的读者可以看我的另一篇博客:【异常】浅析异常体系及为什么一定会执行finally块-CSDN博客

        测试:

【并发】保证共享变量在多线程并发时的线程安全_第3张图片

        (2)乐观锁

        AtomicInteger原子类

public class AdderTest {

    static AtomicInteger i = new AtomicInteger(0);
    static CountDownLatch latch = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {

        Runnable task = new Runnable() {
            @Override
            public void run() {
                int x = 0;
                while (x++ < 100000) {
                    i.getAndIncrement();
                }
                latch.countDown();
            }
        };

        Thread adder1 = new Thread(task, "Adder-1");
        Thread adder2 = new Thread(task,"Adder-2");

        //开启两个加法器线程
        adder1.start();
        adder2.start();

        //主线程阻塞,等待两个加法器线程完成操作
        latch.await();

        System.out.println(i);
    }
}

        将静态变量i的实现改为AtomicInteger原子类。多线程操作AtomicaInteger对象时,线程安全由AtomicaInteger内部的Unsafe类基于CAS(乐观锁思想的一种体现)来保证。

        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张图片

你可能感兴趣的:(java,开发语言)