第 2 章 对象及变量的并发访问
本章主要内容
synchronized 对象监视器为 Object 时的使用。synchronized 对象监视器为 Class 时的使用。非线程安全是如何出现的。关键字 volatile 的主要作用。关键字 volation 与 synchronized 的区别及使用情况。
2.1 synchronized 同步方法
“非线程安全”其实会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是“脏读”,也就是取到的数据其实是被更改过的。而“线程安全”就是以获得的实例变量的值是经过同步处理的,不会出现脏读的现象。
2.1.1 方法内的变量为线程安全
“非线程安全”问题存在于“实例变量”中,如果是方法内部的私有变量,则不存在“非线程安全”问题,所得结果也就是“线程安全”的了。
方法中的变量不存在非线程安全问题,永远都是线程安全的。这是方法内部的变量是私有的特性造成的。
2.1.2 实例变量非线程安全
如果多个线程共同访问 1 个对象中的实例变量,则有可能出现“非线程安全”问题。
用线程访问的对象中如果有多个实例变量,则运行的结果有可能出现交叉的情况。如果对象仅有 1 个实例变量,则有可能出现覆盖的情况。
在两个线程访问同一个对象中的同步方法时一定是线程安全的。
2.1.3 多个对象多个锁
两个线程分别访问同一个类的两个不同实例的相同名称的同步方法,效果却是以异步的方式运行的。
如果多个线程访问多个对象,则 JVM 会创建多个锁。
同步的单词为 synchronized,异步的单词是 asynchronized.
2.1.4 synchronized 方法与锁对象
调用用关键字 synchronized 声明的方法一定是排队运行的。另外需要牢牢记住“共享”这两个字,只有共享资源的读写访问才需要同步化,如果不是共享资源,那么根本就没有同步的必要。
A 线程先持有 object 对象的 Lock 锁,B 线程可以以异步的方式调用 object 对象中的非 synchronized 类型的方法。
A 线程先持有 object 对象的 Lock 锁,B 线程如果在这时调用 object 对象中的 synchronized 类型的方法则需要等待,也就是同步。
2.1.5 脏读
虽然在赋值时进行了同步,但在取值时有可能出现一些意想不到的意外,这种情况就是脏读(dirtyRead)。发生脏读的情况是在读取实例变量时,此值已经被其他线程更改过了。
脏读是通过 synchronized 关键字解决的。
当 A 线程调用 anyObject 对象加入 synchronized 关键字的 X 方法时,A 线程就获得了 X 方法锁,更准确地讲,是获得了对象的锁,所以其他线程必须等 A 线程执行完毕才可以调用 X 方法,但 B 线程可以随意调用其他的非 synchronized 同步方法。
当 A 线程调用 anyObject 对象加入 synchronized 关键字的 X 方法时,A 线程就获得了 X 方法所在对象的锁,所以其他线程必须等 A 线程执行完毕才可以调用 X 方法,而 B 线程如果调用声明了 synchronized 关键字的非 X 方法时,必须等 A 线程将 X 方法执行完,也就是释放对象锁后才可以调用。
脏读一定会出现操作实例变量的情况下,这就是不同线程“争抢”实例变量的结果。
2.1.6 synchronized 锁重入
关键字 synchronized 拥有锁重入的功能,也就是在使用 synchronized 时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。这也证明在一个 synchronized 方法/块的内部调用本类的其他 synchronized 方法/块时,是永远可以得到锁的。
“可重入锁”的概念是:自己可以造次获取自己的内部锁。
可重入锁也支持在父子类继承的环境中。
当存在父子类继承关系时,子类是完全可以通过“可重入锁”调用父类的同步方法的。
2.1.7 出现异常,锁自动释放
当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
2.1.8 同步不具有继承性
同步不可以继承。
2.2 synchronized 同步语句块
用关键字 synchronized 声明方法在某些情况下是有弊端的,比如 A 线程调用同步方法执行一个长时间的任务,那么 B 线程则必须等待比较长时间,这样的抢矿下可以使用 synchronized 同步语句块来解决。
2.2.1 synchronized 方法的弊端
弊端就是 A 线程调用同步方法执行一个长时间的任务,那么 B 线程则必须等待比较长时间。解决这样的问题可以使用 synchronized 同步块。
2.2.2 synchronized 同步代码块的使用
当两个并发线程访问同一个对象 object 中的 synchronized(this) 同步代码块中,一段时间内只能有一个线程被执行,另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
2.2.3 用同步代码块解决同步方法的弊端
当一个线程访问 object 的一个 synchronized 同步代码块时,另一个线程仍然可以访问该 object 对象中的非 synchronized(this) 同步代码块。
2.2.4 一半异步,一半同步
不在 synchronized 块中就是异步执行,在 synchronized 块中就是同步执行。
2.2.5 synchronized 代码块间的同步性
在使用同步 synchronized(this) 代码块时需要注意的是,当一个线程访问 object 的一个 synchronized(this) 同步代码块时,其他线程对同一个 object 中所有其他 synchronized(this) 同步代码块的访问将被阻塞,这说明 synchronized 使用的 “对象监视器” 是一个。
2.2.6 验证同步 synchronized(this) 代码块是锁定当前对象的
和 synchronized 方法一样,synchronized(this) 代码块也是锁定当前对象的。
2.2.7 将任意对象作为对象监视器
多个线程调用同一个对象中的不同名称的 synchronized 同步方法或 synchronized(this) 同步代码块时,调用的效果就是按顺序执行,也就是同步的,阻塞的。
这说明 synchronized 同步方法或 synchronized(this) 同步代码块分别有两种作用。
(1)synchronized 同步方法
1)对其他 synchronized 同步方法或 synchronized(this) 同步代码块调用呈阻塞状态。
2)同一时间只有一个线程可以执行 synchronized 同步方法中的代码。
(2)synchronized(this) 同步代码块
1)对其他 synchronized 同步方法或 synchronized(this) 同步代码块调用呈阻塞状态。
2)同意时间只有一个线程可以执行 synchronized(this) 同步代码块中的代码。
使用 synchronized(this) 格式来同步代码块,其实 Java 还支持对 “ 任意对象 ” 作为 “ 对象监视器 ” 来实现同步的功能。这个 “ 任意对象 ” 大多数是实例变量及方法的参数,使用格式为 synchronized(非 this 对象)。
根据前面对 synchronized(this) 同步代码块的作用总结可知,synchronized(非 this 对象) 格式的作用只有 1 种:synchronized(非 this 对象 x)同步代码块。
1)在多个线程持有 “对象监视器” 为同一个对象的前提下,同一时间只有一个线程可以执行 synchronized(非 this 对象 x)同步代码块中的代码。
2)当持有 “ 对象监视器 ” 为同一个对象的前提下,同一时间只有一个线程可以执行 synchronized( 非 this 对象 x )同步代码块中的代码。
锁非 this 对象具有一定的优点:如果在一个类中有很多个 synchronized 方法,这时虽然能实现同步,但会受到阻塞,所以影响运行效率;但如果使用同步代码块锁非 this 对象,则 synchronized(非 this) 代码块中的程序与同步方法是异步的,不与其他锁 this 同步方法争抢 this 锁,则可大大提高运行效率。
使用 “ synchronized( 非 this 对象 x ) 同步代码块”格式进行同步操作时,对象监视器必须是同一个对象。如果不是同一个对象监视器,运行的结果就是异步调用了,就会交叉运行。
同步代码块放在非同步 synchronized 方法中进行声明,并不能保证调用方法的线程的执行同步 / 顺序性,也就是线程调用方法的顺序是无须的,虽然在同步块中执行的顺序是同步的,这样极易出现 “ 脏读 ” 问题。使用 “ synchronized( 非 this 对象 x ) 同步代码块 ” 格式也可以解决 “ 脏读 ” 问题。
2.2.8 细化验证 3 个结论
“ synchronized ( 非 this 对象 x ) ” 格式的写法是将 x 对象本身作为 “ 对象监视器 ”,这样就可以得出以下 3 个结论:
1)当多个线程同时执行 synchronized(x){} 同步代码块时呈同步效果。
2)当其他咸亨执行 x 对象中 synchronized 同步方法时呈同步效果。
3)当其他线程执行 x 对象方法里面的 synchronized(this) 代码块时也呈现同步效果。
但需要注意:如果其他线程调用不加 synchronized 关键字的方法时,还是异步调用。
2.2.9 静态同步 synchronized 方法与 synchronized(class) 代码块
关键字 synchronized 还可以应用在 static 静态方法上,如果这样写,那就是对当前的 *.java 文件对应的 Class 类进行持锁。
给静态方法加关键字 synchronized 和将 synchronized 关键字加到非 static 方法上使用的效果是一样的,都是同步的效果。其实还是有本质上的不同的,synchronized 关键字加到 static 静态方法上是给 Class 类上锁,而 synchronized 关键字加到非 static 静态方法上是给对象上锁。而 Class 锁可以对类的所有对象实例起作用。
同步 synchronized(class) 代码块的作用其实和 synchronized static 方法的作用一样。
2.2.10 数据类型 String 的常量池特性
在 JVM 中具有 String 常量池缓存的功能。
将 synchronized(string) 同步块与 String 联合使用时,要注意常量池带来的一些例外。
如果 String 的两个值是相同的,两个线程持有相同的锁,就会造成一个线程不行执行,这就是 String 常量池所带来的问题。因此在大多数的情况下,同步 synchronized 代码块都不使用 String 作为锁对象,而改用其他,比如 new Object() 实例化一个 Object 对象,但它并不放入缓存中。
2.2.11 同步 synchronized 方法无限等待与解决
同步方法容易造成死循环。可以使用同步块来解决问题。
2.2.12 多线程的死锁
Java 线程死锁是一个经典的多线程问题,因为不同的线程都在等待根本不可能被释放的锁,从而导致所有的任务都无法继续完成。在多线程技术中。“死锁”是必须避免的,因为这会造成线程的“假死”。
可以使用 JDK 自带的工具来监测是否有死锁的现象。jps 命令、jstack 命令。
死锁是程序设计的 Bug,在设计程序时就要避免双方互相持有对方的锁的情况。
2.2.13 内置类与静态内置类
关键字 synchronized 的知识点还涉及内置类的使用。
2.2.14 内置类与同步:实验 1
在内置类中有两个同步方法,但使用的确实不同的锁,打印的结果也是异步的。
2.2.15 内置类与同步:实验 2
同步代码块 synchronized(class2) 对 class2 上锁后,其他线程只能以同步的方式调用 class2 中的静态同步方法。
2.2.16 锁对象的改变
在将任何数据类型作为同步锁时,需要注意的是,是否有多个线程同时持有锁随想,如果同时持有相同的锁对象,则这些线程之间就是同步的;如果分别获得锁对象,这些线程之间就是异步的。
只要对象不变,即使对象的属性被改变,运行的结果还是同步。
2.3 volatile 关键字
关键字 volatile 的主要作用是使变量在多个线程间可见。
2.3.1 关键字 volatile 与死循环
如果不是在多继承的情况下,使用继承 Thread 类和实现 Runnable 接口在取得程序运行的结果上并没有什么太大的区别。如果一旦出现“多继承”的情况,则用实现 Runnable 接口的方式来处理多线程的问题就是很有必要的。
2.3.2 解决同步死循环
在方法中处理 while() 循环,导致程序不能继续执行后面的代码,线程就无法停止下来。解决的办法是用多线程技术。
将 while() 循环的执行放入线程中,然后出现死循环,解决的办法是使用 volatile 关键字。
关键字 volatile 的作用是强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值。
2.3.3 解决异步死循环
通过使用 volatile 关键字,强制的从公共内存中读取变量的值。
使用 volatile 关键字增加了实例变量在多个线程之间的可见性。但 volatile 关键字最致命的缺点是不支持原子性。
下面将关键字 synchronized 和 volatile 进行一下比较:
1)关键字 volatile 是线程同步的轻量级实现,所以 volatile 性能肯定比 synchronized 要好,并且 volatile 只能修饰于变量,而 synchronized 可以修饰方法,以及代码块。随着 SDK 新版本的发布,synchronized 关键字在执行效率上得到大提升,在开发中使用 synchronized 关键字的比率还是比较大的。
2)多线程访问 volatile 不会发生阻塞,而 synchronized 会出现阻塞。
3)volatile 能保证数据的可见性,但不能保证原子性;而 synchronized 可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。
4)再次重申一下,关键字 volatile 解决的是变量在多个线程之间的可见性;而 synchronized 关键字解决的是多个线程之间访问资源的同步性。
线程安全包含原子性和可见性两个方面,Java 的同步机制都是围绕这两个方面来确保线程安全的。
2.3.4 volatile 非原子的特性
关键字 volatile 虽然增加了实例变量在多个线程之间的可见性,但它不具备同步性,那么也就不具备原子性。
关键字 volatile 主要使用的场合是在多个线程中可以感知实例变量被更改了,并且可以获得最新的值使用,也就是用多线程读取共享变量时可以获得最新值使用。
关键字 volatile 提示线程每次从共享内存中读取变量,而不是从私有内存中读取,这样就保证了同步数据的可见性。
表达式 i++ 的操作步骤分解如下:
1)从内存中取出 i 的值;
2)计算 i 的值;
3)将 i 的值写到内存中。
假如在第 2 步计算值的时候,另外一个线程也修改 i 的值,那么这个时候就会出现脏数据。解决的办法其实就是使用 synchronized 关键字。所以说 volatile 本身并不处理数据的原子性,而是强制对数据的读写及时影响到主内存的。
变量在内存中工作的过程如下图:
由此,可以得出一下结论:
1)read 和 load 阶段:从主存复制变量到当前线程工作内存;
2)use 和 assign 阶段:执行代码,改变共享变量值;
3)store 和 write 阶段:用工作内存数据刷新主存对应变量的值。
在多线程环境中,use 和 assign 时多次出现的,但这一操作并不是原子性,也就是在 read 和 load 之后,如果主内存 count 变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,也就是私有内存和共有内存中的变量不同步,所以计算出来的结果会和预期不一样,也就出现了非线程安全问题。
对于用 volatile 修饰的变量,JVM 虚拟机只是保证从主内存加载到线程工作内存的值是最新的。也就是说,volatile 关键字解决的是变量读时的可见性问题,但无法保证原子性,对于多个线程访问同一个实例变量还是需要加锁同步。
2.3.5 使用原子类进行 i++ 操作
除了在 i++ 操作时使用 synchronized 关键字实现同步外,还可以使用 AtomicInteger 原子类进行实现。
原子操作是不能分割的整体,没有其他线程能够中断或检查正在原子操作中的变量。一个原子(atomic)类型就是一个原子操作可用的类型,它可以在没有锁的情况下做到线程安全(thread-safe)。
2.3.6 原子类也并不完全安全
原子类在具有有逻辑性的情况下输出结果也具有随机性。
出现这种情况是因为方法是原子的,但方法和方法之间的调用却不是原子的。解决这样的问题必须要用同步。
2.3.7 synchronized 代码块有 volatitle 同步的功能
关键字 synchronized 可以使多个线程访问同一个资源具有同步性,而且它还具有将线程工作内存中的私有变量与公共内存中的变量同步的功能。
关键字 synchronized 可以保证在同一时刻,只有一个线程可以执行某一个方法或某一个代码块。它包含两个特征:互斥性和可见性。同步 synchronized 不仅可以解决一个线程看到对象处于不一致的状态,还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护之前所有的修改效果。
2.4 本章总结
学习完多线程同步后就可以有效控制线程间处理数据的顺序性,及对处理后的数据进行有效值的保证,更好地对线程执行结果有正确的预期。