关键字volatile的主要作用是使变量在多个线程间可见。
关键字volatile与死循环:
如果不是在多继承的情况下,使用继承Thread类和实现Runnable接口在取得成效运行结果上并没有什么太大的区别。如果一旦出现“多继承”的情况,则用实现Runnable接口的方式来处理多线程的问题是很有必须要的。创建如下代码:
public class PrintString {
private boolean isContinuePrint = true;
public boolean isContinuePrint(){
return true;
}
public void setContinuePrint(boolean isContinuePrint){
this.isContinuePrint = isContinuePrint;
}
public void printStringMethod(){
try{
while(isContinuePrint==true){
System.out.println("run printStringMethod threadName="+Thread.currentThread().getName());
Thread.sleep(1000);
}
}catch(Exception e){
e.printStackTrace();
}
}
}
public class Run1 {
public static void main(String[] args) {
PrintString printString = new PrintString();
printString.printStringMethod();
System.out.println("我要停止它! stopThread="+Thread.currentThread().getName());
printString.setContinuePrint(false);
}
}
执行结果如下:
程序开始运行后,根本停不下来。停不下来的原因主要就是main线程一直在处理while()循环,导致程序不能继续执行后面的代码。解决的办法当然使用多线程技术。
解决同步死循环:
创建如下代码:
public class PrintString implements Runnable{
private boolean isContinuePrint = true;
public boolean isContinuePrint(){
return isContinuePrint;
}
public void setContinuePrint(boolean isContinuePrint){
this.isContinuePrint = isContinuePrint;
}
public void printStringMethod(){
while(isContinuePrint){
System.out.println("run printStringMethod threadName="+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
printStringMethod();
}
}
public class Run {
public static void main(String[] args) {
PrintString printString = new PrintString();
new Thread(printString).start();
System.out.println("我要停止它! stopThreadName="+Thread.currentThread().getName());
printString.setContinuePrint(false);
}
}
执行结果如下:
解决异步死循环:
在使用volatile关键字之前,先创建如下代码看看:
public class RunThread extends Thread{
private boolean isRunning = true;
public boolean isRunning(){
return isRunning;
}
public void setRunning(boolean isRunning){
this.isRunning = isRunning;
}
@Override
public void run() {
System.out.println("进入run了");
while(isRunning){
}
System.out.println("线程被停止了");
}
}
public class Run {
public static void main(String[] args) throws InterruptedException {
RunThread runThread = new RunThread();
runThread.start();
Thread.sleep(1000);
runThread.setRunning(false);
System.out.println("已经赋值为false");
}
}
执行结果如下:
出现死循环了。造成的原因是启动RunThread线程时,变量private boolean isRunning = true;存在于公共堆栈及线程的私有堆栈中,线程一直在私有堆栈中获取isRunning的值是true。而代码runThread.setRunning(false);虽然执行了,更新的却是公共堆栈中的isRunning变量的值为false,所以一直就是死循环的状态。
通过使用volatile关键字,强制的从公共内存中读取变量的值。使用volatile关键字增加了实例变量在多个线程之间的可见性。但volatile关键字最致命的缺点是不支持原子性。
下面将关键字volatile和synchronized进行一些比较:
- 关键字volatile是线程同步的轻量级实现,所有volatile性能肯定比synchronized要好,并且volatile只能修饰于变量,而synchronized可以修改方法及代码块。随着JDK新版本的发布,synchronized关键字在执行效率上得到很大提升,在开发中使用synchronized关键字的比率还是比较大的。
- 多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。
- volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。
线程安全包含原子性和可见性两个方面,Java的同步机制都是围绕这两个方面来确保线程安全的。
volatile非原子的特性:
关键字volatile虽然增加了实例变量在多个线程之间的可见性,但它却不具备同步性,也就是不具备原子性。创建如下代码进行验证:
public class MyThread extends Thread{
volatile private static int count = 0;
private static void addCount(){
for(int i=0;i<100;i++){
count++;
}
System.out.println("count="+count);
}
@Override
public void run() {
addCount();
}
}
public class Run {
public static void main(String[] args) {
MyThread[] myThreads = new MyThread[100];
for(int i=0;i<100;i++){
myThreads[i] = new MyThread();
}
for(int i=0;i<100;i++){
myThreads[i].start();
}
}
}
执行结果如下:
出现两个相同的值,修改MyThread的代码如下:
public class MyThread extends Thread{
volatile private static int count = 0;
synchronized private static void addCount(){
for(int i=0;i<100;i++){
count++;
}
System.out.println("count="+count);
}
@Override
public void run() {
addCount();
}
}
执行结果如下:
在addCount方法前面添加了synchronized关键字,这样的话也就没有必要在变量前加volatile关键字。
关键字volatile提示线程每次从共享内存中读取变量,而不是从私有内存中读取,这样就保证了同步数据的可见性。但是这里需要注意的是:如果修改实例变量中的数据,如i++,也就是i=i+1,则这样的操作其实并不是一个原子操作,也就是非线程安全的。表达式i++的操作步骤分解如下:
- 从共享内存中取出i的值。
- 计算i的值。
- 将i的值写到内存中。
加入在第2步的时候,另外一个线程也修改了i的值,那么这个时候就会线程脏数据。解决的办法就是使用synchronized关键字。
下面用图来演示一下使用volatile关键字出现非线程安全的原因。变量在内存中工作的过程如下图所示:
在多线程的环境中,use和asign是多次出现的的,但这一操作并不是原子性,也就是在read和load之后,如果主内存count变量方式修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,也就是私有内存和公共内存中的变量不同步,所以计算出来的结果会和预期不一样,也就出现了非线程安全的问题。
对于用volatile修饰的变量,JVM虚拟机只是保证从主内存加载到线程工作内存的值是最新的。例如线程1和线程2在进行read和load的操作中,发现主内存中count的值都是5,那么都会加载这个最新的值。也就是说,volatile关键字解决的是变量读取时的可见性问题,但无法保证原子性,对于多个线程访问同一个实例变量还是需要锁同步。
使用原子类进行i++操作:
除了在i++操作时使用synchronized关键字实现同步外,还可以使用AtomicInteger原子类进行实现。
原子操作是不能分割的整体,没有其他线程能够中断或检查正在原子操作的变量。一个原子(atomic)类型就是一个原子操作可用的类型,它可以在没有锁的情况下做到线程安全(thread-safe)。创建如下代码:
public class AddCountThread extends Thread{
private AtomicInteger atomicInteger = new AtomicInteger();
@Override
public void run() {
for(int i=0;i<10000;i++){
System.out.println(atomicInteger.incrementAndGet());
}
}
}
public class Run {
public static void main(String[] args) {
AddCountThread addCountThread = new AddCountThread();
Thread t1 = new Thread(addCountThread);
t1.start();
Thread t2 = new Thread(addCountThread);
t2.start();
Thread t3 = new Thread(addCountThread);
t3.start();
Thread t4 = new Thread(addCountThread);
t4.start();
Thread t5 = new Thread(addCountThread);
t5.start();
}
}
执行结果如下:
程序成功累加到50000。
原子类也并不安全:
原子类在具有逻辑性的情况下输出结果也具有随机性。创建如下代码进行验证:
public class MyService {
public static AtomicLong atomicLong = new AtomicLong();
public void addNum(){
System.out.println(Thread.currentThread().getName()+"加了100之后的值是:"+atomicLong.addAndGet(100));
//再加1
atomicLong.addAndGet(1);
}
}
public class MyThread extends Thread{
private MyService myService;
public MyThread(MyService myService) {
this.myService = myService;
}
@Override
public void run() {
myService.addNum();
}
}
public class Run {
public static void main(String[] args) throws InterruptedException {
MyService myService = new MyService();
MyThread[] myThreads = new MyThread[5];
for(int i=0;i<myThreads.length;i++){
myThreads[i] = new MyThread(myService);
}
for(int i=0;i<myThreads.length;i++){
myThreads[i].start();
}
Thread.sleep(1000);
System.out.println(myService.atomicLong.get());
}
}
执行结果如下:
打印顺序出错了,应该是每加1次100再加1次1。出现这样的情况是因为addAndGet()方法是原子的,但是方法与方法之间却不是原子的。解决这样的问题必须要用同步。修改MyService代码如下:
public class MyService {
public static AtomicLong atomicLong = new AtomicLong();
synchronized public void addNum(){
System.out.println(Thread.currentThread().getName()+"加了100之后的值是:"+atomicLong.addAndGet(100));
//再加1
atomicLong.addAndGet(1);
}
}
从运行结果可以看到,是每次加100后再加一,这就是我们想要的过程,结果是505的同时还保证过程中累加的顺序也是正确的。
synchronized代码块有volatile同步的功能:
关键字synchronized可以使多个线程访问同一资源具有同步性,而且它还具有将线程工作内存中的私有变量与公共内存中的变量进行同步的功能,创建示例代码如下:
public class Service {
private boolean isContinueRun = true;
public void runMethod(){
while(isContinueRun){
}
System.out.println("停下来了");
}
public void stopMethod(){
isContinueRun = false;
}
}
public class ThreadA extends Thread{
private Service service;
public ThreadA(Service service) {
this.service = service;
}
@Override
public void run() {
service.runMethod();
}
}
public class ThreadB extends Thread{
private Service service;
public ThreadB(Service service) {
this.service = service;
}
@Override
public void run() {
service.stopMethod();
}
}
public class Run {
public static void main(String[] args) throws InterruptedException {
Service service = new Service();
ThreadA threadA = new ThreadA(service);
threadA.start();
Thread.sleep(1000);
ThreadB threadB = new ThreadB(service);
threadB.start();
System.out.println("已经发起停止的命令");
}
}
执行结果如下:
得到这个结果是各线程间的数据没有可视性造成的,而关键字synchronized可以具有可视性。修改Service类的代码如下:
public class Service {
private boolean isContinueRun = true;
public void runMethod(){
String anyString = new String();
while(isContinueRun){
synchronized (anyString) {
}
}
System.out.println("停下来了");
}
public void stopMethod(){
isContinueRun = false;
}
}
执行结果如下:
关键字synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法或某一个代码块。它包含两个特性:互斥和可见性。同步synchronized不仅可以解决一个线程看到对象处于不一致的状态,还可以保障进入同步方法或同步代码块的每个线程,都看到由同一个锁保护之前所有的修改效果。
学习多线并发,要着重“外练互斥,内修可见“。
本文总结:
对synchronized有了一定认识,知道什么时候需要使用synchronized关键字。通过学习多线程同步可以控制线程处理数据的顺序,及对处理后的数据进行有效值的保证,更好的对线程执行结果有正确的预期。