线程的创建很简单,一般是集成Thread类或者实现Runnable接口,我就不细说了。然后,要牢记多线程的3大特性:
多线程的三个特性:原子性、可见性、有序性
原子性:是指一个操作是不可中断的。即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。比如,对于一个静态全局变量int i,两个线程同时对它赋值,线程A给他赋值为1,线程B给他赋值为-1。那么不管这两个线程以何种方式。何种步调工作,i的值要么是1,要么是-1.线程A和线程B之间是没有干扰的。这就是原子性的一个特点,不可被中断。
可见性:是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。显然,对于串行来说,可见性问题是不存在的。
有序性:在并发时,程序的执行可能会出现乱序。给人的直观感觉就是:写在前面的代码,会在后面执行。有序性问题的原因是因为程序在执行时,可能会进行指令重排,重排后的指令与原指令的顺序未必一致。
而共享变量的写操作出错,最重要的是原子性,一般多线程的问题主要抓住这个。
线程安全问题
一般多线程编程都会遇到线程安全的问题,线程安全总体来说是因为多个线程竞争共享资源造成的。比如:
public class Test{
private int num = 0;
public void add(int value){
this.num = this.num + value;
}
}
两个线程分别加了2和3到count变量上,两个线程执行结束后count变量的值应该等于5。如果两个线程同时执行这个对象的add()方法,会造成这种现象:线程A先读到num为0,此时恰好线程B也读到num为0,然后A,B同时执行加2和加3的操作,如果A先赋值num为2,然后B又赋值num为3,会造成最后结果为3;或者反过来,造成num为2,使得最后的结果无法预料。
如果线程并没有共享资源,那么多线程执行的代码是安全的,比如:
类方法中局部变量或者局部对象引用
public class Test{
public void add(int value){
int num = 0;
String a = new String("aa");
num = num + value;
}
}
还有一种安全的方法,就是每个线程都是执行同一个类不同对象的方法,虽然代码相同,但是不同的对象空间,也不会出现问题,如servlet。
线程状态
线程的状态实现通过 Thread.State 常量类实现,有 6 种线程状态:new(新建)、runnnable(可运行)、blocked(阻塞)、waiting(等待)、time waiting (定时等待)和 terminated(终止)。状态转换图如下:
线程状态流程大致如下:
- 线程创建后,进入 new 状态
- 调用 start 或者 run 方法,进入 runnable 状态
- JVM 按照线程优先级及时间分片等执行 runnable 状态的线程。开始执行时,进入 running 状态
- 如果线程执行 sleep、wait、join,或者进入 IO 阻塞等。进入 wait 或者 blocked 状态
- 线程执行完毕后,线程被线程队列移除。最后为 terminated 状态。
ThreadLocal
ThreadLocal与线程同步无关,它虽然提供了一种解决多线程环境下成员变量的问题,但是它并不是解决多线程共享变量的问题。
它的API介绍如下:
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
所以ThreadLocal与线程同步机制不同,线程同步机制是多个线程共享同一个变量,而ThreadLocal是为每一个线程创建一个单独的变量副本,故而每个线程都可以独立地改变自己所拥有的变量副本,而不会影响其他线程所对应的副本。可以说ThreadLocal为多线程环境下变量问题提供了另外一种解决思路。
ThreadLocal定义了四个方法:
- get():返回此线程局部变量的当前线程副本中的值。
- initialValue():返回此线程局部变量的当前线程的“初始值”。
- remove():移除此线程局部变量当前线程的值。
- set(T value):将此线程局部变量的当前线程副本中的值设定为指定值。
除了这四个方法,ThreadLocal内部还有一个静态内部类ThreadLocalMap,该内部类才是实现线程隔离机制的关键,get()、set()、remove()都是基于该内部类操作。ThreadLocalMap提供了一种用键值对方式存储每一个线程的变量副本的方法,key为当前ThreadLocal对象,value则是对应线程的变量副本。
对于ThreadLocal需要注意的有两点:
- ThreadLocal实例本身是不存储值,它只是提供了一个在当前线程中找到副本值得key。
-
是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中,有些小伙伴会弄错他们的关系。
下图是Thread、ThreadLocal、ThreadLocalMap的关系
ThreadLocal示例
package com.xushu.multi;
public class Test{
private static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){
// 实现initialValue()
@Override
protected Integer initialValue() {
return 0; //这里返回了一个0
}
};
public int nextSeq(){
count.set(count.get() + 1);
return count.get();
}
private static class SeqThread implements Runnable{
private Test te;
SeqThread(Test te) {
this.te = te;
}
@Override
public void run() {
for(int i = 0; i < 3; i++){
System.out.println(Thread.currentThread().getName() + " seqCount :" + te.nextSeq());
}
}
}
public static void main(String[] args) {
Test te = new Test();
Thread t1 = new Thread(new SeqThread(te));
Thread t2 = new Thread(new SeqThread(te));
Thread t3 = new Thread(new SeqThread(te));
Thread t4 = new Thread(new SeqThread(te));
t1.start();
t2.start();
t3.start();
t4.start();
}
}
可以看出,每个线程都有自己的一个变量副本,所以从根本上避免了读同一个变量。但是,如果在initialValue()方法中,如果return的是一个共有变量,那就是所有的线程都访问同一个变量了,所以ThreadLocal就失效了。这篇文章有解析。
ThreadLocal源码解析
ThreadLocal虽然解决了这个多线程变量的复杂问题,但是它的源码实现却是比较简单的。ThreadLocalMap是实现ThreadLocal的关键,我们先从它入手。
ThreadLocalMap
ThreadLocalMap其内部利用Entry来实现key-value的存储,如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
从上面代码中可以看出Entry的key就是ThreadLocal,而value就是值。同时,Entry也继承WeakReference,所以说Entry所对应key(ThreadLocal实例)的引用为一个弱引用(关于弱引用这里就不多说了,感兴趣的可以关注这篇博客Java 理论与实践: 用弱引用堵住内存泄漏)
ThreadLocalMap的源码稍微多了点,我们就看两个最核心的方法getEntry()、set(ThreadLocal> key, Object value)方法。
set(ThreadLocal> key, Object value)
private void set(ThreadLocal<?> key, Object value) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
// 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置
int i = key.threadLocalHashCode & (len-1);
// 采用“线性探测法”,寻找合适位置
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// key 存在,直接覆盖
if (k == key) {
e.value = value;
return;
}
// key == null,但是存在值(因为此处的e != null),说明之前的ThreadLocal对象已经被回收了
if (k == null) {
// 用新元素替换陈旧的元素
replaceStaleEntry(key, value, i);
return;
}
}
// ThreadLocal对应的key实例不存在也没有陈旧元素,new 一个
tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
int sz = ++size;
// cleanSomeSlots 清楚陈旧的Entry(key == null)
// 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
这个set()操作和我们在集合了解的put()方式有点儿不一样,虽然他们都是key-value结构,不同在于他们解决散列冲突的方式不同。集合Map的put()采用的是拉链法,而ThreadLocalMap的set()则是采用开放定址法(具体请参考散列冲突处理系列博客)。掌握了开放地址法该方法就一目了然了。
set()操作除了存储元素外,还有一个很重要的作用,就是replaceStaleEntry()和cleanSomeSlots(),这两个方法可以清除掉key == null 的实例,防止内存泄漏。在set()方法中还有一个变量很重要:threadLocalHashCode,定义如下:
private final int threadLocalHashCode = nextHashCode();
从名字上面我们可以看出threadLocalHashCode应该是ThreadLocal的散列值,定义为final,表示ThreadLocal一旦创建其散列值就已经确定了,生成过程则是调用nextHashCode():
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
nextHashCode表示分配下一个ThreadLocal实例的threadLocalHashCode的值,HASH_INCREMENT则表示分配两个ThradLocal实例的threadLocalHashCode的增量,从nextHashCode就可以看出他们的定义。
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);
}
由于采用了开放定址法,所以当前key的散列值和元素在数组的索引并不是完全对应的,首先取一个探测数(key的散列值),如果所对应的key就是我们所要找的元素,则返回,否则调用getEntryAfterMiss(),如下:
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];
}
return null;
}
这里有一个重要的地方,当key == null时,调用了expungeStaleEntry()方法,该方法用于处理key == null,有利于GC回收,能够有效地避免内存泄漏。
get()
- 返回当前线程所对应的线程变量
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的成员变量 threadLocal
ThreadLocalMap map = getMap(t);
if (map != null) {
// 从当前线程的ThreadLocalMap获取相对应的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
// 获取目标值
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
首先通过当前线程获取所对应的成员变量ThreadLocalMap,然后通过ThreadLocalMap获取当前ThreadLocal的Entry,最后通过所获取的Entry获取目标值result。
getMap()方法可以获取当前线程所对应的ThreadLocalMap,如下:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
set(T value)
- 设置当前线程的线程局部变量的值。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
获取当前线程所对应的ThreadLocalMap,如果不为空,则调用ThreadLocalMap的set()方法,key就是当前ThreadLocal,如果不存在,则调用createMap()方法新建一个,如下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
initialValue()
- 返回该线程局部变量的初始值。
protected T initialValue() {
return null;
}
该方法定义为protected级别且返回为null,很明显是要子类实现它的,所以我们在使用ThreadLocal的时候一般都应该覆盖该方法。该方法不能显示调用,只有在第一次调用get()或者set()方法时才会被执行,并且仅执行1次。
remove()
- 将当前线程局部变量的值删除。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
该方法的目的是减少内存的占用。当然,我们不需要显示调用该方法,因为一个线程结束后,它所对应的局部变量就会被垃圾回收。
参考文献
1.并发编程网
网友评论