2

线程间的协作

线程之间相互配合,完成某项工作,比如:一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。前者是生产者,后者就是消费者,这种模式隔离了“做什么”(what)和“怎么做”(How),简单的办法是让消费者线程不断地循环检查变量是否符合预期在while循环中设置不满足的条件,如果条件满足则退出while循环,从而完成消费者的工作。却存在如下问题:

1)难以确保及时性

2)难以降低开销。如果降低睡眠的时间,比如休眠1毫秒,这样消费者能更加迅速地发现条件变化,但是却可能消耗更多的处理器资源,造成了无端的浪费。

等待/通知机制

是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

notify()

随机通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入WAITING状态

notifyAll()

通知所有等待在该对象上的线程

wait()

调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回.需要注意,调用wait()方法后,会释放对象的锁

wait(long)

超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回

wait (long,int)

对于超时时间更细粒度的控制,可以达到纳秒

等待和通知的标准范式

等待方遵循如下原则

1)获取对象的锁

2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件

3)条件满足则执行对应的逻辑

通知方遵循如下原则

1)获得对象的锁

2)改变条件

3)通知所有等待在对象上的线程

在调用wait()、notify()系列方法之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法、notify()系列方法,进入wait()方法后,当前线程释放锁,在从wait()返回前,线程与其他线程竞争重新获得锁, 执行notify()系列方法的线程退出调用了notifyAll的synchronized代码块的时候后,他们就会去竞争。如果其中一个线程获得了该对象锁,它就会从wait()后面的代码继续往下执行,在它退出synchronized代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕

notify和notifyAll应该用谁

尽可能用notifyall(),谨慎使用notify(),因为notify()只会唤醒一个线程,我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程

ThreadLocal

与Synchonized的比较

ThreadLocalSynchonized都用于解决多线程并发訪问。可是ThreadLocal与synchronized有本质的差别。synchronized是利用锁的机制,使变量或代码块在某一时该仅仅能被一个线程訪问。而ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时间訪问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享

ThreadLocal的使用

Spring的事务就借助了ThreadLocal类:Spring会从数据库连接池中获得一个connection,然会把connection放进ThreadLocal中,也就和线程绑定了,事务需要提交或者回滚,只要从ThreadLocal中拿到connection进行操作

ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:

  • void set(Object value)

设置当前线程的线程局部变量的值

  • public Object get()

该方法返回当前线程所对应的线程局部变量

  • public void remove()

将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度

  • protected Object initialValue()

返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null

下面是ThreadLocal的一段示例代码
/**
 * ThreadLocal线程局部变量
 *
 * ThreadLocal是使用空间换时间,synchronized是使用时间换空间
 * 比如在hibernate中session就存在与ThreadLocal中,避免synchronized的使用
 *
 * 运行下面的程序,理解ThreadLocal
 * 
 */
import java.util.concurrent.TimeUnit;

public class ThreadLocal2 {
    //volatile static Person p = new Person();
    static ThreadLocal<Person> tl = new ThreadLocal<>();
    
    public static void main(String[] args) {
                
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            System.out.println(tl.get());
            System.out.println(Thread.currentThread().getName() + ": " + tl.get());
        }).start();
        
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            tl.set(new Person());
            System.out.println(Thread.currentThread().getName() + ": " + tl.get());

        }).start();
    }
    
    static class Person {
        String name = "zhangsan";
    }
}

运行结果:

Thread-1: com.juc.c_022_RefTypeAndThreadLocal.ThreadLocal2$Person@18047445
Thread-0: null

ThreadLocal源码解析

image.png

每个线程都有自己的ThreadLocalMap,这样就做到了线程隔离,其中EntryKey就是ThreadLocalvalueThreadLocal保存的类型的value,用Map的原因是线程可能有不同ThreadLocal,正好保存在Map的不同Entry
ThreadLocal set函数

image.png

ThreadLocal get函数

image.png

上面先取到当前线程,然后调用getMap方法获取对应的ThreadLocalMap,ThreadLocalMap是ThreadLocal的静态内部类,然后Thread类中有一个这样类型成员,所以getMap是直接返回Thread的成员

image.png
image.png

看下ThreadLocal的内部类ThreadLocalMap源码

image.png

可以看到有个Entry内部静态类,它继承了WeakReference,总之它记录了两个信息,一个是ThreadLocal<>类型,一个是Object类型的
值。getEntry方法则是获取某个ThreadLocal对应的值,set方法就是更新或赋值相应的ThreadLocal对应的值

回顾我们的get方法,其实就是拿到每个线程独有的ThreadLocalMap
然后再用ThreadLocal的当前实例,拿到Map中的相应的Entry,然后就可以拿到相应的值返回出去。当然,如果Map为空,还会先进行map的创建,初始化等工作

ThreadLocal内存泄漏分析

预备知识,强软弱虚引用

强引用

就是指在程序代码之中普遍存在的,类似Object obj=new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例

软引用:大对象、常用对象的缓存

是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行第二次回收(只要内存不足,不管引用是否还在,都会进行回收)。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用

弱引用:没有容器指向的时候就要清除的缓存(ThreadLocal、WeakHashMap

也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例(只要进行GC就会回收)。在JDK 1.2之后,提供了WeakReference类来实现弱引用

虚引用:管理堆外内存

也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象实例被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用

Entry为什么用软引用,是否有内存泄漏

为什么Entry使用弱应用

若是强引用,即使ThreadLocalRef = null,但key的引用依然指向ThreadLocal对象,所以不能释放,会有内存泄漏,而使用弱引用则不会
image.png
image.png

使用弱引用就不会内存泄漏吗
  1. 还是会有内存泄漏,ThreadLocal被回收,key的值变为null,则导致整个value再也无法被访问到,因此依然存在内存泄漏
  2. 只有当前thread结束以后,current thread就不会存在栈中,强引用断开,Current Thread、Map value将全部被GC回收。最好的做法是不在需要使用ThreadLocal变量后,都调用它的remove()方法,清除数据
  3. 其实考察ThreadLocal的实现,我们可以看见,无论是get()、set()在某些时候,调用了expungeStaleEntry方法用来清除Entry中Key为null的Value,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。只有remove()方法中显式调用了expungeStaleEntry方法

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用

总结
  1. JVM利用设置ThreadLocalMap的Key为弱引用,来避免内存泄露
  2. JVM利用调用remove、get、set方法的时候,回收弱引用
  3. 当ThreadLocal存储很多Key为null的Entry的时候,而不再去调用remove、get、set方法,那么将导致内存泄漏
  4. 使用线程池 + ThreadLocal时要小心,因为这种情况下,线程是一直在不断的重复运行的,从而也就造成了value可能造成累积的情况
  5. ThreadLocalMap中保存的其实是对象的一个引用,必须保证不同线程的引用指向不同对象,否则会导致每个线程保存的是同一个value(set的是一个static对象),没有起到线程隔离的作用

DragonflyDavid
182 声望19 粉丝

尽心,知命


引用和评论

0 条评论