多线程高级讲解一:常见的线程安全的类有哪些 ?

Vector 与 ArrayList的区别:

他们的原理都是通过数组实现的,增删慢,查询快。

Vector是线程安全的,ArrayList线程不安全,效率高。

 

我们去看源码:

Vector 的add方法是加了synchronized关键字,所以他是一个同步方法,线程是安全的,效率低,工作中即使要使用线程安全的List集合,也不使用Vector,而是用Collections工具类中的   Collections.synchronizedList(arrayList);他可以将线程不安全的ArrayList转换为线程安全的List。

    /**
     * Appends the specified element to the end of this Vector.
     *
     * @param e element to be appended to this Vector
     * @return {@code true} (as specified by {@link Collection#add})
     * @since 1.2
     */
    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

ArrayList的add方法源码,没有使用任何多线程的锁,所以他是线程不安全的。

    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

 

 

HashMap 与 HashTable的区别:

还是看源码:

HashMap: 线程不安全,可以有null键,也可以有null值。

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

 

HashTable的put方法被synchronized修饰,所以HashTable是线程安全的。从第一段源码可以看出不能有null值。 当然也不能有null键。

    public synchronized V put(K key, V value) {
        // Make sure the value is not null
// 这段源码可以看出,值不能为null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }

        addEntry(hash, key, value, index);
        return null;
    }

HashTable的get方法,也是线程安全的。

    public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        return null;
    }

 

通常情况下,也不会使用HashTable,因为在多线程的环境下,HashTable的get方法,只能让一个线程同时操作,所以他的效率根本就不高。所以在jdk1.5的时候就出现了 ConcurrentHashMap。

还有 java 提供了 Collectiions的类,有一个  Collections.synchronizedMap(hashMap); 他是可以将线程不安全的HashMap转换为线程安全的Map的集合。

 

接下来就说一下,为什么多线程不用HashTable,而使用ConcurrentHashMap。

原因就是HashTable虽然线程安全,但是效率低。而ConcurrentHashMap底层原理就是HashTable,只不过做了分段处理。

上面的源码也看见了,HashTable使用了Synchronized所以不管是put数据,还是get数据,都是加锁了的,所以多线程在操作map集合的时候,也是一条一条执行的,这样效率特别低。

在jdk1.5时,工程师就想了个办法,把这个ConcurrentHashMap集合,按照索引进行拆分。比如ConcurrentHashMap里有100条数据,就将这160条数据,按照索引拆分为16个HashTable,这16个HashTable就是16个不同的线程,专业名词叫:分段锁,通过16个线程操作ConcurrentHashMap来提高程序的效率。但是拆分出来的16个HashTable,每个HashTable中还有16条数据,这16条数据是同一把锁。所以如果是查询整个集合或者查询数据较多的情况下,推荐使用ConcurrentHashMap。如果查询某1个2个的话,还是可以直接使用HashTable。还有上面我故意说成16个HashTable,是因为ConcurrentHashMap最高支持分成16段。

在jdk1.8以前,这个ConcurrentHashMap底层原理是 :数组 + 分段锁。

 

但是在jdk1.8之后,java重写了HashMap 和 ConcurrentHashMap。所以他们的底层原理又有所改变。

1.8以后的ConcurrentHashMap 和 HashMap 底层原理是:数组+链表+红黑树, 只不过一个线程安全,一个不安全。

有兴趣的可以看看这篇文章:彻底搞清楚ConcurrentHashMap的实现原理(含JDK1.7和JDK1.8的区别)

我觉得大概原理也都差不多,只不过性能更优化了,比如说1.8以前的ConcurrentHashMap是直接分段,这样如果数据量较大的情况下,其实效率也还是比较低。1.8之后,加入了红黑树的数据结构,当数据量达到一定程度之后,就使用红黑树存储数据,为后面查询,提高效率。

 

这是ConcurrentHashMap的put方法。

public V put(K key, V value) {
        return putVal(key, value, false);
    }

    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {

        /底层是用的HashTable,这里也说明了,不能有null健 和 null值。
        if (key == null || value == null) throw new NullPointerException();

        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

 

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第3章 多线程(二) Java 高级程序设计 Java高级程序设计-多线程(二)全文共34页,当前为第1页。 回顾 进程一般代表一个应用程序,一个进程中可以包含多个线程。 合理使用多线程能够提高程序的执行效率,处理高并发应用。 线程的创建有继承Thread和实现Runnable接口两种方式,通过Runnable方式可以更加容易实现多线程之间资源共享。 通过sleep可以使线程进入休眠状态,通过join方法可以让线程处于等待,其他线程执行完毕后继续执行。 线程生命周期包括:新建 就绪 运行 阻塞 死亡5种状态。 Java高级程序设计-多线程(二)全文共34页,当前为第2页。 本章内容 掌握同步代码块的使用 掌握同步方法的使用 理解线程死锁 掌握 ThreadLocal 的使用 使用多线程模拟猴子采花 使用同步方法模拟购票 使用多线程模拟购物订单生成 使用 ThreadLocal 模拟银行取款 Java高级程序设计-多线程(二)全文共34页,当前为第3页。 3.1 同步代码块 线程安全问题 同步代码块的使用 使用多线程模拟猴子采花 20 25 Java高级程序设计-多线程(二)全文共34页,当前为第4页。 3.1 线程安全 多线程编程时,由于系统对线程的调度具有一定的随机性,所以,使用多个线程操作同一个数据时,容易出现线程安全问题。 当多个线程访问同一个资源时,如果控制不好,也会造成数据的不正确性。 以银行取钱为例: 用户输入账户、密码,系统判断用户的账户、密码是否匹配 用户输入取款金额 系统判断账户余额是否大于取款金额 如果余额大于等于取款金额,则取款成功,否则取款失败 Java高级程序设计-多线程(二)全文共34页,当前为第5页。 3.1.1 模拟银行取款 使用多线程并发模拟两个账户并发取钱的问题: 创建账户(Account),用于封装用户的账号和余额 public class Account { // 用户账号 private String no; // 账户中余额 private double balance; public Account() { } // 构造方法用于初始化账户、余额 public Account(String no, double balance) { this.no = no; this.balance = balance; } //getter和setter省略 Java高级程序设计-多线程(二)全文共34页,当前为第6页。 3.1.1 模拟银行取款 创建模拟两个线程的取款 DrawThread,该继承 Thread 。取钱的业务逻辑为当余额不足时无法提取现金,当余额足够时系统吐出钞票,减少余额 public class DrawThread extends Thread { // 模拟用户账户 private Account account; // 当前线程索取钱数 private double drawAccount; //完成数据初始化工作 public DrawThread(String name, Account account, double drawAccount) { super(name); this.account = account; this.drawAccount = drawAccount; } public void run() { // 账户余额大于取钱数据 if (account.getBalance() >= drawAccount) { System.out.println(this.getName() + "\t 取款成功 ! 吐钞 :" + drawAccount); // 修改余额 account.setBalance(account.getBalance() - drawAccount); System.out.println("\t 余额 : " + account.getBalance()); } else { System.out.println(this.getName() + " 取钱失败!余额不足 "); } } } // 当多个线程同时修改同一个共享数据时,将涉及数据安全问题 Java高级程序设计-多线程(二)全文共34页,当前为第7页。 3.1.1 模拟银行取款 由于多线程并发问题,一个线程执行余额操作可能未完毕,另外一个线程读取或者也在操作余额,必然会引起数据的不准确性。 这个时候需要在线程中加入对数据的保护机制,从而达到防止并发引起的数据不准确。 Java高级程序设计-多线程(二)全文共34页,当前为第8页。 3.1.2 同步代码块的使用 Java中多线程中引入了同步监视器,使用同步监视器的常用方式是使用同步代码块,保

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值