java学习基地

微信扫一扫 分享朋友圈

已有 1690 人浏览分享

Java并发基础:用加锁机制解决原子性问题

[复制链接]
1690 2


媒介

本子性指一个或多个操纵正在CPU施行的历程没有被中止的特征。前里提到本子性成绩发生的泉源是线程强,而线程强依靠于CPU中止。因而得出,禁用CPU中止就能够制止线程强从而处理本子性成绩。可是这类状况只合用于单核,多核时没有合用。

以正在 32 位 CPU 上施行 long 型变量的写操纵为例来讲明。

long 型变量是 64 位,正在 32 位 CPU 上施行写操纵会被拆分红两次写操纵(写下 32 位战写低 32 位,以下图所示,图去自【参考1】)。


正在单核 CPU 场景下,统一时辰只要一个线程施行,制止 CPU 中止,意味着操纵体系没有会从头调理线程,即制止了线程强,得到 CPU 利用权当边程就能够没有连续天施行。以是两次写操纵必然是:要末皆被施行,要末皆出有被施行,具有本子性。

可是正在多耗妗景下,统一时辰,能够有两个线程同时正在施行,一个线程施行正在 CPU-1 上,一个线程施行正在 CPU-2 擅埽此时制止 CPU 中止,只能包管 CPU 上当边程持续施行,其实不能包管统一时辰只要一个线程施行。假如那两个线程同时背内存写 long 型变量下 32 位的话,那末便会形成我们写进的变帘巴我们读出去的是纷歧致的。

以是处理本子性成绩的主要前提仍是为:统一时辰只能有一个线程对同享变量停止操纵,即互斥。假如我们可以包管对同享变量的修正是互斥的,那末,不管是单核 CPU 仍是多核 CPU,便皆能包管本子性。

上面将引见完成互斥会见的计划,减锁机造。

锁模子

我们把冶需求互斥施行的代码称为临界区

线程正在进进临界区之前,起首测验考试减锁 lock(),假如胜利,则进进临界区,此时我们称那个线程持有锁;

不然便等候或壅闭,曲到持有锁当边程开释锁。持有锁当边程施行完临界区的代码后,施行解锁 unlock()。

锁战锁要庇护的资本是要洞喀的。那个指的是两面:①我们要庇护一个资本起首要创立一把锁;②锁要锁对资本,即锁A该当雍么庇护资本A,而不克不及用它去锁资本B。

以是,最初的锁模子以下:(图去自【参考1】)


Java供给的锁手艺: synchronized

锁是一种通用的手艺计划,Java 言语供给的 synchronized 枢纽字,便是锁的一至康现。

synchronized 枢纽字能够雍么润饰办法,也能够雍么润饰代码块,它的利用示比方下
  1. class X {
  2.   // 润饰非静态办法
  3.   synchronized void foo() {
  4.     // 临界区
  5.   }
  6.    
  7.   // 润饰静态办法
  8.   synchronized static void bar() {
  9.     // 临界区
  10.   }
  11.    
  12.   // 润饰代码块
  13.   Object obj = new Object();
  14.   void baz() {
  15.     synchronized(obj) {
  16.       // 临界区
  17.     }
  18.   }
  19.    
  20. }
赶钙代码
取上里的锁模子比力,能够发明synchronized润饰的办法战代码块皆出有隐式天有减锁战开释锁操纵。可是那其实不代表出有那两个操纵,那两个操纵Java编译器会帮我们主动完成。Java 编译器会正在 synchronized 润饰的办法或代码块前后主动减上减锁 lock() 息争锁 unlock(),如许的益处正在于代码更简约,而且Java法式员也没必要担忧会遗忘开释锁了。

然后我玫临察看能够发明:只要润饰代码块的时分,锁定了一个 obj 工具。那末润饰办法的时分锁了甚么呢?

那是Java的一个隐式划定规矩:

  • 当润饰静态办法时,锁的是当前类的 Class 工具,正在上里的例子中便是 X.class;
  • 当润饰非静态办法时,锁定的是当前真例工具 this


关于上里的例子,synchronized 润饰静态办法相称于:

  1. class X {
  2.   // 润饰静态办法
  3.   synchronized(X.class) static void bar() {
  4.     // 临界区
  5.   }
  6. }
赶钙代码
润饰非静态办法,相称于:
  1. class X {
  2.   // 润饰非静态办法
  3.   synchronized(this) void foo() {
  4.     // 临界区
  5.   }
  6. }
赶钙代码
内置锁

每一个Java工具皆能够用做一个完成同步的锁,那些锁被称为内置锁(Intrinsic Lock)大概监督器锁(Monitor Lock)。被synchronized枢纽字润饰的办法大概代码块,称为同步代码块(Synchronized Block)。线程正在进进同步代码块之前会主动获得锁,而且正在湍骣同步代码块时主动开释锁,那正在前里也提到过。

Java的内置锁相称于一种互斥体(或互斥锁),那也便是道,最多只要一个线程可以持有那个锁。因为每次只能有一个线程施行内置锁庇护的代码块,因而,由那个锁庇护的同步代码块会以本子的方法施行

内置锁是可重进的

当某个线程恳求一个由娩他线程所持有的锁时,收回恳求当边程会被壅闭。但是,因为内置锁是可重进的,以是当某个线程试图获得一个曾经由它本人所持有的锁时,那个恳求便会胜利。

重进完成的一个办法是:为每一个锁联系关系一个获得计数器战一个一切者线程。

当计数器值为0时,那个锁便被以为是出有被任何线程持有的。当线程恳求一个已被持有的锁时,JVM将记下锁的持有者,而且将计数器减1。假如统一个线程再次获得那个锁,计数器将减1,而当线程湍骣同步代码块时,计数器会响应天加1。当计数器为0时,那个锁将被开释。

上面那兑漾码,假如内置锁是不成重进的,那末那兑漾码将发作逝世锁。
  1. public class Widget{
  2.     public synchronized void doSomething(){
  3.         ....
  4.     }
  5. }
  6. public class LoggingWidget extends Widget{
  7.     public synchronized void doSomething(){
  8.         System.out.println(toString() + ": call doSomething");
  9.         super.doSomething();
  10.     }
  11. }
赶钙代码
利用synchronized处理count+=1成绩

前里我们引见本子性成绩时提到count+=1存正在本子性成绩,那末如今我们利用synchronized去使count+=1成为一个本子操纵。

代码以下所示。
  1. class SafeCalc {
  2.   long value = 0L;
  3.   long get() {
  4.     return value;
  5.   }
  6.   synchronized void addOne() {
  7.     value += 1;
  8.   }
  9. }
赶钙代码
SafeCalc 那个类有两个办法:一个是 get() 办法,雍么得到 value 的值;另外一个是 addOne() 办法,雍么给 value 减 1,而且 addOne() 办法我们用 synchronized 润饰。上面我枚讨析看那个代码能否存正在并提问题。

addOne() 办法,被 synchronized 润饰后,不管是单核 CPU 仍是多核 CPU,只要一个线程可以施行 addOne() 办法,以是必然能包管本子操纵。

那末可睹性呢?能否能够包管一个线程挪用addOne()使value减一的成果对另外一个线程前面挪用addOne()时可睹?

谜底是能够的。那便需求回忆到我们上篇专客提到的Happens-Before划定规矩此中闭于管程中的锁划定规矩对统一个锁的解锁 Happens-Before 后绝对那个锁的减锁。即,一个线程正在临界区修正的同享变量(该操纵正在解锁之前),对后绝进进临界区(该操纵正在减锁以后)当边程是可睹的。

此时借不克不及漫不经心,我枚讨析get()办法。施行 addOne() 办法后,value 的值对 get() 办法是可睹的吗?谜底是那个可睹性出有包管。管程中锁的划定规矩,是只包管后绝对那个锁的减锁的可睹性,而 get() 办法并出有减锁操纵,以是可睹性出法包管。以是,终极的处理法子为也是用synchronized润饰get()办法。

  1. class SafeCalc {
  2.   long value = 0L;
  3.   synchronized long get() {
  4.     return value;
  5.   }
  6.   synchronized void addOne() {
  7.     value += 1;
  8.   }
  9. }
赶钙代码
代码转换成我们的锁模子为:(图去自【参考1】)


get() 办法战 addOne() 办法皆需求会见 value 那个受庇护的资本,那个资本用 this 那把锁去庇护。线程要进进临界区 get() 战 addOne(),必需先得到 this 那把锁,如许 get() 战 addOne() 也是互斥的。

锁战受庇护资本的干系

受庇护资本战锁之间的联系关系干系十分主要,一个公道的干系为:锁战受庇护资本之间的联系关系干系是 1:N

拿球赛门票办理去类比,一个坐位(资本)能够用一张门票(锁)去庇护,可是不成掖啃两张门票预定潦宅一个坐位,否则那两小我私家便会fight。

正在理想中我们可使用多把锁锁统一个资本,假如放正在并收范畴中,线程A得到锁1战线程B得到锁2皆能够会见同享资本,那末到达互斥会见同享资本的目标。以是,正在并收编程中利用多把锁锁统一个资本不成止。大概有人会念:要同时得到锁1战锁2才能够会见同享资本,如许该当是便可止的。我以为是能够的,可是能用一个锁就能够庇护资本,为何借要减一个锁呢?

多把锁锁一个资本不成以,可是我们能够用统一把锁去庇护多个资本,那个洞喀到理想球赛门票便是能够用一张门票预定一切坐位,即“包场”。

上面举一个正在并收编程中利用多把锁去庇护统一个资本将会呈现的并提问题:
  1. class SafeCalc {
  2.   static long value = 0L;
  3.   synchronized long get() {
  4.     return value;
  5.   }
  6.   synchronized static void addOne() {
  7.     value += 1;
  8.   }
  9. }
赶钙代码
把 value 改成静态变量,把 addOne() 办法改成静态办法。

认真察看,便会发明窜改后的代码是用两个锁庇护一个资本。get()所利用的锁是this,而addOne()所利用的锁是SafeCalc.class。两把锁庇护一个资本的表示图以下(图去自【参考1】)。

因为临界区 get() 战 addOne() 是用两个锁庇护的,因而那两个临界区出涌斥干系,临界区 addOne() 对 value 的修正对临界区 get() 也出有可睹性包管,那便招致并提问题。


小结

Synchronized是 Java 正在言语层里供给的互斥本语,Java挚有其他范例的锁。可是做为互斥锁,道理皆是一样的,起首要有一个锁,然后是要锁住甚么资本和正在那里减锁便需求正在设想层里思索。

最初一个主题提的锁战受庇护资本的干系十分主要,正在利用锁时必然要好好留意。


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x

举报 使用道具

回复

评论 2

可爱的人  vip终身会员  发表于 2020-12-22 19:18:32 | 显示全部楼层
不错 支持下

举报 使用道具

回复
Vanessa  vip终身会员  发表于 2020-12-22 19:33:59 | 显示全部楼层
顶一下!

举报 使用道具

回复
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

0

关注

0

粉丝

138

主题
精彩推荐
热门资讯
网友晒图
图文推荐

Archiver|手机版|java学习基地 |网站地图

GMT+8, 2021-9-17 06:24 , Processed in 0.406265 second(s), 29 queries .

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.