如何实现一个线程安全的数据结构

一. 如何实现一个线程安全的数据结构

1.饿汉模式

public class Singletan {
    private static Singletan instance = new Singletan();    
    public Singletan(){}    
    public static Singletan getInstance() {
        return instance;
    }
}

2.静态内部类

//加载内部类SingletanHolder,从而实例化instance
public class Singletan {
    private static class SingletanHolder {
        private static final Singletan INSTANCE = new Singletan();
    }
    public Singletan(){}
    public static final Singletan getInstance() {
        return SingletanHolder.INSTANCE;
    }
}

3.CAS:Compare and Swap(比较和交换)

  • 乐观锁,无锁算法。CAS有3个参数:内存值V、旧值A、要修改的新值B,当且仅当旧值 A 和内存值 V 相同时,才将内存值 V 修改为 B,否则会尝试重新获取 V 的当前值,重新比较。
  • 优点:没有线程切换和阻塞的额外开销,支持较大并行度。
  • 缺点:①在并发量较高时,如果许多线程反复尝试去更新一个变量,却又一直更新失败,会消耗很多CPU资源。②银行取款 ABA 问题,所以不仅要比较旧值和内存值,还要比较变量的版本号是否一致,只有一致才进行操作。

银行取款ABA问题:假如账户开始有100元,在取款机上取走50,取款机出现问题一共提交了两次请求(线程1,线程2),第二次请求(线程2)在执行时因为某种原因被阻塞了,这时候有人往你的账户打了50元,线程2恢复了可执行状态,这个时候就会出现问题,原本线程2应该执行失败的,但是比较后仍然与旧值一致,这样就造成了账户实际上扣款了两次!

二.Java中满足线程安全的数据结构

所谓 线程安全 就是:一段操纵共享数据的代码能够保证在同一时间内被多个线程执行而仍然保持其正确性的,就被称为是线程安全的。

线程安全是保证执行业务逻辑正确的基本前提,为此在多线程开发中,我们尽量采用能保证线程安全的数据结构。

JDK已经为大家准备好了一批好用的线程安全容器类,可以大大减少开发工作量,例如HashTable,ConcurrentHashMap,CopyOnWriteArrayList,CopyOnWriteArraySet,ConcurrentLinkedQueue,Vector,StringBuffer等。本文主要对这些数据结构的功能及其常见使用场景进行说明与比较。

在这里插入图片描述

1、HashTable

HashTable实现了Map接口,为此其本身也是一个散列表,它存储的内容是基于key-value的键值对映射。

HashTable中的key、value都不可以为null;具有无序特性;由于其方法函数都是同步的(采用synchronized修饰),不会出现两个线程同时对数据进行操作的情况,因此保证了线程安全性。

HashTable使用synchronized来修饰方法函数来保证线程安全,但是在多线程运行环境下效率表现非常低下。因为当一个线程访问HashTable的同步方法时,其他线程也访问同步方法就会粗线阻塞状态。比如当一个线程在添加数据时候,另外一个线程即使执行获取其他数据的操作也必须被阻塞,大大降低了程序的运行效率。

2、ConcurrentHashMap

我们知道HashMap是线程不安全的,ConcurrentHashMap是HashMap的线程安全版。

但是与HashTable相比,ConcurrentHashMap不仅保证了多线程运行环境下的数据访问安全性,而且性能上有长足的提升。

ConcurrentHashMap允许多个修改操作并发运行,其原因在于使用了锁分段技术:首先讲Map存放的数据分成一段一段的存储方式,然后给每一段数据分配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。这样就保证了每一把锁只是用于锁住一部分数据,那么当多线程访问Map里的不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率。

上述的处理机制明显区别于HashTable是给整体数据分配了一把锁的处理方法。为此,在多线程环境下,常用ConcurrentHashMap在需要保证数据安全的场景中去替换HashMap,而不会去使用HashTable,同时在最新版的JDK中已经推荐废弃使用HashTable。

3、CopyOnWriteArrayList

CopyOnWriteArrayList实现了List接口,提供的数据更新操作都使用了ReentrantLock的lock()方法来加锁,unlock()方法来解锁。

当增加元素的时候,首先使用Arrays.copyOf()来拷贝形成新的副本,在副本上增加元素,然后改变原引用指向副本。读操作不需要加锁,而写操作类实现中对其进行了加锁。因此,CopyOnWriteArrayList类是一个线程安全的List接口的实现,在高并发的情况下,可以提供高性能的并发读取,并且保证读取的内容一定是正确的,这对于读操作远远多于写操作的应用非常适合(注意: 如上述更新操作会带来较大的空间与性能开销,如果更新操太过频繁,反而不太合适使用)。

4、CopyOnWriteArraySet

CopyOnWriteArraySet是对CopyOnWriteArrayList使用了装饰模式后的具体实现。所以CopyOnWriteArrayList的实现机理适用于CopyOnWriteArraySet,此处不再赘述。

Java里的List和Set的之间的特性比较结论同样适用于CopyOnWriteArrayList与CopyOnWriteArraySet之间的比较;此外,CopyOnWriteArrayList与CopyOnWriteArraySet都是线程安全的。

5、ConcurrentLinkedQueue

ConcurrentLinkedQueue可以被看作是一个线程安全的LinkedList,使用了非阻塞算法实现的一个高效、线程安全的并发队列;其本质是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当添加一个元素时会添加到队列的尾部;当获取一个元素时,会返回队列头部的元素。

ConcurrentLinkedQueue应该算是在高并发环境中性能最好的队列,没有之一。

6、Vector

Vector通过数组保存数据,继承了Abstract,实现了List;所以,其本质上是一个队列。

但是和ArrayList不同,Vector中的操作是线程安全的,它是利用synchronized同步锁机制进行实现,其实现方式与HashTable类似。

7、StringBuffer与StringBuilder

在Java里面,字符串操作应该是最频繁的操作了,为此有必要把StringBuffer与StringBuilder两个方法类比较一下。

首先,对于频繁的字符串拼接操作,是不推荐采用效率低下的“+”操作的。一般是采用StringBuffer与StringBuilder来实现上述功能。但是,这两者也是有区别的:前者线程安全,后者不是线程安全的。

StringBuffer是通过对方法函数进行synchronized修饰实现其线程安全特性,实现方式与HashTable、Vector类似。

总结:

  1. HashTable是线程安全类;通过对其方法函数进行synchronized修饰实现其特性,效率低下,目前已被jdk废弃,不再推荐使用。
  2. 在多线程环境下,我们常用ConcurrentHashMap在需要保证数据安全的场景中去替换HashMap;此外ConcurrentHashMap也有不错的性能表现
  3. CopyOnWriteArrayList类是一个线程安全的List接口的实现,在高并发的情况下,可以提供高性能的并发读取,并且保证读取的内容一定是正确的,这对于读操作远远多于写操作的应用非常适合。
  4. CopyOnWriteArraySet是对CopyOnWriteArrayList使用了装饰模式后的具体实现,可理解为线程安全的Set。
  5. ConcurrentLinkedQueue应该算是在高并发环境中性能最好的队列;在多线程的队列应用场景中,强烈推荐使用。
  6. Vector中的操作是线程安全的,它是利用synchronized同步锁机制进行实现,其实现方式与HashTable类似。
  7. StringBuffer与StringBuilder常用于字符串拼接;前者线程安全,后者不是线程安全的;在多线程环境中下,考虑数据安全使用前者,否则使用后者。

你可能感兴趣的:(如何实现一个线程安全的数据结构)