二、并发编程与多线程-2.3、线程安全

2.3、线程安全

2.3.1、说说你对线程安全的理解

答:
首先,线程安全是对于多线程或者并发的情况下来说的,如果是单线程操作,则无所谓线程安全了。在多线程环境下保证线程安全,无非就是保证访问对象时的原子性、有序性和可见性

  • 原子性:当一个线程执行一系列程序指令的时候,它应该是不可中断的,这和数据库里面的原子性是一样的。CPU的上下文切换,是导致出现多线程原子性问题的核心原因。JDK提供了synchronized关键字来解决原子性问题。
  • 有序性:程序编写的指令顺序和最终CPU运行的指令顺序应该一致。如果出现不一致的现象也称之为指令重排,所以有序性问题也会导致可见性问题。可见性问题和有序性问题可以通过JDK提供的volatile关键字来解决。
  • 可见性:指的是多线程环境下,读和写可能发生在不同的线程里,有可能出现某个线程对共享变量修改之后,对其他线程不是实时可见的,但应该是实时可见的。

2.3.2、Java保证线程安全的方式有哪些?

答:
针对原子性问题:

  1. JDK提供了非常多的Atomic类,比如AtomicInteger、AtomicLong、AtomicBoolean等,这些类都是通过CAS来保证原子性的。
  2. 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会造成内存泄漏的问题,需要注意以下情况:

  1. 使用完ThreadLocal后没有手动调用remove()方法进行清理。如果不手动清理,ThreadLocalMap中的Entry会持有ThreadLocal对象的强引用,而ThreadLocal对象在外部不再被引用时,会导致内存泄漏。
  2. 线程池环境下没有进行清理。在使用线程池的情况下,由于线程池中的线程对象是复用的,如果没有手动清理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();
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值