并发编程与高并发学习笔记一

一,线程安全性
1.定义:
当多个线程访问某个类时,不管运行时环境采用 任何调度方式 或者这些进程将如何交替执行,并且在主调代码中
不需要任何额外的同步或协同,这个类都能表现出 正确的行为,那么称这个类是线程安全的
2.线程安全性体现在三个方面:
原子性:提供了互斥访问,同一时刻只能有一个线程来对他操作
可见性:一个线程对主内存的修改可以及时被其他线程观察到
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序
3.并发模拟代码:
//并发模拟代码
public class CountExample {
    //请求总数
    public static int clientTotal = 5000;
    //同时并发执行的线程数
    public static int threadTotal = 200;
    //全局变量
    public static int count = 0;
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        //信号灯,同时允许执行的线程数
        final Semaphore semaphore = new Semaphore(threadTotal);
        //计数器,
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);

        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(()->{
                try {
                    //获取信号灯,当并发达到一定数量后,该方法会阻塞而不能向下执行
                    semaphore.acquire();
                    add();
                    //释放信号灯
                    semaphore.release();
                }catch (InterruptedException e){
                    System.out.println("exception");
                    e.printStackTrace();
                }
                //闭锁,每执行一次add()操作,请求数就减一
                countDownLatch.countDown();
            });
        }

        //等待上面的线程都执行完毕,countDown的值减为0,然后才向下执行主线程
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //打印count的值
        System.out.println("count:"+count);

        //关闭线程池
        executorService.shutdown();

    }

    private static void add(){
        count++;
    }
}



.原子性-Atomic包
1.AtomicInteger类中提供了incrementAndGet方法;
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
2.incrementAndGet方法又调用了Unsafe类的getAndAddInt方法
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;
}
3.getAndAddInt方法又是如何保证原子性的呢?该方法调用了compareAndSwapInt方法(就是我们说的CAS)
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
compareAndSwapInt方法是native方法,这个方法是java底层的方法(不是通过java实现的)
4.原理解析:

public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;
}
Object var1:传进来的AtomicInteger对象
long var2:是传进来的值,当前要进行加一的值 (比如要进行2+1的操作, var2就是2)
int var4:是传进来的值,进行自增要加上的值 (比如要进行2+1的操作, var4就是1)
int var5:是通过调用底层的方法this.getIntVolatile(var1, var2);得到的底层当前的值
while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)):
通过do{} while()不停的将当前对象的传进来的值和底层的值进行比较,
如果相同就将底层的值更新为:var5+var4(加一的操作),
如果不相同,就重新再从底层取一次值,然后再进行比较,这就是CAS的核心。
帮助理解:
AtomicInteger里面存的值看成是工作内存中的值
把底层的值看成是主内存中的值。在多线程中,工作内存中的值和主内存中的值会出现不一样的情况。

线程安全的代码:
//线程安全的并发
public class CountExample2 {
    //请求总数
    public static int clientTotal = 5000;
    //同时并发执行的线程数
    public static int threadTotal = 200;
    //全局变量
    public static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        //信号灯,同时允许执行的线程数
        final Semaphore semaphore = new Semaphore(threadTotal);
        //计数器,
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(()->{
                try {
                    //获取信号灯,当并发达到一定数量后,该方法会阻塞而不能向下执行
                    semaphore.acquire();
                    add();
                    //释放信号灯
                    semaphore.release();
                }catch (InterruptedException e){
                    System.out.println("exception");
                    e.printStackTrace();
                }
                //闭锁,每执行一次add()操作,请求数就减一
                countDownLatch.countDown();
            });
        }

        //等待上面的线程都执行完毕,countDown的值减为0,然后才向下执行主线程
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //打印count的值
        System.out.println("count:"+count.get());
        //关闭线程池
        executorService.shutdown();
    }
    private static void add(){
        //count++;
        count.incrementAndGet();
    }
}
 
5.还有AotimcLong和LongAddr
他俩的区别没听懂???????

6.CAS中的ABA问题
描述:在CAS操作时,其他线程将变量的值从A改成了B,然后又将B改回了A。
解决思路:每次变量改变时,将变量的版本号加1,只要变量被修改过,变量的版本号就会发生递增变化
使用的类:AtomicStampedReference,
调用compareAndSet方法:
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
stamp是每次更新时就维护的, 通过对比来判断是不是一个版本号,expectedStamp == current.stamp

7.AtomicBoolean
代码:
//让某一段代码只执行一次
public class CountExample3 {
    //请求总数
    public static int clientTotal = 5000;
    //同时并发执行的线程数
    public static int threadTotal = 200;
    public static AtomicBoolean isHappened = new AtomicBoolean(false);
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        //信号灯,同时允许执行的线程数
        final Semaphore semaphore = new Semaphore(threadTotal);
        //计数器,
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(()->{
                try {
                    //获取信号灯,当并发达到一定数量后,该方法会阻塞而不能向下执行
                    semaphore.acquire();
                    test();
                    //释放信号灯
                    semaphore.release();
                }catch (InterruptedException e){
                    System.out.println("exception");
                    e.printStackTrace();
                }
                //闭锁,每执行一次add()操作,请求数就减一
                countDownLatch.countDown();
            });
        }

        //等待上面的线程都执行完毕,countDown的值减为0,然后才向下执行主线程
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //打印count的值
        System.out.println("isHappened:"+isHappened.get());
        //关闭线程池
        executorService.shutdown();
    }

    private static void test(){
        //如果是false,就更新为true
       if (isHappened.compareAndSet(false,true)){
           System.out.println("execute");
       }
    }
}

.原子性-锁
1.synchronized:依赖JVM
(1).synchronized修饰的对象有四种:
修饰代码块:作用范围是大括号括起来的代码,作用于调用的对象
修饰方法:作用范围是整个方法,作用于调用的对象
修饰静态方法:作用范围是整个静态的方法,作用于这个类的所有对象
修饰类:作用范围是synchronized括号括起来的部分,作用于这个类的所有对象

2.Lock:依赖特殊的cpu指令,代码实现,ReentrantLock

3.原子性-对比
synchronized:不可中断锁,适合竞争不激烈,可读性好.(竞争激烈时性能下降非常快)
Lock:可中断锁(调用unlock即可),多样化同步,竞争激烈时能维持常态
Atomic:竞争激烈时呢能维持常态,比Lock性能好,只能同步一个值

.可见性
1.定义:一个线程对主内存的修改可以及时被其他线程观察到
2.导致共享变量在线程间不可见的原因:
线程交叉执行
重排序结合交叉执行
共享变量更新后的值没有在工作内存与主内存之间及时更新
3.可见性-synchronized
java内存模型(JMM)关于synchronized的两条规定:
A.线程解锁前,必须把共享变量的最新值刷新到主内存
B.线程加锁时,将清空工作内存中的共享变量的值,从而在使用共享变量时,需要从主内存中重新读取最新的值(注意:
加锁和解锁是同一把锁)
4.可见性-volatile
(1).通过加入内存屏障和禁止重排序优化来实现
A.对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存
B.对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量
通俗地说就是,volatile变量每次被线程访问时,都强迫从主内存读取该变量的值,而当该变量发生变化时,又会强迫线程
将最新的值刷新到主内存,这样,任何时候不同的线程总能看到该变量的最新值
(2).内存屏障是如何禁止重排序的呢?
图示:
(3).注意:volatile可不具有原子性,用它修饰变量是没有用的
比如:public static volatile int count = 0;这样的变量虽然用volatile修饰了,但是并不是线程安全的
//volatile并不保证原子性
public class VolatileExample {

    //请求总数
    public static int clientTotal = 5000;
    //同时并发执行的线程数
    public static int threadTotal = 200;
    //全局变量
    public static volatile int count = 0;

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        //信号灯,同时允许执行的线程数
        final Semaphore semaphore = new Semaphore(threadTotal);
        //计数器,
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);

        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(()->{
                try {
                    //获取信号灯,当并发达到一定数量后,该方法会阻塞而不能向下执行
                    semaphore.acquire();
                    add();
                    //释放信号灯
                    semaphore.release();
                }catch (InterruptedException e){
                    System.out.println("exception");
                    e.printStackTrace();
                }
                //闭锁,每执行一次add()操作,请求数就减一
                countDownLatch.countDown();
            });
        }

        //等待上面的线程都执行完毕,countDown的值减为0,然后才向下执行主线程
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //打印count的值
        System.out.println("count:"+count);

        //关闭线程池
        executorService.shutdown();

    }

    private static void add(){
        count++;
        /*
        count++操作其实有三步:
            获取count
            +1操作
            将count刷新到主内存
        当有多个线程同时执行count++时,因为上面有三步,所以保证不了线程的安全性
         */
    }
}



(4).volatile适用场景一:
使用volatile必须具备两个条件:
A.对变量的写操作,不依赖于当前值
B.该变量没有包含在具有其他变量的式子中
所以,volatile特别适合作为状态标记量
例子:
volatile boolean inited = false; //inited用来标识线程初始化是否完成
//线程一: 线程一负责初始化
context = loadContext(); //初始化操作
init = true;//初始化完成后修改状态

//线程二: 线程二必须保证初始化完成才能执行
while(!inited){//所以线程二不断的判断是否为inited是否true,只有为true时,线程二才开始执行
sleep();
}
doSomethingWithConfig(context);
(5).volatile适用场景二:
用来双重检测,单例模式中的双重检测机制。
.有序性
1.定义:
java内存模型中,允许编译器和处理器对指令进行重排序,重排序过程是不会影响到单线程程序的执行的,但是会影响到
多线程并发执行的正确性
2.保证有序性的方式有哪些?
volatile,synchronized,Lock
3.happens-before原则 (即先行发生原则),共有8个规则:
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作
volatile规则L:对一个变量的写操作先行发生于后面对这个变量的读操作
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
后四个是显而易见的,重点是前四个
线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
线程中规则:对线程interrupt()方法的调用先行发生于 被中断线程的代码检测到 中断事件的发生
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束,
Thread.isAlive()的返回值手段检测到线程已经终止执行
对象终结规则:一个对象的初始化完成先发生于他的finalize()方法的开始

所以:如果两个操作的执行顺序无法通过happen-before的8个原则推断出来,那么就不能保证他们的有序性。
虚拟机就可以随意的对他们进行重排序

 
原文地址:https://www.cnblogs.com/inspred/p/9520805.html