jdk源码解析之java.lang.Thread

本文详细剖析了Java中Thread类的join方法工作原理,指出其通过wait方法实现线程协作。同时,讨论了未捕获异常的处理机制,包括JVM默认行为和自定义异常处理器的设置。最后,探讨了ThreadLocal的内部实现,特别是ThreadLocalMap如何在每个线程中保存独立的变量副本,以及使用时的注意事项,如内存泄漏问题。
摘要由CSDN通过智能技术生成

Thread线程类,虽然使用很多,但是里面有一些代码原理可能我们并未探究过,作者尝试去揭示这些代码的原理。

目录

1.join方法理解与注意事项
2.未捕捉异常处理
3.ThreadLocal.ThreadLocalMap成员变量

1. join方法理解与注意事项

join方法jdk中的解释:Waits for this thread to die. 等待这个线程结束。this指的就是调用join()的线程对象。也许初学者会搞混到底是谁阻塞,谁执行。我们查看如下代码:

public class ThreadSource2  extends Thread{
	@Override
	public void run() {
		System.out.println("进入run方法");
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("run执行结束!");
	}	
	public static void main(String[] args) throws Exception {
		Thread thread = new Thread(new ThreadSource2());
		thread.start();
		thread.join();
		System.out.println("main执行结束");
	}
}
输出:
进入run方法
run执行结束!
main执行结束

要理解的是,这里的thread对象,是在堆中创建的一个Java线程对象的引用。JVM虚拟机并不能自己创建线程,每一个Java线程对象需要一个系统操作线程支持。所以当我们去查看如下join方法的源码时:

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {//public final native boolean isAlive();
                wait(0);//这里wait(0)调用的是Object的 public final native void wait(long timeout)
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

其原理就是调用本地方法isAlive询问操作系统这个线程结束没?如果没有结束,那就调用本地方法的wait()等待。直到另一个线程调用了this.notifyAll()。这里的wait等待是指调用thread.join()的当前线程等待,截取源码中wait的注释如下:

Causes the current thread to wait until either another thread invokes the java.lang.Object.notify() method or the java.lang.Object.notifyAll() method for this object, or a specified amount of time has elapsed.

上述描述了是当前线程等待,直到另一个线程在这个对象上调用了唤醒方法。因此回到我们的代码,我们在main方法中调用thread.join(),那就是main方法线程需要等待thread对象指向的系统操作线程结束,才可以继续执行。
此处需要注意的是,因为thread.join()是通过thread.wait()方法实现等待的。是jdk内置的线程协作方法,因此它并不建议我们直接用线程的引用对象去调用wait,notifyAll()。这可能导致开发者意料之外的问题。

It is recommended that applications not use wait, notify, or notifyAll on Thread instances.

2.未捕捉异常处理

在线程中,我们会去捕捉异常,然后将异常打印出来。那么如果我们未捕捉到的异常会怎么样呢?
在Thread源码中,提供了一个接口UncaughtExceptionHandler ,当有未捕捉的异常而即将结束时,JVM会通过getUncaughtExceptionHandler 获取该线程的未捕捉异常处理器,并调用uncaughtException 方法。它会将线程和错误信息作为参数传入。如果没有设置处理器,那就会使用当前线程组的处理器处理。

 /**
 When a thread is about to terminate due to an uncaught exception the Java Virtual Machine will query the thread for its UncaughtExceptionHandler using getUncaughtExceptionHandler and will invoke the handler's uncaughtException method, passing the thread and the exception as arguments
 */
 public interface UncaughtExceptionHandler {
        /**
         * Method invoked when the given thread terminates due to the
         * given uncaught exception.
         * <p>Any exception thrown by this method will be ignored by the
         * Java Virtual Machine.
         * @param t the thread
         * @param e the exception
         */
        void uncaughtException(Thread t, Throwable e);
    }

我们用如下代码看看JVM如何打印错误的。

public class ThreadSource3  extends Thread{
	@Override
	public void run() {
			int i = 1/0;
	}	
	public static void main(String[] args) {
		Thread thread = new Thread(new ThreadSource3());
		//thread.setUncaughtExceptionHandler(new ThreadSource3().new MyExceptionHandler());
		thread.start();
		
		
	}
}

执行结果:
在这里插入图片描述
这里是JVM帮我们处理了异常的打印。其实我们也可以自己设置线程的异常处理器,获取错误信息之后,我们可以按照自己的需要处理,比如传入指定的日志中。日志中可以记录好是哪一个线程组的哪一个线程抛出什么错误。代码如下:

public class ThreadSource3  extends Thread{
	@Override
	public void run() {
			int i = 1/0;
	}	
	public static void main(String[] args) {
		Thread thread = new Thread(new ThreadSource3());
		thread.setUncaughtExceptionHandler(new ThreadSource3().new MyExceptionHandler());
		thread.start();
		
		
	}

	public class MyExceptionHandler implements UncaughtExceptionHandler{

		/**
		 * @see java.lang.Thread.UncaughtExceptionHandler#uncaughtException(java.lang.Thread, java.lang.Throwable)
		 */
		@Override
		public void uncaughtException(Thread t, Throwable e) {
			e.printStackTrace();
			System.err.println("未捕捉的异常:"+t +",错误:"+e.getMessage());
			
		}
		
	}
}

在这里插入图片描述
如上图,我们打印了堆栈信息和线程信息。

3.ThreadLocal.ThreadLocalMap成员变量

在我们的Thread中有一个变量:

 /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

也许我们的代码从未调用过,但实际却很有可能使用过。查看如下代码:

/** 线程封闭示例 */
public class Demo7 {
	/** threadLocal变量,每个线程都有一个副本,互不干扰 */
	public static ThreadLocal<String> value = new ThreadLocal<String>() {
		@Override
		protected String initialValue() {
			return "初始值";
		}
	};

	/**
	 * threadlocal测试
	 * 
	 * @throws Exception
	 */
	public void threadLocalTest() throws Exception {

		// threadlocal线程封闭示例
		value.set("这是主线程设置的123"); // 主线程设置值
		String v = value.get();
		System.out.println("线程1执行之前,主线程取到的值:" + v);

		new Thread(new Runnable() {
			@Override
			public void run() {
				String v = value.get();
				System.out.println("线程1取到的值:" + v);
				// 设置 threadLocal
				value.set("这是线程1设置的456");

				v = value.get();
				System.out.println("重新设置之后,线程1取到的值:" + v);
				System.out.println("线程1执行结束");
			}
		}).start();

		Thread.sleep(5000L); // 等待所有线程执行结束

		v = value.get();
		System.out.println("线程1执行之后,主线程取到的值:" + v);

	}

	public static void main(String[] args) throws Exception {
		new Demo7().threadLocalTest();
	}
}

在这里插入图片描述
我们可以得到的结论是:主线程和线程1通过ThreadLocal对象取到的都是各自线程中的保存的值。如果未设置值就使用创建对象是初始化方法返回的值。那么它是如何设计的呢,查看ThreadLocal源码:

 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 getMap(Thread t) {
        return t.threadLocals;
    }
 void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

我们可以看到在调用set方法时,它会获取当前线程(主线程中调用获取的就是主线程,在线程1中调用获取的就是线程1对象的引用)。然后通过线程对象获取一个Map,这如果map为空会以当前线程和设置的值初始化一个ThreadLocalMap。这个map也就是Thread类中的静态类ThreadLocal.ThreadLocalMap,它是专门定制用于保存当前线程变量的一个hashMap。其底层结构和HashMap相同。也就是说每一个线程对象,都有一个自己的ThreadLocalMap成员变量。而这个Map的key是ThreadLocal的对象引用,值是调用set方法传入的值。

   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();
    }

如上get方法,我们可以看到不同的线程调用同一个threadLocal对象的get方法,拿到的是不同线程对象的Map对象。只是这些Map对象都刚好使用了这个threadLocal对象作为key。
几点需要注意的是:

  • 可以自定义初始值。
  • 如果使用线程池,在执行完任务后需要通过threadLocal.remove()清除该线程的map对象中的键值对。否则下一个用户的请求进入后,获取到上一个用户保存在线程中的值,是有风险的。
  • 使用线程池时,ThreadLocalMap中有强引用指向外部的value对象,导致该对象无法被回收,内存泄漏。如下代码:
/** 线程封闭示例 */
public class Demo8 {
	/** threadLocal变量,每个线程都有一个副本,互不干扰 */
	public static ThreadLocal<Object> value = new ThreadLocal<Object>();


	public static void main(String[] args) throws Exception {
		Thread.sleep(30000);
		 Object object = new Object[20*1024*1024];
		value.set(object);
		object = null;
		Thread.sleep(1000000);
	}
}

对于new Object[2010241024]有两个地方引用它,一个是object,一个是ThreadLocal.ThreadLocalMap中的Entry对它的引用。只是将object置为null,该对象并不能被垃圾回收器回收。上面程序运行的的堆图如下,可以看到在某个时间点,因为创建了大对象,堆区占用内存增大,下降的位置是手动执行了fullGC,但是堆空间大部分并未释放。
在这里插入图片描述
我们对比如下的代码和堆的图示,可以比较清楚的得到二者的差异:

/** 线程封闭示例 */
public class Demo8 {
	/** threadLocal变量,每个线程都有一个副本,互不干扰 */
	public static ThreadLocal<Object> value = new ThreadLocal<Object>();


	public static void main(String[] args) throws Exception {
		Thread.sleep(30000);
		 Object object = new Object[20*1024*1024];
		object = null;
		Thread.sleep(1000000);
	}
}

在这里插入图片描述
可以看到手动执行GC后,堆内存大幅度下降,那个大的Object数组占用的堆空间得到了释放。
在线程池的场景中,Thread对象未销毁,有一个强引用指向ThreadLocalMap,而ThreadLocalMap有强引用指向Entry,Entry中有Object数组对象的引用。此时可能Entry中保存的是(null,value),只要在程序使用完该对象后,调用ThreadLocal的remove或者再调用set()就会释放掉该对象数组的引用

to be continue…

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值