什么是可见性?要知道这个我们需要先来了解一下多线程的内存模型。
多线程简易内存模型
如上图,可知,每个线程都有一个独立的内存空间,线程是直接到自己的工作内存中读取数据。
可见性
要理解可见性,我们先来看一个例子。
public class Test{
public static void main(String[] args) throws InterruptedException {
Service t1 = new Service();
t1.start();
Thread.sleep(500);
t1.setFlag(false);
}
}
class Service extends Thread{
private boolean flag = true;
@Override
public void run() {
System.out.println("线程开始了");
while(flag){
//Thread.sleep(1); 加上这句会让线程内存与公共内存同步
}
System.out.println("线程停下来了");
}
public void setFlag(boolean flag){
this.flag = flag;
}
}
运行结果:
线程开始了
发现线程开始之后,无法正常停止,这种情况也就是32位的jvm上的-server模式,为什么不能停止线程呢?事实上,jvm对这种情况做了优化,也就是说,线程会直接到线程自身的工作内存中获取数据,而不会去公共内存中获取数据,也不会把公共内存中的数据同步到自身内存中,所以这里引出可见性。
可见性,所谓可见性,就是让某个变量在多个线程中可见,这就意味着,需要让各个线程从公共内存中获取数据,这就需要volatile关键字。
volatile关键字
引入volatile关键字后的内存读取情况。
引入volatile关键字后,程序修改为:
public class Test{
public static void main(String[] args) throws InterruptedException {
Service t1 = new Service();
t1.start();
Thread.sleep(500);
t1.setFlag(false);
}
}
class Service extends Thread{
volatile private boolean flag = true;
@Override
public void run() {
System.out.println("线程开始了");
while(flag){
//Thread.sleep(1); 加上这句会让线程内存与公共内存同步
}
System.out.println("线程停下来了");
}
public void setFlag(boolean flag){
this.flag = flag;
}
}
输出结果:
线程开始了
线程停下来了
原子性
先来看一个例子。
public class Test{
public static void main(String[] args) throws InterruptedException {
Service services[] = new Service[100];
for (int i = 0; i < services.length; i++) {
services[i] = new Service();
}
for (int i = 0; i < services.length; i++) {
services[i].start();
}
Thread.sleep(2000);
System.out.println(Service.count);
}
}
class Service extends Thread{
public static int count = 0;
@Override
public void run() {
addCount();
}
public void addCount(){
for (int i = 0; i < 100; i++) {
count++;
}
}
}
输出结果:
9975
奇怪的事情发生了,明明这里是一个静态变量,即使多个线程并发,那么应该都是获取的同一个count++,那么结果应该是10000才对啊,为什么是9975呢?
事实上,这里就涉及到原子性的问题,这里count++分三步,先获取count的值,再count=count+1,再写入内存中 ,这三步并不是一个整体,那么也就意味着,当一个线程读取到count之后,然后++,而另外一个线程也读取到同样值的count,然后++,两个线程写入的值就是一样了,这就少计算了一次,从而导致count并不能正确的到10000,那么应该如何解决这个问题呢?
来看下面这段代码:
public class Test{
public static void main(String[] args) throws InterruptedException {
Service services[] = new Service[100];
for (int i = 0; i < services.length; i++) {
services[i] = new Service();
}
for (int i = 0; i < services.length; i++) {
services[i].start();
}
Thread.sleep(2000);
System.out.println(Service.count);
}
}
class Service extends Thread{
public static int count = 0;
@Override
public void run() {
try {
addCount();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized public static void addCount() throws InterruptedException{
for (int i = 0; i < 100; i++) {
count++;
}
}
}
发现与上面那段代码不同的地方在于,将addCount()改成了静态,也就是内存中只有这一个方法,同时在前面加上了synchronize,这就意味着,只有当一个线程count++for循环执行完成还之后,才能执行另一个线程的for。
如下这种写法根上面的写法的意思是一样的,这里都是为了争抢”aaa”常量锁,只有当count++完了,锁才会被释放,另外一个线程才能够再活动该锁,上面的写法优于下面的写法,因为他不会每循环一次就去判断一次锁的状态。
public static void addCount() throws InterruptedException{
for (int i = 0; i < 100; i++) {
synchronized ("aaa") {
count++;
}
}
}