第 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 本章总结

  学习完多线程同步后就可以有效控制线程间处理数据的顺序性,及对处理后的数据进行有效值的保证,更好地对线程执行结果有正确的预期。

Java多线程编程核心技术-第2章-对象及变量的并发访问-读书笔记的更多相关文章

  1. java多线程编程核心技术(二)--对象及变量的并发访问

    1.方法内部的私有变量是线程安全的,实例变量是线程不安全的. 2.A线程先持有object对象的lock锁,B线程可以以异步的方式调用object对象中的非synchronized类型的方法. 3.A ...

  2. Java多线程编程核心技术-第1章-Java多线程技能-读书笔记

    第 1 章 Java 多线程技能 本章主要内容 线程的启动 如何使线程暂停 如何使线程停止 线程的优先级 线程安全相关的问题 1.1 进程和多线程的概念及线程的优点 进程是操作系统结构的基础:是一次程 ...

  3. Java多线程编程核心技术-第5章-定时器 Timer-读书笔记

    第 5 章 定时器 Timer 定时 / 计划功能在移动开发领域使用较多,比如 Android 技术.定时计划任务功能在 Java 中主要使用的就是 Timer 对象,他在内部使用多线程的方式进行处理 ...

  4. Java多线程编程核心技术-第4章-Lock的使用-读书笔记

    第 4 章 Lock 的使用 本章主要内容 ReentrantLocal 类的使用. ReentrantReadWriteLock 类的使用. 4.1 使用 ReentrantLock 类 在 Jav ...

  5. Java多线程编程核心技术,第二章,对象和变量并发访问

    1,方法内部变量是线程安全的 2,实例变量非线程安全 3,synchronized是锁对象不是锁方法(锁对象是可以访问非synchronized方法,不可访问同个和其他synchronized方法 4 ...

  6. Java多线程编程核心技术-第7章-拾遗增补-读书笔记

    第 7 章 拾遗增补 本章主要内容 线程组的使用. 如何切换线程状态. SimpleDataFormat 类与多线程的解决办法. 如何处理线程的异常. 7.1 线程的状态 线程对象在不同的运行时期有不 ...

  7. java多线程编程核心技术——第三章

    第一节等待/通知机制 1.1不使用等待/通知机制实现线程间的通讯 1.2什么是等待/通知机制 1.3等待/通知机制的实现 1.4方法wait()锁释放与notify()锁不释放 1.5当interru ...

  8. java多线程编程核心技术——第七章补漏拾遗

    本章主要知识点: 1)线程组的使用 2)如何切换线程状态 3)SimpleDateFormat类与多线程的解决方法 4)如何处理线程异常. 这本书基本来到了终点,其实在第四章来说,核心(基础)的线程知 ...

  9. java多线程编程核心技术——第四章总结

    第一节使用ReentrantLock类 1.1使用ReentrantLock实现同步:测试1 1.2使用ReentrantLock实现同步:测试2 1.3使用Condition实现等待/同步错误用法与 ...

随机推荐

  1. Linux性能优化实战学习笔记:第三十一讲

    一.上节回顾 上一节,我们一起回顾了常见的文件系统和磁盘 I/O 性能指标,梳理了核心的 I/O 性能观测工具,最后还总结了快速分析 I/O 性能问题的思路. 虽然 I/O 的性能指标很多,相应的性能 ...

  2. [LeetCode] 152. Maximum Product Subarray 求最大子数组乘积

    Given an integer array nums, find the contiguous subarray within an array (containing at least one n ...

  3. Python __name__的使用

    __name__是什么 * __开头代表是系统变量; * __name__ 是标识模块名字的系统变量. 当前模块是主模块时, 模块名就是"__main__"; 当模块是被调用(im ...

  4. SpringBoot+EventBus使用教程(二)

    简介 继续上篇,本篇文章介绍如何集成spring-boot-starter-guava-eventbus使用EventBus,最新的版本好像已经不叫spring-boot-starter-guava- ...

  5. ECMAScript 初探 - 基础篇

    ECMAScript 语言的标准是由 Netscape.Sun.微软.Borland 等公司基于 JavaScript 和 JScript 锤炼.定义出来的. ECMAScript 仅仅是一个描述,定 ...

  6. Beta冲刺(4/7)——2019.5.26

    作业描述 课程 软件工程1916|W(福州大学) 团队名称 修!咻咻! 作业要求 项目Beta冲刺(团队) 团队目标 切实可行的计算机协会维修预约平台 开发工具 Eclipse 团队信息 队员学号 队 ...

  7. 一步一步的理解javascript的预编译

    首先,我们要知道javascript是单线程.解释性语言.所谓解释性语言,就是翻译一句执行一句.而不是通篇编译成一个文件再去执行. 其实这么说还没有这么直观,读一句执行一句那是到最后的事了.到JS执行 ...

  8. JavaScaript学习笔记第(一)

    js由三部分组成,分别是ECMAScript.DOM.BOM 其中ECMAScript规定了js的语法 js是一门解释型语言.脚本语言.动态类型语言.基于对象语言 书写js代码和CSS一样,有三个书写 ...

  9. html页面的渲染And<script>位置的影响

    周末加班敲代码的时用到了<script>标签,突然想到了一个问题:别的自测项目里面<script>我把他放在了不同位置,这里应该会对代码的执行与渲染后影响吧?于是今天专门进行了 ...

  10. 我是如何一步步编码完成万仓网ERP系统的(三)登录

    https://www.cnblogs.com/smh188/p/11533668.html(我是如何一步步编码完成万仓网ERP系统的(一)系统架构) https://www.cnblogs.com/ ...