1、并发编程中的三个概念

原子性:一个或多个操作。要么全部执行完成并且执行过程不会被打断,要么不执行。最常见的例子:i++/i--操作。不是原子性操作,如果不做好同步性就容易造成线程安全问题。

可见性:多个线程访问同一个变量,一个线程改变了这个变量的值,其他线程可以立即看到修改的值。可见性的问题,有两种方式保证。一是volatile关键字,二是通过synchronized和lock。详细在后面。

有序性:程序执行的顺序按照代码的先后顺序执行。

要了解有序性需要了解一下指令重排序。处理器为了提供运行效率,会将代码优化,不保证各个语句的执行顺序,但会保证执行结果跟代码顺序执行一致,其不影响单线程的执行结果,但会影响线程并发执行的正确性。指令重排序会考虑指令之间的数据依赖性,如果一个指令B必须用到指令A的结果,那么处理器会保证A在B之前执行。

要保证并发程序正确的执行,必须要保证原子性、可见性及有序性。只要有一个没有被保证,就可能导致程序运行不正确。


2、Java内存模型

Java内存模型规定:所有变量存在主内存,每个线程有自己的工作内存。线程对变量的操作必须在工作内存进行,而不能直接对主内存进行操作。并且每个线程不能访问其他线程的工作内存。

JAVA语言本身提供的对原子性、可见性及有序性的保证:

原子性:java中,对于引用变量,和大部分的原始数据类型的读写(除long 和 double外)操作都是原子的。这些操作不可被中断,要么执行,要么不执行。对于所有被声明为volatile的变量的读写,都是原子的(除long和double外)

可见性:java提供了volatile关键字来保证可见性。

当一个共享变量被volatile修饰时,它会保证修改的值立即被更新到主内存。其他线程读取时会从内存中读到新值。普通的共享变量不能保证可见性,其被写入内存的时机不确定。当其他线程去读,可能读到的是旧的值。
另外通过synchronized和lock也可以保证可见性。它们能保证同一时刻只有一个线程获取锁然后执行同步代码。并在释放锁之前对变量的修改刷新到住内存中。以此来保证可见性

有序性:java内存模型中,允许编译器和处理器对指令进行重排序。其会影响多线程并发执行的正确性。在java里可以通过volatile关键字,还有synchronized和lock来保证有序性。

synchronized和lock保证每个时刻只有一个线程执行同步代码,使得线程串行化执行同步代码,保证了有序性。volatile如何保证的讲解在后面。


3、volatile关键字详解

  一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰后,就具备了两层语义:保证了不同线程对这个变量进行操作时的可见性和禁止了指令重排序。

  关于volatile保证可见性的原因我们上面已经讲过了,现在来看看volatile通过禁止指令重排序来保证一定的有序性的意思:

  1、当程序执行到volatile变量的读操作或写操作时,在其之前的操作的更改肯定全部已经进行,且结果对后面的操作可见。其后面的操作肯定还没有进行

  2、在进行指令优化时,不能将在volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放在其前面执行。

一个拓展:详情会在明年开始学习JVM时进行补充。
volatile关键字的原理和实现机制:
在加入volatile关键字时,会多出一个lock前缀指令。lock前缀指令相当于一个内存屏障,其提供三个功能。
  1、它会强制将对缓存的修改操作立即写入主内存。
  2、如果是写操作,它会导致其他CPU中对应的缓存行无效
  3、它确保指定重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面。即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。

volatile关键字能保证可见性和一定的有序性,那它能保证对变量的操作是原子性吗?

答案是不能的。如常见的自增操作是不具备原子性的,它包括读取变量的原始值,进行加一操作,写入工作内存三个子操作。这就导致进行自增时可能发生子操作被分割执行。

如一个某个时刻变量i的值是10。线程A对i进行自增操作,在读取i的原始值后被阻塞,然后线程B对i进行自增,去读取i的原始值。由于A没有对i进行修改,所以B在主内存中读取到的是原始值并进行加1。然后把11写入主内存。然后A对i进行操作。由于已经读取了i的值,此时A的工作内存中i的值还是10,A对i进行自增加一后,把11写入主内存。两个线程分别进行了一次自增操作,但是结果却是11。

要注意的是:volatile无法保证对变量的任何操作都是原子性的。

使用volatile关键字时必须具备两个条件:

  1、对变量的写操作不依赖于当前值。

  2、该变量没有包含在具有其他变量的不变式中。

即保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。


4、ThreadLocal

  ThreadLocal,线程本地变量也可以叫线程本地存储。其为每一个线程维护了一个独立的变量副本。将对象的可见范围限制在同一个线程内。

  ThreadLoca类中提供了几个常用方法:

public T get() { }---获取ThreadLocal在当前线程中保存的变量副本
public void set(T value) { }---设置当前线程中变量的副本
public void remove() { }---移除当前线程中变量的副本
protected T initialValue() { }---protected修饰的方法。ThreadLocal提供的只是一个浅拷贝,如果变量是一个引用类型,那么就要重写该函数来实现深拷贝。建议在使用ThreadLocal一开始时就重写该函数

  ThreadLocal与synchronized这样的所机制不同,ThreadLocal是为了解决同一个变量如何不被多个线程共享,而锁更强调如果同步多个线程去正确共享一个变量。从开销上讲,锁机制是用时间换空间,ThreadLocal是用空间换时间。

  ThreadLocal中含有一个叫ThreadLocalMap的内部类,该类为一个采用了线性探测法实现的HashMap,它的key为ThreadLocal对象。ThreadLocalMap正是用来存储变量副本的。ThreadLocal中只含有三个成员变量,都与ThreadLocalMap的hash策略相关。唯一的threadLocalHashCode是用来寻址的hashCode。

  1、要获得当前线程私有的变量副本时需调用get()函数,它会先调用getMap()去获取当前线程的ThreadLocalMap,这个函数需要接收当前线程的实例作为参数。如果得到的ThreadLocalMap为null,那么就调用setInitialValue()函数来进行初始化,如果不为null,就通过map来获取变量副本并返回。

public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

    2、setInitialValue()函数会先调用initialValue()函数来生成初始值,该函数默认返回null。我们可以通过重写这个函数来返回我们想要在ThreadLocal中维护的变量。之后调getMap()函数去获得ThreadLocalMap。如果该map已经存在,那么就用新获得的value去覆盖旧值。否则调用createMap()创建新的map。

   private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

set()和remove()函数都是通过getMap()来获得ThreadLocalMap然后对其进行操作

public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

getMap()与createMap()函数实现如下,我们可以观察到ThreadLocalMap是存放在Thread中的

 ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocal的设计初衷就是为了避免多个线程去并发访问同一个对象,尽管它是线程安全的。因此如果用普遍的方法,通过一个全局的线程安全的map来存储多个线程的变量副本就违背了ThreadLocal的本意。在每个Thread中存放与它关联的ThreadLocalMap是完全符合其设计思想的。当想对线程局部变量进行操作时,只要把Thread作为key来获取Thread中的ThreadLocalMap即可。这种设计相比采用一个全局map的方法会占用很多内存空间,但其不需要额外采取锁等线程同步方法而节省了时间上的消耗。


ThreadLocal中的内存泄露问题:

  如果ThreadLocal被设置为null后,并且没有任何强引用指向它,根据垃圾回收的可达性分析算法,ThreadLocal将被回收。这样的话,ThreadLocalMap中就会含有key为null的Entry,而且ThreadLocalMap是在Thread中的,只要线程迟迟不结束,这些无法访问到的value就会形成内存泄露。为了解决这个问题,ThreadLocalMap中的getEntry()、set()和remove()函数都会清理key为null的Entry,以下面的getEntry()函数为例。

private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length; while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
}

  要注意的是ThreadLocalMap的key是一个弱引用。在这里我们分析一下强引用key和弱引用key的差别

  强引用key:ThreadLocal被设置为null,由于ThreadLocalMap持有ThreadLocal的强引用,如果不手动删除,那么ThreadLocal将不会回收,产生内存泄漏。

  弱引用key:ThreadLocal被设置为null,由于ThreadLocalMap持有ThreadLocal的弱引用,即便不手动删除,ThreadLocal仍会被回收,ThreadLocalMap在之后调用set()、getEntry()和remove()函数时会清除所有key为null的Entry。

  ThreadLocalMap仅仅含有这些被动措施来补救内存泄露问题,如果在之后没有调用ThreadLocalMap的set()、getEntry()和remove()函数的话,那么仍然会存在内存泄漏问题。在使用线程池的情况下,如果不及时进行清理,内存泄漏问题事小,甚至还会产生程序逻辑上的问题。所以,为了安全地使用ThreadLocal,必须要像每次使用完锁就解锁一样,在每次使用完ThreadLocal后都要调用remove()来清理无用的Entry。

多线程-volatile关键字和ThreadLocal的更多相关文章

  1. Volatile关键字和ThreadLocal变量的简单使用

    原创:转载需注明原创地址 https://www.cnblogs.com/fanerwei222/p/11812459.html package thread; /** * volatile关键字和T ...

  2. java多线程 -- volatile 关键字 内存 可见性

    内存可见性(Memory Visibility) 1 内存可见性(Memory Visibility)是指当某个线程正在使用对象状态而另一个线程在同时修改该状态,需要确保当一个线程修改了对象状态后,其 ...

  3. [Java多线程] volatile 关键字正确使用方法

    volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性,即多线程环境中,使用 volatile 关键字的变量仅可以保证不同线程读取变量时,可以读到最新修改的变量值,但是 ...

  4. Java 多线程 - Volatile关键字

    作者: dreamcatcher-cx 出处: <http://www.cnblogs.com/chengxiao/> 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明 ...

  5. JAVA多线程---volatile关键字

    加锁机制既可以确保可见性又可以保证原子性,而volatile变量只能确保可见性. 当把变量声明为volatile时候 编译器与运行时都会注意到这个变量是共享的,不会将该变量上的操作与其他内存操作一起重 ...

  6. Java多线程-----volatile关键字详解

       volatile原理     Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程.当把变量声明为volatile类型后, 编译器与运行时都会注意 ...

  7. Java多线程——volatile关键字、发布和逸出

    1.volatile关键字 Java语言提供了一种稍弱的同步机制,即volatile变量.被volatile关键字修饰的变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在每次读取volatit ...

  8. zz剖析为什么在多核多线程程序中要慎用volatile关键字?

    [摘要]编译器保证volatile自己的读写有序,但由于optimization和多线程可以和非volatile读写interleave,也就是不原子,也就是没有用.C++11 supposed会支持 ...

  9. 重新想象 Windows 8 Store Apps (48) - 多线程之其他辅助类: SpinWait, SpinLock, Volatile, SynchronizationContext, CoreDispatcher, ThreadLocal, ThreadStaticAttribute

    [源码下载] 重新想象 Windows 8 Store Apps (48) - 多线程之其他辅助类: SpinWait, SpinLock, Volatile, SynchronizationCont ...

随机推荐

  1. Android--字符串和Drawable之间互相转化

    //将字符串转化成Drawable public synchronized static Drawable StringToDrawable(String icon) { if (icon == nu ...

  2. Elasticsearch5.4署遇到的问题

    问题一 can not run elasticsearch as root Elastic 不建议通过root用户启动ES服务器,如果非要用root启动,可以在config/jvm.options配置 ...

  3. matlab练习程序(Levenberg-Marquardt法最优化)

    上一篇博客中介绍的高斯牛顿算法可能会有J'*J为奇异矩阵的情况,这时高斯牛顿法稳定性较差,可能导致算法不收敛.比如当系数都为7或更大的时候,算法无法给出正确的结果. Levenberg-Marquar ...

  4. chrome新版打开新标签页自动打开谷歌主页

    最近更新了chrome,发现新版有个问题. 打开标签页时会自动跳转至 https://www.google.com/webhp?ie=UTF-8&gws_rd=cr&rct=j 导致我 ...

  5. 洗礼灵魂,修炼python(41)--巩固篇—从游戏《绝地求生-大逃杀》中回顾面向对象编程

    声明:本篇文章仅仅以游戏<绝地求生>作为一个参考话题来介绍面向对象编程,只是作为学术引用,其制作的非常简易的程序也不会作为商业用途,与蓝洞公司无关. <绝地求生>最近很火,笼络 ...

  6. Pandas:让你像写SQL一样做数据分析

    1. 引言 Pandas是一个开源的Python数据分析库.Pandas把结构化数据分为了三类: Series,1维序列,可视作为没有column名的.只有一个column的DataFrame: Da ...

  7. Django【进阶篇】

    目录 一.Model 二.admin 三.Form组件 四.Cookie 五.Session 六.分页 七.序列化 一.Model 数据库的配置 1.django默认支持sqlite,mysql, o ...

  8. 【PAT】B1056 组合数的和(15 分)

    就看着代码量一直到没什么好说的了 #include<stdio.h> int main(){ int N,K;scanf("%d",&N); int sum=0 ...

  9. Linux 小知识翻译 - 「端口和端口号」

    这次说说「端口」和「端口号」. 平时经常会听人说「打开了80号端口」,为了安全「不要打开多余的端口」等等.那么,这里的端口或者端口号是什么呢? 首先,「端口」是TCP或者UDP上使用的概念,经常被比喻 ...

  10. Mybatis&orcale update语句中接收参数为对象

    Mybatis的 mapper.xml 中 update 语句使用 if 标签判断对像属性是否为空值. UserDTO是传过来参数的类型,userDTO是在mapperDao接口中给更新方法的参数起的 ...