Java多线程——对象及变量的并发访问
用代码征服天下 人气:1Java多线系列文章是Java多线程的详解介绍,对多线程还不熟悉的同学可以先去看一下我的这篇博客Java基础系列3:多线程超详细总结,这篇博客从宏观层面介绍了多线程的整体概况,接下来的几篇文章是对多线程的深入剖析。
本篇文章主要介绍Java多线程中的同步,也就是如何在Java语言中写出线程安全的程序,如何在Java语言中解决非线程安全的相关问题。多线程中的同步问题是学习多线程的重中之重,这个技术在其他的编程语言中也涉及,如C++或C#。
同步和异步:
1、概念:
同步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为同步机制存在,A线程请求不到,怎么办,A线程只能等待下去
异步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为没有同步机制存在,A线程仍然请求的到,A线程无需等待
2、特点:
显然,同步最最安全,最保险的。而异步不安全,容易导致死锁,这样一个线程死掉就会导致整个进程崩溃,但没有同步机制的存在,性能会有所提升
3、同步阻塞与异步阻塞:
一个线程/进程经历的5个状态, 创建,就绪,运行,阻塞,终止。各个状态的转换条件如上图,其中有个阻塞状态,就是说当线程中调用某个函数,需要IO请求,或者暂时得不到竞争资源的,操作系统会把该线程阻塞起来,避免浪费CPU资源,等到得到了资源,再变成就绪状态,等待CPU调度运行。
同步是指两个线程的运行是相关的,其中一个线程要阻塞等待另外一个线程的运行。异步的意思是两个线程不相关,自己运行自己的。
线程安全问题:
定义:
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
线程安全问题概况来说有三方面:原子性、可见性和有序性。
原子性:
原子(Atomic)的字面意思是不可分割的(lndivisible)。对于涉及共享变量访问的操作,若该操作从其执行线程以外的任意线程来看是不可分割的,那么该操作就是原子操作,相应地我们称该操作具有原子性(Atomicity)。
在生活中我们可以找到的一个原子操作的例子就是人们从ATM机提取现金:尽管从ATM软件的角度来说,一笔取款交易涉及扣减户主账户余额、吐钞器吐出钞票、新增交易记录等一系列操作,但是从用户(我们)的角度来看ATM取款就是一个操作。该操作要么成功了,即我们拿到现金(户主账户的余额会被扣减),这个操作发生过了;要么失败了,即我们没有拿到现金,这个操作就像从来没有发生过一样(当然,户主账户的余额也不会被扣减)。除非ATM软件有缺陷,否则我们不会遇到吐钞口吐出部分现金而我们的账户余额却被扣除这样的部分结果。在这个例子中,户主账户余额就相当于我们所说的共享变量,而ATM机及其用户(人)就分别相当于上述定义中原子操作的执行线程和其他线程。
可见性:
在多线程环境下,一个线程对某个共享变量进行更新之后,后续访问该变量的线程可能无法立刻读取到这个更新的结果,甚至永远也无法读取到这个更新的结果。这就是线程安全问题的另外一个表现形式:可见性(Visibility)。
如果一个线程对某个共享变量进行更新之后,后续访问该变量的线程可以读取到该更新的结果,那么我们就称这个线程对该共享变量的更新对其他线程可见,否则我们就称这个线程对该共享变量的更新对其他线程不可见。可见性就是指一个线程对共享变量的更新的结果对于读取相应共享变量的线程而言是否可见的问题。多线程程序在可见性方面存在问题意味着某些线程读取到了旧数据(Stale Data),而这可能导致程序出现我们所不期望的结果。
如上图所示,线程1修改X变量,是在自己工作内存中进行修改的,并未及时刷新到主内存中,如果这时候线程2去读取主内存中的数据X读取到的还是0,但实际上X已经被修改成1了,这就是线程可见性有可能出现的问题。我们可以使用synchronized关键字来解决线程可见性问题。
有序性:
有序性(Ordering)指在什么情况下一个处理器上运行的一个线程所执行的内存访问操作在另外一个处理器上运行的其他线程看来是乱序的(Out of order)。所谓乱序,是指内存访问操作的顺序看起来像是发生了变化。
public class Singleton { private Singleton() { } private volatile static Singleton instance; public Singleton getInstance(){ if(instance==null){ synchronized (Singleton.class){ if(instance==null){ instance = new Singleton(); } } } return instance; } }
上面代码中的instance=new Person(),这条语句实际上包含了三步操作
- 分配对象的内存空间;
- 初始化对象;
- 设置instance指向刚分配的内存地址
由于重排序的原因,可能会出现以下运行顺序
如果2和3进行了重排序的话,线程B进行判断if(instance==null)时就会为true,而实际上这个instance并没有初始化成功,显而易见对线程B来说之后的操作就会出错。我们可以使用volatile关键字来解决线程有序性问题
示例:
下面我们来看两个线程安全的例子:
(1)、不共享数据的情况
class Mythread extends Thread{ private int count=5; public Mythread(String name) { this.setName(name); } @Override public void run() { while(count>0) { count--; System.out.println("由 "+this.currentThread().getName()+" 计算,count="+count); } } } public class Test01 { public static void main(String[] args) throws InterruptedException { /** * 下面创建了三个线程A,B,C */ Mythread A=new Mythread("A"); Mythread B=new Mythread("B"); Mythread C=new Mythread("C"); A.start(); B.start(); C.start(); } }
运行结果:
由 B 计算,count=4 由 A 计算,count=4 由 C 计算,count=4 由 A 计算,count=3 由 A 计算,count=2 由 B 计算,count=3 由 A 计算,count=1 由 C 计算,count=3 由 A 计算,count=0 由 B 计算,count=2 由 C 计算,count=2 由 B 计算,count=1 由 C 计算,count=1 由 C 计算,count=0 由 B 计算,count=0
由结果可以看出,一共创建了三个线程,每个线程都有各自的count变量,自己减少自己的count变量的值。这样的情况就是不共享变量,不会发生线程安全问题。
(2)、共享数据的情况:
class Mythread extends Thread{ private int count=3; @Override public void run() { count--; System.out.println("由 "+this.currentThread().getName()+" 计算,count="+count); } } public class Test01 { public static void main(String[] args) throws InterruptedException { //A,B,C三个线程共享一个变量 Mythread thread=new Mythread(); Thread A=new Thread(thread,"A"); Thread B=new Thread(thread,"B"); Thread C=new Thread(thread,"C"); A.start(); B.start(); C.start(); } }
运行结果:注意,这里的结果不一定是这样,也有可能是其他
由 B 计算,count=0 由 A 计算,count=1 由 C 计算,count=0
由结果我们可以知道,B和C计算的值都为0,说明B和C对count进行了同样的处理,产生了“非线程安全问题”。与我们想要的结果不同,我们希望值是依次递减的。
在JVM中,实现count--实际上一共需要三步:
- 取得原有的count值
- 计算count-1
- 对count进行赋值
在这三步中如果有多个线程同时访问就可能会出现非线程安全问题。假设A先执行来到第一步,获取count值为3,然后进行减一操作,A执行完后,此时count的值为2;B和C同时取得count值为2,然后同时减一,此时count值为0,因为B和C都执行了减一操作,最后赋值的时候B和C都为0
那么我们可不可以给线程设置一道“安检”,类似于过机场安检,每个人需要排队进行安检,不许抢先进行安检。
我们将Mythread的run()方法改成如下:
public synchronized void run() { count--; System.out.println("由 "+this.currentThread().getName()+" 计算,count="+count); }
现在运行结果如下:
由 A 计算,count=2 由 B 计算,count=1 由 C 计算,count=0
我们在上面的run()方法上面加上了synchronized关键字,现在的结果就是正确的了。下面来详细介绍synchronized关键字。
synchronized关键字:
一、synchronized同步方法:
在上面的例子中我们已经初步了解了“线程安全”与“非线程安全”相关的技术点,它们是学习多线程技术时一定会遇到的经典问题。“非线程安全”其实会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是“脏读”,也就是取到的数据其实是被更改过的。而“线程安全”就是以获得的实例变量的值是经过同步处理的,不会出现脏读的现象。
1、方法内的变量为线程安全的:
非线程安全问题存在于“实例变量”中,如果是方法内部的私有变量,则不存在非线程安全问题。
class NameTest{ public void add(String name) { try { int num=0; if(name.equals("a")) { num=100; System.out.println("a set over!"); Thread.sleep(2000); }else { num=200; System.out.println("b set over"); } System.out.println(name+" num="+num); }catch(Exception e) { e.printStackTrace(); } } } class ThreadA extends Thread{ private NameTest nA; public ThreadA(NameTest nA) { this.nA=nA; } @Override public void run() { nA.add("a"); } } class ThreadB extends Thread{ private NameTest nB; public ThreadB(NameTest nB) { this.nB=nB; } @Override public void run() { nB.add("b"); } } public class Test01 { public static void main(String[] args) throws InterruptedException { NameTest n=new NameTest(); ThreadA aThreadA=new ThreadA(n); aThreadA.start(); ThreadB bThreadB=new ThreadB(n); bThreadB.start(); } }
运行结果:
a set over! b set over b num=200 a num=100
结果显示,a num=100,b num=200;说明两个线程之间并未发生非线程安全问题,因为他们操作都是之间内部的变量。
2、实例变量非线程安全:
还是上面的例子,我们只改一行代码,将NameTest修改如下,其他代码保持不变:
class NameTest{ //将num修改为全局变量 private int num=0; public void add(String name) { try { if(name.equals("a")) { num=100; System.out.println("a set over!"); Thread.sleep(2000); }else { num=200; System.out.println("b set over"); } System.out.println(name+" num="+num); }catch(Exception e) { e.printStackTrace(); } } }
现在我们来看一下运行结果:
a set over! b set over b num=200 a num=200
现在我们可以看到a和b的num值都为200,发生了线程安全问题。
这时我们只需要在add方法上加上 synchronized 关键字即可(public synchronized void add),此时的运行结果就正确了。
运行结果: a set over! a num=100 b set over b num=200
实验结论:在两个线程访问同一个对象中的同步方法时一定是线程安全的。本实验由于是同步访问,b必须等待a执行完了才可以执行,所以先打印出a,然后打印出b。
3、多个对象多个锁:
class NameTest{ //将num修改为全局变量 private int num=0; public synchronized void add(String name) { try { if(name.equals("a")) { num=100; System.out.println("a set over!"); Thread.sleep(2000); }else { num=200; System.out.println("b set over"); } System.out.println(name+" num="+num); }catch(Exception e) { e.printStackTrace(); } } } class ThreadA extends Thread{ private NameTest nA; public ThreadA(NameTest nA) { this.nA=nA; } @Override public void run() { nA.add("a"); } } class ThreadB extends Thread{ private NameTest nB; public ThreadB(NameTest nB) { this.nB=nB; } @Override public void run() { nB.add("b"); } } public class Test01 { public static void main(String[] args) throws InterruptedException { //下面创建了两个NameTest对象 NameTest n1=new NameTest(); NameTest n2=new NameTest(); ThreadA a=new ThreadA(n1); a.start(); ThreadB b=new ThreadB(n2); b.start(); } }
运行结果:
a set over! b set over b num=200 a num=100
有的读者看到这里可能有疑问了,add()方法不是已经用synchronized修饰了吗?而synchronized修饰的方法时同步方法,那么因为先把a执行完毕,再执行b,为什么会实现这样的结果?
请读者仔细阅读代码,上一个实验中我们只创建了一个NameTest对象,而这个实验中我们创建了两个NameTest对象,两个线程操作的是同一个类的不同实例,所以会产生这样的结果。synchronized实现同步其实是给需要同步执行的代码加上了锁,当A线程获取到这把锁后,其他线程便不能获得到这个锁,直到A执行完毕释放锁后,其他线程才可以去拥有这个锁然后执行相应代码。
关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁,所以在上面的示例中,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁Lock,那么其他线程只能处于等待状态。前提是多个线程访问的是同一个对象。但如果多个线程访问多个对象,则JVM便会创建多个锁,上面的示例就是创建了两个锁。
4、synchronized方法与对象锁:
上面的示例中我们初步接触了锁,下面我们来深入了解一下synchronized与锁的关系。
class MyObject{ public synchronized void methodA() { try { System.out.println("begin methon threadName= "+Thread.currentThread().getName()); Thread.sleep(5000); System.out.println("end"); }catch(Exception e) { e.printStackTrace(); } } public void methodB(){ try { System.out.println("begin methon threadName= "+Thread.currentThread().getName()+" begin time= "+System.currentTimeMillis()); Thread.sleep(5000); System.out.println("end"); }catch(Exception e) { e.printStackTrace(); } } } //线程A class ThreadA extends Thread{ private MyObject object; public ThreadA(MyObject object) { this.object=object; } @Override public void run() { object.methodA(); } } class ThreadB extends Thread{ private MyObject object; public ThreadB(MyObject object) { this.object=object; } @Override public void run() { object.methodB(); } } public class Test01 { public static void main(String[] args) throws InterruptedException { MyObject object=new MyObject(); ThreadA a=new ThreadA(object); a.setName("A"); ThreadB b=new ThreadB(object); b.setName("B"); a.start(); b.start(); } }
上面代码中MyObject类中共有两个方法methodA()和methodB()方法,其中methodA()方法加上了synchronized关键字,是同步方法,methodB()这是普通方法;现在有两个线程类A和B,A线程中run方法调用的是MyObject类中的methodA()方法,B线程中run()方法调用的是MyObject类的methodB()方法,main()方法中创建了两个线程,名称为A和B,现在我们来看一下运行结果:
begin methon threadName= A begin methon threadName= B begin time= 1574825571200 end end
从结果可以看到,两个线程并非同步运行。因为methodB()方法并非同步方法,所以当A线程启动后,B线程依然可以调用methodB()方法。
下面我们将methodB()也加上synchronized关键字,再次运行看一下结果:
begin methon threadName= A end begin methon threadName= B begin time= 1574825932133 end
从这次的运行结果中我们可以清楚的看到A和B同步运行,那么这是为什么呢?线程A和B调用的不是同一个方法啊?我们再来仔细研究一下创建线程的代码:
MyObject object=new MyObject(); ThreadA a=new ThreadA(object); a.setName("A"); ThreadB b=new ThreadB(object); b.setName("B"); a.start(); b.start();
首先我们创建了一个MyObject类的实例,然后创建了线程A和B的实例,我们可以看到创建线程传入的参数是相同的,都是object,所以这两个线程运行时持有的是同一把锁object,所以我们看到的运行结果是同步的。
假如现在我们把创建线程的代码改成下面这样,大家思考结果会是什么?
ThreadA a=new ThreadA(new MyObject()); a.setName("A"); ThreadB b=new ThreadB(new MyObject()); b.setName("B"); a.start(); b.start();
对,结果是不同步的,因为线程A和B用的不是同一把锁
5、synchronized重入锁:
“可重入锁”的概念是:自己可以再次获取自己的内部锁。比如有一个线程获得了该对象的锁还没有释放,当其再次想要获取这个锁时依然可以获取,如果是不可重入锁的话,就会造成死锁。
关键字synchronized拥有锁重入的功能,也就是在使用synchronized的时候,当一个线程得到一个对象锁后,该线程再次此对象的锁依然是可以得到该对象的锁。
class Service{ public synchronized void service1() { System.out.println("service1"); service2(); } public synchronized void service2() { System.out.println("service2"); service3(); } public synchronized void service3() { System.out.println("service3"); } } class MyThread extends Thread{ @Override public void run() { Service service=new Service(); service.service1(); } } public class Test01 { public static void main(String[] args) throws InterruptedException { MyThread thread=new MyThread(); thread.start(); } }
运行结果:
service1 service2 service3
二、synchronized同步代码块:
synchronized同步代码块实现的功能其实和synchronized同步方法是一样的,但是在使用synchronized声明方法时会有一些弊端,比如A线程调用同步方法执行很长时间,那么B线程就必须等待很长时间,这样效率就很低。
synchronized同步代码块就可以将需要进行同步的代码放入同步代码块中,而其他线程安全的代码则放到代码块之外执行,这样就可以提升效率,下面我们来看一段代码:
1、同步方法的弊端:
class Commonutils{ public static long beginTime1; public static long endTime1; public static long beginTime2; public static long endTime2; } //处理任务类 class Task{ private String getData1; private String getData2; public synchronized void doLongTimeTask() { try { System.out.println("begin task"); Thread.sleep(3000); getData1="长时间处理任务后从远程返回的值1 threadName="+Thread.currentThread().getName(); getData2="长时间处理任务后从远程返回的值2 threadName="+Thread.currentThread().getName(); System.out.println(getData1); System.out.println(getData2); System.out.println("end task"); }catch(Exception e) { e.printStackTrace(); } } } class MyThread1 extends Thread{ private Task task; public MyThread1(Task task) { this.task=task; } @Override public void run() { Commonutils.beginTime1=System.currentTimeMillis(); task.doLongTimeTask(); Commonutils.endTime1=System.currentTimeMillis(); } } class MyThread2 extends Thread{ private Task task; public MyThread2(Task task) { this.task=task; } @Override public void run() { Commonutils.beginTime2=System.currentTimeMillis(); task.doLongTimeTask(); Commonutils.endTime2=System.currentTimeMillis(); } } public class Test01 { public static void main(String[] args) { Task task=new Task(); MyThread1 thread1=new MyThread1(task); thread1.start(); MyThread2 thread2=new MyThread2(task); thread2.start(); try { Thread.sleep(10000); }catch(Exception e) { e.printStackTrace(); } long beginTime=Commonutils.beginTime1; if(Commonutils.beginTime2<Commonutils.beginTime1) { beginTime=Commonutils.beginTime2; } long endTime=Commonutils.endTime1; if(Commonutils.endTime2>Commonutils.endTime1) { beginTime=Commonutils.endTime2; } System.out.println("耗时:"+((endTime-beginTime)/1000)); } }
运行结果:
begin task 长时间处理任务后从远程返回的值1 threadName=Thread-0 长时间处理任务后从远程返回的值2 threadName=Thread-0 end task begin task 长时间处理任务后从远程返回的值1 threadName=Thread-1 长时间处理任务后从远程返回的值2 threadName=Thread-1 end task 耗时:6
上面的代码中Task类的doLongTimeTask()方法模拟了一个长时间的操作,MyThread1和MyThread2类的run()方法分别执行了这个任务,由于doLongTimeTask()方法时同步方法,所以当两个线程启动后必须按照先后顺序去执行,时间较长,效率较低。我们想要的结果就是getData1和getData2赋值的过程不会出现线程安全问题,因此我们可以考虑使用synchronized同步代码块来解决这个问题。
2、synchronized同步代码块的使用:
当两个并发线程访问同一个对象中的同步代码块时,一段时间内只能有一个线程被执行,另一个线程必须等待当前线程执行完这个代码块后才能执行该代码块。
我们现在将上面代码中Task类的代码修改为用synchronized同步代码块实现,其他代码不变:
class Task{ private String getData1; private String getData2; public void doLongTimeTask() { try { System.out.println("begin task"); Thread.sleep(3000); String p1="长时间处理任务后从远程返回的值1 threadName="+Thread.currentThread().getName(); String p2="长时间处理任务后从远程返回的值2 threadName="+Thread.currentThread().getName(); synchronized(this) { getData1=p1; getData2=p2; } System.out.println(getData1); System.out.println(getData2); System.out.println("end task"); }catch(Exception e) { e.printStackTrace(); } } }
将getData1和getData2赋值的过程用同步代码块包裹起来,这样整个赋值过程就是线程安全的。
运行结果:
begin task begin task 长时间处理任务后从远程返回的值1 threadName=Thread-0 长时间处理任务后从远程返回的值2 threadName=Thread-1 end task 长时间处理任务后从远程返回的值1 threadName=Thread-1 长时间处理任务后从远程返回的值2 threadName=Thread-1 end task 耗时:3
从运行结果可以看出,使用同步代码块的时间明显比较短。
3、synchronized代码块之间的同步性:
synchronized代码块和synchronized方法一样,默认使用的都是同一把锁,所以两个同步代码块之间也是同步的。我们来看如下代码:
class ObjectService{ public void serviceA() { try { //这里使用synchronized同步代码块实现线程安全 synchronized(this) { System.out.println("A begin time= "+System.currentTimeMillis()); Thread.sleep(2000); System.out.println("A end time= "+System.currentTimeMillis()); } }catch(Exception e) { e.printStackTrace(); } } public void serviceB() { synchronized(this) { System.out.println("B begin time= "+System.currentTimeMillis()); System.out.println("B end time= "+System.currentTimeMillis()); } } } class ThreadA extends Thread{ private ObjectService service; public ThreadA(ObjectService service) { this.service=service; } @Override public void run() { service.serviceA(); } } class ThreadB extends Thread{ private ObjectService service; public ThreadB(ObjectService service) { this.service=service; } @Override public void run() { service.serviceB(); } } public class Test { public static void main(String[] args) { ObjectService service=new ObjectService(); ThreadA a=new ThreadA(service); a.setName("a"); a.start(); ThreadB b=new ThreadB(service); b.setName("b"); b.start(); } }
ObjectService中共有两个方法,serviceA和serviceB,两个方法中都是用了synchronized同步代码块,ThreadA和ThreadB是两个线程,ThreadA线程中执行了serviceA()方法,ThreadB线程中执行了serviceB()方法,我们现在来观察结果是否是同步的:
运行结果:
A begin time= 1574837894142 A end time= 1574837896151 B begin time= 1574837896151 B end time= 1574837896151
从结果我们可以看到,代码是同步执行的,先执行的serviceA()方法,再执行的serviceB()方法,由此可见synchronized(this)同步代码块持有的是同一个锁
4、synchronized(this)同步代码块中的this:
细心的读者可能已经发现了,在上面的synchronized同步代码块中都加上了一个this,synchronized(this),那么这个this代表的是什么呢?
学过java的同学应该都知道,this代表的是当前对象。和synchronized同步方法一样,synchronized(this)同步代码块也是锁定当前对象的。那么这里的可以使用其他对象替代this呢?当然可以。
Java支持使用“任意对象”作为“对象监视器”来实现同步功能,这个任意对象大多数是实例变量及方法的参数,使用格式为synchronized(非this对象)。
class Service{ private String username; private String password; private String anyString=new String(); public void setUsernamePassword(String username,String password) { try { synchronized(anyString) { System.out.println("线程名称为:"+Thread.currentThread().getName()+"在"+System.currentTimeMillis()+"进入代码块"); Thread.sleep(3000); System.out.println("线程名称为:"+Thread.currentThread().getName()+"在"+System.currentTimeMillis()+"离开代码块"); } }catch(Exception e) { e.printStackTrace(); } } } class ThreadA extends Thread{ private Service service; public ThreadA(Service service) { this.service=service; } @Override public void run() { service.setUsernamePassword("a","aa"); } } class ThreadB extends Thread{ private Service service; public ThreadB(Service service) { this.service=service; } @Override public void run() { service.setUsernamePassword("b","bb"); } } public class Test { public static void main(String[] args) { Service service=new Service(); ThreadA a=new ThreadA(service); a.setName("A"); a.start(); ThreadB b=new ThreadB(service); b.setName("B"); b.start(); } }
运行结果:
线程名称为:A在1574839242431进入代码块 线程名称为:A在1574839245431离开代码块 线程名称为:B在1574839245431进入代码块 线程名称为:B在1574839248438离开代码块
这段代码中的同步代码块我们使用的是String对象作为锁,并没有使用,也可以实现同步。这样做还有一个好处,就是使用其他对象作为锁与使用this锁之间是异步的,不与其他的this锁争抢资源,可以提升效率。
小结:
- 当多个线程同时执行synchronized(x){}同步代码块时呈现的是同步
- 当其他线程执行x对象中的synchronized同步方法呈线同步效果
- 当其他线程执行x对象方法里面的synchronized(this)代码块也呈现同步效果
volatile关键字:
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
下面看一段代码:
//线程1 boolean stop = false; while(!stop){ doSomething(); } //线程2 stop = true;
这段代码一开始线程1开始执行,stop的值为false,接着线程2执行将stop的值修改为true。在前面线程安全问题那里介绍了线程可见性,那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
这时我们可以使用volatile关键字来解决可见性问题:
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
加上volatile关键字后,线程2修改变量后会强制将stop的值刷新到主内存中,线程1也会强制去主内存中读取数据,这样就不会出现可见性问题了。
volatile只能解决线程可见性问题,并不能解决线程原子性问题。
现在有这样一个赋值操作:
volatile Map aMap=new HashMap();
这个赋值操作可以分解为以下几步:
objRef=allocate(HaspMap.class);//子操作1:分配对象所需存储空间
invokeConstructor(objRef);//子操作2:初始化objRef引用的对象
aMap=objRef;//子操作3:将对象的引用写入变量
虽然volatile关键字仅保障其中的子操作③是一个原子操作,但是由于子操作①和子操作②仅涉及局部变量而未涉及共享变量,因此对变量aMap的赋值操作仍然是一个原子操作。
加载全部内容