文档章节

关于线程,还有这些是你需要知道的!

CoorChice
 CoorChice
发布于 2017/07/03 17:59
字数 3248
阅读 4569
收藏 266

image

有什么料?

  1. 进一步理解多线程场景下会出现的问题;
  2. 学会正确处理并发操作中的通讯和同步。

现在,多了解些线程吧

在日常开发中,线程常常被用作为提升程序效率的重要手段。在CoorChice的这篇文章中,CoorChice介绍了线程的基本运作。链接:

【你知道Thread线程是如何运作的吗?:http://www.jianshu.com/p/8862bd2b6a29】

本篇,CoorChice将从多线程的角度来进一步介绍线程的相关知识。首先,我们需要了解一些基本知识。

主内存和工作内存

  • 主内存
    暂且可以理解为内存模型中堆内存。它储存了进程的所有共享变量。我们知道,一个进程中可能存在包括主线程在内的多条线程。++主内存中的共享变量是对所有线程可见的。++
  • 工作内存
    为了提高效率,每个线程都配有一个私有的工作内存。主内存中的共享变量需要拷贝到线程的私有内存中,之后线程对该变量的操作就是在自己的工作内存中进行的。++当值发生改变时,在线程退出之前,会被更新到主内存中。++

想了解更多和Java内存相关的知识,可以看看CoorChice的这几篇文章:

  1. 【Android内存基础——Java内存管理机制:http://www.jianshu.com/p/54241ca3da5c】
  2. 【Android内存基础——内存抖动:http://www.jianshu.com/p/69e6f894c698】
  3. 【Android内存基础——内存泄漏:http://www.jianshu.com/p/b4325fecdcda】

共享变量和非共享变量

  • 共享变量
    如果一个变量在多条线程的工作内存中都有拷贝,那么就认定它是一个共享变量。++事实上,类的成员变量、静态变量都是共享变量。++ 如上所术,共享变量对进程中的所有线程都是可见的。我们经常遇到的并发问题通常就是由它引起的。

  • 非共享变量:
    就是线程中的私有变量。这些变量对其它线程来说是不可见。当线程退出时,它们会被回收的。非共享变量的值需要通过通讯手段才能传递到其它线程,这个后面再提。

其它

  • 原子操作:
    就是不可分割的,连续不断的操作。比如后面将要提到的Read操作。

  • 可见性:
    一个线程对共享变量值的修改,能够被其它线程即时看到,就称该共享变量具有可见性。

由共享变量引发的问题

现在,筒靴们已经知道了共享变量对进程中的所有线程都是可见的。并且当一个线程需要使用它时,需要先拷贝一份到自己的工作内存中,然后再工作内存中操作这个copy的对象。下面这张图展示线程中操作共享变量的过程。

image

图中展示了线程对共享变量的读取/写入操作。可以看到,++它们分别由两个原子操作构成。++注意,CoorChice这句话的意思是,通常意义上的读取一个变量或者写入一个变量的操作都不是原子操作,而是分两步完成的。

读取

  1. read:
    将主内存中的变量值读取到线程的工作内存中。
  2. load:
    将read到的值赋给新建的拷贝变量。

写入

  1. store:
    将线程的工作内存中的,共享变量的拷贝变量的值传到主内存中。
  2. write:
    将store后的值赋给主内存中共享变量。

你看,不论是读取还是写入,由于都需要两步完成,所以就很可能发生中途被中断的情况。比如下面这段代码每次执行的结果都有可能不一样。

int goods = 0;

@Test
public void testThread() {
    for (int i = 0; i < 3; i++) {
      new Thread(() -> {
        while (goods != 10) {
          goods++;
          System.out.println(
          Thread.currentThread().getName() + 
            " -> Goods = " + goods);
        }
      }, "Thread - " + i).start();
    }
  }

第一次运行结果:

Thread - 0 -> Goods = 1
Thread - 0 -> Goods = 3
Thread - 0 -> Goods = 4
Thread - 1 -> Goods = 2
Thread - 2 -> Goods = 6
Thread - 2 -> Goods = 8
Thread - 2 -> Goods = 9
Thread - 2 -> Goods = 10
Thread - 0 -> Goods = 5
Thread - 1 -> Goods = 7

第二次运行结果

Thread - 0 -> Goods = 1
Thread - 1 -> Goods = 2
Thread - 0 -> Goods = 3
Thread - 0 -> Goods = 5
Thread - 0 -> Goods = 6
Thread - 2 -> Goods = 7
Thread - 2 -> Goods = 9
Thread - 1 -> Goods = 4
Thread - 2 -> Goods = 10
Thread - 0 -> Goods = 8

这个例子之所以会得到这种结果,是因为当一个线程执行时,另一个线程插入执行。关键插入的地方可能有:

  1. 在共享变量goods读/写的过程中。
  2. goods++操作包含的+1、赋值等操作中。

这样的结果我们肯定是不能接受的,事实上如果操作的是非基本类型变量,那么你的程序可能会脆弱不堪,随时面临着崩溃。我们希望程序能够高效且正确的运行,就需要解决多线程场景下的通讯(信息或数据传递)和同步(有序执行)的问题。

image

多线程的通讯和同步

目前,我们大致有两套解决多线程问题的模型。

  • 基于内存共享的模型。就是线程之间通过共享内存实现通讯,即共享内存中的信息是公共可见的,但需要显示的进行同步。不然就会出现上面例子中错乱的问题。不难看出,共享内存模型特点是是隐式通讯,显示同步的。Java选择的并发解决方案就是基于共享内存的。这就是为什么我们常常需要在Java使用synchronized或者Lock来进行同步操作的原因。
  • 基于消息传递的模型。就是线程之间通过发送/接收消息来实现同步。由于发送消息和接收消息总是具有先后顺序的(先有发送,后有接收),所以这种模型的特点是隐式同步,显示通讯,即需要在发送消息的时候附加需要传递的信息来进行通信。Android中的Handler机制就是基于消息传递模型的。关于Handler机制CoorChice的这篇文章中有详细的讲述:【你知道Thread线程是如何运作的吗?:http://www.jianshu.com/p/8862bd2b6a29】

下面,我们了解下Java中的同步手段。

线程同步手段

synchronized

synchronized关键字相信大家都不陌生,我们常常把它加到方法或代码块上用于同步:

public synchronized void testThread() {
    ...
  }

或者这样来同步代码块:

public void testThread() {
    Object object = new Object();
    
    synchronized (this){ //本类实例的对象锁
      ...
    }
    
    synchronized (object){ //指定的对象锁
      ...
    }
    
    synchronized (Object.class){ //类锁。注意,这表示该类所有对象实例同时只能有一个访问该代码块。
      ...
    }
}

在进行同步时,需要时刻注意,你需要把同步加在真正需要同步的地方,而不是大段的进行同步,那样会有效降低程序效率的!记住:同步粒度尽可能的小!

Lock

与sycnhronized相比,Lock相当于是手动实现同步。在Java中,实现了一个ReentrantLock来帮助我们实现同步。使用起来也比较简单,我们只需要在需要同步的代码块前段加锁,末端释放锁即可。看个例子吧。

int goods = 0;

public void testThread() {
    Lock lock = new ReentrantLock();
    for (int i = 0; i < 3; i++) {
      new Thread(() -> {
        lock.lock();
        while (goods < 10) {
          goods++;
          System.out.println(
            Thread.currentThread().getName() +
              " -> Goods = " + goods);
        }
        lock.unlock();
      }, "Thread - " + i).start();
    }
  }

同样是上面那个例子,这次看看运行结果吧。


Thread - 0 -> Goods = 1
Thread - 0 -> Goods = 2
Thread - 0 -> Goods = 3
Thread - 0 -> Goods = 4
Thread - 0 -> Goods = 5
Thread - 0 -> Goods = 6
Thread - 0 -> Goods = 7
Thread - 0 -> Goods = 8
Thread - 0 -> Goods = 9
Thread - 0 -> Goods = 10

使用Lock实现同步需要注意在发生异常的地方及时释放锁,否则将会导致其它等待获取锁的线程一直阻塞下去!此外,如果使用mLock.tryLock()获取锁可以根据返回值判断是否成功获取到了锁。

final有同步作用吗?

答案是肯定的,但是它只能保证某些情况下的同步。它们是什么情况呢?就是对于不可变对象而言的。不可变对象(成员变量由基本类型或final修饰,或其它不可变对象组成的对象)意味着在安全发布后,我们不能再修改它,所以对于所有可以见到它的线程而言,它是相同的。

对于可变对象(就是非不可变对象喽,例如普通的List、Map等),即使使用了final进行修饰,在并发场景下,你仍然需要进行显示的同步。因为可变对象的内容是可以被修改的。看个例子,筒靴们可能会理解得更清晰。

final AlterableObj obj = new AlterableObj();
@Test
public void testThread_2() {
  for (int i = 0; i < 10; i++) {
    new Thread(() -> {
      while (obj.var < 100) {
        obj.var++;
        System.out.println(
          Thread.currentThread().getName() +
            " -> AlterableObj.var = " + obj.var)
      }
    }, "Thread - " + i).start();
  }
}
class AlterableObj{
  public int var = 0;
}

运行结果比较长,我仅截取一部分能说明问题的:

...
Thread - 2 -> AlterableObj.var = 42
Thread - 2 -> AlterableObj.var = 43
Thread - 2 -> AlterableObj.var = 44
Thread - 1 -> AlterableObj.var = 40
Thread - 4 -> AlterableObj.var = 46
Thread - 4 -> AlterableObj.var = 48
Thread - 4 -> AlterableObj.var = 49
Thread - 4 -> AlterableObj.var = 50
Thread - 4 -> AlterableObj.var = 51
Thread - 3 -> AlterableObj.var = 39
...

看,已经发生错乱了!所以fianl并不能保证不可变对象的同步。

image

volatile有同步作用吗?

++volatile的主要作用是保证被修饰变量的可见性。++ 这意味着,++被volatile修饰的变量的读/写操作类似于是原子性的++,即read和load,stroe和write过程变得连续而不可被中断。所以,某种意义上说,volatile是有同步作用的,但是范围非常小,通常不能满足我们的需求。

此外,volatile能够在一定程度上保证程序的有序性。JVM在编译时会对程序进行指令重排,但这不会影响执行结果。如果一个变量被volatile修饰,那么发生在它读/写操作之前的程序指令,一定不会被重排到它的读/写操作之后。比如:

volatile int a = 0;

int b = 1;
int c = 2;

int a = 3;

int d = 4;
int e = 5;

上面代码中,int a = 3像一道屏障一样,使得int b = 1int c = 2一定发生在int d = 4int e = 5之前。

它们自带同步属性

java.util.concurrent包下,Java为我们提供了不少常用对象的线程安全版,比如AtomicXXX系列ConcurrentXXX系列CopyOnWriteXXX系列等。一般情况下,你可以放心的使用它们,而不用担心多线程场景下的各种麻烦问题!

使用多线程吧!

现在,筒靴们应该能够合理的使用多线程来提高程序效率了吧。

在Android中,由于主线程(UI线程)负责绘制界面,所以是万万阻塞不得!如果在主线程中不小心混入了耗时操作,后果是很可怕的。轻则导致界面卡顿,重则导致ANR!相关知识可以看看CoorChice的这篇文章:【用两张图告诉你,为什么你的App会卡顿?:http://www.jianshu.com/p/df4d5ec779c8】

对于复杂计算、数据读/写、网络访问等耗时操作,我们都应该放到线程中进行。现在设备通常都具备多个cpu,比如8核设备可以至少并行运行8条线程!不搞点并发操作简直是暴遣天物啊。我们只需要谨慎的处理好线程间的通讯及同步问题即可。当然,这并不像说的那么容易,需要多花点时间去思考和尝试。Java也提供了一些高效且简化的类来帮助我们合理的进行并发编程,比如CoorChice在这篇文章中介绍的:【Android线程——使用ExecutorService类来实现线程的管理:http://www.jianshu.com/p/067a3b8b79ea】

总结

  • 抽出空余时间写文章分享需要动力,还请各位看官动动小手 【点个赞】,给CoorChice点鼓励
  • CoorChice一直在不定期的创作新的干货,想要上车只需进到【个人主页】点个关注就好了哦。发车喽~

本篇主要介绍了关于多线程场景下一些需要注意的点,筒靴们在进行并发操作时需要根据这些特点谨慎的处理线程间的通讯和同步。

参考链接

  1. Java Volatile Keyword:http://tutorials.jenkov.com/java-concurrency/volatile.html
  2. Java内存模型(一):http://www.cloudchou.com/softdesign/post-631.html
  3. Java 多线程-可见性问题:https://mritd.me/2016/03/20/Java-%E5%A4%9A%E7%BA%BF%E7%A8%8B-%E5%8F%AF%E8%A7%81%E6%80%A7%E9%97%AE%E9%A2%98/
  4. Java多线程干货系列—(四)volatile关键字| 掘金技术征文:https://juejin.im/post/590f451c44d904007beaba1b

看到这里的童鞋快奖励自己一口辣条吧!

© 著作权归作者所有

CoorChice
粉丝 48
博文 30
码字总数 87562
作品 0
朝阳
程序员
私信 提问
加载中

评论(26)

尾小戒1993
尾小戒1993
66666
CoorChice
CoorChice 博主

引用来自“wier”的评论

请问,你的画图工具用的什么,比如上面那个gif图,采用什么工具制作的

回复@wier : 用Keynote
wier
wier
请问,你的画图工具用的什么,比如上面那个gif图,采用什么工具制作的
路小磊
路小磊
进击的企鹅
进击的企鹅

引用来自“进击的企鹅”的评论

生造很多概念,共享变量非共享变量什么鬼?主内存工作内存什么鬼?
楼主操作系统学的太烂,cpu工作原理貌似也不懂,建议看看Intel x86 cpu手册,看看linux操作系统原理。这篇文章给人的感觉就是刚入门的人的臆想。这篇文章对计算机基础差的人事严重的误导。

引用来自“CoorChice”的评论

大神出现😮 弱楼主只能在Java模型的基础上学习点表皮的东西。更深入的硬件原理还请大神分享出来,带我学。😉 关于生造概念,弱弱还没点这个技能,不过是站在周巨人的肩膀上跟着走罢了。
intel 编程手册,有中文版的,linux0.11 有这个版本的分析书籍,建议看看。计算机底层原理要熟,工作多年后会认识到,平时用到的知识都要靠大学学的那些东西,tcpip,http,操作系统原理,。。。不懂这些,遇到问题都不知道怎么下手,查问题就靠这些底层知识。
CoorChice
CoorChice 博主

引用来自“小果汁儿”的评论

首先,多线程不一定能升效率。因为你无法保证能百分百利用多核特性,也就是你无法保证你的多个线程真正并行执行;单核上多个线程无论如何都是顺序执行的,更不可能提升效率,相反线程切换会降低效率。不过多线程能提高UI体验。
对的,线程切换是有偿的。
小果汁儿
小果汁儿
首先,多线程不一定能升效率。因为你无法保证能百分百利用多核特性,也就是你无法保证你的多个线程真正并行执行;单核上多个线程无论如何都是顺序执行的,更不可能提升效率,相反线程切换会降低效率。不过多线程能提高UI体验。
CoorChice
CoorChice 博主

引用来自“无著方知尘亦珍”的评论

说的有点惨不忍睹,
内存,无论是物理还是语言内存模型都没有这种划分方式,太粗糙了

变量共享,只说了下,类变量,成员变量,局部变量,却没有说明真正多线程环境下什么情况下才会存在安全问题,一个很简单的道理,类变量不做同步控制就一定会在多线程下发生并发安全问题么?

锁机制,锁的作用范围呢?多锁并存呢?

java.util.concurrent只能保证这些api里自身的并发安全,不能保证你用的代码,不真正理解,能放心用么。

其它易云
这位大神,首先由衷感谢你参与讨论。这篇文章并没有打算很详细的把提及的知识点完全的分析透彻,你可以看到,就像变量共享、java.util.concurrent,我只是提及点到为止而已,作为了解性的我个人觉得差不多够了。
其次就是关于你和上面一位大神@漂鸟少年 说我传达的内存的分类方式是错误的。我想说,我不是创造这些知识的人,当我去学习这些知识的时候需要建立在一个对知识传达者的信任上,即一些知识需要向权威求证,我也怕所学的知识是错误的啊。甚至更可怕的是,我居然不知道学习的知识是错误的。所以互相交流讨论我是很开心,各自了解的知识需要互相交叉对比,看有没有什么是学错了。所以再次感谢你。
可能你们是主攻c之类的?但在Java中,为了实现各个平台的兼容性,Java严谨的设计了JMM来对平台差异性进行屏蔽,其中主内存和工作内存的概念是其核心思想之一。这一点应该不少书上都有提到。就像我前面所说的,一些书对于这些知识,相比而言应该能称得上是可信任的权威了吧?
无著方知尘亦珍
无著方知尘亦珍
说的有点惨不忍睹,
内存,无论是物理还是语言内存模型都没有这种划分方式,太粗糙了

变量共享,只说了下,类变量,成员变量,局部变量,却没有说明真正多线程环境下什么情况下才会存在安全问题,一个很简单的道理,类变量不做同步控制就一定会在多线程下发生并发安全问题么?

锁机制,锁的作用范围呢?多锁并存呢?

java.util.concurrent只能保证这些api里自身的并发安全,不能保证你用的代码,不真正理解,能放心用么。

其它易云
无著方知尘亦珍
无著方知尘亦珍
东拼西凑的杂文,说了半天都没说到“效率”从何而来。还不如换个主题。
什么是线程安全?

给“线程安全”下定义是件非常棘手的事儿。随便Google一下,就能得到成千上万像这样的定义: 1.“线程安全”的代码是指在多线程同时执行的情况下,依然能正常工作的代码。 2.一段代码,如...

忙碌的键盘
2016/10/28
203
2
记一次阿里巴巴一面经历,作为一名java程序员终于找到了自己差距!

面试前的故事 上周在拉勾上收到一个蚂蚁金服的大哥要我的简历,当时很惊讶,居然有蚂蚁金服的找到我,然后想都没想就给了。 受宠若惊呀,我知道自己的水平跟阿里的差距有多远,以前一直没用勇...

我最喜欢三大框架
05/13
41
0
关于c#静态方法和实例方法的辨析和应用

本文将围绕c#静态方法和实例方法讨论一下。针对一些观点,如:"静态方法是常驻内存", 还有"静态方法比实例方法先装载",做一个辨析。同时讨论下何时用静态方法,何时用实例方法。 前几日,在...

mikelij
2010/08/13
0
0
关于iOS多线程

关于iOS多线程 原文地址:http://www.jianshu.com/p/0b0d9b1f1f19 在这篇文章中,我将为你整理一下 iOS 开发中几种多线程方案,以及其使用方法和注意事项。当然也会给出几种多线程的案例,在实...

法斗斗
2016/03/10
15
0
多线程编程--5种方法实现线程同步

1:用Interlocked系列函数实现线程同步; 2:用CRITICAL_SECTION及其系列函数实现线程同步; 3:用RTL_SRWLOCK及其系列函数实现线程同步; 4:用事件内核对象实现线程同步; 5:用信号量内核...

长征2号
2018/01/11
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Jenkins World 贡献者峰会及专家答疑展位

本文首发于:Jenkins 中文社区 原文链接 作者:Marky Jackson 译者:shunw Jenkins World 贡献者峰会及专家答疑展位 本文为 Jenkins World 贡献者峰会活动期间的记录 Jenkins 15周岁啦!Jen...

Jenkins中文社区
32分钟前
8
0
杂谈:面向微服务的体系结构评审中需要问的三个问题

面向微服务的体系结构如今风靡全球。这是因为更快的部署节奏和更低的成本是面向微服务的体系结构的基本承诺。 然而,对于大多数试水的公司来说,开发活动更多的是将现有的单块应用程序转换为...

liululee
46分钟前
7
0
OSChina 周二乱弹 —— 我等饭呢,你是不是来错食堂了?

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @ 自行车丢了:给主编推荐首歌 《クリスマスの夜》- 岡村孝子 手机党少年们想听歌,请使劲儿戳(这里) @烽火燎原 :国庆快来,我需要长假! ...

小小编辑
今天
474
9
玩转 Springboot 2 之热部署(DevTools)

Devtools 介绍 SpringBoot 提供了热部署的功能,那啥是热部署累?SpringBoot官方是这样说的:只要类路径上的文件发生更改,就会自动重新启动应用程序。在IDE中工作时,这可能是一个有用的功能...

桌前明月
今天
6
0
CSS--列表

一、列表标识项 list-style-type none:去掉标识项 disc:默认实心圆 circle:空心圆 squire:矩形 二、列表项图片 list-style-img: 取值:url(路径) 三、列表项位置 list-style-position:...

wytao1995
今天
10
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部