Thread之线程安全、ThreadLocal和synchronized

在这里插入图片描述

线程安全

线程安全问题指的是在多线程中,各线程之间因为同时操作所产生的数据污染或其他非预期的程序运行结果。

1. 非线程安全示例

比如 A 和 B 同时给 C 转账的问题,假设 C 原本余额有 100 元,A 给 C 转账 100 元,正在转的途中,此时 B 也给 C 转了 100 元,这个时候 A 先给 C 转账成功,余额变成了 200 元,但 B 事先查询 C 的余额是 100 元,转账成功之后也是 200 元。当 A 和 B 都给 C 转账完成之后,余额还是 200 元,而非预期的 300 元,这就是典型的线程安全的问题。

时间线线程 A线程 BC 的余额
1查看 C 的余额100
2查看 C 的余额100
3转账:余额 += 100200
4转账:余额 += 100200

2. 非线程安全代码示例

上面的内容没看明白没关系,下面来看非线程安全的具体代码:

class ThreadSafeTest {

    static int number = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> addNumber());
        Thread thread2 = new Thread(() -> addNumber());
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("number:" + number);
    }

    public static void addNumber() {
        for (int i = 0; i < 10000; i++) {
            ++number;
        }
    }
}

以上程序执行结果如下:

number:12085

每次执行的结果可能略有差异,不过几乎不会等于(正确的)累计之和 20000 。

3. 线程安全的解决方案

线程安全的解决方案有以下几个维度:

  • 数据不共享,单线程可见,比如 ThreadLocal 就是单线程可见的;

  • 使用线程安全类,比如 StringBuffer 和 JUC(java.util.concurrent)下的安全类;

  • 使用同步代码或者锁。

ThreadLocal

ThreadLocal 诞生于 JDK 1.2 ,用于解决多线程间的数据隔离问题。也就是说 ThreadLocal 会为每一个线程创建一个单独的变量副本。

ThreadLocal 最典型的使用场景有两个:

  • ThreadLocal 可以用来管理 Session,因为每个人的信息都是不一样的,所以就很适合用 ThreadLocal 来管理;

  • 数据库连接,为每一个线程分配一个独立的资源,也适合用 ThreadLocal 来实现。

其中,ThreadLocal 也被用在很多大型开源框架中,比如 Spring 的事务管理器,还有 Hibernate 的 Session 管理等。

1. ThreadLocal 基础使用

ThreadLocal 常用方法有 set(T)、get()、remove() 等,具体使用请参考以下代码。

ThreadLocal threadLocal = new ThreadLocal();

// 存值
threadLocal.set(Arrays.asList("hello", "world"));

// 取值
List list = (List) threadLocal.get();
System.out.println(list.size());
System.out.println(threadLocal.get());

// 删除值
threadLocal.remove();
System.out.println(threadLocal.get());

以上程序执行结果如下:

2
[hello, world]
null

2. ThreadLocal 数据共享

既然 ThreadLocal 设计的初衷是解决线程间信息隔离的,那 ThreadLocal 能不能实现线程间信息共享呢?

答案是肯定的,只需要使用 ThreadLocal 的子类 InheritableThreadLocal 就可以轻松实现,来看具体实现代码:

ThreadLocal inheritableThreadLocal = new InheritableThreadLocal();
inheritableThreadLocal.set("老王");
new Thread(() -> System.out.println(inheritableThreadLocal.get())).start();

以上程序执行结果如下:

老王

从以上代码可以看出,主线程和新创建的线程之间实现了信息共享。

3. ThreadLocal 的正确使用方式(内存泄漏问题)

提示
这里不是 ThreadLocal 导致的内存泄露,而是用 ThreadLocal 的程序员写错了代码导致了内存泄漏。

// jvm 内存参数调低以便于快速触发内存不足:-Xmx50M
public static void main(String[] args) throws InterruptedException {
    ThreadPoolExecutor t = new ThreadPoolExecutor(
        4, 4, 
        0, TimeUnit.SECONDS, 
        new ArrayBlockingQueue<>(1000));
    for (int i = 0; i < 100; i++) {
        Thread.sleep(200);
        t.execute(() -> {
                System.out.println(Thread.currentThread().getName());
                ThreadLocal<int[]> threadLocal = new ThreadLocal<>();
                int[] arr = new int[1024 * 1024];
                threadLocal.set(arr);
                //threadLocal.remove();
            });
    }
    t.shutdown();
}

执行上述代码,你会发现 java.lang.OutOfMemoryError: Java heap space 异常。原因在于:

ThreadLocal 并不存储数据,而是依靠每个线程中的 ThreadLocalMap 来存储数据。毫无疑问,ThreadLocalMap 是一个 Map 结构,其中存的是键值对:键是 ThreadLocal 对象,而值就是你所要存储的值。

简而言之,你以为存进 ThreadLocal 中的数据实际上并没有存到 ThreadLocal 中,而是存到了一个 Map 中的 Value 部分。例如,你有 3 个 ThreadLocal 分别存的是 String 类型的值、Integer 类型的值和 Double 类型的值:那么 ThreadLocalMap 中的内容类似如下:

KeyValue
thread_local_1 对象的引用String 对象的引用(即,内存地址)
thread_local_2 对象的引用Integer 对象的引用(即,内存地址)
thread_local_3 对象的引用Double 对象的引用(即,内存地址)

但是,问题在于:每个程中有且仅有一个 ThreadLocalMap ,并且,它的 Key 是 ThraedLocal 的软引用

也就是意味着,当 ThreadLocal 对象没有其它的强引用时,在 JVM 进行垃圾回收时,它们会被释放掉,那么这就意味着,ThreadLocalMap 中会出现 Key 为 null 的键值对,并且,键值对中的值将无法被 JVM 回收,从而最终出现内存溢出。

解决这个问题的关键在于:在不再需要的时候,一定要记得调用 .remove 方法做 .set 方法的反向操作,移除存储的数据。这就是正确使用 ThreadLocal 的方式。

也就是说,在你没有主动调用 remove 方法的情况下,只要线程还在,那么线程的 ThreadLocalMap 就在,ThreadLocalMap 在,那么其中就始终记录的 String 对象、Integer 对象、Double 对象的内存地址,那么它们就始终不会被销毁、回收,直到线程正常、非正常结束。

synchronized 关键字及其原理

synchronized 关键字

synchronized 是 Java 提供的同步机制,当一个线程正在操作同步代码块(synchronized 修饰的代码)时,其他线程只能阻塞等待原有线程执行完再执行。

synchronized 可以修饰代码块或者方法,示例代码如下:

// 修饰代码块
synchronized (this) {
    // do something
}

// 修饰方法
synchronized void method() {
    // do something
}

使用 synchronized 完善本文开头的非线程安全的代码。

方法一:使用 synchronized 修饰代码块 ,代码如下:

class ThreadSafeTest {

    static int number = 0;

    public static void main(String[] args) 
            throws InterruptedException {

        Thread thread1 = new Thread(() -> {
            // 同步代码
            synchronized (ThreadSafeTest.class) {
                addNumber();
            }
        });

        Thread thread2 = new Thread(() -> {
            // 同步代码
            synchronized (ThreadSafeTest.class) {
                addNumber();
            }
        });

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("number:" + number);
    }

    public static void addNumber() {
        for (int i = 0; i < 10000; i++) {
            ++number;
        }
    }
}

以上程序执行结果如下:

number:20000

方法二:使用 synchronized 修饰方法 ,代码如下:

class ThreadSafeTest {
    static int number = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> addNumber());
        Thread t2 = new Thread(() -> addNumber());
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("number:" + number);
    }

    public synchronized static void addNumber() {
        for (int i = 0; i < 10000; i++) {
            ++number;
        }
    }
}

以上程序执行结果如下:

number:20000

synchronized 实现原理

synchronized 本质是通过进入和退出的 Monitor 对象来实现线程安全的。

以下面代码为例:

public class SynchronizedTest {
    public static void main(String[] args) {
        synchronized (SynchronizedTest.class) {
            System.out.println("Java");
        }
    }
}

JVM(Java 虚拟机)采用 monitorentermonitorexit 两个指令来实现同步的:

  • monitorenter 指令相当于加锁;

  • monitorexit 相当于释放锁。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值