前言:
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关键字,只能做用于本类。如图,直接加上去就好了,非常简单
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应运而生。它底层的原理是每次你要进行新增和修改操作的话,就先复制一份出来,操作复制出来的那一份。那么我另外一个线程要是在遍历数据的话,就不会受影响了~数据不是最新的,但是数据最终一致性,也不影响另外一个线程的操作
从源码我们可以清晰地看出来
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
网友评论