Java并发编程(1)

前言:
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 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

运行结果:


image.png

你可能感兴趣的:(Java并发编程(1))