java进阶

多线程并发总结录(二)--线程间共享

2021-01-13  本文已影响0人  Jack_Ou

线程间的共享

1. 内置锁Synchronized

​ Java 支持多个线程同时访问一个对象或者对象的成员变量,关键字synchronized 可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。

1.1 Synchronized的用法和用处

​ 用处:有多个线程会修改到某个属性的地方,需要对修改处加锁,保证每次只有一个线程可以修改。

1.2 对象锁

​ 对象锁是用于对象实例方法,或者一个对象实例上的。但是需要注意的是,被锁的对象不能发生改变,更不能创建对象,因为这会导致锁失效。不同对象的锁可以同时操作同一属性。

1.3 类锁

​ 类锁其实锁的是每个类对应的class 对象,类锁是用于类的静态方法或者一个类的class 对象上的

2. 轻量级锁volatile的用法和使用场景

​ volatile关键字可以保证不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

​ volatile虽然保证了操作可见性,但是不能保证变量在多线程操作下的线程安全。所以volatile的使用场景是:只有一个线程写,多个线程读的场景。

3. ThreadLocal的辨析

3.1 ThreadLocal的使用
public class ThreadLocalTest {

    static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();

    /**
     * 运行3个线程
     */
    public void StartThreadArray(){
        Thread[] runs = new Thread[3];
        for(int i=0;i<runs.length;i++){
            runs[i]=new Thread(new TestThread(i));
        }
        for(int i=0;i<runs.length;i++){
            runs[i].start();
        }
    }
    
    /**
     *类说明:测试线程,线程的工作是将ThreadLocal变量的值变化,并写回,看看线程之间是否会互相影响
     */
    public static class TestThread implements Runnable{
        int id;
        public TestThread(int id){
            this.id = id;
        }

        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            threadLocal.set("线程"+id);
            if(id==1) {
                // 只有当是线程1的时候才会执行
                threadLocal2.set(id);
                System.out.println(threadName+":"+threadLocal2.get());
            }
            System.out.println(threadName+":"+threadLocal.get());
        }
    }

    public static void main(String[] args){
       ThreadLocalTest test = new ThreadLocalTest();
        test.StartThreadArray();
    }
}
运行结果:
Thread-0:线程0
Thread-2:线程2
Thread-1:1
Thread-1:线程1

从结果可以看出,threadLocal在每个线程中都完成了安全的赋值,threadLocal2在线程1完成了线程安全的赋值。

究竟ThreadLocal是如何保证线程安全的呢?

先说说结论:通过每个线程使用ThreadLocal的副本数据才保证线程安全的。意思是每个线程都会拿到threadLocal的初始值,然后在自己线程中备份一个这个值,当对这个值进行操作的时候,各自线程使用各自备份的这个值,其他线程无法修改自己线程的值,所以保证了线程安全。

3.2 ThreadLocal实现解析
//java.lang.ThreadLocal中
public T get() {
    // 拿到当前线程
    Thread t = Thread.currentThread();
    // 拿到当前线程类中的threadLocals变量
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 从map中拿到Entry, key是当前的threadlocal对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 从entry中拿到值
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

//java.lang.Thread中
public class Thread implements Runnable {
    ...
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ...
}

​ 从以上代码和如下ThreadLocal图解可知道,每一个线程都持有一个ThreadLocalMap对象,ThreadLocalMap中保存Entry对象,其中每一个Entry都包括<Key,Value>键值对,键是用户建的ThreadLocal对象,值是初始化值。然后当线程需要处理到ThreadLocal中的值时,每一个线程会将值拷贝一份到线程中进程独自操作这个值,从而实现了线程安全。

ThreadLocal图解.png
3.3 ThreadLocal引发的泄露问题

​ (坚持三个原则:发现问题,定位问题,解决问题)

发现问题:

​ 运行一下代码(别释放remove注释),设置一下堆区大小,很快就会发现OOM了。

public class MemoryLeak {
    static Executor executor = new ScheduledThreadPoolExecutor(5);
    static ThreadLocal<LocalValue> threadLocal = new ThreadLocal<>();

    static class LocalValue {
        private byte[] a = new byte[1024 * 1024 * 10];
    }

    public static void main(String[] args) {
        for (int i = 0; i < 500; i++) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    threadLocal.set(new LocalValue());
                    System.out.println("use thread local");
                    // threadLocal.get(); 以下是使用threadlocal
                    // ....使用代码

                    // threadLocal.remove();
                }
            });
        }
    }
}

定位问题:

​ 结合下图和从3.2分析可以看到,当前线程是会持有ThreadLocalMap对象,虽然map中key持有的threadlocal对象,他是弱引用,在GC的时候会被回收,但是Value值是强应用,在GC的时候,只要线程没有运行结束,value对象不会被释放。所以在线程里一直创建对象,就会导致内存泄漏。

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
ThreadLocal内存泄漏问题.png

​ 其实我们在测试的过程中会发现只会泄漏一部分内存,原因是什么呢?

​ 查看ThreadLocal中方法调用栈:

​ get() -> replaceStaleEntry() -> expungeStaleEntry()

​ set() -> replaceStaleEntry() -> expungeStaleEntry()

​ remove() -> expungeStaleEntry()

结论:从调用来看,get、set、remove最终都会调用到expungeStaleEntry(),expungeStaleEntry()会删除map中key为null的节点。但是每次get和set不会立马调用,所以才会导致泄漏一部分。

解决问题:

​ 因为remove()方法会立马调用到expungeStaleEntry()来清除key为空的过时条目。所以在使用完成之后,最好调用一下remove()方法,尽快回收不用内存空间(如上面代码屏蔽掉的代码)。

3.4 ThreadLocal的线程不安全

​ 运行如下代码可以发现如果Number声明成静态对象,到导致线程不安全。因为静态对象在堆空间中只有一份,每次修改之后ThreadLocal在每个线程中备份的那份都是随线程修改这个值一直改变的,所以会存在线程不安全。正确做法:声明放在threadlocal中的对象不能是静态的即可。

public class ThreadLocalUnsafe implements Runnable {

    // ThreadLocal使用静态的对象作value,会导致线程不安全
    // public static Number number = new Number(0);
    public Number number = new Number(0);

    @Override
    public void run() {
        //每个线程计数加一
        number.setNum(number.getNum()+1);
      //将其存储到ThreadLocal中
        value.set(number);
        //输出num值
        System.out.println(Thread.currentThread().getName()+"="+value.get().getNum());
    }

    public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
    };

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new ThreadLocalUnsafe()).start();
        }
    }

    private static class Number {
        public Number(int num) {
            this.num = num;
        }

        private int num;

        public int getNum() {
            return num;
        }

        public void setNum(int num) {
            this.num = num;
        }

        @Override
        public String toString() {
            return "Number [num=" + num + "]";
        }
    }

}
3.5 Synchronized和ThreadLocal的区别

​ ThreadLocal是一个线程隔离的变量存储的管理实体(注意:不是存储用的),它以Java类方式表现;
​ synchronized是Java的一个保留字,只是一个代码标识符,它依靠JVM的锁机制来实现临界区的函数、变量在CPU运行访问中的原子性。

​ 虽然两个实现线程安全的手段不同,设计初衷也不同,没有可比性。

​ 但是我还是想简单的总结一下:synchronized实现线程安全的方案是 时间换空间的方案;而ThreadLocal实现线程安全的方式是空间换时间的方案

测试用例代码见: git@github.com:oujie123/UnderstandingOfThread.git

上一篇下一篇

猜你喜欢

热点阅读