多线程问题,一直是我们老生常谈的一个问题,在面试中也会被经常问到,如何去学习理解多线程,何为线程安全性,那么大家跟我的脚步一起来学习一下。
定义:
当多个线程访问某个类时,不管运行时环境采用何种调度方式 或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现正确的行为,那么称这个类时线程安全的。
线程的安全性主要体现在三个方法
1、访问(读/写)某个共享变量的操作从其执行线程以外的线程来看,该操作要么已经执行结果,有么尚未执行,也就是说其他线程不会看到“该操作执行了部分的效果”。
2、访问同一组共享变量的原子操作 不能够被交错的。
在java中实现原子性的两种方式:
在java语言中,除long/double之外的任何类型的变量的写操作都是原子操作。 java语言中任何变量的读操作都是原子操作。 需要注意的是 原子操作 + 原子操作 != 原子操作 例如 i++ 先读后写 读跟写都是原子操作,但是 i++并不是原子操作
下面用代码讲一下实现的两种方式
例子
/**
* @author yukong
* @date 2018/8/29
* @description 线程不安全
*/
public class CountExample {
/**
* 并发线程数目
*/
private static int threadNum = 1000;
/**
* 闭锁
*/
private static CountDownLatch countDownLatch = new CountDownLatch(threadNum);
private static Integer i = 0;
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int j = 0; j < threadNum; j++) {
executorService.execute(() -> {
add();
});
}
// 使用闭锁保证当所有统计线程完成后,主线程输出统计结果。 其实这里也可以使用Thread.sleep() 让主线程阻塞等待一会儿实现
countDownLatch.await();
System.out.println(i);
}
private static void add() {
countDownLatch.countDown();
i++;
}
}
上面这段代码很明显因为i++不是原子性操作,所以不是线程安全的。
那么根据上面讲的,我们可以使用锁,或者atomic包下的类实现。
一个线程对共享变量的修改能够及时被其他线程所观察。
这句话怎么理解呢?
在JMM(Java Memory Model)的定义中,所有的变量都需要存储在主体内存中,主内存是共享内存区域,所有的线程都能访问的,但是线程对变量的操作(读、写)必须在工作内存中完成。
1、首先将变量从主内存中拷贝到自己的工作内存。
2、对变量进行读写操作。
3、操作完成,将变量回写到主内存中。
从上面可以得知,线程不能直接操作主内存的变量,必须要在工作内存中操作。
简单了解一下JMM的规定,那么我们就可以很容易的理解可见性了。
1535527111889.png
由上图可知 ,在多线程情况下,线程对共享变量的的操作都是拷贝一份副本到自己的工作内存中操作的,然后才写回到主内存中,这就可能存在一个问题,线程1修改了共享变量X的值,但是还未写回主内存,另外一个线程2又对主内存中的同一个共享变量x进行操作,但此时线程1工作内存中的变量x对线程n并不可以,这种工作内存与主内存同步延迟的问题就造成了可见性问题,另外指令重排序也会导致可见性问题。
那么对于可见性问题,使用什么解决方法呢?
为什么synchronized能保证可见性呢?根据JMM关于synchronized的规定
那么volatile又是怎么实现可见性的呢?
其实volatile是通过加入内存屏障和禁止指令重排序优化来实现的。
那大家可能就会想问了,我把上面的代码的i变量用volatile
修饰一下,是不是就保证线程安全,输出的结果就是1000呢,答案是否定的,volatile保证的是可见性,并不能保证原子性。但是利用volatile可见性这个特点,我们可以利用它完全一些线程中的通信
volatile boolean flag = false;
// thread a
{
flag = true;
// do somethings
}
// thread b
{
while (flag) {
// do somethings
}
}
这样就完全一个线程中通信的案例。
在JMM(java 内存 模型)中,运行编译器和处理器对指令就行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响多线程并发执行的正确性。
在java中,可以通过volatile
关键字来保证一定的有序性。另外也可以通过synchronized
和Lock
来保证有序性。很显然,synchronized跟lock保证每个时刻是只有一个线程执行同步代码,相当于让线程属性执行同步代码,自然保证了有序性。
另外java内存模型也具备一些先天的有序性
,即不需要通过任何手段就能够保证的有序性,这个通常也称为Happen-Before
原则。如果两个操作的资源无法从Happen-Before
原则推导出来,那么他们就不能保证它的有序性,虚拟机就可以随机对他们进行重排序。
那么下面就详细介绍Happen-Before
(先行发生原则):
如果一个操作具有以上的三种特性,那么我们称它为线程安全的。