并发编程8

synchronized关键字的使用和优化

synchronized锁消除优化

  • package BingFaBianCheng.bingFaBianCheng8.shadow.test;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.TimeUnit;
    
    @Slf4j(topic = "enjoy")
    public class Test7 {
    
        int i=0;
    
        public static   void main(String[] args) throws InterruptedException {
    
        }
    
    
        public  void a(){
            i++;
        }
    
    
        public  void b(){
            Object o = new Object();
            synchronized (o) {
                i++;
            }
        }
    }
    
    
  • a()方法和b()方法执行效率哪个高?

  • 怎么证明哪个效率高?— jmh技术

    • 可重复、可观测,执行10次,取平均结果

创建jmh项目

  • 下面的命令会创建一个maven项目

  • mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jmh -DarchetypeArtifactId=jmh-java-benchmark-archetype -DgroupId=com.enjoy.jmh -DartifactId=zl -Dversion=1.0.0-SNAPSHOT -DarchetypeCatalog=local
    
  • 会报错—searching for remote catalog,下载不下来一个文件,需要手动下载

  • 并把它放到你的maven仓库中— archetype-catalog.xml

  • 再在上面命令的基础上加一条-DarchetypeCatalog=local 表示用本地的,不用在线下载

利用创建的jmh项目测试上面两个方法

  • 需要加一个pom引用----javax.annotation

  • @Fork(1)// 只fork一个线程
    @BenchmarkMode(Mode.AverageTime)// 平均时间
    @Warmup(iterations=3)// 预热
    @Measurement(iterations=5)// 一共执行多少次
    @OutputTimeUnit(TimeUnit.NANOSECONDS)// 时间单位
    public class MyBenchmark {
    
    	static int i =1;
    	
        @Benchmark
    	public  void a(){
            i++;
        }
    
        // 这两个方法时间差不多
        // 因为jit对代码做了优化,锁消除
        // 因为这是一个局部对象,这个对象没有逃出这个方法,加锁没有任何意义
    	@Benchmark
        public  void b(){
            Object o = new Object();
            synchronized (o) {
                i++;
            }
        }
    }
    
  • 利用idea中maven下的package方法直接打包

  • 在target下生成了两个jar包,一个项目自己的,一个benchmarks.jar

  • 然后执行java -jar benchmarks.jar

  • 运行a方法执行时间和运行b方法执行时间是差不多的,都是2.6纳秒左右

  • jit(即时编译器)

    • i++和带有synchronized的i++方法(锁是一个局部对象),用jmh测试出来效率基本一样
    • 分析
      • i++这个方法一直在锁里面,
    • 锁消除:以局部变量作为锁对象,jit会认为这个synchronized没有用,会去掉

测试锁消除

  • @Fork(1)// 只fork一个线程
    @BenchmarkMode(Mode.AverageTime)// 平均时间
    @Warmup(iterations=3)// 预热
    @Measurement(iterations=5)// 一共执行多少次
    @OutputTimeUnit(TimeUnit.NANOSECONDS)// 时间单位
    public class MyBenchmark {
    
    	static int i =1;
    	
        @Benchmark
    	public  void a(){
            i++;
        }
    
        
        static Object o = new Object();
        
    	@Benchmark
        public  void b(){
            
            synchronized (o) {
                i++;
            }
        }
    }
    
  • 此时a方法执行时间还是2.6纳秒,b方法执行时间是31纳秒,差别在10倍以上

  • 锁粗化很难证明

线程切换问题

  • @Slf4j(topic = "enjoy")
    public class BasicLock {
    
        public synchronized void x(){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("x");
        }
    
    
        public synchronized void y(){
            log.debug("y");
        }
    }
    
  • /**
     * 1、等1s  打印x  打印y
     * 2、先打印y  等1s  x
     */
    @Slf4j(topic = "enjoy")
    public class TestBasic {
        public static void main(String[] args) {
            BasicLock basicLock = new BasicLock();
    
    
            new Thread(()->{
                log.debug("start");
                basicLock.x();
            },"t1").start();
    
            new Thread(()->{
                log.debug("start");
                basicLock.y();
            },"t2").start();
        }
    }
    
  • 虽然x方法睡眠了一秒钟,但是x方法和y方法到底谁先执行是随机的

静态方法和普通方法的两把锁测试

  • @Slf4j(topic = "enjoy")
    public class BasicLock1 {
    
        public synchronized static void x(){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("x");
        }
    
    
        public synchronized void y(){
            log.debug("y");
        }
    
        public void z(){
            log.debug("z");
        }
    }
    
  • /**
     * 永远y之前
     */
    @Slf4j(topic = "enjoy")
    public class TestBasic3 {
        public static void main(String[] args) {
            BasicLock basicLock = new BasicLock();
            BasicLock basicLock1 = new BasicLock();
    
            new Thread(()->{
                log.debug("start");
                basicLock.x();
            },"t1").start();
    
            new Thread(()->{
                log.debug("start");
                basicLock1.y();
            },"t2").start();
    
    
        }
    }
    
  • 这是两把不互斥的锁,所以不会互相影响,而x又会睡眠一秒钟,所以百分之百是y方法先执行

同一个对象,但是方法一个是静态方法,一个是普通方法

  • import lombok.extern.slf4j.Slf4j;
    
    @Slf4j(topic = "enjoy")
    public class TestBasic4 {
        public static void main(String[] args) {
            BasicLock1 basicLock1 = new BasicLock1();
    
    
            new Thread(()->{
                log.debug("start");
    
                basicLock1.x();
            },"t1").start();
    
            new Thread(()->{
                log.debug("start");
    
                basicLock1.y();
            },"t2").start();
    
    
        }
    }
    
  • x锁的是BsicLock1.class类对象

  • y锁的是this,实例对象

  • 对象不对,synchronized锁不同的对象,也是彼此互不影响

synchronized基本使用

demo1

锁住实例
  • package com.shadow.demo1;
    
    import lombok.extern.slf4j.Slf4j;
    
    /**
     * synchronized关键字
     * synchronized关键字锁定的是对象不是代码块,demo中锁的是object对象的实例
     * 锁定的对象有两种:1.类的实例 2.类对象(类锁)
     * 加synchronized关键字之后不一定能实现线程安全,具体还要看锁定的对象是否唯一。
     */
    @Slf4j(topic = "enjoy")
    public class Demo1 {
    
        private int count = 10;
        private Object object = new Object();
    
        public void test(){
            synchronized (object){
                count--;
                log.debug(Thread.currentThread().getName() + " count = " + count);
            }
        }
    
    }
    
  • 实际锁住的是object里面的对象头

锁住实例2
  • package com.shadow.demo1;
    
    import lombok.extern.slf4j.Slf4j;
    
    @Slf4j(topic = "enjoy")
    public class Demo2 {
    
        private int count = 10;
    
        public  void test(){
            //synchronized(this)锁定的是当前类的实例,这里锁定的是Demo2类的实例
            synchronized (this){
                count--;
                log.debug(Thread.currentThread().getName() + " count = " + count);
            }
        }
    
    }
    
    
  • 此时锁住的是demo2实例的对象头

锁住方法
  • package com.shadow.demo1;
    
    import lombok.extern.slf4j.Slf4j;
    
    @Slf4j(topic = "enjoy")
    public class Demo3 {
    
        private int count = 10;
    
        //直接加在方法声明上,相当于是synchronized(this)
        public synchronized void test(){
            count--;
            log.debug(Thread.currentThread().getName() + " count = " + count);
        }
    
    }
    
    
  • 相当于锁住this

锁住静态方法
  • package com.shadow.demo1;
    
    import lombok.extern.slf4j.Slf4j;
    
    @Slf4j(topic = "enjoy")
    public class Demo4 {
    
        private static int count = 10;
    
        //synchronize关键字修饰静态方法锁定的是类的对象
        public synchronized static void test(){
            count--;
            log.debug(Thread.currentThread().getName() + " count = " + count);
        }
    
        public static void test2(){
            synchronized (Demo4.class){//这里不能替换成this
                count--;
            }
        }
    
    }
    
    
  • 静态方法不能锁住this,一定要锁住静态对象Demo4.class

demo2

死循环不释放锁,导致其他线程无法执行
  • package BingFaBianCheng.bingFaBianCheng8.shadow.demo2;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * 锁对象的改变
     * 锁定某对象o,如果o的属性发生改变,不影响锁的使用
     * 但是如果o变成另外一个对象,则锁定的对象发生改变
     * 应该避免将锁定对象的引用变成另外一个对象
     */
    @Slf4j(topic = "enjoy")
    public class Demo1 {
    
        Object o = new Object();
    
        public void test(){
            synchronized (o) {
                //t1 在这里无线执行
                while (true) {
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.debug(Thread.currentThread().getName());
                }
            }
        }
    
        public static void main(String[] args) {
            Demo1 demo = new Demo1();
    
            new Thread(demo :: test, "t1").start();
    
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            Thread t2 = new Thread(demo :: test, "t2");
    
            //demo.o = new Object();
            //t2能否执行?
            t2.start();
        }
    
    }
    
    
  • t2不能执行

  • 但是如果更改了锁对象,此时t2也能获取到锁,因为t1一直在死循环,而相当于换了一把新锁,就锁已经失效了,所以此时t2可以获取到锁

  • 因为不要轻易替换锁对象

字符串作为锁定的对象
  • package BingFaBianCheng.bingFaBianCheng8.shadow.demo2;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * 不要以字符串常量作为锁定的对象
     *
     */
    @Slf4j(topic = "enjoy")
    public class Demo2 {
    
        String s1 = "hello";
    
        String s2 = "hello";
    
        public void test1(){
            synchronized (s1) {
               log.debug("t1 start...");
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("t1 end...");
            }
        }
    
        public void test2(){
            synchronized (s2) {
                log.debug("t2 start...");
            }
        }
    
        public static void main(String[] args) {
            Demo2 demo = new Demo2();
            //启动t1
            new Thread(demo :: test1,"t1").start();
            //启动t2
            new Thread(demo :: test2,"t2").start();
        }
    
    }
    
    
  • 虽然s1和s2是两个不同的引用,实际上s1和s2是常量池里同一个对象,所以是同一把锁

  • 所以尽量不要用字符串作为锁

锁的粒度
  • package BingFaBianCheng.bingFaBianCheng8.shadow.demo2;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * 同步代码快中的语句越少越好
     * 比较test1和test2
     * 业务逻辑中只有count++这句需要sync,这时不应该给整个方法上锁
     * 采用细粒度的锁,可以使线程争用时间变短,从而提高效率
     */
    @Slf4j(topic = "enjoy")
    public class Demo3 {
    
        int count = 0;
    
        public synchronized void test1(){
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            count ++;
    
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
    
        /**
         * 局部加锁
         */
        public void test2(){
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            synchronized (this) {
                count ++;
            }
    
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
    }
    
    
  • 没有必要对整个代码加锁,只应该对读写区域进行加锁

  • synchronized只对有io操作的部分加持才有,比如锁住Thread.sleep方法没有任何影响,控制锁的粒度,提高效率

demo3
多线程非同步状态造成计算错误— count–
  • package BingFaBianCheng.bingFaBianCheng8.shadow.demo3;
    
    import lombok.SneakyThrows;
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * 第一个线程减了个1变成9了,还没打印,第二个线程又减了个1,第三个线程又减了个1,
     * 这时候虽然第一个线程只减了一个1但是却打印出来一个7(这里情况是不一定的)
     * 可以给方法加上synchronized
     */
    @Slf4j(topic = "enjoy")
    public class Demo1 implements Runnable{
    
        private int count = 10;
    
        @SneakyThrows
        @Override
        public synchronized void run() {
            //TimeUnit.SECONDS.sleep(1);
            /**
             * get
             * --
             * set
             */
            count--;
            log.debug(Thread.currentThread().getName() + " count = " + count);
        }
    
    
        public static void main(String[] args) {
            Demo1 demo = new Demo1();
            for (int i = 0; i < 5; i++) {
                new Thread(demo,"t" + i).start();
            }
        }
    
    }
    
    
  • 因为count–不是一个原子性操作,造成计算错误

锁不同的对象,同步锁没有起作用
  • package BingFaBianCheng.bingFaBianCheng8.shadow.demo3;
    
    import lombok.extern.slf4j.Slf4j;
    
    @Slf4j(topic = "enjoy")
    public class Demo2 implements Runnable{
    
        private int count = 10;
    
        @Override
        public synchronized void run() {
            count--;
            log.debug(Thread.currentThread().getName() + " count = " + count);
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 5; i++) {
                //相比较Demo1,这里是new了五个对象,每个线程对应都拿到各自的锁标记,可以同时执行。
                Demo2 demo = new Demo2();
                new Thread(demo,"t" + i).start();
            }
        }
    
    }
    
    
  • 每一个线程都有去获取锁,但是获取的是不同的锁,所以没有起到线程隔离的作用

demo4

同步方法可不可以调用非同步方法
  • package BingFaBianCheng.bingFaBianCheng8.shadow.demo4;
    
    import lombok.extern.slf4j.Slf4j;
    
    //同步方法和非同步方法是否可以同时调用? 可以
    @Slf4j(topic = "enjoy")
    public class Demo{
    
    
    
    
        public synchronized void test1(){
            log.debug(Thread.currentThread().getName() + " test1 start...");
            try {
                //睡眠5s 由于还要t2要执行 cpu回去执行t2
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug(Thread.currentThread().getName() + " test1 end...");
        }
    
        public void test2(){
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " test2");
        }
    
        public static void main(String[] args) {
            Demo demo = new Demo();
            //正在执行一个同步方法  没有释放锁
            new Thread(demo :: test1,"t1").start();
            //不影响其他线程执行非同步方法(就算他是一个同步方法,如果锁的不是同一个对象也不影响)
            new Thread(demo :: test2,"t2").start();
        }
    
    }
    
  • 执行同一个类中的同步方法不会影响同一类中的非同步方法,即一个线程执行同步方法了,又来一个线程执行同一类中的非同步方法,后一个线程不会因为前一个线程而阻塞,是会同时并发执行的

  • 实质上还是因为不是同一把锁,非同步方法都没有锁,所以就更不是同一把锁了

demo5

脏读问题
  • package BingFaBianCheng.bingFaBianCheng8.shadow.demo5;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * 脏读问题
     * 实际业务当中应该看是否允许脏读,
     * 不允许的情况下对读方法也要加锁
     */
    @Slf4j(topic = "enjoy")
    public class Demo {
    
        //卡的持有人
        String name;
    
        //卡上的余额
        double balance;
    
        /**
         *
         * @param name
         * @param balance
         */
        public synchronized void set(String name,double balance){
            this.name = name;
            try {
                // 模拟存钱耗时
                // 操作数据库,服务调用等等...
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.balance = balance;
        }
    
        public synchronized double getBalance(String name){
            return this.balance;
        }
    
    
        public static void main(String[] args) {
            Demo demo = new Demo();
    
            //2s
            new Thread(()->demo.set("zl",100.0)).start();
    
            try {
                // 睡眠1秒钟
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //1s之后 结果 0
            // 此时还有存完钱
            log.debug(demo.getBalance("zl")+"");//
    
    
    
    
    
            try {
                // 睡眠2秒钟
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //3s之后就算100
            // 此时已经存完钱了
            log.debug(demo.getBalance("zl")+"");
        }
    
    }
    
    
  • 一个人正在存,一个人正在取,会产生脏读问题,读取到了中间结果

  • 项目是否允许脏读问题? — 架构师决定

  • 如果不允许脏读,读取余额的时候也加锁,此时不是立马返回结果的,会转菊花,所以读是否加锁,需要看实际项目中是否允许脏读

demo6

锁的重入
  • package BingFaBianCheng.bingFaBianCheng8.shadow.demo6;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.TimeUnit;
    
    //一个同步方法调用另外一个同步方法,能否得到锁?
    //重入  synchronized默认支持重入
    @Slf4j(topic = "enjoy")
    public class Demo {
    
        synchronized void test1(){
            log.debug("test1 start.........");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            test2();
        }
    
        /**
         * 为什么test2还需要加sync
         *
         * 他本身就包含在test1 而test1已经加了sync
         */
        synchronized void test2(){
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("test2 start.......");
        }
    
    
    
        public static void main(String[] args) {
            Demo demo= new Demo();
            demo.test1();
        }
    
    }
    
  • 为什么test2还要加synchronized? t2本身就包含在t1中,t1又加了synchronized

  • 为了防止别的方法直接调用t2,而synchronized本身是支持锁重入的

demo7

锁重入的另一种情况—继承
  • package BingFaBianCheng.bingFaBianCheng8.shadow.demo7;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.TimeUnit;
    
    //这里是重入锁的另外一种情况,继承
    @Slf4j(topic = "enjoy")
    public class Demo {
    
        synchronized void test(){
            log.debug("demo test start........");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("demo test end........");
        }
    
        public static void main(String[] args) {
                new Demo2().test();
        }
    
    }
    @Slf4j(topic = "enjoy")
    class Demo2 extends Demo {
    
        @Override
        synchronized void test(){
            log.debug("demo2 test start........");
            super.test();
            log.debug("demo2 test end........");
        }
    
    }
    
  • 子类重写的父类同步方法,又再次调用父类的同步方法,是可以调用的,说明父类和子类用的是同一个锁对象,也算是一种重入

demo8

synchronized里面发生异常
  • package BingFaBianCheng.bingFaBianCheng8.shadow.demo8;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * synchronized 和异常的关系
     * T2线程能否执行?
     */
    @Slf4j(topic = "enjoy")
    public class Demo {
        Object o = new Object();
    
        int count = 0;
    
         void test(){
             synchronized(o) {
                 //t1进入并且启动
                 log.debug(Thread.currentThread().getName() + " start......");
                 //t1 会死循环 t1 讲道理不会释放锁
                 while (true) {
                     count++;
                     log.debug(Thread.currentThread().getName() + " count = " + count);
                     try {
                         TimeUnit.SECONDS.sleep(1);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                     //加5次之后 发生异常
                     /**
                      * 如果程序发生异常如果没有try 则会释放锁
                      * 反之不会释放锁
                      */
                     if (count == 5) {
                         int i = 1 / 0;
                     }
                 }
             }
        }
    
        public static void main(String[] args) {
            Demo demo11 = new Demo();
           // Runnable r = () -> demo11.test();
           // new Thread(r, "t1").start();
    
            new Thread(()->{
                demo11.test();
            },"t1").start();
    
    
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            new Thread(()->{
                demo11.test();
            }, "t2").start();
        }
    
    }
    
    
  • 线程t1中除法运算会发生异常,如果没有try-catch,会释放锁,此时t2获取到锁

  • 线程t1中除法运算发生异常,如果try-catch了,此时会一直死循环,不释放锁

demo9

并发编程中最主要的问题
  • package com.shadow.demo9;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * volatile 关键字,使一个变量在多个线程间可见
     * mian,t1线程都用到一个变量,java默认是T1线程中保留一份副本,这样如果main线程修改了该变量,
     * t1线程未必知道
     *
     * 使用volatile关键字,会让所有线程都会读到变量的修改值
     *
     * 在下面的代码中,running是存在于堆内存的t对象中
     * 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个副本,
     * 并不会每次都去读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行
     *
     *
     * 但是这可能是个错误
     * 关于这个例子  在后面会专门花时间再讲
     */
    @Slf4j(topic = "enjoy")
    public class Demo {
    
        boolean running = true;
        List<String> list = new ArrayList<>();
    
    
        /**
         * t1线程
         */
        public void test(){
            log.debug("test start...");
            boolean flag =running;
                while (running){
    
                }
            log.debug("test end...");
        }
    
        public static void main(String[] args) {
            Demo demo = new Demo();
    
            new Thread(demo :: test,"t1").start();
    
            try {
                // 主线程睡眠100毫秒,此时t1一定会被调用运行
                // t1一直在死循环空转
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            // 修改了死循环的中断条件
            // 但是仍然无法停止空转
            demo.running = false;
        }
    
    }
    
  • 是不是中间变量的可见性问题?

    • main线程私有栈、t1线程私有栈、共享变量字段running(存在堆里面)
  • 怎么终止死循环?

    • 1.在while死循环加一句打印(sout)(同步方法),死循环就会停止,

    • 2.定义一个全局变量list,然后在while死循环里面,list.add(xx),死循环也会停止

    • 3.在共享变量前面加volatile,死循环也会停止

      • volatile实际是禁止了指令重排序,跟可见性没有关系
  • 为什么去掉共享变量的volatile就不能终止死循环了?

    • jvm自身的激进优化

    • test方法运行在t1的线程中,在死循环中每次跨栈去拿running时,jvm发现while中什么都没有做,发现没有任何意义,会对这段代码进行优化,会新增一个临时变量,把共享变量赋值给临时变量,然后先判断这个临时变量,而不是跨栈去查共享变量(effective java这本书242页)(R大)

    • boolean flag =running;
      if(flag){
          while (running){
      
          }
      }     
      
    • java基本没有可见性问题,happens before原则已经规避了可见性问题

    • 这个问题的优化是在机器码这个级别解决(指令级别优化),不是在class文件级别

demo10

volatile不能解决原子性,只能保证可见性
  • package BingFaBianCheng.bingFaBianCheng8.shadow.demo10;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 比如说第一个线程加到100了,还没往上加,另外一个线程来了,把100拿过来执行方法,
     * 然后第一个线程继续加到101,第二个线程也加到101,他两往回写都是101,线程不会管你加到哪儿了,
     * 虽然说加了2但是实际上只加了1.
     * volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,
     * 也就是说volatile不能替代synchronized或者说volatile保证不了原子性
     */
    @Slf4j(topic = "enjoy")
    public class Demo {
    
        volatile int count = 0;
    
        public void test(){
            for (int i = 0; i < 10000; i++) {
                count ++;
            }
        }
    
    
        public static void main(String[] args) {
            Demo demo = new Demo();
    
            List<Thread> threads = new ArrayList();
    
            //new 10個线程
            for (int i = 0; i < 10; i++) {
                threads.add(new Thread(demo::test, "t-" + i));
            }
    
            //遍历这个10个线程  依次启动
            threads.forEach((o)->o.start());
    
            //等待10个线程执行完
            threads.forEach((o)->{
                try {
                    o.join();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
    
            log.debug(demo.count+"");
        }
    
    }
    
    
  • count++不是原子性操作,有3步操作,get 、++、set

  • volatile替代synchronized,synchronized可以同时保证可见性和原子性

demo11

synchronized既保证了原子性又保证了可见性
  • package BingFaBianCheng.bingFaBianCheng8.shadow.demo11;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.ArrayList;
    import java.util.List;
    
    @Slf4j(topic = "enjoy")
    public class Demo {
    
        int count = 0;
    
        //相比较上一个例子,synchronized既保证了原子性又保证了可见性
        public synchronized void test(){
            for (int i = 0; i < 10000; i++) {
                count ++;
            }
        }
    
        public static void main(String[] args) {
            Demo demo = new Demo();
    
            List<Thread> threads = new ArrayList<Thread>();
    
            for (int i = 0; i < 10; i++) {
                threads.add(new Thread(demo::test, "thread-" + i));
            }
    
            threads.forEach((o)->o.start());
    
            threads.forEach((o)->{
                try {
                    o.join();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
           log.debug(demo.count+"");
        }
    
    }
    
    

demo12

AtomicInteger
  • package BingFaBianCheng.bingFaBianCheng8.shadow.demo12;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * atomicXXX
     * 一道面试题:多个atomic类连续调用能否构成原子性?
     */
    @Slf4j(topic = "enjoy")
    public class Demo {
    
        AtomicInteger count = new AtomicInteger(0);
    
    
        public void test(){
            for (int i = 0; i < 10000; i++) {
                if(count.get() < 1000){
                    //count++
                    count.incrementAndGet();
                }
            }
        }
        public static void main(String[] args) {
            Demo demo = new Demo();
    
            List<Thread> threads = new ArrayList();
    
            for (int i = 0; i < 10; i++) {
                threads.add(new Thread(demo::test, "thread-" + i));
            }
    
            threads.forEach((o)->o.start());
    
            threads.forEach((o)->{
                try {
                    o.join();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
    
            log.debug(demo.count+"");
        }
    
    }
    
    
  • 单个atomic操作是原子性的

    • count.incrementAndGet(),是原子性的count++
  • 多个atomic操作不是原子性的

    • if(count.get() < 1000){
          //count++
          count.incrementAndGet();
      }
      
    • 此时计算结果还是可能有问题,因为还是有线程切换

  • 这种多原子操作的,只能通过加锁来解决

demo13

线程安全的list
  • package com.shadow.demo13;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 一道面试题:实现一个容器,提供两个方法,add,size
     * 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,
     * 当个数到5个时,线程2给出提示并结束线程2
     *
     * 这里list在两个线程之间不保证可见性,所以线程2始终结束不了
     */
    @Slf4j(topic = "enjoy")
    public class Container1 {
    
        List lists = new ArrayList();
    
        public void add(Object o){
            lists.add(o);
        }
    
        public int size(){
            return lists.size();
        }
    
        public static void main(String[] args) {
            Container1 c = new Container1();
    
            new Thread(()->{
                for (int i = 0; i < 10; i++) {
                    c.add(new Object());
                    log.debug("add " + i);
    
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }," t1").start();
    
            new Thread(()->{
                while (true) {
                    if (c.size() == 5) {
                        break;
                    }
                }
                log.debug("t2线程结束");
            }, "t2").start();
        }
    
    }
    
    
  • 此时t2压根就不会结束,

改进list–加volatile
  • package com.shadow.demo13;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 一道面试题:实现一个容器,提供两个方法,add,size
     * 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,
     * 当个数到5个时,线程2给出提示并结束
     *
     * 有两个问题,第一由于没有加同步,可能size等于5的时候,有另外一个线程加了一下才break,不是很精确
     * 第二个问题就是浪费cpu,T2线程用的是死循环
     */
    
    @Slf4j(topic = "enjoy")
    public class Container2 {
    
        volatile List lists = new ArrayList();
    
        public void add(Object o){
            lists.add(o);
        }
    
        public int size(){
            return lists.size();
        }
    
        public static void main(String[] args) {
            Container2 c = new Container2();
    
            new Thread(()->{
                for (int i = 0; i < 10; i++) {
                    c.add(new Object());
                    log.debug("add " + i);
    
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
    
                }
            }," t1").start();
    
            /**
             * 浪费性能
             * 当条件不满足的我应该放弃CPU 阻塞
             * 当条件满足的时候才执行  wait  notify
             */
            new Thread(()->{
                while (true) {
                    if (c.size() == 5) {
                        break;
                    }
                }
                log.debug("t2线程结束");
            }, "t2").start();
    
        }
    
    }
    
    
  • 理论还是存在问题,如果不睡眠,就会更容易出现,假设当list的size加到5个了,t2执行c.size==5的判断时,时间片又给t1了,并且又加了1个,此时再切换为线程t2时,已经不满足了,完美错过,还会是死循环

  • 这个问题在多核电脑上很难复现,单核电脑上则很容易复现

  • 最大的问题还是浪费性能,一直死循环

持续优化,当条件不满足时放弃cpu,而不是空转浪费cpu
  • package BingFaBianCheng.bingFaBianCheng8.shadow.demo13;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 一道面试题:实现一个容器,提供两个方法,add,size
     * 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,
     * 线程2给出提示并结束
     *
     * 这里虽然T2能够及时收到消息唤醒,但是wait会释放锁,notify不会释放锁,所以T1线程结束后
     * T2线程才执行完成
     */
    @Slf4j(topic = "enjoy")
    public class Container3 {
    
        volatile List lists = new ArrayList();
    
        public void add(Object o){
            lists.add(o);
        }
    
        public int size(){
            return lists.size();
        }
    
        public static void main(String[] args) {
            Container3 c = new Container3();
            Object lock = new Object();
    
            new Thread(()->{
                synchronized (lock) {
                    log.debug("t2启动");
                    if (c.size() != 5) {
                        try {
                            lock.wait();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    log.debug("t2结束");
                }
            }," t2").start();
    
    
    
    
            new Thread(()->{
                log.debug("t1启动");
                synchronized (lock) {
                    for (int i = 0; i < 10; i++) {
                        c.add(new Object());
                        log.debug("add " + i);
    
                        if (c.size() == 5) {
                            lock.notify();
    
                        }
    
                        try {
                            TimeUnit.SECONDS.sleep(1);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }, "t1").start();
    
        }
    
    }
    
    
  • wait会释放锁,notify不会释放锁

  • t2被唤醒后,会进入entryList去抢锁,但是此时t1都没有释放锁,所以t2拿不到锁,一直要到t1释放锁后才能执行

t1再次wait释放锁
  • package BingFaBianCheng.bingFaBianCheng8.shadow.demo13;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 一道面试题:实现一个容器,提供两个方法,add,size
     * 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,
     * 当个数到5个时,线程2给出提示并结束
     *
     * 相比较上一个例子,这里T1里面用wait释放锁,T2能够及时结束
     */
    @Slf4j(topic = "enjoy")
    public class Container4 {
    
        volatile List lists = new ArrayList();
    
        public void add(Object o){
            lists.add(o);
        }
    
        public int size(){
            return lists.size();
        }
    
        public static void main(String[] args) {
            Container4 c = new Container4();
            Object lock = new Object();
    
            new Thread(()->{
                synchronized (lock) {
                    log.debug("t2启动");
                    while (c.size() < 5) {
                        try {
    					lock.wait();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    log.debug("t2结束");
                    // 再次唤醒t1
                    lock.notify();
                }
            }," t2").start();
    
            new Thread(()->{
                log.debug("t1启动");
                synchronized (lock) {
                    for (int i = 0; i < 10; i++) {
                        c.add(new Object());
                        log.debug("add " + i);
                        if (c.size() == 5) {
                            lock.notify();
                            try {
                                lock.wait();//要释放锁,T2才能得到锁得以执行
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                        try {
                            TimeUnit.SECONDS.sleep(1);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }, "t1").start();
    
        }
    
    }
    
    
两个线程来回wait-notify,很别扭,有没有更优的方法
  • package BingFaBianCheng.bingFaBianCheng8.shadow.demo13;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 一道面试题:实现一个容器,提供两个方法,add,size
     * 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,
     * 当个数到5个时,线程2给出提示并结束
     *
     * CountDownLatch
     * 使用await和countdown方法替代wait和notify
     * CountDownLatch不涉及锁定,当count的值为零时当前线程继续运行
     * 相当于是发令枪,运动员线程调用await等待,计数到0开始运行
     * 当不涉及同步,只是涉及线程通信的时候,用synchronized加wait,notify就显得太重了
     */
    @Slf4j(topic = "enjoy")
    public class Container5 {
    
        volatile List lists = new ArrayList();
    
        public void add(Object o){
            lists.add(o);
        }
    
        public int size(){
            return lists.size();
        }
    
        public static void main(String[] args) {
            Container5 c = new Container5();
    
            CountDownLatch latch = new CountDownLatch(1);
    
            String s = new String("XXXXX");
            new Thread(()->{
                log.debug("t2启动");
                try {
                    //阻塞
                    latch.await();//准备
                } catch (Exception e) {
                    e.printStackTrace();
                }
                log.debug("t2结束");
                
            }," t2").start();
    
            new Thread(()->{
                log.debug("t1启动");
                for (int i = 0; i < 10; i++) {
                    c.add(new Object());
                    log.debug("add " + i);
                    if (c.size() == 5) {
                        // 因为countDownLatch等于1
                        // 释放一次后就等于0了
                        latch.countDown();
                    }
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }, "t1").start();
        }
    
    }
    
    
  • 只有当CountDownLatch等于0的时候,会自动把所有因为countDownLatch阻塞的线程全部唤醒,所以此时t2就会继续执行,否则就阻塞

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值