总结Java的多线程处理--线程与锁(一)同步synchronized

线程与锁模型

线程与锁模型是比较原始的一种处理并发的方式,主要是对底层硬件的运行过程形式化,这是它的优点也是缺点。
线程与锁模型非常直接,几乎所有的编程语言都提供了支持,但是如果不了解该模型,那么程序会很容易出错,而且难以维护。

为什么需要锁

我们先来看一段多线程的代码:

public class Counting {
  public static void main( String[] args) throws InterruptedException {
    class Counter {
      private int count = 0;
      public void increment() { ++count; }
      public int getCount() { return count;}
    }
    final Counter counter = new Counter();
    class CountingThread extends Thread {
    public void run() {
      for(int x = 0; x < 10000; x++)
        counter.increment();
      }
    }
    CountingThread t1 = new CountingThread();
    CountingThread t2 = new CountingThread();
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(counter.getCount());

这是创建了两个线程t1与t2,每一个线程都调用了counter.increment(),10000次,看上去特别简单,但是每次运行都会有不同的结果,这是因为在操作counter对象的时候发生了竞态条件。

竞态条件是指代码的行为取决于各操作的时序。


如果不理解我们先看一下java编译器是如何处理的++count

//获取值
getfield #2
//将常量i 1 进栈
iconst_1
//加i
iadd
//更新值
putfield#2

问题就出在这里,如果同时调用increment(),两个线程在获取值的时候是同一个值如100,那么放回去的时候虽然操作了两次increment(),但是实际结果是101。

synchronize

java中遇到这种问题可以有一种的解决办法,进行同步(synchronize)访问。
只需要在之前的代码中这么改一下:

······
  public synchronized void increment() { ++count; }
······

那么在线程使用increment函数的时候会获得该函数的锁,其他线程将不能访问,直到该线程返回时释放锁。
现在因为增加了同步的代码,执行都会获得正确的结果--20000。

但是synchronize也会带来很多坑,下面一一介绍。

乱序编译

static boolan isReady = false
static int number = 0;
static Thread t1 = new Thread() {
    public void run() {
      number = 100;
      isReady = true;
    }
static Thread t2 = new Thread() {
    public void run() {
      if (isReady)
     System.out.println(number);
      else 
     System.out.println("not ready");
    }

想想看如果同时运行上面的代码会发生什么事?

结果:

  1. 打印not ready
  2. 打印100。
  3. 打印0。 (为什么?)

为什么 number = 100; isReady = true;语句发生了颠倒?

但是事实上是有可能发生的:

  1. 编译器的静态优化会打乱。
  2. JVM的动态优化会打乱。
  3. 硬件可以通过乱序执行来优化性能。

但实际上还有更糟糕的,有时候一个线程产生的修改可能对于另外一个线程来讲是不可见的。

从常识上来说,无论是编译器,JVM还是硬件都不应该改变代码的原有逻辑,这里我们需要有明确的标准来知道可能会发生什么,那就是Java内存模型。

在Java内存模型中还说明了上面一个问题的答案:

如果读线程与写线程不进行同步,就不能保证可见性。
所以除了increment()之外,也应该对getCount()方法同步,不然可能会得到一个已经失效的值。

死锁

有一个著名的问题--哲学家进餐问题


1.png

如图。
哲学家的状态可能是「思考」也可能是「饥饿」,如果是饥饿,他就会将两边的筷子拿起来,并且进餐一段时间。进餐结束后哲学家就会返回筷子。
那么代码可以这么写

synchronized(左边的筷子) {
  synchronized(右边的筷子)
}

这样会出现一个问题,如果所有的哲学家在某个时刻,将左边的筷子都拿起来,就都不能拿到右边的筷子了,而且也不能释放左边的筷子,这样程序就会一直卡组,这就是死锁。

如果解决?
我们可以给筷子设置编号,只能先拿小的,然后拿大的。
或者给哲学家拿筷子的顺序进行设置。
虽然可以解决但是依然暴露出synchronized的问题。

方法内部的陷阱

private synchronized void update {
  for (Person person: persons)
    person.eat();
}

这段逻辑看上去没有问题,方法加上了synchronized,所以多线程使用的时候也会同步访问。
但实际上也是有一个陷阱,在eat方法这个地方。
因为对eat方法不了解,所以可能eat方法中也调用了synchronized函数,这样就是使用了两把锁,就像之前的哲学家进餐问题一样,可能会发生死锁。
解决的办法是将persons拷贝一份:

private void update {
  ArrayList<Person> personsCopy;
  synchronized(this) {
  personCopy = (ArrayList<Person>)persons.clone();
  for (Person person: persons)
    person.eat();
}

这样调用方法的时候不需要加锁,而且也减少了持有锁的时间。

总结

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

推荐阅读更多精彩内容