Java深入理解之线程:Synchronized的应用与理解

一、线程安全问题

    提起java多线程与并发就不得不提起Synchronized关键字,本篇就介绍一下博主对该关键字的理解与应用。
    Synchronized一般用于解决线程安全问题,那么我们首先来看一看为什么会由线程安全问题。在JVM中,程序运行的实体是一个个的进程,而进程在创建时也会为自身开辟一段空间存放自身线程内的私有数据。同时,我们新建的基本变量、对象实例、静态变量等存放在程序共享内存(堆、栈、静态方法区等)之中,这部分内存对每个线程都是开放的,但线程不会直接在共享内存中对变量进行修改,而是先将变量复制到自身私有线程内,产生一个变量副本,在私有内存中修改完毕后(此时变量不具备可见性)再将此变量写回至共享内存中,完成一次变量的修改。
    在线程的私有内存中,变量副本是不具备可见性的。因此,在多个线程同时对同一个共享变量操作时,每个线程中的修改对于其他线程而言都是不可见的,而各个线程之间的运行也是不具备有序性的,最终使共享内存中的变量值与预期不一致,此时便会出现线程安全问题。
     需要注意的是,这里提到的共享变量并非只有静态变量,任何变量都可能发生线程安全问题,在线程安全问题中,我们要关注的是变量的内存而非变量本身。也就是说我们可以得到线程安全问题的发生条件:

     多个线程 同时同一个内存地址 进行读写操作时,会引发线程安全问题。

    以下是一个比较经典的例子,线程A和线程B同时对num自加100000次,按正常逻辑,最终num应该为200000,但实际结果与预期总会有出入:

	public class IncreaseTest {
    private int num = 0;
    public  void increase(){
            for (int i = 0; i < 1000000; i++) {
                num++;
            }
    }
    public int getNum(){
        return  num;
    }
}


public class HashMapActivity extends Activity {
    private static final String TAG = "HashMapActivity";
    private TextView tv_showDemo;
    private IncreaseTest it,it2;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_hash_map);
        tv_showDemo = (TextView)findViewById(R.id.tv_showDemo);
        it = new IncreaseTest();
        it2 = new IncreaseTest();
        Thread t1 = new Thread(new myRunnable1());
        Thread t2 = new Thread(new myRunnable2());
        try {
            t1.start();
            t2.start();
            //join()防止父线程提前结束,会等待子线程执行完后再结束
            t1.join();
            t2.join();
        }catch (Exception e){
            e.printStackTrace();
        }
        Log.d(TAG,"num is : " +it.getNum());
    }

    private class myRunnable1 implements Runnable{
        @Override
        public void run() {
            it.increase();
        }
    }

    private class myRunnable2 implements Runnable {
        @Override
        public void run() {
            it.increase();
        }
    }

    运行多次每次的值都不符合预期,且变化无规律:

09-23 15:38:44.759 18272 18272 D HashMapActivity: num is : 1391757
09-23 15:39:02.363 18407 18407 D HashMapActivity: num is : 1446119
09-23 15:39:15.233 18523 18523 D HashMapActivity: num is : 1613234

     解决该问题的方法其实就是通过为increase()实例方法添加Synchronized关键字即可,接下来我们就讨论一下该关键字的作用。

二、Synchronized的理解与应用

    在各种资料书籍中都有介绍Synchronized关键字有以下三种用法:

     直接作用于实例方法: 相当于对当前实例加锁,进入同步代码前要获得当前实例的锁;
     直接作用于静态方法: 相当于对当前类加锁,进入同步代码前要获得当前类的锁。
     指定加锁对象,作用于代码块:对给定对象加锁,进入同步代码前要获得给定对象的锁;

    这三种对代码块、对实例、对类加锁相信都看烦了已经,但往往在程序中一看到Synchronized还是一脸懵逼,理不清楚同步锁到底什么时候应该加,应该加给谁?
    要回答以上问题,我们还得从源头入手,我们再来看一下线程安全问题的原因:

     多个线程 同时同一个内存地址 进行读写操作时,会引发线程安全问题。

    这里有两个关键点,即同时及同一个内存地址,Synchronized要解决线程安全,则必须打破这两个条件才可以。首先,同步锁的作用机制即拥有锁的线程才可以运行,其他线程必须等待锁的持有者执行完毕才可以再竞争锁,解决了同时的问题。那么我们只需要分析,同一个内存地址是否有可能被多个线程同时访问即可知道什么时候该加锁以及把锁加给谁。
    此外,我们还需要弄明白一下为什么给实例方法加Synchronized就可以锁到实例,这是因为java其实和C++、VB等一样,都是先编译、再执行的,方法Increase()编译后的可执行代码只有1块。如果创建了两个对象it、it2,它们内部的“方法表”中都会记下指向可执行代码块的“函数指针”;至于如何让函数调用的时候能够访问对应对象的成员,其实就是调用时,把 it 或 it2 作为 this。你可以简单理解为,方式会自动加上一个参数,按照 Increase(this) 的定义编译/执行。静态方法的区别就是不加这个参数,所以和对象实例无关的。
接下来我们通过几个场景分析一下具体的使用方法。

场景一:针对非静态成员变量的实例锁

     在刚才的例子里,之所以发生线程安全问题,是因为线程t1和t2同时访问了对象it的num属性,此时的内存状态如下图,it对象的实例引用存放于栈中,对象内容存放于堆中,线程t1,t2同时访问非静态成员变量num,即构成了对同一个内存地址进行访问。
在这里插入图片描述
     解决该问题也很简单,就是对increase()方法添加Synchronized进行同步就可以,如此,先获得锁的线程执行完毕后,另一个线程才可以继续操作,这样num最终的运算结果就符合预期了。

    public synchronized void increase(){
            for (int i = 0; i < 1000000; i++) {
                num++;
            }
    }
09-25 10:20:02.761  9147  9147 D HashMapActivity: num is : 2000000
09-25 10:22:16.977  9563  9563 D HashMapActivity: num is : 2000000
09-25 10:23:24.775  9868  9868 D HashMapActivity: num is : 2000000

     当然,如果两个线程同时访问的是不同的实例对象,那么就不会产生线程安全问题,因为不同的实例其非静态成员变量在堆中有不同的内存地址,相互之间不会影响,所以我们说这里对实例方法添加的Synchronized锁的是当前实例,不同实例之间不会触发锁的互斥。
在这里插入图片描述

    private class myRunnable1 implements Runnable{
        @Override
        public void run() {
            //IncreaseTest.sIncrease();
            it.increase();
        }
    }

    private class myRunnable2 implements Runnable {
        @Override
        public void run() {
            //IncreaseTest.sIncrease();
            it2.increase();
        }
    }

运行结果:
09-25 10:41:06.313 12216 12216 D HashMapActivity: it num is : 1000000 it2 num is :1000000
09-25 10:41:50.594 12449 12449 D HashMapActivity: it num is : 1000000 it2 num is :1000000
09-25 10:42:36.345 12678 12678 D HashMapActivity: it num is : 1000000 it2 num is :1000000

    总结: 对于非静态成员变量,一个实例享有独自的内存地址,而不同实例之间的内存地址不同,多个线程同时访问相同实例时会触发锁的互斥,同时访问不同实例时就不会产生互斥,即表现为实例锁。

场景二:静态成员变量的类锁

    以上我们讨论的是非静态成员变量,但当我们把num设置为静态成员变量后发现,两个线程同时访问不同的实例时也会引发线程安全问题,因为静态成员变量存储在方法区中,每个实例共享同一个静态变量,即访问的是同一个内存地址。此外,虽然对increase()方法加了锁,但调用时实际是increase(it) 和 increase(it2),因此不会触发锁的互斥。

public class IncreaseTest {
    private static int num = 0;

    public synchronized void increase(){
            for (int i = 0; i < 1000000; i++) {
                num++;
            }
    }    
    ...
}
09-26 16:17:45.988  8539  8539 D HashMapActivity: it num is : 1539043 it2 num is :1539043
09-26 16:21:49.796  8539  8539 D HashMapActivity: it num is : 3395161 it2 num is :3395161
09-26 16:21:55.138  8539  8539 D HashMapActivity: it num is : 5395161 it2 num is :5395161

    此时内存状态如下:
在这里插入图片描述

    解决该场景的问题,就需要把increase()方法也设置为静态方法,这样该方法就成为了类方法,与实例再无关系,不同的对象访问时就会触发锁的互斥而不会引发线程安全问题:

public class IncreaseTest {
    private static int num = 0;

    public static synchronized void increase(){
            for (int i = 0; i < 1000000; i++) {
                num++;
            }
    }    
    ...
}
09-26 16:17:45.988  8539  8539 D HashMapActivity: it num is : 2000000 it2 num is :2000000
09-26 16:21:49.796  8539  8539 D HashMapActivity: it num is : 2000000 it2 num is :2000000
09-26 16:21:55.138  8539  8539 D HashMapActivity: it num is : 2000000 it2 num is :2000000

    此时内存状态如下:
在这里插入图片描述

场景三:指定对象,为代码块加锁

    以上是两种单独的场景,但在实际使用中,确有很大限制,例如静态方法里不能调用非静态对象或者一个长方法中只有一部分需要同步,这时候给整个方法加锁虽然保证了线程安全,但却牺牲了不少效率,好在Synchronized还为我们提供了一种用法,基于一个对象,为一片代码块加锁,该对象必须为已实例化的对象。这种场景原理不变,该对象为非静态时,相当于给当前实例加锁; 而该对象为静态时,则相当于给当前类加锁,使用方法如下:

public class IncreaseTest {
    private static int num = 0;
    //非静态对象,为当前实例加锁; 静态对象为当前类加锁
    private Object lock = new Object();


    private static Object ob = new Object();

    public  void increase(){
        ...
        
        synchronized (lock) {
            for (int i = 0; i < 1000000; i++) {
                num++;
            }
        }
        
        ...
    }
}

    以上就是博主对安全和Synchronized关键字的理解及应用方法,希望能帮助到各位,有缺陷的地方还请指正~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章