Effective Java(3rd)-Item78 同步对共享可变数据的访问

  synchronized关键字确保一次只有一个线程可以执行一个方法或块。许多程序员认为同步只是一种互斥的方法,以防止一个线程在另一个线程修改对象时看到对象处于不一致的状态。在这个观点中,对象以一致的状态创建(item17),并由访问它的方法锁定。这些方法观察状态,并可选地引起状态转换,将对象从一个一致的状态转换为另一个一致的状态。正确使用同步可以保证没有方法会观察到处于不一致状态的对象。

  这种观点是正确的,但它只是故事的一半。没有同步,一个线程的更改可能对其他线程不可见。同步不仅阻止线程观察处于不一致状态的对象,而且确保每个进入同步方法或块的线程都能看到由同一锁保护的所有先前修改的效果。
  语言规范保证读取或写入变量是原子性的,除非变量的类型是long或double [JLS, 17.4, 17.7]。换句话说,读取long或double之外的变量将保证返回某个线程存储在该变量中的值,即使多个线程同时修改该变量,并且没有同步。
  您可能听说过,为了提高性能,在读取或写入原子数据时应该避免同步。这种建议大错特错。虽然语言规范保证线程在读取字段时不会看到任意值,但它不保证由一个线程编写的值对另一个线程可见。同步是线程之间可靠通信以及互斥所必需的。这是由于语言规范中称为内存模型的一部分,它指定了一个线程所做的更改何时以及如何对其他线程可见[JLS, 17.4;Goetz06, 16)。
  即使数据是原子可读和可写的,无法同步访问共享可变数据的后果也可能是可怕的。考虑从一个线程停止到另一个线程的任务。.库提供Thread.stop方法,但是这个方法很久以前就被弃用了,因为它本质上是不安全的——使用它会导致数据损坏。不要使用Thread.stop.从一个线程停止到另一个线程的推荐方法是让第一个线程轮询一个布尔字段,该字段最初为false,但第二个线程可以将其设置为true,以指示第一个线程将停止自身。由于读写布尔字段是原子性的,一些程序员在访问该字段时不需要同步:

image.png

  您可能希望这个程序运行大约一秒钟,之后主线程将stoprequired设置为true,从而导致后台线程的循环终止。
然而,在我的机器上,程序永远不会终止:后台线程永远循环!
  问题是,在缺乏同步的情况下,无法保证后台线程何时(如果有的话)看到主线程所做的stoprequest值的更改。在缺乏同步的情况下,虚拟机可以很好地转换这段代码:


image.png

变成


image.png

  这种优化称为提升,这正是OpenJDK服务器虚拟机所做的。其结果是活力丧失:这个程序没有取得进展。解决此问题的一种方法是同步对stoprequired字段的访问。程序在大约一秒内结束,正如预期:

image.png

  注意,写方法(requestStop)和读方法(stoprequired)都是同步的。仅同步写方法是不够的!除非读和写操作同步,否则不能保证同步工作。有时,只同步写(或读)的程序可能在某些机器上显示有效,但在这种情况下,外观是具有欺骗性的。
  即使没有同步,StopThread中同步方法的操作也是原子性的。换句话说,这些方法上的同步仅用于其通信效果,而不是互斥。虽然在循环的每个迭代上同步的成本很小,但是有一种正确的替代方法,它不那么冗长,而且性能可能更好。如果stoprequest声明为volatile,则可以省略StopThread的第二个版本中的锁定。虽然volatile修饰符不执行互斥,但它保证任何读取字段的线程都会看到最近写入的值:

image.png

  在使用volatile时一定要小心。考虑下面的方法,它应该生成序列号:


image.png

  该方法的目的是确保每次调用返回一个惟一的值(只要不超过2^32次调用)。方法的状态由一个原子可访问的字段nextSerialNumber组成,该字段的所有可能值都是合法的。因此,不需要同步来保护它的不变量。不过,如果没有同步,该方法将无法正常工作。
  问题是增量运算符(++)不是原子的。它对nextSerialNumber字段执行两个操作:首先读取值,然后返回一个新值,等于旧值加1。如果第二个线程在读取旧值和写入新值之间读取字段,则第二个线程将看到与第一个线程相同的值,并返回相同的序列号。这是一个安全故障:程序计算错误的结果。
  修复generateSerialNumber的一种方法是将synchronized修饰符添加到它的声明中。这确保了不会交叉调用多个调用,并且该方法的每次调用都将看到以前所有调用的效果.一旦您这样做了,您就可以并且应该从nextSerialNumber中删除volatile修饰符。要防弹方法,使用long而不是int,或者在nextSerialNumber即将换行时抛出异常。
  更好的方法是,遵循项目59中的建议并使用类AtomicLong,它是java.util.concurrent.atomic的一部分。这个包为单变量上的无锁、线程安全编程提供了基本类型。虽然volatile只提供同步的通信效果,但是这个包也提供原子性。这正是我们想要的generateSerialNumber,它很可能优于同步版本:

image.png

  避免本项目中讨论的问题的最佳方法是不共享可变数据。要么共享不可变数据(item17),要么完全不共享。换句话说,将可变数据限制在一个线程中。如果您采用此策略,重要的是对其进行文档化,以便随着程序的发展维护该策略。深入了解您正在使用的框架和库也很重要,因为它们可能会引入您不知道的线程。

  一个线程可以暂时修改一个数据对象,然后与其他线程共享它,只同步共享对象引用的操作。其他线程无需进一步同步就可以读取对象,只要它不再被修改。这些对象被认为是有效不可变的(Goetz06, 3.5.4)。将这样的对象引用从一个线程转移到其他线程称为安全发布[Goetz06, 3.5.3]。有很多方法可以安全地发布对象引用:您可以将它存储在静态字段中,作为类初始化的一部分;您可以将其存储在易失性字段、final字段或使用普通锁定访问的字段中;或者您可以将其放入并发集合中( item81 )。

  总之,当多个线程共享可变数据时,每个读取或写入数据的线程必须执行同步。在缺乏同步的情况下,不能保证一个线程的更改对另一个线程可见。同步共享可变数据失败的代价是活动和安全失败。这些故障是最难调试的故障之一。它们可能是间歇性的,并且依赖于时间,并且程序行为可能在不同VM之间发生根本的变化。如果只需要线程间通信,而不需要互斥,那么volatile修饰符是一种可接受的同步形式,但是正确使用它可能会比较棘手。
本文写于2019.7.22,历时2天

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,718评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,683评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,207评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,755评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,862评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,050评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,136评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,882评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,330评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,651评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,789评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,477评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,135评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,864评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,099评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,598评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,697评论 2 351

推荐阅读更多精彩内容