2.3、线程安全
2.3.1、说说你对线程安全的理解
答:
首先,线程安全是对于多线程或者并发的情况下来说的,如果是单线程操作,则无所谓线程安全了。在多线程环境下保证线程安全,无非就是保证访问对象时的原子性、有序性和可见性。
- 原子性:当一个线程执行一系列程序指令的时候,它应该是不可中断的,这和数据库里面的原子性是一样的。CPU的上下文切换,是导致出现多线程原子性问题的核心原因。JDK提供了synchronized关键字来解决原子性问题。
- 有序性:程序编写的指令顺序和最终CPU运行的指令顺序应该一致。如果出现不一致的现象也称之为指令重排,所以有序性问题也会导致可见性问题。可见性问题和有序性问题可以通过JDK提供的volatile关键字来解决。
- 可见性:指的是多线程环境下,读和写可能发生在不同的线程里,有可能出现某个线程对共享变量修改之后,对其他线程不是实时可见的,但应该是实时可见的。
2.3.2、Java保证线程安全的方式有哪些?
答:
针对原子性问题:
- JDK提供了非常多的Atomic类,比如AtomicInteger、AtomicLong、AtomicBoolean等,这些类都是通过CAS来保证原子性的。
- Java还提供了各种锁机制,比如用synchronized关键字加锁。
针对有序性问题:
可以使用synchronized关键字定义同步代码块或者同步方法来保证有序性,还可用Lock接口保证有序性,如ReentrantLock。
针对可见性问题:
可以使用synchronized关键字加锁来解决。也可以使用volatile关键字,而且性能要优于synchronized关键字加锁的方式,volatile关键字适用于对变量的写操作不依赖于当前值的场景,比如状态标记量等,因为volatile关键字不能保证原子性。
扩展:
保证访问对象时线程安全的方式还有很多,比如还可以使用ThreadLocal实现多个线程之间的数据隔离。
2.3.3、如何安全中断一个正在运行的线程?
答:
Java Thread的API里面虽然提供了一个stop方法可以强行终止线程,但是这种方式是不安全的,因为有可能线程的任务还没有完成,突然中断会导致运行结果不正确。
**Java Thread里提供了一个interrupt()方法,这个方法配合isInterrupted()方法来使用,就可以实现安全地中断线程运行。**这种实现方式并不是强制中断,而是告诉正在运行的线程,你可以停止了,至于何时实际中断,则取决于正在运行的线程,比如return,所以它能够保证线程运行结果的安全性。
示例:
Runnable runnable = new Runnable() {
public void run() {
while(true) {
if(Thread.currentThread().isInterrupted()) {
System.out.println("线程被中断了");
return;
} else {
System.out.println("线程没有被中断");
}
}
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(10);
thread.interrupt();
System.out.println("线程中断了,程序到这里了");
运行结果:
线程没有被中断
线程没有被中断
线程没有被中断
线程没有被中断
线程没有被中断
线程没有被中断
线程没有被中断
线程没有被中断
线程没有被中断
线程没有被中断
线程没有被中断
线程没有被中断
线程没有被中断
线程中断了,程序到这里了
线程被中断了
扩展:
线程是操作系统进行运算调度的最小单位,所以线程是系统级别的概念。
在Java里实现的线程,最终的执行和调度都是由操作系统完成的,JVM只是对操作系统层面的线程做了一层包装而已。
我们在Java里调用start()方法启动一个线程的时候,只是告诉操作系统这个线程可以被执行了,但是最终交给CPU是由操作系统的调度算法决定的。
从理论上来说,在Java层面中断一个正在运行的线程,只能像Linux的kill命令结束进程的方式一样,强制终止。
2.3.4、SimpleDateFormat是线程安全的吗?
答:
先说答案,SimpleDateFormat不是线程安全的。
再说原因,因为SimpleDateForma类的t内部维护了一个Calendar对象,而Calendar对象是非线程安全的。如果多个线程同时调用SimpleDateFormat的format()和parse()方法,会导致结果混乱或错误。
为了线程安全,可以使用ThreadLocal来保证每个线程拥有独立的SimpleDateFormat对象。具体做法是将SimpleDateFormat对象放入ThreadLocal中,每个线程通过ThreadLocal获取自己的SimpleDateFormat对象,确保对象的独立性。
示例:
public class ThreadSafeDateFormat {
private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT_TL = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public static String formatDate(Date date) {
return DATE_FORMAT_TL.get().format(date);
}
public static Date parseDate(String dateString) throws ParseException {
return DATE_FORMAT_TL.get().parse(dateString);
}
}
2.3.5、并发场景中,ThreadLocal会造成内存泄漏吗?
答:
ThreadLocal是Java提供的一个线程隔离的工具,用于解决多线程环境下共享变量的线程安全问题。它通过为每个线程提供独立的变量副本,使得每个线程操作的变量都是独立的,从而避免了对同一个变量的竞争条件。
ThreadLocal的实现原理是:每个ThreadLocal对象内部维护一个ThreadLocalMap,该Map的key为线程对象,value为该线程对象对应的变量副本。当通过ThreadLocal的get()方法获取变量时,ThreadLocal会根据当前线程获取对应的变量副本;当通过ThreadLocal的set()方法设置变量时,ThreadLocal会根据当前线程设置对应的变量副本。这样,每个线程都拥有自己的变量副本,从而保证了线程安全。
关于ThreadLocal会造成内存泄漏的问题,需要注意以下情况:
- 使用完ThreadLocal后没有手动调用remove()方法进行清理。如果不手动清理,ThreadLocalMap中的Entry会持有ThreadLocal对象的强引用,而ThreadLocal对象在外部不再被引用时,会导致内存泄漏。
- 线程池环境下没有进行清理。在使用线程池的情况下,由于线程池中的线程对象是复用的,如果没有手动清理ThreadLocal的变量副本,可能造成不同任务之间的数据干扰。
为了避免ThreadLocal的内存泄漏问题,可以在使用完ThreadLocal后手动调用remove()方法进行清理,或者使用Java 8中的新特性,使用ThreadLocal的弱引用版本:InheritableThreadLocal,它会在线程结束后自动清理ThreadLocal的变量副本。此外,在使用线程池时,也可以在任务执行完毕后手动调用remove()方法清理变量副本。
示例:
public class ThreadLocalExample {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread thread1 = new Thread(new MyRunnable());
Thread thread2 = new Thread(new MyRunnable());
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static class MyRunnable implements Runnable {
@Override
public void run() {
// 设置ThreadLocal的变量副本
threadLocal.set((int) (Math.random() * 100));
System.out.println(Thread.currentThread().getName() + " - Value: " + threadLocal.get());
// 清理ThreadLocal的变量副本
threadLocal.remove();
}
}
}