美文网首页
java并发(一):线程基础篇

java并发(一):线程基础篇

作者: 放开那个BUG | 来源:发表于2018-10-26 20:42 被阅读89次

线程的创建很简单,一般是集成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需要注意的有两点:
  1. ThreadLocal实例本身是不存储值,它只是提供了一个在当前线程中找到副本值得key。
  2. 是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.并发编程网

相关文章

网友评论

      本文标题:java并发(一):线程基础篇

      本文链接:https://www.haomeiwen.com/subject/xtrltqtx.html