【拒绝一问就懵】之有必要单独讲讲线程

有什么料?

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

如今,多了解些线程吧

在平常开发中,线程经常被用做为提高程序效率的重要手段。在CoorChice的这篇文章中,CoorChice介绍了线程的基本运做。连接:html

【拒绝一问就懵】之从Thread讲到Handlejava

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

主内存和工做内存

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

想了解更多和Java内存相关的知识,能够看看CoorChice的这几篇文章:
1. 【拒绝一问就懵】之你多少要懂点内存回收机制编程

2.【拒绝一问就懵】之没据说过内存抖动吧安全

3.【拒绝一问就懵】之不可忽视的内存泄露bash

共享变量和非共享变量

  • 共享变量

    若是一个变量在多条线程的工做内存中都有拷贝,那么就认定它是一个共享变量。++事实上,类的成员变量、静态变量都是共享变量。++ 如上所术,共享变量对进程中的全部线程都是可见的。咱们常常遇到的并发问题一般就是由它引发的。网络

  • 非共享变量:

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

其它

  • 原子操做:

    就是不可分割的,接二连三的操做。好比后面将要提到的Read操做。并发

  • 可见性:

    一个线程对共享变量值的修改,可以被其它线程即时看到,就称该共享变量具备可见性。app

由共享变量引起的问题

如今,筒靴们已经知道了共享变量对进程中的全部线程都是可见的。而且当一个线程须要使用它时,须要先拷贝一份到本身的工做内存中,而后再工做内存中操做这个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++操做包含的+一、赋值等操做中。

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

image

多线程的通信和同步

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


  • 基于内存共享的模型。

就是线程之间经过共享内存实现通信,即共享内存中的信息是公共可见的,但须要显示的进行同步。否则就会出现上面例子中错乱的问题。不难看出,共享内存模型特色是是隐式通信,显示同步的。Java选择的并发解决方案就是基于共享内存的。这就是为何咱们经常须要在Java使用synchronized或者Lock来进行同步操做的缘由。


  • 基于消息传递的模型。

就是线程之间经过发送/接收消息来实现同步。因为发送消息和接收消息老是具备前后顺序的(先有发送,后有接收),因此这种模型的特色是隐式同步,显示通信,即须要在发送消息的时候附加须要传递的信息来进行通讯。Android中的 Handler 就是基于消息传递模型的。关于Handler机制 CoorChice的这篇文章中有详细的讲述:【拒绝一问就懵】之从Thread讲到Handle

下面,咱们了解下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的这篇文章:【拒绝一问就懵】之Activity的启动流程

对于复杂计算、数据读/写、网络访问等耗时操做,咱们都应该放到线程中进行。如今设备一般都具有多个cpu,好比8核设备能够至少并行运行8条线程!不搞点并发操做简直是暴遣天物啊。咱们只须要谨慎的处理好线程间的通信及同步问题便可。固然,这并不像说的那么容易,须要多花点时间去思考和尝试。Java也提供了一些高效且简化的类来帮助咱们合理的进行并发编程,好比CoorChice在这篇文章中介绍的:【面试必备】简单了解下ExecutorService

总结

  • 抽出空余时间写文章分享须要动力,还请各位看官动动小手 【点个赞】,给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

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

相关文章
相关标签/搜索