非线程安全的ListHelper

本文深入分析了Java并发编程实战中ListHelper类的线程安全问题,指出其非线程安全的原因在于锁不一致,并通过实验验证了此问题的存在。文章还提供了一个正确的实现方式,即确保所有同步操作使用相同的锁,从而实现线程安全。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java并发编程实战这本书里提到了使用Collections.synchronizedList可以创建线程安全的容器,同时给了一个没有正确使用该容器的反例ListHelper,这个反例看起来实现了同步,然而由于锁不一致导致它并不是一个线程安全的类。代码如下:

class ListHelper <E> {
    public List<E> list = Collections.synchronizedList(new ArrayList<E>());
    public synchronized boolean putIfAbsent(E x) {
        boolean absent = ! list.contains(x);
        if (absent)
            list.add(x);
        return absent;
    }
}

首先我们要知道这个类为什么不是线程安全的?很简单这个类使用了两个不同的锁,putIfAbsent方法上使用的锁是该方法的调用者ListHelper对象,然而list.contains(x)方法使用的却不是这个锁。查看contains的源码,它使用的锁是mutex。

        public boolean contains(Object o) {
            synchronized (mutex) {return c.contains(o);}
        }

显然这个mutex锁是SynchronizedCollection提供的。这就导致了锁不一致的情况,也就会导致线程安全问题。

那么我们如何证明ListHelper是非线程安全的呢?

如下是证明方案,分别启动两个线程,第一个线程循环100000次调用putIfAbsent方法添加数据。第二个线程同样循环100000次添加,但使用了list作为锁对象来进行同步。

public class Test {
	public static void main(String[] args) throws Exception {
		ListHelper<Integer> helper = new ListHelper<>();
		Thread thread1 = new Thread(new Runnable() {
			@Override
			public void run() {
				Test.t1(helper);
			}
		});

		Thread thread2 = new Thread(new Runnable() {
			@Override
			public void run() {
				Test.t2(helper);
			}
		});

		thread1.start();
		thread2.start();
		thread1.join();
		thread2.join();
		System.out.println(helper.list.size());
	}

	private static void t1(ListHelper<Integer> helper) {
		for (int i = 0; i < 100000; i++)
			helper.putIfAbsent(i);
	}

	private static void t2(ListHelper<Integer> helper) {
		for (int i = 0; i < 100000; i++)
			synchronized (helper.list) { // correct way to synchronize
				if (!helper.list.contains(i))
					helper.list.add(i);
			}
	}
}

如果这段测试代码打印的结果大于100000.那么ListHelper就是非线程安全的,如下所示,确实非线程安全。

当然这个案例关注的问题是:单纯来看putIfAbsent 这个方法,它本身一定是线程安全的,但由于该方法使用的锁不正确,导致了putIfAbsent所属的类却不是线程安全的。如果你开启100000个线程交替执行putIfAbsent方法,那么始终输出100000这个结果。

同时案例引发的思考是:如果引用外部线程安全的容器,那么必须保证这个容器和类方法使用同一个锁,这个类才是线程安全的类。

所以,ListHelper要改造成线程安全的类,必须使用和list一致的锁,即使用如下的同步代码块的方式:

class ListHelper <E> {
    public List<E> list = Collections.synchronizedList(new ArrayList<E>());
    public boolean putIfAbsent(E x) {
        synchronized (list) {
            boolean absent = ! list.contains(x);
            if (absent)
                list.add(x);
            return absent;
        }
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alphathur

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值