Java并发编程(一)(原子性、可见性、有序性)

一、原子性

1.JMM

        不同的硬件和操作系统在操作内存上是有一定差异的,java为了可以跨平台,在同一套代码下不同的硬件和操作系统出现各种不同的问题,使用JMM屏蔽掉各种操作系统和硬件操作内存的差异。

        JMM规定,所有的变量都需要存储在主内存中,当一个线程需要操作变量时,需要将变量从主内存中拿到放入线程内存(CPU内存),然后再进行操作,在操作完成后再放入到主内存中(不一定)。

2.什么是原子性

        一个操作是不可分割、不可中断的,一个线程在执行过程中,另一个线程不可影响。

        java多线程操作临界资源时,预期的结果与最终的结果一致。

3.不满足原子性带来的问题

        例:当多个线程同时操作同一个变量,输出的结果与预期的不符(两个线程对0同时增加100次,预期返回结果为200)

public class Demo01 {

    private static int COUNT = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                increment();
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                increment();
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(COUNT);
    }

    private static void increment() {
        COUNT++;
    }

}

4.如何保证原子性

4.1.synchronize锁

        synchronize锁可以保证多线程在同时操作临界资源时,只有一个线程正在操作临界资源。

    private static void calculation() {
        synchronized (COUNT){
            COUNT++;
        }
    }

4.2.lock锁

        lock锁是在java1.5时出现的,java1.5时的lock锁比synchronize性能好很多,在java1.6之后二者性能相差不大,在并发比较多时,推荐使用ReentrantLock锁,性能较好。

    private static void calculation() {
        reentrantLock.lock();
        try {
            COUNT++;
        } catch (Exception e) {

        } finally {
            reentrantLock.unlock();
        }
    }

4.3.ThreadLocal

        ThreadLocal保证原子性,是让多线程不去操作资源,每个线程使用自己的资源。

public class ThreadLockDemo {

    public static void main(String[] args) {
        ThreadLocal tl1 = new ThreadLocal<>();
        ThreadLocal tl2 = new ThreadLocal<>();
        tl1.set("aaa");
        tl2.set("bbb");
        System.out.println("main->" + tl1.get());
        System.out.println("main->" + tl2.get());
        Thread thread = new Thread(() -> {
            System.out.println("thread->" + tl1.get());
            System.out.println("thread->" + tl2.get());
        });
        thread.start();
    }

}

4.4.cas

        cas是compare and swap,也就是比较与交换的缩写,是一条CPU的并发原语。

        cas在替换内存中的某个值时,会先判断该值与预期的是否一致,如果一致才进行替换,不一致则重新获取值进行操作,满足了原子性的操作。

        Java中基于Unsafe的类提供了对CAS的操作的方法,JVM会帮助我们将方法实现CAS汇编指令。

        但是要清楚CAS只是比较和交换,在获取原值的这个操作上,需要自己实现。

public class CasLockDemo {

    private static AtomicInteger COUNT = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                calculation();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                calculation();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(COUNT);
    }

    private static void calculation() {
        COUNT.incrementAndGet();
    }

}

4.4.1.cas的问题

  • cas只能保证一个变量的原子性,无法保证一个代码块的原子性。
  • 自旋时间过长:若其他线程对该变量进行不停的修改,该线程在比较时无法获取到预期结果的值,就会一直进行判断不断的获取值。

二、可见性

1.什么是可见性

        可见性的问题是基于CPU出现的,CPU的处理速度非常快,如果每次处理数据都要从主内存中拿取数据的话,效率是非常低的。因此CPU提供了L1,L2,L3的三级缓存,CPU从主内存中获取到值时,放到CPU的三级缓存中,下次直接从缓存中获取数据,效率会得到提升。

2.三级缓存带来的问题

        这样也带来了可见性的问题,CPU都是多核的,每个线程的三级缓存都是独立的,在多线程操作过程中,若当前线程对数据进行操作后没有及时同步到主内存,其他线程拿到的数据就是错误的,出现了数据不一致的问题。

public class Demo1 {

    private static Boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (flag) {

            }
            System.out.println("thread线程停止了");
        });
        thread.start();
        Thread.sleep(100);
        flag = false;
        System.out.println("主线程将flag改为true");
    }

}

3.如何保证可见性

3.1.volatile关键字

        volatile是一个关键字,用于修饰成员变量,该成员变量被volatile修饰时,CPU操作该变量会将该值在内存中设置为无效,每次从主内存中获取最新的变量值。

  • volatile属性的变量被写:JMM会将当前CPU缓存中的变量值及时同步到主内存中。
  • volatile属性的变量被读:JMM会将CPU缓存中的变量设置为无效,从主内存中获取变量的值。
public class VolatileDemo {

    private static volatile Boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (flag) {

            }
            System.out.println("thread线程停止了");
        });
        thread.start();
        Thread.sleep(100);
        flag = false;
        System.out.println("主线程将flag改为true");
    }

}

3.2.synchronize锁

        涉及到synchronize锁的方法或代码块,在获取到锁资源后,会将内部所用到的变量从CPU缓存中移除,重新从缓存中获取一份最新的数据,在锁资源释放后,会立即将缓存中的变量同步到主内存中。

public class SynchronizedDemo {

    private static Boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (flag) {
                synchronized (SynchronizedDemo.class){

                }
            }
            System.out.println("thread线程停止了");
        });
        thread.start();
        Thread.sleep(100);
        flag = false;
        System.out.println("主线程将flag改为true");
    }

}

3.3.Lock锁

public class LockDemo {

    private static Boolean flag = true;

    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (flag) {
                lock.lock();
                try {

                } finally {
                    lock.unlock();
                }
            }
            System.out.println("thread线程停止了");
        });
        thread.start();
        Thread.sleep(100);
        flag = false;
        System.out.println("主线程将flag改为true");
    }

}

3.4.final关键字

        final修饰成员变量,代表该变量的值不可进行修改,在一定程度上解决了可见性的问题。

        同时需要注意fiinal和volatile不可以同时修饰。

三、有序性

1.指令重排

        .java文件在执行前,会编译成CPU可以识别的指令,CPU为了提高执行效率,会在满足一定条件下,不影响最终结果的前提下,对指令进行重排。

        因此可以说,java程序在CPU中是乱序执行的。

2.指令重排造成的问题

        在多线程环境下,由于出现了指令重排,重排的这一部分语句还没执行完,就切换到了其他线程,导致结果与预期的结果不符,这就是编译器的编译优化带来的问题程序有序性的问题。

img

 img

3.如何解决有序性

3.1.happens-before原则

  1. 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。

  2. 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。

  3. volatile的happen-before原则: 对一个volatile变量的写操作happen-before对此变量的任意操作。

  4. happen-before的传递性原则: 如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。

  5. 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。

  6. 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。

  7. 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。

  8. 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。

JMM只有在不出现上述8中情况时,才不会触发指令重排效果。

不需要过分的关注happens-before原则,只需要可以写出线程安全的代码就可以了。

3.2.volitale关键字

        如果想要程序对一个属性的操作不进行指令重排,除了满足happens-before原则,还可以对该属性加volitale关键字,就不会出现指令重排的问题了。

        加了volitale后,会在两个操作之间,添加上一道指令,这个指令就可以避免上下执行的其他指令进行重排序。

3.3.synchronize锁

         synchronized为可能会重排序的部分加锁,让其在宏观上或者说执行结果上看起来没有发生重排序。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值