ThreadLocal

ThreadLocal 是 Java 中的一个类,它提供了一种线程局部变量的能力。这意味着,如果你创建了 ThreadLocal 的变量,那么访问这个变量的每个线程都会有这个变量的一个独立初始化的副本,即每个线程都可以独立地改变自己的副本,而不会影响到其他线程的副本。这是通过 ThreadLocal 内部为每个使用该变量的线程提供一个独立的变量副本来实现的。

使用场景

  • 数据库连接管理:每个线程都可以维护自己的数据库连接,避免了多线程环境下连接共享的问题。
  • 用户会话管理:在 web 应用中,每个请求处理线程都可以有自己的会话信息,如用户ID、权限等。
  • 线程敏感的数据存储:如每个线程都有自己的日志记录器,用于记录线程相关的日志信息。

示例代码

import java.util.concurrent.ThreadLocalRandom;  
  
public class ThreadLocalExample {  
  
    // 创建一个ThreadLocal变量  
    private static final ThreadLocal<Integer> threadLocalVar = ThreadLocal.withInitial(() -> ThreadLocalRandom.current().nextInt(1, 100));  
  
    public static void main(String[] args) throws InterruptedException {  
  
        // 创建两个线程  
        Thread thread1 = new Thread(() -> {  
            System.out.println("Thread 1: " + threadLocalVar.get());  
            threadLocalVar.set(1000); // 修改threadLocalVar的值  
            System.out.println("Thread 1 after set: " + threadLocalVar.get());  
        });  
  
        Thread thread2 = new Thread(() -> {  
            System.out.println("Thread 2: " + threadLocalVar.get());  
            // 注意:thread2中的threadLocalVar.get()与thread1中的值无关,因为是线程隔离的  
        });  
  
        thread1.start();  
        thread2.start();  
  
        thread1.join();  
        thread2.join();  
  
        // 如果需要,可以手动清理ThreadLocal中的数据,防止内存泄漏  
        // threadLocalVar.remove();  
    }  
}

注意事项

  • 内存泄漏:由于 ThreadLocal 变量的生命周期与线程的生命周期相同,如果线程长时间运行且 ThreadLocal 变量被设置为非 null,则可能会导致内存泄漏。因此,在不需要时,应该调用 remove() 方法来清除 ThreadLocal 变量中的数据。
  • 继承性:在 Java 中,子线程会继承父线程中的 ThreadLocal 变量,但这在大多数情况下不是期望的行为。如果需要避免这种继承性,可以在创建线程之前调用 InheritableThreadLocal 替代 ThreadLocal,但这会增加额外的复杂性和性能开销。

 start() 和 join()

在Java多线程编程中,start() 和 join() 是两个非常重要的方法,它们分别用于启动线程和等待线程执行完毕。这两个方法都定义在Thread类中,是处理线程同步和执行顺序的关键工具。

start() 方法

  • 作用start() 方法用于启动线程,使线程处于就绪状态(ready state),等待CPU的调度执行。一旦线程被启动,它会执行其run()方法中的代码。
  • 注意
    • 每个线程只能被启动一次。尝试再次启动一个已经启动的线程将导致IllegalThreadStateException
    • 调用start() 方法会启动线程的执行,但并不会立即执行run()方法中的代码。它只是将线程的状态设置为就绪,并使其有机会被JVM的调度器选中执行。
    • start() 方法是由Java虚拟机调用的,而非直接由用户调用run()方法。直接调用run()方法将只是同步地执行run()方法中的代码,而不会启动新的线程。

join() 方法

  • 作用join() 方法用于等待当前线程(即调用join()方法的线程)直到另一个线程(即join()方法的参数所指定的线程)执行完毕。这意呀着,如果一个线程A调用了另一个线程B的join()方法,那么线程A将暂停执行,直到线程B执行完毕。
  • 参数join() 方法可以接受一个可选的long millis参数和一个int nanos参数(从Java 5开始),用于指定等待的最大时间。如果线程B在指定的时间内还没有结束,那么线程A将不再等待,继续执行。
  • 注意
    • join() 方法是阻塞的,它会阻塞调用它的线程直到目标线程结束。
    • join() 方法可以用于实现线程间的同步,确保某些操作在另一个线程完成后才进行。
    • 在使用join()方法时,需要注意死锁和性能问题,因为不当的使用可能会导致程序挂起或响应变慢。

示例

public class ThreadExample {  
    public static void main(String[] args) {  
        Thread thread = new Thread(() -> {  
            try {  
                Thread.sleep(1000); // 模拟耗时操作  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
            System.out.println("线程执行完毕");  
        });  
  
        thread.start(); // 启动线程  
  
        try {  
            thread.join(); // 等待线程执行完毕  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
  
        System.out.println("主线程继续执行");  
    }  
}

在这个示例中,主线程(main方法所在的线程)启动了一个新线程,并调用了该线程的join()方法。因此,主线程会等待新线程执行完毕后再继续执行。

wait() 和 sleep() 

在Java多线程编程中,wait() 和 sleep() 是两个用于控制线程状态的方法,但它们属于不同的类和具有不同的用途和行为。

wait() 方法

  • 所属类wait() 方法是 Object 类的一个方法,因此Java中的任何对象都可以调用它。不过,它通常是在同步方法或同步块中,由拥有对象锁的线程调用的。
  • 作用wait() 方法使当前线程等待,直到另一个线程调用该对象的 notify() 或 notifyAll() 方法。调用 wait() 方法会释放当前线程持有的对象锁,直到其他线程调用该对象的 notify() 或 notifyAll() 方法,或者等待时间超时(如果调用的是 wait(long timeout) 或 wait(long timeout, int nanos))。
  • 注意
    • wait() 方法必须在同步方法或同步块中调用,否则会抛出 IllegalMonitorStateException
    • 调用 wait() 方法会释放对象锁,允许其他线程访问该对象的同步代码块。
    • wait() 方法可以被中断,如果线程在等待过程中被中断,它会抛出 InterruptedException

sleep() 方法

  • 所属类sleep() 方法是 Thread 类的一个静态方法。
  • 作用sleep() 方法使当前正在执行的线程暂停执行指定的时间(以毫秒为单位),让出CPU给其他线程。不过,它不会释放对象锁。
  • 注意
    • sleep() 方法可以在任何地方被调用,不需要同步代码块或同步方法。
    • sleep() 方法不会释放对象锁,因此如果当前线程持有某个对象的锁,并调用了 sleep() 方法,其他线程仍然无法访问该对象的同步代码块。
    • sleep() 方法可以被中断,如果线程在休眠过程中被中断,它会抛出 InterruptedException

示例

public class ThreadExample {  
    public static void main(String[] args) {  
        final Object lock = new Object();  
  
        Thread thread = new Thread(() -> {  
            synchronized (lock) {  
                try {  
                    System.out.println("线程等待...");  
                    lock.wait(); // 释放lock对象的锁,并等待  
                    System.out.println("线程被唤醒");  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        });  
  
        thread.start();  
  
        try {  
            Thread.sleep(1000); // 主线程休眠1秒  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
  
        synchronized (lock) {  
            lock.notify(); // 唤醒等待lock对象的线程  
        }  
    }  
}

ThreadLocal 多线程并发问题

 

ThreadLocal 是 Java 中一个非常有用的工具类,它用于为每个使用该变量的线程提供一个独立的变量副本,从而隔离了各个线程对这些变量的访问。这样做的好处是避免了线程间的数据共享,从而避免了数据同步和线程安全问题。然而,即使使用 ThreadLocal,也需要注意一些潜在的并发问题或不当使用的情况。

1. 内存泄漏

ThreadLocal 可能导致内存泄漏,尤其是在使用线程池(如 ExecutorService)时更为常见。当 ThreadLocal 变量被设置为 null 后,如果该线程结束运行并被回收,那么其对应的 ThreadLocal 变量值会被垃圾回收器回收。但在使用线程池时,线程是重用的,这意味着线程的生命周期可能远长于 ThreadLocal 变量的生命周期。如果 ThreadLocal 变量在不再需要时没有被显式地调用 remove() 方法清除,那么这些值将一直存在于线程中,直到线程被销毁,从而导致内存泄漏。

解决方案

  • 在使用完 ThreadLocal 后,显式调用 ThreadLocal.remove() 方法来清除数据。
  • 可以通过 InheritableThreadLocal 来实现父子线程间数据传递,但也要注意内存泄漏问题。
  • 尽可能使用弱引用(WeakReference)的 ThreadLocal 实现,比如 WeakThreadLocal(需要自行实现或寻找第三方库),这样可以减少内存泄漏的风险,因为弱引用不会被 JVM 视为强引用链的一部分,从而在垃圾回收时更容易被回收。

2. 初始值问题

ThreadLocal 允许你通过 initialValue() 方法来为每个线程提供一个初始值。但如果在 ThreadLocal 变量被访问之前没有设置初始值,并且也没有通过 set() 方法显式地设置值,那么第一次访问 ThreadLocal 变量时将会调用 initialValue() 方法。这可能导致在并发环境下,如果 initialValue() 方法的实现比较复杂或耗时,会影响到程序的性能。

解决方案

  • 确保 initialValue() 方法的实现是简单且高效的。
  • 在程序逻辑中,尽可能地在创建 ThreadLocal 变量后,立即通过 set() 方法设置初始值,避免在并发环境下调用 initialValue() 方法。

3. 线程安全问题

虽然 ThreadLocal 本身就是为了解决线程安全问题而设计的,但如果你将 ThreadLocal 变量用于存储共享资源(例如,将 ThreadLocal 变量用作锁或其他共享对象的引用),那么它就不再是线程安全的。

解决方案

  • 确保 ThreadLocal 变量仅用于存储线程私有的数据。
  • 如果需要线程间的共享和同步,考虑使用其他同步机制,如 synchronizedReentrantLockSemaphore 等。

综上所述,虽然 ThreadLocal 提供了一种方便的线程隔离数据的方式,但在使用时仍需注意避免内存泄漏、合理处理初始值问题,并确保不会将其用于共享资源的存储。

 Java中的引用类型

在Java中,引用类型是一种特殊的数据类型,用于存储对对象的引用,而不是对象本身。Java中的引用类型可以根据不同的分类标准有不同的划分方式。从广义上来说,Java中的类(class)、接口(interface)、数组(array)等都是引用类型。而具体到引用的强度或生命周期管理方面,Java中的引用类型可以分为以下几种:

1. 强引用(Strong Reference)

  • 特点:强引用是最常见的引用类型,也是默认的引用类型。只要强引用存在,垃圾回收器就不会回收它所指向的对象。
  • 用途:适用于长期存活的对象。
  • 示例:通过new关键字创建的对象,其引用就是强引用。

2. 软引用(Soft Reference)

  • 特点:软引用用于描述有用但非必须的对象。当内存不足时,垃圾回收器会回收软引用所指向的对象。
  • 用途:适用于实现内存敏感的高速缓存。
  • 实现方式:通过SoftReference类来实现。

3. 弱引用(Weak Reference)

  • 特点:弱引用用于描述非必须的对象,其生命周期比软引用更短暂。在垃圾回收时,只要发现弱引用对象,就会立即回收。
  • 用途:适用于实现那些非必需对象的缓存,如监听器列表等。
  • 实现方式:通过WeakReference类来实现。

4. 虚引用(Phantom Reference)

  • 特点:虚引用主要用于对象回收时的处理工作。与软引用和弱引用不同,虚引用的get方法始终返回null,即不能通过虚引用来获取对象的实例。
  • 用途:在对象被回收时执行特定的清理操作,如管理堆外内存等。
  • 实现方式:通过PhantomReference类来实现,并且通常与ReferenceQueue联合使用。

5. 自定义引用类型(非官方分类)

虽然Java官方主要定义了上述四种引用类型,但在实际应用中,开发者可以根据需要自定义引用类型。这种自定义通常是通过扩展Reference类或其子类(如SoftReferenceWeakReference等)来实现的,以满足特定的内存管理需求。

总结

Java中的引用类型从广义上可以分为类、接口、数组等,而从引用的强度和生命周期管理方面则可以分为强引用、软引用、弱引用和虚引用四种。每种引用类型在内存管理和对象生命周期方面都有不同的特点和用途。在实际开发中,选择合适的引用类型取决于对象的生命周期和内存管理需求。

 

--end--

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值