【Java多线程学习笔记二】对象及变量的访问控制

【Java多线程学习笔记二】对象及变量的访问控制_第1张图片

对象及变量的并发访问

        • 2. 对象及变量的并发访问
          • 2.1 synchronized 同步方法
            • 2.1.1 方法内的变量为线程安全
            • 2.1.2 实例变量非线程安全
            • 2.1.3 多个对象多个锁
            • 2.1.4 synchronized 方法与锁对象
            • 2.1.5 脏读
            • 2.1.6 synchronized 锁重入
            • 2.1.7 出现异常,锁自动释放
            • 2.1.8 同步不具有继承性
          • 2.2 synchronized 同步代码块
            • 2.2.1 synchronized 方法的弊端
            • 2.2.2 synchronized 同步代码块的使用
            • 2.2.3 用同步代码块解决同步方法的弊端
            • 2.2.4 一半同步,一半异步
            • 2.2.5 synchronized 代码块间的同步性
            • 2.2.6 验证同步 synchronized(this) 代码块是锁定当前对象的
            • 2.27 将任意对象作为监视器
            • 2.2.8 细化验证三个结论
            • 2.2.9 静态同步 synchronized 方法与 synchronized(class) 代码块
            • 2.2.10 数据类型 String 的常量池特性
            • 2.2.11 同步 synchronized 方法无限等待与解决
            • 2.2.12 多线程的死锁
            • 2.2.13 内置类与静态内置类
          • 2.3 valatile 关键字
            • 2.3.1 关键字 volatile 与死循环
            • 2.3.2 解决同步死循环
            • 2.3.4 volatile 非原子的特性
            • 2.3.5 使用原子类进行 i++ 操作
            • 2.3.6 原子类也并不完全安全
            • 2.3.7 synchronized 代码块有 volatile 同步的功能


2. 对象及变量的并发访问

2.1 synchronized 同步方法

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

2.1.1 方法内的变量为线程安全

非线程安全问题存在于【实例变量】中,如果是方法内部的私有变量,则不存在非线程安全问题,所得结果也就是线程安全的了。

方法中的变量不存在非线程安全的问题,永远都是线程安全的,这时方法内部的变量是私有的特性造成的。

2.1.2 实例变量非线程安全

如果是多个线程共同访问 1 个对象中的实例变量,则有可能出现【非线程安全】问题。

为了解决非线程安全问题,只要在方法前加关键字 synchronized 即可。

2.1.3 多个对象多个锁

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

但如果多个线程访问多个对象,则 JVM 会创建多个锁。

同步的单词为 synchronized,异步的单词为 asynchronized

2.1.4 synchronized 方法与锁对象

调用关键字 synchronized 声明的方法一定是排队运行的,另外,只有共享资源的读写访问才需要同步化,如果不是共享资源,那么根本就没有同步的必要。

两个线程 A 和 B:

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

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

当 A 线程调用 anyObject 对象加入 synchronized 关键字的 X 方法时,A 线程就获得了 X 方法锁,更准确的讲,是获得了对象的锁,所以其他线程必须等 A 线程执行完毕后才可以调用 X 方法,但 B 线程可以随意调用其他非 synchronized 同步方法

当 A 线程调用 anyObject 对象加入 synchronized 关键字的 X 方法时,A 线程就获得了 X 方法所在的对象的锁,所以其他线程必须等 A 线程执行完毕后才可以调用 X 方法,而 B 线程如果调用声明了 synchronized 关键字的非 X 方法时,必须等 A 线程将 X 方法执行完毕后,也就是释放对象锁后才可以调用,这时 A 线程已经执行了一个完整的任务,故不存在脏读的情况。

脏读一定会出现操作实例变量的情况,这就是不同线程【争抢】实例变量的结果。

2.1.6 synchronized 锁重入

【可重入锁】的概念是:自己可以再次获取自己的内部锁

比如有 1 个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这俄格对象的锁的时候,还是可以获取的,如果不可锁重入的话,就会造成死锁。

关键字 synchronized 拥有锁重入的功能,也就是在使用 synchronized 时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。这也证明在一个 synchronized 方法/块的内部调用本类的其他 synchronized 方法/块时,是永远可以得到锁的。

当存在父子类继承关系时,子类是完全可以通过【可重入锁】调用父类的同步方法的。

2.1.7 出现异常,锁自动释放

当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

2.1.8 同步不具有继承性

同步不能继承,还得在子类的方法中添加 synchronized 关键字。

2.2 synchronized 同步代码块

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

2.2.1 synchronized 方法的弊端

比如 A 线程调用同步方法执行一个长时间的任务,那么 B 线程必须等待比较长的时间,耗时较长。

2.2.2 synchronized 同步代码块的使用

当两个并发线程访问同一个对象 object 中的 synchronized 同步代码块时,一段时间内只能有一个线程被执行,另一个线程必须等待当前线程执行完以后才能执行该代码块。

2.2.3 用同步代码块解决同步方法的弊端
2.2.4 一半同步,一半异步

不在 synchronized 块中就是异步执行,在 synchronized 块中就是同步执行。

2.2.5 synchronized 代码块间的同步性

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

2.2.6 验证同步 synchronized(this) 代码块是锁定当前对象的

和 synchronized 方法一样,synchronized(this) 代码块也是锁定当前对象的。

2.27 将任意对象作为监视器

多个线程调用同一个对象中的不同名称的 synchronized 同步方法或 synchronized(this) 同步代码块时,调用的效果就是按顺序执行,也就是同步的,阻塞的。

这说明 synchronized 同步方法或 synchronized(this) 同步代码块分别有两种作用。

  1. synchronized 同步方法
    1. 对其他 synchronized 同步方法或 synchronized(this) 同步代码块调用呈阻塞状态。
    2. 同一时间只有一个线程可以执行 synchronized 同步方法中的代码。
  2. synchronized(this) 同步代码块
    1. 对其他 synchronized 同步方法或 synchronized(this) 同步代码块调用呈阻塞状态。
    2. 同一时间只有一个线程可以执行 synchronized(this) 同步代码块中的代码。
  3. 对任意对象作为【对象监视器】来实现同步的功能
    1. 在多个线程持有【对象监视器】为同一个对象的前提下,同一时间只有一个线程可以执行 synchronized(非 this 对象 x)同步代码块中的代码。

锁定 this 对象具有一定的有点:如果一个类中有很多个 synchronized 方法,这时虽然能实现同步,但会受到阻塞,所以影响效率;但如果使用同步代码块锁非 this 对象,则 synchronized(非 this) 代码块中的程序与同步方法是异步的,不与其他锁 this 同步方法争抢 this 锁,则可大大提高运行效率。

使用 synchronized(非 this 对象 x) 同步代码块格式进行操作时,对象监视器必须是同一个对象。如果不是同一个对象监视器,运行的结果就是异步调用了,就会交叉运行。

同步代码块放在非同步 synchronized 方法中进行声明,并不能保证调用方法的线程的执行同步/顺序性,也就是线程调用方法的顺序是无序的,虽然在同步代码块中执行的顺序是同步的这样极易出现【脏读】问题。

2.2.8 细化验证三个结论
  • 当多个线程同时执行 synchronized(x){} 同步代码块时呈同步效果。
  • 当其他线程执行 x 对象中 synchronized 同步方法时呈同步效果。
  • 当其他线程执行 x 对象方法里面的 synchronized(this) 代码块时也呈现同步效果。

但需要注意:如果其他线程调用不加 synchronized 关键字的方法时,还是异步调用。

2.2.9 静态同步 synchronized 方法与 synchronized(class) 代码块

关键字 synchronized 还可以应用在 static 静态方法上,如果这样写,*那是对当前的 .java 文件对应的 Class 类进行持锁Class 锁可以对类的所有对象实例起作用(即使是两个不同对象)

2.2.10 数据类型 String 的常量池特性

在大多数情况下,同步 synchronized 代码块都不能使用 String 作为锁对象,而改用其他,比如 new Object() 实例化一个 Object 对象,但不把他放入缓存中。

2.2.11 同步 synchronized 方法无限等待与解决

同步方法容易造成死循环,因为同步方法锁住的是实例对象,其他线程无法获取锁对象时,就无法执行方法。

2.2.12 多线程的死锁

2.2.13 内置类与静态内置类

2.3 valatile 关键字

关键字 volatile 的主要作用是使变量在多个线程间可见。

2.3.1 关键字 volatile 与死循环

2.3.2 解决同步死循环

对变量赋 volatile 关键字,可以使当前线程访问变量时,强制性地从公共堆栈中进行取值。

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

synchronized 与 volatile 比较:

  • 关键字 volatile 是线程同步的轻量级实现,所以 volatile性能肯定比 synchronized 要好,并且 volatile 只能修饰变量,而 sychronized 可以修饰方法,以及代码块。
  • 多线程访问 volatile 不会发生阻塞,而 sychronized 会出现阻塞。
  • volatile 能保证数据的可见性,但不能保证原子性;而 synchronized 可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据同步。
  • 关键字 volatile 解决的是变量在多个线程之间的【可见性】,而 sychronized 关键字解决的是多个线程之间访问资源的【同步性】。
2.3.4 volatile 非原子的特性

关键字 volatile 虽然增加了实例变量在多个线程之间的可见性,但它却不具备同步性,那么也不具备原子性。

关键字 volatile 主要使用的场合是在多个线程中可以感知实例变量被更改了,并且可以获取最新的值使用,也就是用多线程读取共享变量时可以获得最新值使用。

关键字 volatile 提示线程每次从共享内存中读取变量,而不是从私有内存中读取,这样就保证了同步数据的可见性。

表达式 i++ 的操作步骤:

  • 从内存中取出 i 的值;
  • 计算 i 的值;
  • 将 i 的值写入内存中;

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

  • read 和 load 阶段:从主存复制变量到当前线程工作内存;
  • use 和 assign 阶段:执行代码,改变共享变量值;
  • store 和 write 阶段:用工作内存数据刷新主存对应变量的值;

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

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

2.3.5 使用原子类进行 i++ 操作

除了在 i++ 操作时使用 synchronized 关键字实现同步外,还可以使用 AtomicInteger 原子类实现

原子操作是一个不能分割的操作,没有其他线程能够中断或检查正在原子操作中的变量。

private AtomicInteger count = new AtomicInteger(0);

count.incrementAndGet();
2.3.6 原子类也并不完全安全

原子类在具有有逻辑性的情况下输出结果也具有随机性。

原因在于:addAndGet( ) 方法是原子性的,但方法和方法之间的调用却不是原子性的,这样的问题必须要用同步。

2.3.7 synchronized 代码块有 volatile 同步的功能

关键字 synchronized 可以使多个线程访问同一个资源具有同步性,而且它还具有将线程工作内存中的私有变量与公共内存中的变量同步的功能。

关键字 synchronized 可以保证在同一时刻,只有一个线程可以执行某一个方法或某一个代码块。它包含两个特征:互斥性和可见性。同步 synchronized 不仅可以解决一个线程看到对象处于不一致的状态,还可以保证同步方法或者代码块的每个线程,都能看到由同一个锁保护之前所有的修改效果。

学习多线程并发,要注重【外练互斥,内修可见】,这是掌握多线程、学习多线程并发的重要技术点。

你可能感兴趣的:(Java多线程)