0、背景

  并发编程其实解决的就是“非线程安全”的问题,是指多个线程对同一个对象中的实例变量进行并发访问时产生的“脏读”的情况,也就是取到的数据是被其他线程更改过的。Java对于解决并发问题提供了两个关键字供开发者使用,分别是 synchronized 和 volatile

  1、synchronized的用法

  synchronized关键字用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。

     按照使用方法可分为 synchronized方法 和 synchronized代码块,按照同步的对象可分为 同步实例变量 和 同步Class类

  交叉之后就是下面四种:

  1)public synchronized void test(){};

  2)public synchronized static void test(){};

  3)synchronized(this) {}

  4)synchronized(Test.class) {}

  其中1和3、2和4的作用是一样,前者锁的是对象实例,后者锁的是该对象的class文件;

  synchronized方法控制对类成员变量的访问:每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突。

  打个比方:一个object就像一个大房子,大门永远打开。房子里有很多房间(也就是方法)。这些房间有上锁的(synchronized方法), 和不上锁之分(普通方法)。房门口放着一把钥匙(key),这把钥匙可以打开所有上锁的房间。另外我把所有想调用该对象方法的线程比喻成想进入这房子某个房间的人。所有的东西就这么多了,下面我们看看这些东西之间如何作用的。一个人想进入某间上了锁的房间,他来到房子门口,看见钥匙在那儿(说明暂时还没有其他人要使用上锁的 房间)。于是他走上去拿到了钥匙,并且按照自己 的计划使用那些房间。注意一点,他每次使用完一次上锁的房间后会马上把钥匙还回去。即使他要连续使用两间上锁的房间,中间他也要把钥匙还回去,再取回来。这时其他人可以不受限制的使用那些不上锁的房间,一个人用一间可以,两个人用一间也可以,没限制。但是如果当某个人想要进入上锁的房间,他就要跑到大门口去看看了。有钥匙当然拿了就走,没有的话,就只能等了。

  对于synchronized(非 this 对象 x) 格式的写法,是将对象 x 本身作为“对象监视器”。当一个类中有多个synchronized方法,虽然可使实现同步,但是也会受到阻塞,影响运行效率;使用上述这种方式,可以避免不必要的方法间的同步,大大提高效率;

  这种使用方式有以下3个结论:

  1)当多个线程同时执行synchronized(x){}同步代码块时呈同步效果;

  2)当其他线程执行x对象中的synchronized方法时呈同步效果;

  3)当其他线程执行x对象中的synchronized(this)代码块时呈同步效果;

  使用synchronized关键字时,有以下需要注意的点:

  1)synchronized锁的可重入性:是指当一个线程获得一个对象的锁后,再次请求此对象锁时是可以再次得到该对象的锁的。也就是说在一个synchronized方法或者代码块内调用本类的其他synchronized方法或者代码块时,是永远可以得到锁的;当存在父子继承关系时,子类完全可以通过“可重入锁”调用父类的同步方法;

  2)出现异常时锁自动释放:当一个线程的代码抛出异常时,其所持的锁会自动释放;

  3)同步不具备继承性:父类中的同步方法并不能被子类继承,也就是说子类在重写父类的同步方法时,也必须自己加上synchronized关键字,不然是不会自动从父类继承的;

  4)使用synchronized方法会有性能问题,只有方法结束才会释放锁,可以用synchronized(this) 来替代;

  5)当要判断两个线程是否会同步执行,最主要的是判断两线程需要执行的代码所持的是否是同一“对象监视器”;

  6)String的常量池:当使用synchronized(string)时,需考虑字符串的常量池;

  7)当设计同步逻辑时,一定要避免死锁;  

  2、volatile的用法

  当线程访问一个变量时,会从主内存copy一份副本到自己的工作内存,让后执行完读写后,再将其写回到主内存,如下图:

  这种逻辑有时就会造成主内存和工作内存内数据不一致,线程运行过程中,内部无法实时获取主内存的最新值,而且也无法把最新值更新到主内存;

  但是我们可以使用volatile关键字修饰该变量,强制的从公共内存读取变量的值,流程如下:

  使用volatile关键字增加了实例变量在多个线程间的可见性,但是volatile关键字最致命的是不支持原子性;

  volatile和synchronized的区别:

  1)volatile是线程同步的轻量级实现,所以性能更好;但是volatile只能修饰变量,而synchronized不仅可以修饰变量,还可以修饰方法以及代码块;

  2)多线程访问volatile不会发生阻塞,而synchronized会出现阻塞;

  3)volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,同时间接地保证可见性,因为他会将私有内存和公共内存中的数据进行同步;

  4)volatile解决的是变量在多个线程间的可见性;而synchronized关键字解决的是多个线程间访问资源的同步性;

  容易出错的i++:

  volatile只是保证了变量的可见性,保证读到的最新的值;但是如果修改了变量的值,如i++;其不是一个原子操作,对于volatile来说就无法保证线程安全;i++可以拆解如下,如果第二步中i的值发生了改变,就会出现脏数据:

  1)从内读取i的值;

  2)计算i的值;

  3)将i的值写到内存中;

  变量在内存中工作的过程如下:

  在多线程环境中,use和assign是多次出现的,但这一操作并不是原子性,也就是在read和load之后,如果主内存count发生变化,线程工作内存中的值由于已经加载,不会产生变化,也就是私有内存和公共内存中的变量不同步,导致出现了线程安全问题。

 

posted on 2017-04-25 20:37  离不开水的鱼  阅读(420)  评论(0)    收藏  举报