【Java核心-进阶】线程

本文详细探讨了Java线程的本质,解释了Java线程如何一对一映射到操作系统内核线程,分析了其优缺点。文章还介绍了线程状态和关键方法,建议使用Java并发包中的工具替代wait、notify。此外,讲解了线程创建的两种方式,强调使用线程池的重要性,并提醒注意线程池可能导致的资源问题。同时,提到了守护线程、Thread.onSpinWait()和ThreadLocal的使用,特别指出ThreadLocal可能导致的内存泄漏问题及其解决办法。
摘要由CSDN通过智能技术生成

 

一个最简单的HelloWorld程序,背后也有多个线程。

Java代码

 

  1. public static void main(String[] args) {  

  2.   System.out.println("Hello World");  

  3.   

  4.   ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();  

  5.   long[] threadIds = threadMXBean.getAllThreadIds();  

  6.   ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(threadIds);  

  7.   for (ThreadInfo threadInfo : threadInfos) {  

  8.     System.out.println(threadInfo.getThreadId() + ": " + threadInfo.getThreadName());  

  9.   }  

  10. }  

 

输出:

Hello World

5: Attach Listener

4: Signal Dispatcher

3: Finalizer

2: Reference Handler

1: main

 

java线程的本质

Java 线程 一对一 映射 操作系统内核线程。

java.lang.Thread 中有很多关键的方法是本地方法(Native):

Java代码

 

  1. public class Thread {  

  2.   private native void start0();  

  3.   private native void interrupt0();  

  4.   private native void setPriority0();  

  5.   ...  

  6. }  

 

与内核线程一对一的机制的

优点:提供了 精细强大的线程管理能力。

缺点:其高复杂性使得 并发编程非常困难。

 

未来Java也会提供类似 Python协程 的轻量级用户线程(Fiber)(Loom)。

Python协程机制可以让一个线程执行多个协程。与多线程模式相比,它 避免了线程切换的开销,甚至 避免线程间的锁机制。

还可用 “多进程 + 协程” 的模式充分利用多核CPU。

Python 生成者-消费者 协程示例:(生产一个,消费一个;单个线程轮换执行生产和消费的过程)

Python代码

 

  1. def consumer():  

  2.     r = ''  

  3.     while True:  

  4.         n = yield r  

  5.         if not n:  

  6.             return  

  7.         print('[消费者] 正在消费 %s...' % n)  

  8.         r = '200-OK'  

  9.   

  10. def produce(c):  

  11.     c.send(None)  

  12.     n = 0  

  13.     while n < 5:  

  14.         n = n+1  

  15.         print('[生产者] 正在生产 %s...' % n)  

  16.         r = c.send(n)  

  17.         print('[生产者] 消费者返回:%s' % r)  

  18.     c.close()  

  19.   

  20. c = consumer()  

  21. produce(c)  

 

线程状态与方法图

 

Thread 和 Object 中的 wait、notify 等方法过于晦涩难用。

推荐使用java并发包中的工具来达到同步控制的目的。如,CountDownLatch、CyclicBarrier、Semaphore

 

创建线程

直接创建 Thread 实例

一般可以通过 Runnable 简单直接的创建线程:

Java代码

 

  1. Runnable task = () -> System.out.println("Hello World");  

  2. Thread thread = new Thread(task);  

  3. thread.start();  

  4. thread.join();  

 

使用线程池

一般推荐使用线程池来处理稍复杂的多线程场景,以减少创建和销毁线程的开销(包括内存和CPU),降低大量线程“过度切换”的风险。

推荐手动创建线程池,以明确指定运行规则,规避资源耗尽的风险。

  • Executors.newFixedThreadPool 和 newSingleThreadExecutor 可能会耗费非常大的内存,甚至OOM

  • Executors.newCachedThreadPool 和 newScheduleThreadPool允许最大线程数是 Integer.MAX_VALUE。这可能会导致创建非常的线程,甚至OOM

线程池(ExecutorService)创建示例:

Java代码

 

  1. // org.apache.commons.lang3.concurrent.BasicThreadFactory  

  2. ThreadFactory threadFactory = new BasicThreadFactory.Builder()  

  3.     .namingPattern("example-schedule-pool-%d")  

  4.     .daemon(true)  

  5.     .build();  

  6.   

  7. ScheduledExecutorService executorService  

  8.     = new ScheduledThreadPoolExecutor(1, threadFactory);  

Java代码

 

  1. ThreadFactory threadFactory = new ThreadFactoryBuilder()  

  2.     .setNameFormat("demo-pool-%d")  

  3.     .build();  

  4.   

  5. ExecutorService executorService = new ThreadPoolExecutor(  

  6.     5, 200,  

  7.     0L, TimeUnit.MILLISECONDS,   

  8.     new LinkedBlockingQueue<Runnable>(1024),  

  9.     threadFactory,  

  10.     new ThreadPoolExecutor.AbortPolicy());  

 

守护线程

如果需要一个长期驻留的线程,但是不希望它影响应用的退出,那么可将该线程设置为 守护线程。

JVM 发现只有守护线程存在时,会结束进程。

具体方法:在启动线程前,调用 Thread.setDaemon(true) 方法:

Java代码

 

  1. Thread daemonThread = new Thread();  

  2. daemonThread.setDaemon(true);  

  3. daemonThread.start();  

 

Thread.onSpinWait()

该方法的意思就是“自旋等待”。即,线程在等待期间一直占用着CPU。

这是一直性能优化的技术,用于避免线程切换的开销。

但 Thread.onSpinWait() 方法只是对 JVM 的一个暗示,并没有任何行为上的保障。

JVM 可能会利用 CPU 的 pause 指令。

 

ThreadLocal

谨慎使用 ThreadLocal ! 注意预防 OOM。

ThreadLocal 提供了一种保存 线程私有 信息的机制。

ThreadLocal 保存的信息在线程的整个生命周期内都有效。

所以可以通过 ThreadLocal 在一个线程关联的 不同业务模块 之间传递信息,如 事务ID、Cookie 等上下文信息。

 

ThreadLocalMap

ThreadLocal 以 弱引用 的形式被保存。

每个线程(Thread)内部都有一个 ThreadLocalMap 字段来存储属于线程自己的 ThreadLocal 对象。

ThreadLocalMap 内部有一个数组保存这些信息;

数组中的每个元素(Entry),也就是 键值对,都是 弱引用形式的ThreadLocal对象:

Java代码

 

  1. class Entry extends WeakReference<ThreadLocal<?>> {  

  2.   Object value;  

  3.   Entry(ThreadLocal<?> k, Object v) {  

  4.     super(k);  

  5.     value = v;  

  6.   }  

  7. }  

 

通常,弱引用 和 引用队列(ReferenceQueue) 配合垃圾回收机制使用:

创建弱引用对象的同时,将对象注册到一个引用队列中;

当对象的 可达性 成为弱引用时,对象会被添加到该引用队列,用于后续的自定义操作。

Java代码

 

  1. public WeakReference(T referent, ReferenceQueue<? super T> q);  

 

但是 ThreadLocal 并没有结合 ReferenceQueue 使用。

也就是说,ThreadLocalMap 中 废弃项的回收依赖于显式的触发;

否则就要等到线程结束,ThreadLocalMap 被回收后,废弃项才会被回收。

ThreadLocalMap 中的几个关键方法,set、remove、rehash,会触发废弃项的回收。

所以在使用ThreadLocal时,为了预防OOM:

  • 应用须自己负责删除废弃项:ThreadLocal.remove()

  • 不要和线程池配合使用。因为 worker 线程往往不会退出

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值