Java线程安全:可见性,原子性,有序性

Java线程安全

可见性,原子性,有序性

Java内存模型(JMM)

Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。

  • 所有的变量都存储在主内存中。
  • 每个线程都有自己独立的工作内存,里面保持该线程使用到的变量副本。

  • 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中进行读写。

  • 不同线程之间无法直接访问其他线程工作内存的变量,所以线程间变量值的传递需要通过主内存来完成。

如何实现可见性

要实现共享变量的可见性,必须保证两点:

  • 线程修改后的共享变量值能够及时从工作内存刷到主内存中。
  • 其他线程能够及时把共享变量的最新值从主内存更新到自己的工作内存中。

synchronized实现可见性

JMM关于synchronized的两条规定:

  • 线程解锁前,必须把共享变量的最新值刷新到主内存中。
  • 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时候需要从主内存中重新读取最新值。
线程执行互斥代码的过程:
  • 获取互斥锁
  • 清空工作内存
  • 从主内存拷贝变量最新值到工作内存中
  • 执行代码
  • 将更改后的共享变量的值刷新到主内存中
  • 释放互斥锁
Synchronized的使用
  • 修饰代码快:{}里的代码块,作用与调用对象
  • 修饰方法:整个方法,作用于调用的对象
  • 修饰静态的方法:整个静态方法,作用于所有对象
  • 修饰类:{}括起来的部分,作用于所有对象
public class SyncExample {

    //修饰代码快
    public void test1(String name){
        synchronized (this){
            for (int i = 0; i < 10; i++){
                System.out.println(name + ":" + i);
            }
        }
    }

    //修饰方法
    public synchronized void test2(String name){
        for (int i = 0; i < 10; i++){
            System.out.println(name + ":" +i);
        }
    }

    public static void main(String[] args) {
        final SyncExample sync1 = new SyncExample();
        SyncExample sync2 = new SyncExample();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> sync1.test1("sync1"));
        executorService.execute(() -> sync2.test1("sync2"));

    }
}
//结果
sync2:0
sync1:0
sync2:1
sync1:1
sync2:2
sync1:2
sync1:3
sync2:3
sync1:4
sync2:4
sync1:5
sync2:5
sync1:6
sync2:6
sync1:7
sync2:7
sync1:8
sync2:8
sync1:9
sync2:9

sync1和sync2的test1交替执行,说明同步代码块作用在当前对象,不同对象调用互不影响。

当换成test2()时,结果和上相同,说明synchronized修饰一个方法时,作用和同步代码块相同,都是作用当前对象。

所以,如果一个方法中是一个完整的同步代码快,它和synchronized修饰一个方法是等同的。

当修饰一个类时

扫描二维码关注公众号,回复: 5150286 查看本文章
  public static void test3(){
        synchronized (SyncExample.class){
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread() + "test3 :" + i + " ");
            }
        }
    }

Thread[pool-1-thread-1,5,main]test3 :0 
Thread[pool-1-thread-1,5,main]test3 :1 
Thread[pool-1-thread-1,5,main]test3 :2 
Thread[pool-1-thread-1,5,main]test3 :3 
Thread[pool-1-thread-1,5,main]test3 :4 
Thread[pool-1-thread-2,5,main]test3 :0 
Thread[pool-1-thread-2,5,main]test3 :1 
Thread[pool-1-thread-2,5,main]test3 :2 
Thread[pool-1-thread-2,5,main]test3 :3 
Thread[pool-1-thread-2,5,main]test3 :4 

从结果上可以看出,修饰一个类时,作用于这个类的所有对象。

volatile 实现可见性

volatile通过加入内存屏障和禁止重排序优化来实现可见性

  • 对volatile变量执行写操作时,会在写操作后加入一条store屏障指令
  • 对volatile变量执行读操作时,会在读操作前加入一条load屏障指令

volatile变量在每次被线程访问时,都强迫从主内存中读取该变量的值,而当发生变化时,会强迫线程将最新的值刷新到主内存。

  • 线程写volatile变量的过程
  1. 改变线程工作内存中volatile变量副本的值。
  2. 将改变后的副本的值重工作内存刷新到主内存
  • 线程度volatile变量的过程
  1. 将主内存中读取volatile变量的最新值到工作内存中。
  2. 从工作内存中读取volatile变量的副本。

volatile不能保证原子性

private int number = 0;
number ++;

number ++ 不是原子操作,可以分为三步
1.读取number的值,2.将number值+1,3.写入最新的numnber的值

public class TestAtomicDemo {

    public static void main(String[] args) {
        AtomicDemo demo = new AtomicDemo();
        for (int i = 0; i < 10; i ++){
            new Thread(demo).start();
        }
    }

}

class AtomicDemo implements Runnable{

    private volatile int serialNumber = 0;

    public void run() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +":"+getSerialNumber());
    }

    public int getSerialNumber(){
        return serialNumber ++;
    }
}


Thread-0:0
Thread-1:1
Thread-3:3
Thread-2:2
Thread-5:4
Thread-4:5
Thread-6:7
Thread-7:7
Thread-8:6
Thread-9:8

可以看到Thread6和Thread7的执行结果相同。

原子性

原子性:提供了互斥访问,同一时刻,只能由一个线程对它进行操作。

jdk1.5后java.util.concurrent.atomic包下面提供可常用的原子变量。CAS(Compare-And-Swap)算法保证数据原子性。

private volatile AtomicInteger serialNumber = new AtomicInteger(0);

 public int getSerialNumber(){
        return serialNumber.incrementAndGet();
    }

AtomicInteger的incrementAndGet()方法是如何保证原子性的呢?

private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
    private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");

    private volatile int value;
    /**
     * Atomically increments the current value,
     * with memory effects as specified by {@link VarHandle#getAndAdd}.
     *
     * <p>Equivalent to {@code addAndGet(1)}.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return U.getAndAddInt(this, VALUE, 1) + 1;
    }

从源码(我的是JDK10)中可以看到AtomicInteger使用了一个Unsafe类的getAndAddInt方法。

/**
     * Atomically adds the given value to the current value of a field
     * or array element within the given object {@code o}
     * at the given {@code offset}.
     *
     * @param o object/array to update the field/element in
     * @param offset field/element offset
     * @param delta the value to add
     * @return the previous value
     * @since 1.8
     */
    @HotSpotIntrinsicCandidate
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!weakCompareAndSetInt(o, offset, v, v + delta));
        return v;
    }

getIntVolatile方法,如果没有其他线程来处理Object o,这个方法返回只v就等于offset。
weakCompareAndSetInt方法就是CAS算法核心
offset为内存值,v是预估值,v+delta为更新值,当且仅当offset==v,才会+1操作。

有序性

Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序的过程不会影响单线程的执行结果,却会影响到多线程并发执行的正确性。

volatile, synchronized,Lock,可以保证有序性

##### happens-before原则

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前的操作先于后的操作。Java虚拟机只会对没有数据依赖的代码进行重排序。
  2. 锁定规则,一个unLock操作先行发生于后面对同一个锁的lock操作。
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。
  4. 传递规则:提现了happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C
  5. 线程启动规则:假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行后确保对线程B可见。
  6. 线程终结规则:假定线程A在执行的过程中,通过制定ThreadB.join()等待线程B终止,那么线程B在终止之前对共享变量的修改在线程A等待返回后可见。
  7. 对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始。

猜你喜欢

转载自www.cnblogs.com/Godfery/p/10353625.html