Java并发编程:synchronized&volatile

根据前面讲的 Java内存模型,已经接触不少synchronized,而且它非常强大,能解决大部分的并发问题,今天我们一起来学习它吧。
以下是本文包含的知识点:
1.Java的线程安全
2.synchronized的用法
3.synchronized的实现原理
4.volatile关键字
 
一、Java的线程安全
我们这里讨论的线程安全,限定于多个线程之间存在共享数据访问的这个前提下。如果一段代码根本不会和其它线程共享数据,那么也不存在线程安全的问题。
那我们应该如何实现线程安全呢?
互斥同步是一种常见的并发正确性保障手段,同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个线程使用。而互斥是实现同步的一种手段。
在Java中最基本的互斥同步手段就是 synchronized关键字。还有一种是重入锁( ReentrantLock),后面会讲到。
 
二、synchronized的用法
可以参考之前的文章Thread类的使用,线程同步一节有讲到
1.作用于代码块上,只同步这一段代码:
synchronize(this){//this指锁定当前对象
     num++;
     System. out.println(name + ", 你是第" + num + "个使用timer的线程" );
}
 2.放在方法声明中,表明整个方法为同步方法:
public synchronized  void  add(String name) {//还可以修饰static方法
    num++;
    System. out.println(name + ", 你是第" + num + "个使用timer的线程" );
}
 有几点需要注意的地方:
1.当一个线程正在访问一个对象的synchronized方法时,其它线程不能访问该对象的其它synchronized方法。因为该对象的锁还未被释放,其它线程拿不到。
2.当一个线程正在访问一个对象的synchronized方法时,其它线程可以访问该对象的其它非synchronized方法。因为非synchronized方法,不需要锁。
3.当一个线程正在访问一个对象的synchronized方法时,其它线程可以访问其它对象的synchronized方法或非synchronized方法。因为锁的对象不一样。
 
另外,每个类也有自己的锁,被synchronized修饰的static方法,类锁与对象的锁也不会发生互斥。看下面代码:
public synchronized void test1(){//对象锁
     //
}
public synchronized static void test2(){//类锁
     //
}
 当多线程同时访问test1()和test2()时,可以并发访问,不会发生互斥。因为锁的对象不一样,一个是类,一个是对象。
 
三、synchronized的实现原理
synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型参数来指明需要锁定和解锁的对象。如果在程序中明确指定了对象参数,那就是这个对象的reference,如果没有明确指定,那根据synchronized修饰的是实例方法还是静态方法,去取对应的对象实例或Class对象来作为锁对象。
看下面这段代码:
package com.yuwl.thread.demo;

public class TestSynchronized {

     public void test1(){
          
     }
     
     public void test2(){
           synchronized( this){
          }
     }
     
     public synchronized void test3(){
          
     }
     
}
 对其反编译的字节码为:

从反编译的字节码可以看出,加了synchronized的代码块多了两个指令。
 
根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁。如果已经拿到对象的锁,则把计数器加1,相应的在执行monitorexit指令时会将计数器减1,当计算器为0时,锁就被释放了。如果获取对象失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放。对于synchronized方法,执行中的线程会识别该方法的method_info结构是否有ACC_SYNCHRONIZED标记,如果有会自动获取对象的锁,调用方法,最后释放锁。
 
synchronized还有一个重要特性—— 可重入性,不会自己把自己锁死。解释下,比喻一个线程执行同步方法1,而同步方法1又要调用同步方法2,这种情况是可重入的。
 
synchronized对异常的处理,如果同步块有异常发生,线程会自动释放锁。
 
四、volatile关键字
直接先举一个例子:
public class MyThread28 extends Thread
{
    private boolean isRunning = true;

    public boolean isRunning()
    {
        return isRunning;
    }

    public void setRunning(boolean isRunning)
    {
        this.isRunning = isRunning;
    }
    
    public void run()
    {
        System.out.println("进入run了");
        while (isRunning == true){}
        System.out.println("线程被停止了");
    }
}
public static void main(String[] args)
{
    try
    {
        MyThread28 mt = new MyThread28();
        mt.start();
        Thread.sleep(1000);
        mt.setRunning(false);
        System.out.println("已赋值为false");
    }
    catch (InterruptedException e)
    {
        e.printStackTrace();
    }
}
看一下运行结果:
进入run了
已赋值为false

也许这个结果有点奇怪,明明isRunning已经设置为false了, 线程还没停止呢?

这就要从Java内存模型(JMM)说起,这里先简单讲,虚拟机那块会详细讲的。根据JMM,Java中有一块主内存,不同的线程有自己的工作内存,同一个变量值在主内存中有一份,如果线程用到了这个变量的话,自己的工作内存中有一份一模一样的拷贝。每次进入线程从主内存中拿到变量值,每次执行完线程将变量从工作内存同步回主内存中。

出现打印结果现象的原因就是主内存和工作内存中数据的不同步造成的。因为执行run()方法的时候拿到一个主内存isRunning的拷贝,而设置isRunning是在main函数中做的,换句话说 ,设置的isRunning设置的是主内存中的isRunning,更新了主内存的isRunning,线程工作内存中的isRunning没有更新,当然一直死循环了,因为对于线程来说,它的isRunning依然是true。

解决这个问题很简单,给isRunning关键字加上volatile。加上了volatile的意思是,每次读取isRunning的值的时候,都先从主内存中把isRunning同步到线程的工作内存中,再当前时刻最新的isRunning。看一下给isRunning加了volatile关键字的运行效果:    
进入run了
已赋值为false
线程被停止了

看到这下线程停止了,因为从主内存中读取了最新的isRunning值,线程工作内存中的isRunning变成了false,自然while循环就结束了。

volatile的作用就是这样,被volatile修饰的变量,保证了每次读取到的都是最新的那个值。线程安全围绕的是可见性原子性这两个特性展开的,volatile解决的是变量在多个线程之间的可见性,但是无法保证原子性

多提一句,synchronized除了保障了原子性外,其实也保障了可见性。因为synchronized无论是同步的方法还是同步的代码块,都会先把主内存的数据拷贝到工作内存中,同步代码块结束,会把工作内存中的数据更新到主内存中,这样主内存中的数据一定是最新的。  
参考
《深入Java 虚拟机》
http://www.cnblogs.com/xrq730/p/4853578.html

猜你喜欢

转载自yuwenlin.iteye.com/blog/2312207