《Java多线程编程核心技术》(二)对象及变量的并发访问

时间:2021-01-12 23:26:18

第二章 对象及变量的并发访问。

1,synchronized

多线程学习是一定会遇到的经典问题,“非线程安全”其实会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是“脏读”,也就是渠道的数据其实是被更改过的,而“线程安全”就是获得的实例变量的值是经过同步处理过的,不会出现脏读的现象。

①方法内的变量为线程安全。“非线程安全”问题存在于“实例变量”中,如果是方法内部的私有变量,则不存在“非线程安全”问题。

②实例变量非线程安全。两个线程访问同一个对象的同步方法时一定是线程安全的。

③关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法(函数)当做锁,所以,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁Lock,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象。但如果多个线程访问多个对象,则JVM会创建多个锁。

④A线程先持有object对象的Lock锁,B线程可以以异步的方式调用object对象中的非synchronized类型的方法。
A线程先持有object对象的Lock锁,B线程如果在这时调用object对象中的synchronized类型的方法则需等待,也就是同步。

⑤脏读,发生脏读的情况是在读取实例变量时,此值已经被其他线程更改过了。

⑥synchronized锁重入。当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。这也证明在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。“可重入锁”的概念是:自己可以再次获取自己的内部锁,比如有1条线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候,还是可以获取的,如果不可锁重入的话,就会造成死锁。可重入锁也支持在父子类继承的环境中。

⑧出现异常,锁自动释放

2,synchronized同步语句块

用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个长时间的任务,那么B线程则必须等待较长的时间,在这种情况下可以使用同步语句块来解决。

①当一个线程访问object的一个synchronized同步代码块时,另一个线程仍然可以访问该object对象中的非synchronized(this)同步代码块。

②在使用同步代码块时需要注意的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个object中所有其他synchronized(this)同步代码块的访问将被堵塞,这说明synchronized使用的“对象监视器”是一个。

③静态同步synchronized方法与synchronized(class)代码块,对对应的Class类进行持锁。

④程序设计时要避免双方互相持有对方的锁的情况,避免死锁。

3,volatile关键字

主要作用是是变量在多个线程中可见。
《Java多线程编程核心技术》(二)对象及变量的并发访问

使用volatile关键字,强制从公共内存中读取变量的值
《Java多线程编程核心技术》(二)对象及变量的并发访问

关键字synchronized和volatile进行一下比较:
1,关键字volatile只能修饰变量,而synchronized可以修饰方法,以及代码块,随着JDK新版本的发布,synchronized关键字在执行效率上得到很大的提升,在开发中时用synchronized关键字的比率还是比较大的。

2,多线程访问volatile不会发生堵塞,而synchronized会出现堵塞。

3,volatile能保证数据的可见性,但不能保证原子性,而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。

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

关键字volatile主要使用的场合是在多个线程中可以感知实例变量被更改了,并且可以获得最新的值使用,也就是用多线程读取共享变量是可以获得最新值使用。
关键字volatile提示线程每次从共享内存中读取变量,而不是从私有内存中读取,这样就保证了同步数据的可见性。但在这里需要注意的是:如果修改实例变量中的数据,比如i++,也就是i=i+1,则这样的操作其实并不是一个原子操作,也就是非线程安全的。表达式i++的操作步骤分解如下:
(1)从内存中取出i的值。
(2)计算i的值。
(3)将i的值写到内存中。

假如在第二步计算值的时候,另外一个线程也修改i的值,那么这个时候就会出现脏读数据。解决的办法其实就是使用synchronized关键字,这个知识点在前面的案例中已经介绍过了。所以说volatile本身并不处理数据的原子性,而是强制对数据的读写及时影响到主内存的。
用图来演示一下使用关键字volatile时出现非线程安全的原因。变量在内存中工作的过程如下图所示:
(1)read和load阶段:从主存复制变量到当前线程工作内存。
(2)use和assign阶段:执行代码,改变共享变量值。
(3)store和write阶段:用工作内存数据刷新主存对应变量的值。
《Java多线程编程核心技术》(二)对象及变量的并发访问
在多线程环境中,use和assign是多次出现的,但这一操作并不是原子性,也就是在read和load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,也就是私有内存和公共内存中的变量不用补,所以计算出来的结果汇合预期不一样,也就出现了非线程安全问题。

对于用volatile修饰的变量,JVM虚拟机只是保证从主内存加载到线程工作内存的值是最新的,例如线程1和线程2在进行read和load的操作中,发现主内存中的count的值都是5,那么都会加载这个最新的值。也就是说,volatile关键字解决的是变量读取时的可见性问题,但无法保证原子性,对于多个线程访问同一个实例变量还是需要加锁同步。

使用原子类进行i++
除了在i++操作是使用synchronized关键字实现同步外,还可以使用AtomicInteger原子类进行实现。
原子操作是不能分割的整体,没有其他线程能够中断或检查正在原子操作中的变量。一个原子类型就是一个原子操作可用的类型,它可以在没有锁的情况下做到线程安全。
关键字synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法或某一个代码块。它包含两个特征:互斥性和可以见性。同步synchronized不仅可以解决一个线程看到的对象处于不一致的状态,还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护之前所有的修改效果。