美文网首页
Java并发编程(1)

Java并发编程(1)

作者: __y | 来源:发表于2018-12-06 21:47 被阅读20次

    前言:
    Java并发编程是面试官很喜欢问的一块。因此写了一些笔记记录一下学习过程。没有很深的原理,但是大概也能入个们,不会抛出个问题,一问三不知了~

    1.Atomic VS synchronized

    来举一个栗子:
    有这么一个例子,我们创建了两个线程,用同一个对象count;调用其add方法,学会多线程的朋友都知道,这段程序不出问题才怪,两个线程互相竞争,会导致线程安全问题;

    public class Count {
        private int count;
        public  void add() throws InterruptedException {
            for (int i = 0; i < 100; i++) {
                Thread.sleep(100);
                count++;
                System.out.println(this.count);
                }
            }
    }
    
    
    public class Main {
        public static void main(String[] args) {
            Count count = new Count();
            new Thread(() -> {
                try {
                    count.add();
                } catch (InterruptedException ignored) {
    
                }
            }).start();
            new Thread(() -> {
                try {
                    count.add();
                } catch (InterruptedException ignored) {
    
                }
            }).start();
        }
    }
    

    如何解决这种问题呢?机智的大家应该马上想到!synchronized!加把锁,看你还乱不乱来了~所以我们的优化可以是这样的在add方法前面加一个锁;

    public class Count {
        private int count;
        public  synchronized void add() throws InterruptedException {
            for (int i = 0; i < 100; i++) {
                Thread.sleep(100);
                count++;
                System.out.println(this.count);
                }
            }
    }
    

    这样应该解决问题了吧!这下子解决问题了,又不用加班了美滋滋;嘿嘿嘿,产品经理,测试估计都对我佩服的五体投地。然而,你的技术老大看到了这一段烂代码,瞬间骂娘,这效率多么低啊,不知道synchronized效率很低的么!还加方法上,然后让你马上优化!这时候我们想到了以前synchronized的另外一个知识点,整段代码加锁,确实效率低了,那么咱们再进一步,把竞争条件加上锁不就可以了!于是我们又有下面一段代码

    public class Count {
        private int count;
        public   void add() throws InterruptedException {
            for (int i = 0; i < 100; i++) {
                Thread.sleep(100);
                synchronized(this) {
                    count++;//把产生竞争条件的地方锁住!
                    System.out.println(this.count);
                }
    
                }
            }
    }
    

    这下技术老大不会强人锁男了吧!然而,技术老大都是老江湖了,看一了一下;又叫你回去继续想想。于是乎,上网baidu!原来还有Atomic这种好东西!不仅效率上比synchronized好,而且代码更精炼,更容易让人看得懂!

    1.1 原子(Atomic)变量类简单介绍

    其实听到原子这两个字,我们很容易联想到数据库的acid中的原子性,要么一起成功,要么一起打GG。因此,从字面上我们可以得出原子变量类,就是为了保证我们变量的一致性而存在的。
    于是乎我们的代码就这样了

    public class Count {
        private AtomicInteger count = new AtomicInteger();//原子变量
        public   void add() throws InterruptedException {
            for (int i = 0; i < 100; i++) {
                Thread.sleep(100);
                    System.out.println(count.incrementAndGet());
                }
            }
    }
    
    
    image.png
    这些都是我们可以用到的原子类;
    以后我们在多线程环境下如果想保证某一个变量的数据一致性;用原子变量类吧~
    说到原子变量类的话,不得不提一下非常重要的CAS理论,这是JAVA并发类中经常使用的一个算法:
    CAS我搜刮到一个图文并茂的一个博文,大家可以去看一下
    下面是图的链接和CAS理论原理的连接:
    重要提示:
    java3y:https://www.jianshu.com/p/5c9606ee8e01
    链接中介绍的CAS理论非常重要!!!!!!

    2.线程可见,线程封闭

    什么是线程可见,什么又是线程封闭啊;这些概念看上去真是让人头大;那就先来一段有意思的程序

    2.1 指令排序问题

    先定义一个类Visibility1

    public class Visibility1 {
        public static  boolean ready = false;
        public static  int number;
    }
    

    然后定义一个线程类ReaderThread

    public class ReaderThread extends Thread {
        @Override
        public void run() {
            while (!Visibility1.ready) {
              Thread.yield();
              System.out.println(Visibility1.number);
            }
    
        }
    }
    

    然后再来一个Main方法

    public class Main2 {
        public static void main(String[] args) {
            Visibility1.number = 66;
            new ReaderThread().start();
            Visibility1.ready = true;//有一个指令排序的问题,我们写的代码是一条条下去的,但是CPU运行的时候不一定一条条帮你安排
    
        }
    }
    

    分析:
    我们可以分析一下上面的程序;按照我们的平时的思维来说,开始ready肯定为false,因为我们的代码是一条条下去的;正常来说我们应该会输出个66;但是,你会得到一片空白(不信你自己拿去跑一下试试)。我是跑过的,一直都是空白。为什么会造成这个原因呢,因为指令排序问题,意思是我们写的代码是一条条下来,但是加载到内存的时候CPU运行的时候可不是一条条帮你排的。因此,造成没有任何输出的原因就是我们ready直接为true了,导致循环直接结束了。

    那么有人可能会问:你说指令是乱排序的;那int a = 5;int b = 6; int c = a + b;这种例子计算机不就懵逼了吗;这里可以告诉你的是,计算机并没有那么蠢,相反,他更机智,他会先去解决简单的,再来计算复杂的;a;b;这两条赋值语句不一定按顺序,但是c = a + b这条指令一定会在a,b赋值后~

    从上面程序我们可以引出一个问题:指令排序问题

    2.2 计算机缓存问题

    上面我已经抛出了一个指令排序问题,如何解决啊? 先别急,咱们再来看一个问题,计算机缓存问题

    public class Visibility {
        private static  boolean flag;
        public static void main(String[] args) throws InterruptedException {
            new Thread(()-> {
                for(;;) {
                    if (flag) {
                        System.out.println("!=");
                        System.exit(0);
                    }
                }
            }).start();
            Thread.sleep(10);
            new Thread(() -> {
               for(;;) {
                   flag = true;
               }
            }).start();
    
        }
    }
    
    

    这段代码,正常来说 两个线程,怎着也会把flag变为true然后结束掉吧。但是,我们看到的是进入死循环了;这是为什么呢?
    原因在在于下面的这张图
    CPU读取数据的时候会有一个缓存区,读到flag一直是缓存区的,我们其中一条线程是改变了flag在内存中的值;但是由于另外的线程一直读的是cache中的flag值,所以没有退出程序~


    image.png

    2.2. volatile关键字

    上面所述的计算机缓存那个问题,就是我们常说的线程可见性问题,一条线程修改,但是另外一条线程没有看到修改后的结果。这时候我们要解决线程可见性问题可以使用volatile关键字,只能做用于本类。如图,直接加上去就好了,非常简单

    image.png

    volatile 与 synchronized 的比较

    1.线程安全性包括两个方面:一,可见性;二,原子性;volatile只有可见性并没有原子性

    2.volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法

    3.volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。

    关于volatile比较详细的说明:

    https://www.cnblogs.com/hapjin/p/5492880.html

    2.3 线程关闭

    所谓的线程关闭,就是自闭!就是线程之间完全隔离的,你玩你的,我玩我的,我们之间没有任何交集;如何解决线程关闭问题呢?

    • final 不要共享变量

    • 栈关闭:我们知道我们调用一个方法的时候有方法栈的这么一个说法,我们可以在方法的内部声明变量,修改

    • ThreadLocal:线程绑定。(下面会举个例子)
      ThreadLocal:
      将一个对象放进ThreadLocal里面,然后再拿出来,每一个线程都有自己的对象;对象之间不会互相干扰;下面用代码演示

    package threadTest;
    
    public class Visibility {
        private  static ThreadLocal<Local> localThreadLocal = new ThreadLocal<>();
        public static void main(String[] args) throws InterruptedException {
            Local local = new Local();
            new Thread(()-> {
                for(;;) {
                    localThreadLocal.set(local);
                    Local local1 = localThreadLocal.get();
                    local1.setNum(20);
                    System.out.println(Thread.currentThread().getName() + " -------------" + local1.getNum());
                    Thread.yield();
                }
            }).start();
            new Thread(() -> {
               for(;;) {
                   localThreadLocal.set(local);
                   Local local1 = localThreadLocal.get();
                   local1.setNum(30);
                   System.out.println(Thread.currentThread().getName() + " -------------" + local1.getNum());
                   Thread.yield();
               }
            }).start();
    
        }
    }
    

    看一下上面的代码;如果按照我们以前的想法是,上面已经设置了20,会影响下面的线程,导致输出也变成30。但是输出结果确实出人意料的


    image.png

    可以观察到,这两个线程并没有打架,而是相处的非常好。这就是ThreadLocal的厉害之处。每一个线程都有自己的一份对象,你运行你的我运行的,非常和谐~

    3.同步容器和并发容器

    JAVA在处理高并发方面给我提供了很多API;其中要掌握的有同步容器和并发容器。

    3.1 同步容器

    同步容器,顾名思义就是用来同步数据的;其底层都是用了synchronized去实现;到达了同步的效果,但是效率很低。我们要掌握的就是两个同步容器Vector,Hashtable。因为其效率低的缘故,现在已经被淘汰了~而且这两个同步容器也不能真正保证线程安全性;
    举个例子:假设我现在有一个线程要移除一个元素;单独拿出来的操作的话,每一个操作都是原子性的。但是,假设还有另外一个线程已经删除了一个元素。这个时候这个List的长度已经发生改变了,这个时候JVM就会抛出运行时异常(因为list的长度已经发生改变,这个索引也发生了变化)~
    要解决这个问题的话,就要给这给这两个操作加synchronized;这样效率又更低了~

    Vector v = new Vector();
    int lastIndex = v.size() - 1;//这个操作是原子性的
    v.remove(lastIndex);//这个操作也是原子性的
    

    因此,这两个玩意退出历史的舞台了~

    3.2 并发容器

    为了更高效和更安全地解决线程安全问题;Java为我们提供了并发容器,这里介绍比较常用

    3.2.1 ConcurrentHashMap

    ConcurrentHashMap是JDK1.5以后提供给我们的一个并发容器;ConCurrentHashMap的底层是:散列表+红黑树,与HashMap是一样的(jdk1.8)。1.7的时候实现是使用分段锁的机制具体里面的原理,很复杂呀(水平有限,也说不清
    不过可以给大家一个链接参考:
    java3y:https://www.jianshu.com/p/964e1ea36970
    这里介绍一个比价重要的api操作:putIfAbsent()
    这个API的意思是只有当你存入一个key的时候,当不存在的时候才能put,否则为null;这个和redis的setnx有点像 ~
    为啥不介绍其他API呢?因为其他API和我们平时用Map是一样的,那这里就不多赘述了~

    3.2.2 CopyOnWriteArrayList/Set

    上面的容器是Map的线程安全的容器,这次要介绍的是类似ArrayList/Set的线程安全容器类CopyOnWriteArrayList/Set
    这个并发容器要解决的是List在多线程环境下读的问题;假设有这么一个例子:
    A线程在遍历List的一个数据,这个时候B线程同时也在修改这个容器中的数据。那这个时候A线程遍历出来的数据,肯定会有线程安全问题的~
    这时候CopyOnWriteArrayList/Set应运而生。它底层的原理是每次你要进行新增和修改操作的话,就先复制一份出来,操作复制出来的那一份。那么我另外一个线程要是在遍历数据的话,就不会受影响了~数据不是最新的,但是数据最终一致性,也不影响另外一个线程的操作
    从源码我们可以清晰地看出来

    image.png

    4.并发工具类中的闭锁,栅栏,信号量

    在并发工具包java.util.concurrent中还提供给了我们三个常用的工具类,他们分别是闭锁,栅栏,信号量。

    4.1闭锁CountDownLatch

    CountDownLatch:就是一个服务依赖于另外一个服务;比如我们有三个线程:C线程要计算 b + a;很明显C线程的结果集依赖于A线程和B线程的结果,这个时候我们就可以用CountDownLatch。先让A线程,B线程各自计算自己的值,然后C线程才继续走下去。

    下面举个例子:

    例子很简单,就是主线程等A,B两个线程输出完了,主线程才输出。

    public class Main3 {
    
        public static void main(String[] args) {
            final CountDownLatch countDownLatch = new CountDownLatch(2);
            new Thread(() -> {
                try{
                    System.out.println("A线程" + Thread.currentThread().getName() + "正在执行");
                    Thread.sleep(3000);
                    System.out.println("A线程" + Thread.currentThread().getName() + "执行完毕");
                    countDownLatch.countDown();
                }catch(InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
            new Thread(() -> {
                try{
                    System.out.println("B线程" + Thread.currentThread().getName() + "正在执行");
                    Thread.sleep(3000);
                    System.out.println("B线程" + Thread.currentThread().getName() + "执行完毕");
                    countDownLatch.countDown();
                }catch(InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
            try{
                System.out.println("等待两个子线程跑完!" + Thread.currentThread().getName() + "正在执行");
                countDownLatch.await();
                System.out.println("主线程跑完啦!" + Thread.currentThread().getName() + "执行完毕");
            }catch(InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    
    image.png

    4.2栅栏CyclicBarrier

    栅栏CyclicBarrier:是所有线程都执行完了,一起放行。这里和上面的闭锁有点区别,闭锁的话则是一个放行

    这里一定要注意他们的区别!!all or one!

    下面举个例子:

    我们在用CyclicBarrier的时候,调用await()就可以让这个线程先停一停;等所有线程都执行完了,大家一起HAPPY去做其他事!

    package threadTest;
    
    import java.util.concurrent.BrokenBarrierException;
    import java.util.concurrent.CyclicBarrier;
    
    public class Main4 {
        public static void main(String[] args) {
            CyclicBarrier barrier = new CyclicBarrier(3);
            for(int i=0; i<3; i++) {
                new Test(barrier).start();
            }
        }
    
        static class Test extends Thread {
            private CyclicBarrier barrier;
            Test(CyclicBarrier barrier) {
                this.barrier = barrier;
            }
            @Override
            public void run() {
                System.out.println("正在运行,线程" + Thread.currentThread().getName());
                try {
                    Thread.sleep(5000);
                    barrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "----继续执行");
            }
        }
    }
    
    

    运行结果;从运行结果非常直观看出~


    image.png

    4.3信号量Semaphore

    当我们的线程数目和资源不对等的情况下,可以考虑用Semaphore

    下面用例子来解释更清晰:

    比如:我们现在有5台机器,但是有8个工人;这个时候工人和机器是不对等的。那怎么办呢,那肯定一批一批上啊,先上5个人,然后再让其他3个工人进行操作。下面用代码进行演示

    package threadTest;
    
    import java.util.concurrent.Semaphore;
    
    public class Main5 {
        public static void main(String[] args) {
            int n = 8;
            Semaphore semaphore = new Semaphore(5);
            for(int i=0; i<n; i++) {
                new Test(i,semaphore).start();
            }
        }
        static class Test extends Thread{
            private int num;
            private Semaphore semaphore;
            Test(int num, Semaphore semaphore) {
                this.num = num;
                this.semaphore = semaphore;
            }
    
            @Override
            public void run() {
                try {
                    semaphore.acquire();
                    System.out.println("工人" + this.num + "占用一个机器在生产......");
                    semaphore.release();
                    System.out.println("工人" + this.num + "休息去了(释放)......");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
            }
        }
    }
    
    

    运行结果:


    image.png

    相关文章

      网友评论

          本文标题:Java并发编程(1)

          本文链接:https://www.haomeiwen.com/subject/kbpacqtx.html