2、对象及变量的并发控制
学会了多线程的基本使用后,我们该关心线程的安全问题,看一下我们该如何写出安全的程序。
2.1 synchronized 同步方法
“非线程安全”会在多个线程对同一个对象中的实例变量进行并发访问时访问,产生的后果就是“脏读”。
也就是取到的数据其实是被更改过的。
而“线程安全”就是已获得的实例变量的值是经过同步处理的,不会出现脏读的现象。
方法内的变量为线程安全
“非线程安全”问题仅存在于“实例变量”中,方法内部的私有变量,不会出现这种问题。
实例变量非线程安全
如果多个线程共同访问一个对象中的实例变量,则有可能出现“非线程安全”问题。
线程访问的对象中如果有多个实例变量,则运行结果可能出现交叉的情况。
synchronized 同步方法
synchronized 字段所修饰的方法被称为同步方法,同步方法可以保证同一时间段内只有一个线程执行该方法,可以保证线程安全执行。
synchronized 取得的锁都是对象锁
也就是说,如果不同线程访问的是不同对象的同步方法,方法还是异步执行的。
因为大家都可以拿到一个对象锁。
当多线程执行同一个对象的同步方法时,哪个线程先执行同步方法,哪个线程就持有该方法所属对象的锁Lock,那么其它线程只能呈等待状态。
当多线程执访问多个对象,则JVM会创建多个锁。
结论
我们现在知道了同步方法的锁是当前对象,我们来做进一步的测试。
现在有一个类有一个普通方法A,两个同步方法B和C。
第一次使用两个线程分别执行A和B,发现异步执行。
第二层使用两个线程分别执行B和C,不能异步执行。
也就是说:
- 第一个线程先持有对象的Lock锁,第二个线程可以以异步的方式调用对象中的非同步方法。
- 第一个线程先持有对象的Lock锁,第二个线程如果在这时调用对象其他的同步方法则需等待。
synchronized 锁重入
设想一个问题?
如果我在一个同步方法内调用该对象的另一个同步方法?可以正确执行吗?
是可以的,synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,在释放前,可以再次得到该对象的锁。
所以,在一个synchronize方法块内调用本类的其他synchronized方法块时,是永远可以得到锁的。
锁重入一定程度减少了死锁。
可重入锁也支持在父子类继承的环境中,子类可以通过可重入锁调用父类的同步方法。
当一个线程出现异常时,其所持有的锁会自动释放。
synchronized不具有继承性,子类重写还需要加synchronized关键字
2.2 synchronized 同步语句块
同步方法虽然解决了“非线程安全”的问题,但是也出现了一些弊端。
比如A线程调用同步方法执行一个长时间的任务,那么B线程则必须等待比较长的时间。
有时候,我们并不需要对整个方法进行加锁,这时候就可以使用同步代码块来解决。
2.2.1 同步语句块的使用
public void test(){
synchronized(this){
//操作实例变量
}
}
我们无需将使整个方法成为同步的,我们只需要将操作实例变量这种可能引发线程安全问题的代码进行同步处理即可。
半同步半异步,同步语句块外是异步的,同步语句块内是同步的
2.2.2 同步代码块的同步性
和synchronized方法一样,synchronized(this)代码块也是锁定当前对象的。
但是在Java中,同步代码块不止可以使用this作为锁对象,还支持“任意对象”作为“对象监视器”来实现同步的功能,synchronized(非this对象x)。
我们先来总结一下synchronized同步方法和synchronized(this)同步代码块的作用:
- 对其他synchronized方法或synchronized(this)代码块调用呈阻塞状态。
- 同一时间只有一个线程可以执行synchronized同步方法或synchronized(this)同步代码块。
我们再来看看synchronized(非this对象x)的结论:
- 在多个线程持有“对线监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this对象x)的代码。
锁非this对象具有一定的有点,因为我们在不同的方法中可能操纵不同的实例变量,全部使用this锁的话到导致一直线程执行,其它所有线程阻塞,效率会降低。
灵活的使用非this锁,可以更好的提升运行效率。
2.2.3 静态同步synchronized方法与synchronized(class)代码块
从效果上来说,都是同步效果,和将synchronized关键字加到非static上使用效果是一样的。但是本质上是不同的,synchronize关键字加到static方法上是给Class类上锁,而synchronized关键字加到非static方法是给对象上锁。
对象锁和Class锁是不同的锁。
值得注意的是,我们通常不使用Sring作为锁对象,这是因为String的常量池特性。
2.2.4 总结
总结一句话就是,只要你们的锁是同一个对象,或同一个类(其实也是同一个对象),那么你们就需要去竞争,表现出来就是同步的
2.3 volatile关键字
关键字volatile的主要作用是使变量在多个线程间可见。
关键字volatile的作用是强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值
线程安全包含原子性和可见性两个方面,Java的同步机制都是围绕这两个方面来确保线程安全的。
使用volatile关键字增加了实例变量在多个线程之间的可见性。但volatile关键字最致命的缺点就是不支持原子性。
我们来比较一下volatile和synchronized:
- 关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized好,并且volatile只能修饰于变量,而synchronized可以修饰方法,以及代码块。随着JDK新版本的 发布,synchronized关键字在执行效率上得到很大的提升,在开发中使用synchronized关键字的比率还是比较大的。
- 多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。
- volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为他会将私有内存和公共内存的数据做同步。
- 关键字volatile解决的是变量在多个线程的可见性;而synchronized关键字解决的是多个线程之间访问资源的同步性。
我们来看一下volatile带来的问题:
volatile修饰的变量在多个线程对其修改的时候有着以下的流程:
- read和load阶段:从主存复制变量到当前线程工作内存;
- use和assign阶段:执行代码,改变共享变量值;
- store和write阶段:用工作内存数据刷新主存对应变量的值。
在多线程环境中,use和assign是多次出现的,但这一操作并不是原子性,也就是在read和load之后。
如果主内存的变量发生改变之后,线程工作内存中的值由于已经加载,不会产生对应变化,也就是私有内存和公共内存中的值不同步,所以计算出来的结果会和预期不一样。
所以对于用volatile修饰的变量,JVM虚拟机只是保证从主存加载到线程工作的值是最新的。解决的是变量读时的可见性问题,但无法保证原子性,对于多个线程访问同一个实例变量还是需要加锁同步。
2.3.1 使用原子类进行i++操作
对实例变量i++操作时除了使用synchronized实现同步外,还可以使用AtomicInteger原子类进行实现。
原子操作是不能分割的整体,没有其他线程能够中断或检查正在原子操作中的变量。一个原子类型就是一个原子操作可用的 类型,他可以在没有锁的情况下做到线程安全。
原子类也并不完全安全
原子类在具有逻辑性的情况下输出结果也具有随机性。
比如一个方法内调用两次原子操作,多个线程执行该方法,还是会出现与你需要结果不一样的情况。
因为单个原子操作虽具有原子性,但是多个不保证这两个方法都执行完才会执行另一个线程的方法。这时候还是需要synchronized关键字。
2.3.2 synchronized代码块有volatile同步功能
关键字synchronized可以使多个线程访问同一个资源具有同步性,而且他还具有将线程工作内存的私有变量与公共内存中的变量同步的功能。
保证了在同一时刻,只有一个线程可以执行某一个方法或某一个代码块。
它包含了两个特性:互斥性和可见性。
对于多线程并发,要着重“外练互斥,内修可见”。