volatile与synchronized个人理解

作者: DevSiven | 来源:发表于2017-05-26 14:57 被阅读112次

    在文章之前,首先要理解一个jvm运行时内存分配机制以及一些定义。

    (一)定义:

    ** 原子性**:
    对于一个操作或者多个操作,要不全部执行且在执行过程中不可以被中断,要不直接不执行,这种行为成为原子性。

    x = 10;         //能保证原子性,10直接写入x
    y = x;         //不能保证原子性,x读取出来,写入y
    x++;           //不能保证原子性,x读取出来,自增后,写入x
    x = x + 1;     //不能保证原子性,同上
    

    ** 可见性**:
    指的是当存在多个线程访问一个变量的时候,如果其中一个线程改变这个变量值,对于其他线程是可见的,即其他线程可以立即看到这个改变的变量值。

    有序性
    有序性:即程序执行的顺序按照代码的先后顺序执行

    (二)运行时内存分配机制
    在其中一个内存区域中有一块叫jvm虚拟机栈,每个线程都独立拥有一个线程栈。这里先说两个定义名词,但某一对象变量值存储在某一内存空间中,我们这里叫做“主内存”,当某一线程访问该对象变量值,用过对象引用找到该变量值的内存空间,每个线程会先复制这个“主内存”的变量到自己的线程栈中,等待该线程栈的值操作完成后再更新到主内存中,贴下别人整理的图。

    线程栈工作模式.jpg

    所以,这里会衍生出一个问题。如果有多个线程读取这个“主内存”的变量,由于每次都是单独的线程拷贝这个“主内存”的变量到自己的线程栈,处理完成后才会更新到“主内存”中。所以并发的情况下,不能保证当前拷贝的值是同步进行的。

    volatile

    volatile可以解决在线程并发时候的可见性,即在某一线程栈改变一个主内存变量的时候,其他线程栈可以马上看到这个变化。但是volatile并不能完全保证原子性。下面再举例说明下为什么不能完全保证原子性。

    volatile 的可见性
    首先贴下代码,看下一则比较常见的例子

    /**
     * 测试Volatile的可见性demo
     * @author siven
     *
     */
    public class VolatileVisible{
    
        private static int siven = 1;
        public static int getSiven(){
            return siven;
        }
        
        public static void work(){
            siven = 2;
        }
    }
    
    public static void main(String[] args) {
            volatileVisibleAction();
        }
        
        private static void volatileVisibleAction(){
            new Thread(new Runnable() {
                
                public void run() {
                    VolatileVisible.work();
                }
            }).start();
            
            new Thread(new Runnable() {
                
                public void run() {
                    System.out.println("siven : " + VolatileVisible.getSiven());
                }
            }).start();
        }
    
    

    从实际想要的效果,第一个线程将变量siven更新为2,第二个线程再读取siven变量并且打印出来。这样的逻辑是没有什么问题。但是有一定的可能性存在并发问题。线程1改变siven变量值的时候,还没来得及更新到主内存,就被线程2
    读取,所以这里有可能读到的是1。读者可以多运行几次,几率是比较小,截图如下。

    volatile1.png

    因此,假如某一线程栈改变了值,其他线程栈可以立即更新的话就可以避免这种问题。
    修改成以下代码:

    private volatile static int siven = 1;
    

    所以有volatile的存在,保证线程之间的可见性,当然只能处理多线程并发读取问题,为什么这么说呢?看看下面代码:
    VolatileVisible中添加work2方法

    public static void work2(){
                siven ++;
    }
    

    测试方法

        private static void threadAction(){
    
            for (int i = 0; i < 10000; i++) {
                new Thread(new Runnable() {
                    
                    public void run() {
                        VolatileVisible.work2();
                    }
                }).start();
            }
    
    
            while(Thread.activeCount()>1)  //保证前面的线程都执行完
                Thread.yield();
            System.out.println("siven : " + VolatileVisible.getSiven());
    
        }
    

    从实际效果中,一万个线程执行完成后,siven会自加一万次,所以最终想要的结果应该回事10001,但是经过运行观察,很难可以准确得到10001这个结果,如图所示:

    volatile2.png
    这里读者会质疑,siven已经用volatile修饰符修饰了,siven变量改变应该对于所有线程栈是可见才对啊。首先这里我们应该了解siven++;自加操作,其实这个操作并不是原子操作,这里面包括内存中取出siven,然后自加1,接着再写入siven,所以在三个操作中有可能是会被中断的。虽然用了volatile修饰保证线程栈中的可见性,但是只限制于读取的时候可见性,也只有线程栈写入完成之后才会立即更新到主内存并且其他线程栈会马上知道

    举个例子:A线程、B线程都共同访问siven变量,这时候用得是volatile修饰,因此siven变量对于A、B线程都是可见的。当A对siven变量进行非原子操作(例如自加到2)是有可能出现还没执行到最后,B线程已经读取了旧值1,并且自己也自加到2(原来实际效果是想拿到A线程自加后的结果,在自加成3)。因此volatile只是保证线程之间的可见性,但是不能保证线程中操作的原子性。所以也引出了synchronized

    synchronized

    首先在理解synchronized中,要了解对象锁和类锁两个定义。每个java对象都可以用做一个实现同步的锁,这些锁成为内置锁。其实对象锁与类锁的的存在价值与内置锁一致。只是对象锁与类锁的应用场景不一样,即类锁应用在静态方法,对象锁应用在实例方法。我们都知道,每个类都存在多个实例化对象,但是每个类只有一个class对象,所以实例化对象中的对象锁并不会互相干预。

    首先贴下代码:

    /**
     * Synchronized 测试方法
     * @author siven
     */
    
    public class SynchronizedWorker {
        
        
        public static void work(String tag){
            
            for (int i = 0; i < 5; i++) {
                System.out.println(tag + " work : " + i);
            }
            
        }
    
    }
    
    
    private static void synchronizedTest(){
            Thread threadA = new Thread(new Runnable() {
    
                public void run() {
                    SynchronizedWorker.work("A");
                }
            });
    
            Thread threadB = new Thread(new Runnable() {
    
                public void run() {
                    SynchronizedWorker.work("B");
                }
            });
            
            threadA.start();
            threadB.start();
    
        }
    

    输出结果:

    synchronized1.png

    首先这里只是输出了log语句,如果当前不是输出而是改变某一对象里面的变量的时候,很容易出现因为线程并发问题导致对象成员变量被改变或者被重新实例化,特别是android中的回调,很多时候是发生在多线程的,所以很容易发生这种并发问题。如果我们要实际的同步效果,我们可以直接使用synchronized对方法或者代码块进行加锁。例如下面的优化改造:

    (一)直接修饰方法

    
    public synchronized static void work(String tag){
            
            for (int i = 0; i < 5; i++) {
                System.out.println(tag + " work : " + i);
            }
            
        }
    
    

    (二)修饰代码块

        public static void work(String tag){
            
            synchronized (SynchronizedWorker.class) {
                for (int i = 0; i < 5; i++) {
                    System.out.println(tag + " work : " + i);
                }
            }
            
        }
    

    在synchronized 修饰的方法或者代码块中,会将这个区域进行加锁,当第一个线程进行访问的时候,该线程会获取到该锁,如果第二个线程对这个区域进行访问的时候,如果有其他线程占有的锁还没释放的时候,将会暂时性阻塞,等待其他线程锁释放后自己获取后才可以进行访问。

    当然对于前面volatile可以解决可见性,但是不能完全保证原子性的代码案例中也可以通过synchronized 解决,我们只需要该成以下代码即可:

        public static void work2(){
            synchronized (VolatileVisible.class) {
                siven ++;
            }
        }
    

    输出结果:


    synchronized2.png

    注意问题:

    前面也说每个类有多个实例化对象,说明有多个对象锁。如果是synchronized (this)进行代码块加锁,此时只能对当前对象方法进行加锁,与其他对象锁没有任何干预。当然如果是类锁,针对静态方法的,我们没办法通过synchronized (this)进行代码块加锁,可以使用synchronized (**.class)进行加锁

    by siven(qq:708854877 email:sy.wu@foxmail.com)

    2017.5.26

    相关文章

      网友评论

        本文标题:volatile与synchronized个人理解

        本文链接:https://www.haomeiwen.com/subject/puszxxtx.html