并发三大特性

一、原子性

1.1 什么是并发编程的原子性

JMM(Java Memory Model)。不同的硬件和不同的操作系统在内存上的操作有一定差异,Java为了解决相同代码在不同的操作系统上出现的各种问题,用JMM屏蔽掉各种硬件和操作系统带来的差异

让Java的并发编程可以做大跨平台

JMM规定所有变量都会存储在主内存中,在操作的时候,需要从主内存中复制一份到线程内存(CPU内存),在线程内部做计算,然后在写回主内存中(不一定)

原子性的定义:原子性指一个操作时不可分割的,不可中断的,一个线程在执行时,另一个线程不会影响到他

并发编程的原子性用代码阐述:

public class IncrementError {
    private static int count = 0;

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

当前程序:多线程操作共享数据时,预期的结果,与最终的结果不符。

原子性:多线程操作临界资源,预期的结果和最终结果一致

通过对这哥程序的分析,可以查看出,++的操作,一共分了三部,首先是从主内存拿到数据到CPU的寄存器中,然后在寄存器中进行+1操作,最终将结果写回到主内存中

1.2 保证并发编程的原子性

1.2.1 synchronized

因为++操作可以从指令中查看到

image.png

可以在方法上追加synchronized关键字或者采用同步代码块的形式来保证原子性

synchronized可以让避免多线程同时操作临界资源,同一个时间点,只会有一个线程正在操作临界资源

image.png

1.2.2 CAS

到底什么是CAS

compare and swap 也就是比较和交换,他是一条CPU的并发原语

他在替换内存的某个位置的值时,首先查看内存的值与预期值是否一致,如果一致,执行替换操作,这个操作是一个原子性操纵。

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

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

public class CAS {
    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++) {
                count.incrementAndGet();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                count.incrementAndGet();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count.get());
    }
}

Doug Lea 在CAS的基础上帮助我们实现了一些原子类,其中就包括看的AtomicInteger,还有其他很多原子类

CAS的缺点:CAS只能保证一个变量的操作是原子性的,无法实现对多行代码实现原子性

CAS的问题:

  • ABA问题:问题如下,可以引入版本号的方式,来解决ABA的问题,Java中提供了一个类在CAS时,针对版本追加版本号的操作.AtomicStampedReference

    image.png

  • AtomicStampedReference在CAS时,不但会判断原值,还会比较版本信息

    public class CASVersion {
        public static void main(String[] args) {
            AtomicStampedReference<String> reference = new AtomicStampedReference<>("A", 1);
            String value = reference.getReference();
            int stamp = reference.getStamp();
            System.out.println("v-1->value:" + value + ",version:" + stamp);
            boolean b = reference.compareAndSet(value, "B", stamp, ++stamp);
            System.out.println("v-2->yes or no : " + b + ",value:" + reference.getReference() + ",version:" + reference.getStamp());
            // 错误版本号示范
            boolean b1 = reference.compareAndSet("B", "C", 1, ++stamp);
            System.out.println("v-3->yes or no : " + b1 + ",value:" + reference.getReference() + ",version:" + reference.getStamp());
        }
    }
    
  • 自选时间过长问题:

    • 可以指定CAS一共循环多少次,如果超过这个次数,直接失败/或者挂起线程(自旋锁、自适应锁)
    • 可以在CAS一次失败后,将这个操作暂存起来,后续需要获取结果时,将暂存的操作全部执行,在返回最终结果

1.2.3 Lock锁

Lock锁是在JDK1.5由Doug Lea研发的,他的性能相比synchronized在JDK1.5的时期,性能好了很多,但是在JDK1.6对synchronized优化之后,性能相差不大,但是如果涉及并发 比较多的时,推荐ReentrantLock,性能会更好

实现方式:

public class Lock {
    static java.util.concurrent.locks.Lock lock = new ReentrantLock();
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                for (int i = 0; i < 100; i++) {
                    count++;
                }
            } catch (Exception e) {
            } finally {
                lock.unlock();
            }
        });
        Thread t2 = new Thread(() -> {
            lock.lock();
            try {
                for (int i = 0; i < 100; i++) {
                    count++;
                }
            } catch (Exception e) {
            } finally {
                lock.unlock();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

ReentrantLock可以直接对比synchronized,在功能上来说,都是锁

但是ReentrantLock的功能性相比synchronized更丰富

1.2.4ThreadLocal

Java中的四种引用类型

Java中的使用引用类型分别是强,软,弱,虚

Object obj = new Object();

在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用,当一个对象被强引用变量引用时,他始终处于可达状态,他是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收,因此强引用是造成Java内存泄露的主要原因之一

SoftReference是软引用,对于只有软件引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收,软引用通常用在对内存敏感的程序中,作为缓存使用。

弱引用,他比软件引用生命周期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,总会回收该对象占用的内存,可以解决内存泄漏问题,ThreadLocal就是基于弱引用解决内存泄漏的问题

虚引用,他不能单独使用,必须和引用队列联合使用,虚引用的主要作用是跟踪对象被垃圾回收的状态,不过在开发中,我们用的更多的还是强引用

ThreadLocal保证原子性的方式,是不让多线程去操作临界资源,让每个线程去操作属于自己的数据

代码实现:

public class IThreadLocal {
    static ThreadLocal tl1 = new ThreadLocal();
    static ThreadLocal tl2 = new ThreadLocal();

    public static void main(String[] args) {
        tl1.set("main->123");
        tl2.set("main->456");
        Thread t = new Thread(() -> {
            // 是获取不到主线设置的数据的
            System.out.println("线程" + Thread.currentThread().getName() + ",获取tl1数据:" + tl1.get());
            System.out.println("线程" + Thread.currentThread().getName() + ",获取tl2数据:" + tl2.get());
        });
        t.start();
        System.out.println("线程" + Thread.currentThread().getName() + ",获取tl1数据:" + tl1.get());
        System.out.println("线程" + Thread.currentThread().getName() + ",获取tl1数据:" + tl2.get());
    }
}

TheadLocal实现原理:

  • 每个Thread中都存储着一个成员变量,ThreadLocalMap
  • ThreadLocal本身不存书数据,像是一个工具类,基于ThreadLocal去操作ThreadLocalMap
  • ThreadLocalMap本身就是基于Entry[]实现,因为一个线程可以绑定多个ThreadLocal,这样一来,可能需要存储多个数据,所以采用Entry[]的形式实现。
  • 每一个线程都是自己独立的ThreadLocalMap,在基于ThreadLocal对象本身作为key,对value进行存取
  • ThreadLocalMap的key是一个弱引用,弱引用的特点是,即便有弱引用,在GC时,也必须回收,这里是为了在ThreadLocal对象失去引用后,如果key的引用是强引用,会导致ThreadLocal对象无法被回收

ThreadLocal内存泄漏问题:

  • 如果ThreadLocal引用丢,key因为弱引用会被GC回收掉,如果同时线程还没有被回收,就会导致内存泄漏,内存中的value无法被回收,同时也无法获取到。

  • 只需要在使用完毕ThreadLocal对象之后,及时的调用remove方法,移除Entry即可

    image.png

二、可见性

2.1 什么是可见性

可见性问题是基于CPU位置出现的,CPU处理速度非常快,相对CPU来说,去主内存获取数据这件事情太慢了,CPU提供了L1,L2,L3的三级缓存, 每次去主内存拿完数据后,就会存储到CPU的三级缓存,每次去三级缓存拿数据,效率肯定会提升

这就来了问题,现在CPU都是多核,每个线程的工作内存(CPU三级缓存)都是独立的,会告知每个线程中做修改时,只改了自己的内存,没有及时的同步到主内存,导致数据不一致问题。

image.png

可见性问题的代码逻辑

public class VisibilityCASE {
    static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (flag) {
  
            }
            System.out.println(Thread.currentThread().getName() + "线程结束");
        });
        thread.start();
        Thread.sleep(100);
        flag = false;
        System.out.println();
        System.out.println(Thread.currentThread().getName() + "线程修改 flag 为 false");
        // 此时你会发现 thread线程还是没有结束
        // 说明可见性问题存在,thread拿的自己缓存数据flag还是true
    }
}

2.2 解决可见性的方式

2.2.1 volatile

volatile是一个关键字,用来修饰成员变量。

如果属性被volatile修饰,相当于会告诉CPU,对当前属性的操作,不允许使用CPU的缓存,必须去和主内存操作

volatile的内存语义:

  • volatile属性被写:当写一个volatile变量,JMM会将当前线程对应的CPU缓存即使的刷新到主内存中
  • volatile属性被读:当读一个volatile变量,JMM会将对应的CPU缓存中的数据设置为无效,必须去主内存中重新读取新的数据

其实加了volatile就是告知CPU,对当前属性的读写操作,不允许使用CPU缓存,加了volatile修饰的属性,会在转为汇编之后,追加一个lock的前缀,CPU执行这个指令时,如果带有lock前缀会做两个事情:

  • 将当前处理器缓存行的数据写回到主内存
  • 这个写回的数据,在其他的CPU内核的缓存中,直接无效

总结:volatile就是让CPU每次操作这个数据时,必须立即同步到主内存,以及从主内存读取数据

public class Volatile {

    volatile static boolean flag = true;

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

            }
            System.out.println(Thread.currentThread().getName() + "线程结束");
        });
        thread.start();
        Thread.sleep(100);
        flag = false;
    }
}

2.2.2 synchronized

synchronized也可以解决可见性问题的,synchronized的内存原语。

如果涉及到了synchronized的同步代码块或者是同步方法,获取锁资源之后,将内部设涉及到的变量从CPU缓存中移除,必须去主内存中重新拿数据,而且在释放锁之后,会立即将CPU缓存中数据同步到主内存

public class Sync {
    static boolean flag = true;

    public static void main (String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (flag) {
                synchronized (Sync.class) {
                }
            }
            System.out.println(Thread.currentThread().getName() + "线程结束");
        });
        thread.start();
        Thread.sleep(100);
        flag = false;
    }
}

2.2.3 lock

Lock锁保证可见性的方式和synchronized完全不同,synchronized基于他的内存原语,在获取锁和释放锁时,对CPU缓存做一个同步到主内存的操作。

Lock锁是基于volatile实现的,Lock锁内部咋进行加锁和释放锁时,会对一个由volatile修饰的state属性进行加减操作。

如果对volatile修饰的属性进行写操作,CPU会执行带有lock前缀的指令,CPU会将修改的数据,从CPU缓存立即同步到主内存,同时也会将其他的属性也立即同步到主内存中,还会将其他CPU缓存行中的这个数据设置为无效,必须重新从主内存中拉去

public class ILock {

    static boolean flag = true;
    static Lock lock = new ReentrantLock();

    public static void main (String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (flag) {
                lock.lock();
                try {
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    lock.unlock();
                }
            }
            System.out.println(Thread.currentThread().getName() + "线程结束");
        });
        thread.start();
        Thread.sleep(100);
        flag = false;
    }
}

2.2.4 final

final修饰的属性,在运行期间是不允许修改的,这样一来,就间接的保证了可见性,所以多线程读取final属性,值肯定一样

final并不是说每次取数据从主内存读取,他没有这个必要,而且final和volatile是不允许同时修饰一个属性的

final修饰的内容已经不允许在次被写了,而volatile是保证每次读取数据取主内存中去读,并且volatile会影响一定的性能,就不需要同时修饰

image.png

三、有序性

3.1 什么是有序性

在Java中,.java文件中的内容会被编译,在执行前需要再次转为CPU可以识别的指令,CPU在执行这些指令时,为了提升执行效率,在不影响最终结果的前提下(满足一些要求),会对指令进行重排

指令乱序执行的原因,是为了尽可能的发挥CPU的性能。

Java中的程序是乱序执行的

Java程序验证乱序执行的效果

public class Example {
    static int a, b, x, y;

    public static void main (String[] args) throws InterruptedException {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            a = 0;
            b = 0;
            x = 0;
            y = 0;
            Thread t1 = new Thread(() -> {
                a = 1;
                x = b;
            });
            Thread t2 = new Thread(() -> {
                b = 1;
                y = a;
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            if (x == 0 && y == 0) {
                System.out.println("第" + i + "次,x = "+ x + ",y = " + y);
            }
        }
    }
}

单例模式由于指令重排序可能会出现问题:

线程可能会拿到没有初始化的对象,导致在使用时,可能由于内部属性为默认值,导致出现一些不必要的问题

public class Single {

    private static volatile Single INSTANCE;

    private Single () {
    }

    public static Single getInstance () {
        if (INSTANCE == null) {
            synchronized (Single.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Single();
                }
            }
        }
        return INSTANCE;
    }
}

3.2 as-if-serial

as-if-serial语义:

不论指定如何重排序,需要保证单线程的程序执行结果是不变的

而且如果存在依赖的关系,那么也不可以做指令重排

 // 这种情况肯定不能出现指令重排
 int i = 0;
 i++;
 int j = 200;
 j = j * 100;
 j = j + 100;

3.3 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中情况,才不会触发指令重排序效果

3.4 volatile

如果需要让程序对某一个属性不出现指令重排,除了满足happens-before原则之外,还可以基于volatile修饰属性,从而对这个属性的操作,就不会出现指令重排的问题了

volatile如果实现禁止指令重排?

内存屏障概念,将内存屏障看成一条指令

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

  • 20
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值