多线程-volatile关键字和ThreadLocal

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。

上一篇:POJ 3298 Antimonotonicity (思维)


下一篇:缓慢拒绝服务攻击- slowloris.pl