多线程并发系列(五):synchronized线程同步与volatile线程可见

star2017 1年前 ⋅ 304 阅读

非线程安全 是在多个线程对同一个对象中的实例变量进行并发访问,读取的数据不一致,即数据被意外修改过,出现了脏读

synchronized 是 Java 中的关键字,是一种同步锁,可以修改方法、代码块、静态方法、类,使多线程以排队方式进行同步处理。

synchronized 关键字

synchronized 关键字可以保证同一时刻,只有一个线程可以执行某一个方法或某一个代码块。它包含两个特征:互斥性和可见性。

同步 synchronized 不仅可以解决一个线程看到对象处于不一致的状态,还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护之前所有的修改效果。

多线程并发,着重在 外练互斥、内修可见,这是多线程并发的重要技术点。

synchronized 同步方法

在 run 方法前加入 synchronized 关键字,使多个线程在执行 run 方法时,以排队的方式进行同步处理。

当一个线程调用 run 前,先判断 run 方法有没有被上锁,如果上锁,说明有其他线程正在调用 run 方法,必须等其他线程对 run 方法调用结束后才可执行 run 方法。

当一个线程想要执行同步方法里面的代码,线程首先尝试去拿这把锁,如果能够拿到就执行 synchronized 里面的代码。如果不能拿到这把锁,那么这个线程就会不断尝试拿这把锁,直到拿到为止,而且是多个线程同时去争抢这把锁。这样也就实现了排队调用 run 方法的目的,也就达到了同步处理。

synchronized 线程同步

synchronized 可以在任意对象及方法上加锁,而加锁的这段代码称为 互斥区临界区

  1. 方法内的变量为线程安全

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

  2. 实例变量非线程安全

    如果多个线程同时访问一个没有同步的方法,如果两个线程同时操作业务对象中的实例变量,则可能出现线程安全问题。

  3. 多个对象多个锁

    如果多个线程访问同一个类的多个不同的实例的相同名称的同步方法(synchronized),效果是以异步的方式运行的。

    创建了 N 个业务对象,在系统中会产生 N 个锁。虽然方法中使用了 synchronized 关键字,但执行顺序并不是同步的。

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

共享变量读不同步-脏读

synchronized 关键字同步方法可以实现共享变量同步赋值,但取值可能发生脏读,即在读取变量时,此值已被其它线程更改过。

即赋值的方法使用 synchronized 关键字同步,但读取共享变量没有使用此关键字强制同步,就可能会出现异常读取的问题,解决方法是同读的方法加上同步 synchronized 关键字,注意是同一个对象锁。

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

synchronized 锁重入

关键字 synchronized 拥有锁重入的功能,也就是在使用 synchronized 时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁。

即在一个 synchronized 方法 / 块的内部调用本类(同一个 synchronized 对象锁)的其它 synchronized 方法 / 块时,是永远可以得到锁的。

可重入锁:自己可以再次获取自己的内部锁。如果不可重入的话,就会造成死锁。

可重入锁也支持在父子类的继承环境中,子类是完全可以通过 可重入锁 调用父类的同步方法的。

执行异常,自动释放锁

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

同步不具有继承性

子类重写了父类的同步方法(synchronized),但不同继承父类的同步锁,还得在子类的方法中添加 synchronized 关键字。

synchronized 同步代码块块

synchronized 同步方法的弊端

synchronized 同步方法的锁粒度太大了,如果线程执行方法里的某一段代码的时间比较长,因是方法同步,其它线程都得在方法入口一直等待。解决可以使用同步代码块。

synchronized 同步代码块

同步代码块实际与同步方法的锁逻辑是相同的,只是粒度要小,只是锁方法里面的某一段代块。

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

即同步代码块是一半异步,一半同步。不在 synchronized 块中是异步执行,在 synchronized 块中是同步执行。

注意:同步代码块必须注意锁的粒度,通常只同步对类变量的读写操作。

synchronized 代码块间的同步性

在使用 synchronized(this) 代码块时,需要注意,当一个线程访问 object 的一个 synchronized(this) 同步代码块,其他线程对同一个 object 中所有其他 synchronized(this) 同步代码块的访问都将被阻塞,即 synchronized 使用的 对象监视器 是一个,synchronized(this) 代码块也是锁定当前对象。

将任意对象作为监视器

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

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

  1. synchronized 同步方法
    • 对其他 synchronized 同步方法或 synchronized(this) 同步代码块调用呈阻塞状态。
    • 同一时间只有一个线程可以执行 synchronized 同步方法中的代码。
  2. synchronized(this) 同步代码
    • 对其他 synchronized 同步方法或 synchronized(this) 同步代码块调用呈阻塞状态。
    • 同一时间只有一个线程可以执行 synchronized(this) 同步代码块中的代码。

Java 还支持对 任意对象 作为 对象监视器 来实现同步的功能。这个 任意对象 大多数是实例变量及方法的参数,使用格式为 synchronized(非 this 对象),只能作用在代码块上。

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

synchronized(非 this 对象 x) 是将 x 对象本身作为对象监视器,会呈现以下 3 个结论:

  1. 当多个线程同时执行 synchronized(x){} 同步代码块时呈同步效果。
  2. 当其他线程执行 x 对象中 synchronized 同步方法时呈同步效果。
  3. 当其他线程执行 x 对象方法里面的 synchronized(this) 代码块时也呈现同步效果。

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

其实最终总结就是:多线程使用的同步锁是同一个对象,不管是同步方法还是同步代码块,不管是当前多线程并发,还是其它线程,都会呈同步效果。

静态同步 synchronized 方法

关键字 synchronized 还可以应用在 static 静态方法上,这样写是对当前 *.java 文件对应的 Class 类进行持锁,作用也是达到同步效果,和加到非静态方法上的效果是一样的。

静态方法上加锁 与非静态方法上加锁还是有本质上区别的,synchronized 关键字加到静态方法上是给 Class 类加锁,Class 锁对类的所有对象实作起作用;而加到非静态方法上是给对象加锁,只对当前对象起作用。

synchronized(class) 代码块

synchronized(xx.class) 代码块的作用与 synchronized static 方法的作用一样。

内置类与静态内置类的同步

静态类里面的不同方法的同步代码块与其他同步方法分别持有不同的锁(对象监视器),则线程执行是随机异步的。

同步代码块 synchronized(class2) 对 class2 上锁后,其他线程只能以同步的方式调用 class2 中的静态同步方法。

线程同步相关问题

同步方法无限等待与解决

synchronized 同步方法持有的锁是当前对象,若对象有多个同步方法,则可能存在无限等待问题,即执行某一个方法的线程一直持有锁,阻塞其它线程对对象同的其它方法的执行。

解决:使用同步代码块,加不同对象锁,使同一个对象的多个方法可以异步执行。

多线程死锁

若多线程加锁不当,可能出现不同的线程都在等待根本不可能被释放的锁,从而导致所有任务都无法继承完成,即出现了死锁,死锁是必须避免的,因这为会造成线程的 假死

死锁示例:

  1. 同步代码块A使用 lock1 锁,同步代码块A 里面还有一段同步代码块使用 lock2 锁。
  2. 同步代码块B使用 lock2 锁,同步代码块B 里面还有一段同步代码块使用 lock1 锁。

一段代码块或方法有两层锁,外层分别持有不同的锁,但内层锁都是其他线程的外层锁,导致线程互相等待对方持有的锁,就会出现死锁。

注意:嵌套同步代码块容易出现死锁,但不使用嵌套的 synchronized 代码结构也会出现死锁,与嵌套不嵌套无任何关系。

jstack 监死锁:

  1. 首先执行 JDK 自带的 jps 命令,查看 JVM 正在执行的线程的线程ID,线程ID 后面跟了 Run 的是运行的线程。
  2. 执行 jstack -l 3244 ,3244 就是线程ID,可以看到线程栈输出打印多条外线程等待同一锁对象(waiting to lock 0x0420dbb0)。

锁对象被改变

多线程如果同时持有相同的锁,则这些线程是同步的;如果锁对象被改变后,多线程分别获得锁对象持有不同的锁,这些线程就是异步的。

示例:同步代码块使用 String 变量为锁对象 synchronized(str),线程 A 在同步代码块里修改此变量,线程 A 执行后睡眠 几十毫秒,启动 线程 B,线程 B 拿到的是已修改的变量的锁对象。

String 常量池变量锁

JVM 具有 String 常量池缓存功能,如果 synchronized 关键字使用 String 变量,多线程情况下持有的是相同的锁,如果业务耗时较长,其它线程就会被阻塞。

通常情况下,同步 synchronized 代码块不使用 String 变量作为锁对象,而改用其他,如 new Object() 实例化一个 Object 对象,但它并不放入缓存中。

volatile 关键字

volatile 作用

volatile 的作用:是使变量在多个线程间可见。

JVM 设置为 -server 模式时,未使用 volatile 关键字的实例变量存在于公共堆栈和线程入私有堆栈中,JVM 以 server 模式运行,线程一直在私有堆栈中取值,就出现了私有堆栈中的值和公共堆栈中的值不同步。而 volatile 关键字就是强制从公共栈中取得变量值,而不是从线程私有数据栈中取得变量值。

volatile 关键字,强制从公共内存中读取变量的值,增加了实例变量在多个线程之间的可见性。

volatile 与 synchronized 比较

  1. 关键字 volatile 是线程同步的轻量级实现,所以性能比 synchronized 要好,并且 volatile 只能修饰于变量,而 synchronized 可以修饰方法、以及代码块。

    随着 JDK 新版本的发布,synchronized 的执行效率有很大的提升,在开发中使用 synchronized 关键字的概率还是比较大的。

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

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

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

    线程安全包含 原子性可见性,Java 的同步机制都是围绕这两个方面来确保线程安全的。

  5. synchronized 代码块有 volatile 的同步功能。synchronized 可以使多个线程访问同一个资源具有同步性,而且还具有将线程工作内存中的私有变量与公共内存中的变量同步的功能。

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

volatile 提示线程每次从共享内存中读取变量,而不是从私有内存(线程工作内存)中读取。

volatile 本身并不处理数据的原子性,而是强制对数据的读写及时同步到主内存。

volatile 非线程安全原因分析

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

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

在多线程环境下,use 和 assign 是多次出现的,但这一操作并不是原子性,也就是在 read 和 load 后,主内存变量可能被外部更改,而当前线程工作内存中的值由于已加载,不会产生对应的变化,也就是私有内存和公共内存中的变量不同步,这样计算出来的结果就不一样,就产生了线程安全问题。

对于 volatile 修饰的变量,JVM 虚拟机只是保证了主内存加载到线程工作内存的值是最新的。

volatile 关键字解决的是变量读时的可见性问题,但无法保证原子性,对于多个线程访问同一个实例变量,还是需要加锁同步。

原子类进行 i++ 操作

i++ 操作是非原子的,除了使用 synchronized 关键字实现同步外,还可以使用 AtomicInteger 原子类进行实现。使用原子类型在同一执行方法里是原子的,原子类是线程安全的。

原子类并不完全安全,原子类在具有逻辑性的情况下的输出结果也有随机性,原子类的当个方法内是原子操作,但同一个原子类对象执行多个方法,方法与方法之间的调用不是原子的。解决这样的问题必须要同步。

相关参考

  1. 彻底理解 Synchronized
更多内容请访问:IT源点

相关文章推荐

全部评论: 0

    我有话说: