JAVA并发编程:核心理论

Posted 一叶一落秋

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA并发编程:核心理论相关的知识,希望对你有一定的参考价值。

 一、共享性

  数据共享性是线程安全的主要原因之一。 如果所有的数据只在线程内有效,那就不存在线程安全性问题,这也是我们在编程的时候经常不需要考虑线程安全的主要情况之一。但是在多线程编程中,数据共享是不可避免的。最典型的场景就是数据库中的数据,为了保证数据的一致性,我们通常需要共享同一个数据库中的数据,即使在主从的情况下,访问的也是同一份数据,主从只是为了访问的效率和数据安全,而对同一份数据做的副本。

  • 代码段一:
public class DiskMemory {

    private int totalSize = 0;

    private static int TEST_COUNT = 20;

    public int getSize() {
        return 10;
    }

    //2、使用 public synchronized void setSize(int size) {}
    public void setSize(int size) {
        totalSize += size;
    }

    public int getTotalSize() {
        return totalSize;
    }

    public static void main() throws InterruptedException {
        final CountDownLatch countDownLatch = new CountDownLatch(TEST_COUNT);
        ExecutorService service = Executors.newFixedThreadPool(TEST_COUNT * 2);

        final DiskMemory diskMemory = new DiskMemory();

        final Object lock = new Object();

        for (int i = 0; i < TEST_COUNT; i++) {
            service.execute(new Runnable() {
                @Override
                public void run() {
                    try{
                        Thread.sleep(5);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                    int size = diskMemory.getSize();
                    //1、此处限制
                    synchronized (lock) {
                        diskMemory.setSize(size);
                    }
                    countDownLatch.countDown();
                }
            });
        }
        countDownLatch.await();
        System.out.print(diskMemory.getTotalSize() + " ");
        service.shutdown();
    }

    public static void main(String[] args) throws InterruptedException {
        for(int i = 0; i < 30; i++) {
            main();
        }
    }
}

上述代码模拟了多个用户同时向一个银行账号存钱操作,正常情况下20个人多一个账号存10,最后账号应该会是200,但是多次测试会发现,结果并不是这样(虽然也会出现正确情况)

二、互斥性

  资源互斥性是指同时只允许一个访问者对其进行访问,具有唯一性和排它性。我们通常允许多个线程同时对数据进行读操作,但同一时间内只允许一个线程对数据进行写操作。所以我们通常将锁分为共享锁和排它锁,也叫做读锁和写锁。如果资源不具有互斥性,即使是共享资源,我们也不需要担心线程安全。例如:对于不可变得数据共享,所有线程都只能对其进行读操作,所以不需考虑线程安全问题。但是对共享数据的写操作,一般就需要保证互斥性。

  • 代码二
    public class DiskMemory {
    
        private int totalSize = 0;
    
        private static int TEST_COUNT = 20;
    
        public int getSize() {
            return 10;
        }
    
        //2、使用 public synchronized void setSize(int size) {}
        public void setSize(int size) {
            totalSize += size;
        }
    
        public int getTotalSize() {
            return totalSize;
        }
    
        public static void main() throws InterruptedException {
            final CountDownLatch countDownLatch = new CountDownLatch(TEST_COUNT);
            ExecutorService service = Executors.newFixedThreadPool(TEST_COUNT * 2);
    
            final DiskMemory diskMemory = new DiskMemory();
    
            final Object lock = new Object();
    
            for (int i = 0; i < TEST_COUNT; i++) {
                service.execute(new Runnable() {
                    @Override
                    public void run() {
                        try{
                            Thread.sleep(5);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
    
                        int size = diskMemory.getSize();
                        //1、此处限制
                        synchronized (lock) {
                            diskMemory.setSize(size);
                        }
                        countDownLatch.countDown();
                    }
                });
            }
            countDownLatch.await();
            System.out.print(diskMemory.getTotalSize() + " ");
            service.shutdown();
        }
    
        public static void main(String[] args) throws InterruptedException {
            for(int i = 0; i < 30; i++) {
                main();
            }
        }
    }

上述代码对代码一进行了改进,进行了同步锁操作,加入了synchronized关键字,测试执行了30次,每次都输出200,可以增大测试数目进行验证。

三、原子性

  原子性是指对数据的操作是一个独立的、不可分割的整体。换句话说就是一次操作,是一个连续不可中断的过程,数据不会执行一半的时候被其他线程所修改。保证原子性的最简单方式就是操作系统指令,就是说如果一次操作对应一条操作系统指令,这样肯定可以保证原子性。但是很多操作不能通过一条指令就完成。例如:对long类型的运算,很多系统就需要分成多条指令分别对高位和低位进行操作才能完成。还比如,我们经常使用的 i++ 操作,其实需要分为三个步骤:

(1)读取整数i的值

(2)对i进行加一操作

(3)将结果写会内存。

这个过程在多线程下就可能出现如下现象:

 这也是代码段一执行的结果为什么不正确的原因。对于这种组合操作,要保证原子性,最常见的方式是加锁,如Java中的Synchronized关键字或者Lock都可以实现,代码二就是通过Synchronized关键字实现的。除了锁以外,还有一种方式就是CAS,即修改数据之前先比较与之前读取到的值是否一致,如果一致则进行修改,如果不一致则重新执行,这也是乐观锁的实现原理。不过CAS在某些场景下不一定有效,比如另一个线程先修改了某个值,然后再改回原来值,这种情况,CAS无法判断。

四、可见性

  

  上图为JVM模型,可以看出某个线程操作都会有一个工作内存(相当于CPU高级缓存区),对于共享变量,线程每次读取的是工作内存中共享变量的副本,写入的时候也是直接修改工作内存中副本的值,然后在某个时间点上再将工作内存与主内存的值进行同步。这样就导致了一个问题,如果线程1对某个变量进行了修改,线程2却有可能看不到线程1对共享变量所做的修改。

  •  代码三
    package com.lynn.learning.thread;
    
    import java.util.concurrent.CountDownLatch;
    
    /**
     * @Author zhouxk
     * @Date 2019-04-13 23:48
     * @project com.lynn.learning.thread
     * @Version: 1.0
     **/
    public class VisibilityTest {
    
        private static boolean ready;
    
        private static int number;
    
        private static CountDownLatch countDownLatch;
    
        private static void dataInit() {
            ready = false;
            number = 0;
            countDownLatch = new CountDownLatch(2);
        }
    
        private static class ReaderThread extends Thread {
            public void run() {
                try {
                    Thread.sleep(40);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.print(number + " " + ready + " ");
                countDownLatch.countDown();
            }
        }
    
        private static class WriterThread extends Thread {
            public void run() {
                try {
                    Thread.sleep(40);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                number = 100;
                ready = true;
                countDownLatch.countDown();
            }
        }
    
        public static void main(String[] args) throws InterruptedException{
            for(int i = 0; i < 10; i++) {
                main();
            }
        }
    
        public static void main() throws InterruptedException{
            dataInit();
            new ReaderThread().start();
            new WriterThread().start();
            countDownLatch.await();
        }
    }

 从直观上理解,这段代码应该会输入100,ready的值是不会被打印出来的。实际上,如果多次运行上面的代码,可能会出现多种不同的结果。

当前,这个结果也只能说有可能是不可见性造成的,当写线程(ReaderThread)设置ready=true后,读线程(WriterThread)看不到修改后的结果,所以会打印false。这个结果也可能是线程的交替执行造成的。Java中可通过synchronizedhe或volatile来保证可见性。

五、有序性

  为了提高性能,编译器和处理器可能会对指令做重排序。重排序可以分为以下三种:

(1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

(2)指令级并行的重排序。

(3)内存系统重排序。

从(1)源码来看,要么指令1先执行要么指令3先执行。如果指令1先执行,r2不应该看到指令4中写入的值。如果指令3先执行,r1就不应该看到指令2写入的值。但是运行结果却可能出现r2 == 2,r1 == 1的情况,这就是重排序导致的结果。

(2)是一种可能出现的合法的编译结果,编译后,指令1和指令2的顺序就互换了。因此,才会可闲r2 == 2,r1 == 1的结果。java中也是通过synchronized或volatile来保证顺序性。

 

以上是关于JAVA并发编程:核心理论的主要内容,如果未能解决你的问题,请参考以下文章

Java 并发编程:核心理论

JAVA并发编程:核心理论

Java多线程0:核心理论

学妹问我,并发问题的根源到底是什么?

java多线程

Java并发编程:Synchronized及其实现原理