Java并发编程(二)--java线程安全的一些基础

本文多摘自《java并发编程实战》和各种网上资料,因为java并发编程涉及内容太多,本文篇幅有限,只是对并发编程中的一些概念进行普及、和简略说明

1.java并发编程

编写正确的程序很难,编写正确的并发程序则是难上加难,和串行编程相比,并发编程线程安全性可能是非常复杂的,在没有充分的同步情况下,操作执行顺序是不可预测的,甚至产生一些奇怪的结果。所以我们需要了解一些java并发编程的基础概念、常见问题、java并发编程的一些解决方案。

2.线程安全性

2.1线程安全类的定义

在《java并发编程实战》中对线程安全类的定义是

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的

2.2可见性

2.2.1描述

java内存可见性的问题得从java内存模型说起。

Java Memory Model (JAVA 内存模型)描述线程之间如何通过内存(memory)来进行交互。 具体说来, JVM中存在一个主存区(Main Memory或Java Heap Memory),对于所有线程进行共享,而每个线程又有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作并非发生在主存区,而是发生在工作内存中,而线程之间是不能直接相互访问,变量在程序中的传递,是依赖主存来完成的。

Java并发编程(二)--java线程安全的一些基础_第1张图片

所以当多个线程并发读写同一变量时就会产生可见性问题。

2.2.2错误例子

public class NoVisibility {
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        public void run() {
            while (!ready)
                Thread.yield();
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}

2.2.3例子讲解

NoVisibility 可能会一直执行下去,因为主线程在自己的工作内存中修改ready变量的副本,回写到主内存的时间未定,所以读线程可能一直执行下去。
也有可能输出number为0,读线程可能看到了写入的ready值,但却没看到之后写入的number值,这种现象被称为“重排序”

2.3volatile

Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
NoVisibility程序只要ready变量设置为volatile,则主线程设置ready = true,读线程可以立即读取到这个最新值。所以不会一直执行下去。

2.4竞态条件

2.4.1描述

当某个计算的正确性取决于多个线程的交替执行顺序时,那么就发生了竞态条件。换句话说就是正确的结果取决于运气。

2.4.2错误例子

public class AutoIncrement {
public static int i=0;

public static void main(String[] args) throws InterruptedException {
    CountDownLatch latch=new CountDownLatch(50);
    for (int j = 0; j < 50; j++) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int j = 0; j <2000 ; j++) {
                    i++;
                }
                latch.countDown();
            }
        }).start();
    }
    latch.await();
    System.out.println(i);
}

}

2.4.3例子讲解

AutoIncrement新建了50个线程,每个线程i++2000次,当所有线程执行完成时,输出i,正确的答案应该是100000,但是在我机器上执行多次,结果依次是96962,99291,97583。
因为i++是一个复合操作,可以分解了为

int a=i;
i=a+1;

当线程A读取i=0时,b线程同时读取i=0;A线程执行i=0+1操作,设置i为2。B线程也执行i=0+1操作,设置i为2,所以最终AutoIncrement的输出结果一般会小于我们预期的结果

2.5原子性

原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(如sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。

AutoIncrement 类可以稍作改进,让i++变为一个原子操作,就能输出正确的结果了

    for (int j = 0; j < 2000; j++) {
       synchronized (AutoIncrement.class) {
            i++;
        }
    }

现在输出结果就是正确的100000了,但是synchronized锁住了AutoIncrement.class,使所有i++操作都顺序执行,是一种比较消耗性能方式。所以下面来看CAS操作吧

2.6cas

CAS全称Compare And Set,比较后设值,现在大部分CPU都支持此指令,执行成功返回true,失败返回false。不会有中间状态。
java中sun.misc.Unsafe支持这类操作:

  • compareAndSwapObject
  • compareAndSwapInt
  • compareAndSwapLong

但是Unsafe类在普通的java类无法使用,但是有对应的AtomicInteger、AtomicLong、AtomicReference等各种封装类。
修改之后的AutoIncrement类

public class AutoIncrement {
    public static AtomicInteger i  = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(50);
        for (int j = 0; j < 50; j++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 2000; j++) {
                        i.incrementAndGet();
                    }
                    latch.countDown();
                }
            }).start();
        }
        latch.await();
        System.out.println(i.intValue());
    }
}

AtomicInteger.incrementAndGet方法就是调用的Unsafe.getAndAddInt,而Unsafe.getAndAddInt封装的就是Unsafe.compareAndSwapInt操作

2.7重排序

2.7.1描述

在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。可能会调整指令的执行顺序,只会保证在单线程中能够得到正确的结果,所以在缺乏足够同步的多线程程序中,要对内存操作的执行顺序进行判断,几乎无法得到正确的结论。

2.7.2错误例子

public class PossibleReordering {
    static int x = 0, y = 0;
    static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread one = new Thread(new Runnable() {
            public void run() {
                a = 1;
                x = b;
            }
        });
        Thread other = new Thread(new Runnable() {
            public void run() {
                b = 1;
                y = a;
            }
        });
        one.start();
        other.start();
        one.join();
        other.join();
        System.out.println("( " + x + "," + y + ")");
    }
}

2.7.3例子讲解

在没有正确同步情况下,即使要推断最简单的并发程序的行为也很困难,PossibleReordering 可能输出(1,0)(0,1)(1,1),甚至是(0,0)。由于PossibleReordering中每个线程中的各个操作之间不存在数据流的依赖性,因此这些操作可以乱序执行。即使顺序执行,因为工作内存刷新到主内存的不同时序也可能出现这种情况。以下是出现(0,0)结果的情况,只要时间、顺序不同,就可能出现不同的结果。
Java并发编程(二)--java线程安全的一些基础_第2张图片

2.8 happen before

以下列举的是java中的happen before原则,就不进行详细解读了,可以参考【死磕Java并发】—–Java内存模型之happens-before

程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

参考
Java中Volatile关键字详解
java并发之原子性与可见性
【死磕Java并发】—–Java内存模型之happens-before

你可能感兴趣的:(java并发编程)