Java学习笔记程序员架构算法设计模式和编程理论

java的多线程机制

2017-02-10  本文已影响94人  feary

在项目中,经常会使用到多线程,例如android本身封装了handler来进行多线程通信,平时会用到eventbus,rx这样的框架来处理,自己用锁的时候已经很少了,但还是无法完全避免。多线程的概念非常好理解,是为了提高工作效率的一种方法,尤其在现代多cpu多核的计算机下,利用好各个资源是非常有必要的。在android等交互上,为了给用户良好体验,不能阻塞用户交互请求也必须在非ui线程上更新ui。

线程之间如何正确的通信是我们所关注的最关键的点。

首先问题在于线程为什么存在同步和通信的问题,这个可以看些java线程内存模型的文章,简单来说原因在于,jvm为每个线程维护了一个私有内存,线程间是不能直接通信的,他们通过与主存通信完成同步。大家知道,所谓线程,其实就是运行在cpu的指令流,就是方法的顺序执行。比如说,方法 f(),在线程1上被调用的,则这个方法是在线程1上执行的,咋一看像句废话,其实很多人会陷入某个变量是哪个线程的,比如说handler的理解上,经常有说是哪个线程的handler,实际上只是他们的loop方法在那个线程调用了而已,那只是一种易于记忆的说法,希望大家仔细思考。这里也同样说明了另外一个问题,即局部变量是不存在线程同步问题的,因为它的生命周期就是一个方法,即一个局部变量对象只存在于一个线程上。而相应的全局变量是存在线程安全问题的。每个线程内存维护了一份该变量的拷贝,或者说clone更便于理解,拿mips作为模型举例来理解,就是比如全局变量A a;会有一个寄存器存储其的地址,对应地址的内存上存有A的信息,成员变量值什么的,每个线程用到时都会从主存拷贝一份以上的信息。但是并不是立刻同步的,想要立刻写回可用volatile修饰。(非常不建议的做法)

这里,比如A里的成员变量 int mTv = 1;在线程1上做了mTv++运算,变成2,这时还未写回,但是线程2又做了一遍,则出现了同步问题,与我们预期是不一样的。这就是我们所要注意和需要解决的问题。

多线程不是java特有的,甚至java是没有自己的线程模型的,而是直接采用了操作系统的线程模型。很多维护机制都是从操作系统来的。下面介绍一下大家最常见到的volatile,(就是大家经常觉得反正加了保险的那个),这里已经无数工程师劝过,这个要尽量避免使用,因为至今其使用场景都是非常之低的,反而会带来一些其他的问题。那么它到底什么意思呢。volatile修饰变量,有强制从主存读和回写的作用,理解起来很简单,就是用的时候从主存读,改变了就往主存回写。请注意,它完全不能解决我们的多线程问题!!这里就不得不提到另一个人气更高的关键字:synchronized,这也是jdk5唯一实现锁的方式(也就是说volatile根本不能算锁),理解更加简单,比如举个例子,被其修饰的变量是不会被两个线程同时调用改变的,是真真正正的锁,当然这是最重的锁了,大家想一下就知道这种方式是非常保险的,但也是非常不高效的(就是那个最笨的方法)。synchronized可以保证操作是原子性的,volatile是不能保证的。

什么叫原子性呢,大家知道物理学原子某种程度上算是物体组成基本物质了,可以理解为不可分割的,这里只是便于大家理解关键字概念,不涉及物理学知识。强制读写就不是一个原子性操作,比如i++,看起来是一步,实际上,要从主存读,然后把值拷贝给临时变量,临时变量加一赋回i,然后写回主存,也就是说,这整个是可以分割的,是可以进行一半暂停的,这里我们完成了加法运算还没写回主存,另一个线程调用i,从主存读值,显然出现了同步问题。线程工作内存可以说是主存的一份缓存,为了避免缓存不一致,volatile需要废掉此缓存。除了内存缓存之外,在CPU硬件级别也是有缓存的,即寄存器。假如线程A将变量X由0修改为1的时候,CPU是在其缓存内操作,没有及时回写到内存,那么JVM是无法X=1是能及时被之后执行的线程B看到的,JVM在处理volatile变量的时候,也同样用了硬件级别的缓存一致性原则。详细请查阅硬件级别cpu缓存相关的博客。

volatile是可以防止指令重排的,我觉得这个没什么特别大意义,大家感兴趣可以自己了解一下,其实就是汇编里那个流水线,可以算是cpu对代码运行的优化,总之建议大家不要随意使用。使用可以参阅正确使用 Volatile 变量

synchronized这个用法,从对方法的修饰上来讲,其实可以理解为对对象的修饰,即是每个普通方法加了一个锁,这个锁便是对象本身。所以直接以synchronized (a){}为例,这里是给代码块上了一把对象a的锁。锁在Java内存模型里在不同机制下对应不同的数据结构。每个对象都有个长度2个字宽的对象头(在32位虚拟机里,1字宽是4个字节,64位虚拟机里,1字宽是8个字节。如果是数组对象,则对象头是3个字宽,其中第三个字存储数组的长度),这里面存储了对象的hashcode或锁信息,官方称它为“Mark Word”,如下图:

对象头的最后两位存储了锁的标志位,01是初始状态,未加锁,其对象头里存储的是对象本身的哈希码,随着锁级别的不同,对象头里存储不同的内容。偏向锁存储的是当前占用此对象的线程ID;而轻量级则存储指向线程栈中锁记录的指针。以上参考Java的多线程机制系列:(三)synchronized的同步原理。一把锁被一个线程持有,走完锁住得代码快锁会被自动释放,这里也可以在运行一半调用a.wait()方法释放锁,阻塞当前线程。另一个线程执行到某段代码后希望刚才那个线程继续运行,则可以调用a.notify()。当等待在obj上线程收到obj.notify()时,它就能重新获得obj的独占锁,并继续运行。注意了,notify()方法是随机唤起等待在当前对象的某一个线程。

以上可以作为多线程入门的参考,有时间写一下高级并发包的使用。

上一篇下一篇

猜你喜欢

热点阅读