java并发和高并发之线程安全——同步容器

46 篇文章 0 订阅
12 篇文章 0 订阅

一、同步容器

1、同步容器出现原因:

     因为ArrayList  HashSet  HashMap 这几个容器都是线程不安全的,但是使用频率又最为频繁。所以在使用多线程并发地访问这些容器时可能出现线程安全问题。因此要求开发人员在任何用到这些的地方需要做同步处理。如此导致使用时极为不便。对此,java中提供了一些相应的同步容器供使用。

2、常见的同步容器举例:

》ArrayList——》Vector  、Stack

》HashMap——》HashTable(Key  value 不能为null)

》Collections.synchronizedXXX(List、Set、Map)

3、代码演示:

》Vector:

package com.mmall.practice.example.syncContainer;

import lombok.extern.slf4j.Slf4j;

import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

@Slf4j
public class VectorExample1 {

    private static Vector<Integer> list = new Vector<>();
    //请求总数
    private static int threadNum = 200;
    //同时并发执行的线程数
    private static int clientNum = 5000;

    public static void main(String[] args) throws Exception {
        ExecutorService exec = Executors.newCachedThreadPool();
        final Semaphore semp = new Semaphore(threadNum);
        for (int index = 0; index < clientNum; index++) {
            final int threadNum = index;
            exec.execute(() -> {
                try {
                    semp.acquire();
                    func(threadNum);
                    semp.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        exec.shutdown();
        log.info("size:{}", list.size());
    }
    public static void func(int threadNum) {
        list.add(threadNum);
    }
}

 

运行上方代码可以确认,可以上方当前是符合条件的。

但Vecor这个同步容器在某些情况下仍然是线程不安全的。

package com.mmall.practice.example.syncContainer;

import java.util.Vector;

public class VectorExample2 {

    private static Vector<Integer> vector = new Vector<>();

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

        while (true) {

            for (int i = 0; i < 10; i++) {
                vector.add(i);
            }

            Thread thread1 = new Thread() {
                public void run() {
                    for (int i = 0; i < vector.size(); i++) {
                        vector.remove(i);
                    }
                }
            };
            Thread thread2 = new Thread() {
                public void run() {
                    for (int i = 0; i < vector.size(); i++) {
                        vector.get(i);
                    }
                }
            };

            thread1.start();
            thread2.start();
        }
    }
}

运行如上代码,可以发现,运行抛出异常。证实该程序是线程不安全的,vector在某些情况下线程不安全。

》Hashtable  代替hashMap ,

package com.mmall.practice.example.syncContainer;

import com.google.common.collect.Maps;
import com.mmall.practice.annoations.NotThreadSafe;
import com.mmall.practice.annoations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

@ThreadSafe
@Slf4j
public class HashTableExample1 {

    private static Map<Integer, Integer> map = new Hashtable<>();

    //请求总数
    private static int threadNum = 200;
    //同时并发执行的线程数
    private static int clientNum = 5000;

    public static void main(String[] args) throws Exception {
        ExecutorService exec = Executors.newCachedThreadPool();
        final Semaphore semp = new Semaphore(threadNum);
        for (int index = 0; index < clientNum; index++) {
            final int threadNum = index;
            exec.execute(() -> {
                try {
                    semp.acquire();
                    func(threadNum);
                    semp.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        exec.shutdown();
        log.info("size:{}", map.size());
    }

    public static void func(int threadNum) {
        map.put(threadNum, threadNum);
    }
}

 上述代码,预期结果为5000,最后运行下如上代码,你可以发现,结果是5000,即证实hashtable是线程安全的容器。

》Collections 类中的工厂方法创建的线程安全对象,

》》Collections.synchronizedXXX  方法作用于List时:

package com.mmall.practice.example.syncContainer;

import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;

import java.util.Collections;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

@Slf4j
public class CollectionsExample1 {

    private static List<Integer> list = Collections.synchronizedList(Lists.newArrayList());

    //请求总数
    private static int threadNum = 200;
    //同时并发执行的线程数
    private static int clientNum = 5000;

    public static void main(String[] args) throws Exception {
        ExecutorService exec = Executors.newCachedThreadPool();
        final Semaphore semp = new Semaphore(threadNum);
        for (int index = 0; index < clientNum; index++) {
            final int threadNum = index;
            exec.execute(() -> {
                try {
                    semp.acquire();
                    func(threadNum);
                    semp.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        exec.shutdown();
        log.info("size:{}", list.size());
    }

    public static void func(int threadNum) {
        list.add(threadNum);
    }
}

运行结果如下:

》》Collections.synchronizedXXX  方法作用于Sett时:

package com.mmall.practice.example.syncContainer;

import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;

import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

@Slf4j
public class CollectionsExample2 {

    private static Set<Integer> set = Collections.synchronizedSet(Sets.newHashSet());

    //请求总数
    private static int threadNum = 200;
    //同时并发执行的线程数
    private static int clientNum = 5000;

    public static void main(String[] args) throws Exception {
        ExecutorService exec = Executors.newCachedThreadPool();
        final Semaphore semp = new Semaphore(threadNum);
        for (int index = 0; index < clientNum; index++) {
            final int threadNum = index;
            exec.execute(() -> {
                try {
                    semp.acquire();
                    func(threadNum);
                    semp.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        exec.shutdown();
        log.info("size:{}", set.size());
    }

    public static void func(int threadNum) {
        set.add(threadNum);
    }
}

运行结果如下:

》》Collections.synchronizedXXX  方法作用于Mapt时:

package com.mmall.practice.example.syncContainer;

import com.mmall.practice.annoations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.Collections;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

@ThreadSafe
@Slf4j
public class CollectionsExample3 {

    private static Map<Integer, Integer> map = Collections.synchronizedMap(new Hashtable<>());

    //请求总数
    private static int threadNum = 200;
    //同时并发执行的线程数
    private static int clientNum = 5000;

    public static void main(String[] args) throws Exception {
        ExecutorService exec = Executors.newCachedThreadPool();
        final Semaphore semp = new Semaphore(threadNum);
        for (int index = 0; index < clientNum; index++) {
            final int threadNum = index;
            exec.execute(() -> {
                try {
                    semp.acquire();
                    func(threadNum);
                    semp.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        exec.shutdown();
        log.info("size:{}", map.size());
    }

    public static void func(int threadNum) {
        map.put(threadNum, threadNum);
    }
}

运行结果如下:

总结:经过如上多次测试发现,Collections.synchronizedXXX方法可以创建出线程安全的容器对象。

扩展:

集合在使用时容易出现的问题代码示例:集合中删除数据时,会出现如下结果(因此删除数据时,要选择如下success 执行结果的方案哦!)

package com.mmall.practice.example.syncContainer;

import java.util.Iterator;
import java.util.Vector;

public class CollectionRemoveExample {

    // java.util.ConcurrentModificationException  结果报异常
    private static void test1(Vector<Integer> v1) { // foreach
        for(Integer i : v1) {
            if (i.equals(3)) {
                v1.remove(i);
            }
        }
    }

    // java.util.ConcurrentModificationException  结果报异常
    private static void test2(Vector<Integer> v1) { // iterator
        Iterator<Integer> iterator = v1.iterator();
        while (iterator.hasNext()) {
            Integer i = iterator.next();
            if (i.equals(3)) {
                v1.remove(i);
            }
        }
    }

    // success   结果ok
    private static void test3(Vector<Integer> v1) { // for
        for (int i = 0; i < v1.size(); i++) {
            if (v1.get(i).equals(3)) {
                v1.remove(i);
            }
        }
    }   

    public static void main(String[] args) {

        Vector<Integer> vector = new Vector<>();
        vector.add(1);
        vector.add(2);
        vector.add(3);
        vector.add(4);
        vector.add(5);
        test3(vector);
    }
}

如上,注意,在遍历 集合时删除元素容易出问题,在for中是ok的,但在iterator或者vector中是有问题的,所以注意方法的选取。

二、线程安全之并发容器 J.U.C (java.util.concurrent):

1、此处针对之前讲述的线程不安全类与写法中提到的问题提出解决方案,在此讲述下java提供的线程安全的同步容器。

》ArrayList ——》CopyOnWriteArrayList

 相比ArrayList,后者是线程安全的,通过名称,可以简单表现其实现原理。字面意思为:写操作时,复制。当有新元素拷贝到copyOnWriteList中时,它先从原有的数组中拷贝一份出来,在新的数组上进行写操作。写完后,再将原来的数组引用指向新的数组。CopyOnWriteArrayList的整个add 操作都是在锁保护下进行,目的是为了防止在多线程下操作时复制出多个对象,将数据搞混乱。

    但CopyOnWriteArrayList 类也存在缺点,即因为要复制,所以会消耗内存。如果元素比较多,容易引发young GC 或者for GC问题。具体可查看java的垃圾回收机制相关资料。其次,该方案无法实现实时读写的需求。比如set完数据之后,读取时可能读取到旧的数据。虽然最终结果肯定是一致的,但是无法满足即时性需求。

    综上,CopyOnWriteArrayList 更适合读多写少的操作。

    它的设计思想:读写分离、最终一致性、使用时另外开辟空间,防止错误,

   最终需要记住,copyOnWriteArrayList的读操作都是原子性操作不需要加锁,但是写操作都是在锁保护下进行,防止多线程并发出现线程安全问题。

代码示例:

package com.mmall.practice.example.concurrent;

import com.mmall.practice.annoations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

@Slf4j
@ThreadSafe
public class CopyOnWriteArrayListExample {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    private static List<Integer> list = new CopyOnWriteArrayList<>();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("size:{}", list.size());
    }

    private static void update(int i) {
        list.add(i);
    }
}

运行如上代码,结果没有问题。

》HashSet 、TreeSet ——》copyOnWriteArraySet 、ConcurrentSkipListSet

copyOnWriteArraySet   线程安全的,底层用了CopyOnWriteArrayList,它通常也适用于大小较小(防止元素多造成yang GC问题)、只读操作大于可变的操作。适用迭代器遍历时,速度很快,不会发生线程安全问题。

ConcurrentSkipListSet   它是jdk6 添加,支持自然排序、并且在构造器中支持自定义比较器。底层基于map集合,在多线程操作下,执行remove  add  contains 方法都是线程安全的。但是对于批量操作,addAll  removeAll  contrainsAll 无法保证原子性,故无法保证线程安全。所以在多线程中,对于批量操作的方法,要加锁同步保证线程安全。

    注意,这两个类都是无法使用null元素的。因为null无法与其他元素区分开。

代码示例:

package com.mmall.practice.example.concurrent;

import com.mmall.practice.annoations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

@Slf4j
@ThreadSafe
public class CopyOnWriteArraySetExample {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    private static Set<Integer> set = new CopyOnWriteArraySet<>();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("size:{}", set.size());
    }

    private static void update(int i) {
        set.add(i);
    }
}
package com.mmall.practice.example.concurrent;

import com.mmall.practice.annoations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

@Slf4j
@ThreadSafe
public class ConcurrentSkipListSetExample {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    private static Set<Integer> set = new ConcurrentSkipListSet<>();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("size:{}", set.size());
    }

    private static void update(int i) {
        set.add(i);
    }
}

运行如上两个方法,结果都是5000,和预期结果相符,证实为线程安全的。

再次注意:相应的批量操作是不安全的。

》HashMap 、TreeMap ——》ConcurrentHashMap  、ConcurrentSkipListMap

  ConcurrentHashMap :hashmap线程安全的版本,不允许空值。除了少数的删除操作,该类在只读操作大于写操作时,具有很高的并发性,因为该类为读取操作做了很多优化处理。高并发场景下表现优良

ConcurrentSkipListMap :treemap的线程安全版本,底层适用了SkipList 这种跳表的结构实现。

 ConcurrentHashMap和ConcurrentSkipListMap相比,在数据量大时,前者的查询速度是后者的4倍(4个线程1.6万数据时),但是后者的某些特点或优势是前者无法实现的,比如:

   1)ConcurrentSkipListMap中的key是有序的,而ConcurrentHashMap无法实现;

   2)ConcurrentSkipListMap支持更高的并发,存取时间几乎和线程数无关。

   3)在数量级越大时,ConcurrentSkipListMap的优势越明显。在非多线程的情况下,尽量使用TreeMap。对于并发性较低的情况下,使用Collections中的类也是很好的选择。对于高并发的程序,还是使用ConcurrentSkipListMap,需要对key排序时,也用这个类。

代码示例:

package com.mmall.practice.example.concurrent;

import com.mmall.practice.annoations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * ConcurrentHashMap 不允许空值
 * 在实际的应用中,散列表一般的应用场景是:除了少数插入操作和删除操作外,绝大多数都是读取操作,而且读操作在大多数时候都是成功的。
 * 正是基于这个前提,ConcurrentHashMap 针对读操作做了大量的优化。通过 HashEntry 对象的不变性和用 volatile 型变量协调线程间的内存可见性,
 * 使得 大多数时候,读操作不需要加锁就可以正确获得值。这个特性使得 ConcurrentHashMap 的并发性能在分离锁的基础上又有了近一步的提高。
 * ConcurrentHashMap 是一个并发散列映射表的实现,它允许完全并发的读取,并且支持给定数量的并发更新。
 * 相比于 HashTable 和用同步包装器包装的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 拥有更高的并发性。
 * 在 HashTable 和由同步包装器包装的 HashMap 中,使用一个全局的锁来同步不同线程间的并发访问。
 * 同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器。
 * 这虽然保证多线程间的安全并发访问,但同时也导致对容器的访问变成串行化的了。
 * <p>
 * ConcurrentHashMap 的高并发性主要来自于三个方面:
 * <p>
 * 用分离锁实现多个线程间的更深层次的共享访问。
 * 用 HashEntery 对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。
 * 通过对同一个 Volatile 变量的写 / 读访问,协调不同线程间读 / 写操作的内存可见性。
 * 使用分离锁,减小了请求 同一个锁的频率。
 * <p>
 * 通过 HashEntery 对象的不变性及对同一个 Volatile 变量的读 / 写来协调内存可见性,使得 读操作大多数时候不需要加锁就能成功获取到需要的值。
 * 由于散列映射表在实际应用中大多数操作都是成功的 读操作,所以 2 和 3 既可以减少请求同一个锁的频率,也可以有效减少持有锁的时间。
 * 通过减小请求同一个锁的频率和尽量减少持有锁的时间 ,使得 ConcurrentHashMap 的并发性相对于 HashTable 和用同步包装器包装的 HashMap有了质的提高。
 */
@ThreadSafe
@Slf4j
public class ConcurrentExample1 {

    private static Map<Integer, Integer> map = new ConcurrentHashMap<>();

    private static int threadNum = 200;
    private static int clientNum = 5000;

    public static void main(String[] args) throws Exception {
        ExecutorService exec = Executors.newCachedThreadPool();
        final Semaphore semp = new Semaphore(threadNum);
        for (int index = 0; index < clientNum; index++) {
            final int threadNum = index;
            exec.execute(() -> {
                try {
                    semp.acquire();
                    func(threadNum);
                    semp.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        exec.shutdown();
        log.info("size:{}", map.size());
    }

    public static void func(int threadNum) {
        map.put(threadNum, threadNum);
    }
}
package com.mmall.practice.example.concurrent;

import com.mmall.practice.annoations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.Map;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * ConcurrentSkipListMap提供了一种线程安全的并发访问的排序映射表
 * 内部是SkipList(跳表)结构实现,在理论上能够在O(log(n))时间内完成查找、插入、删除操作
 * ConcurrentHashMap是HashMap的线程安全版本,ConcurrentSkipListMap是TreeMap的线程安全版本
 *
 * concurrentHashMap与ConcurrentSkipListMap性能测试
 * 在4线程1.6万数据的条件下,ConcurrentHashMap 存取速度是ConcurrentSkipListMap 的4倍左右
 *
 * 但ConcurrentSkipListMap有几个ConcurrentHashMap 不能比拟的优点:
 * 1、ConcurrentSkipListMap 的key是有序的。
 * 2、ConcurrentSkipListMap 支持更高的并发。ConcurrentSkipListMap 的存取时间是log(N),和线程数几乎无关。
 *      也就是说在数据量一定的情况下,并发的线程越多,ConcurrentSkipListMap越能体现出他的优势。
 *
 *  在非多线程的情况下,应当尽量使用TreeMap。
 *  此外对于并发性相对较低的并行程序可以使用Collections.synchronizedSortedMap将TreeMap进行包装,也可以提供较好的效率。
 *  对于高并发程序,应当使用ConcurrentSkipListMap,能够提供更高的并发度
 *  所以在多线程程序中,如果需要对Map的键值进行排序时,请尽量使用ConcurrentSkipListMap,可能得到更好的并发度
 */
@ThreadSafe
@Slf4j
public class ConcurrentExample2 {

    private static Map<Integer, Integer> map = new ConcurrentSkipListMap<>();

    private static int threadNum = 200;
    private static int clientNum = 5000;

    public static void main(String[] args) throws Exception {
        ExecutorService exec = Executors.newCachedThreadPool();
        final Semaphore semp = new Semaphore(threadNum);
        for (int index = 0; index < clientNum; index++) {
            final int threadNum = index;
            exec.execute(() -> {
                try {
                    semp.acquire();
                    func(threadNum);
                    semp.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        exec.shutdown();
        log.info("size:{}", map.size());
    }

    public static void func(int threadNum) {
        map.put(threadNum, threadNum);
    }
}

如上展示了这两个类的使用方法,当然,运行结果都是线程安全的。

三、安全共享对象策略总结:

 1、线程限制:一个被线程限制的对象,由线程独占,并且只能被占有它的线程修改;

 2、共享只读:一个共享只读的对象,在没有额外同步的情况下,可以被多个线程并发访问,但是任何线程都不能修改它;

 3、线程安全对象:一个线程安全的对象或者容器,在内部通过同步机制来保证线程安全,所以其他线程无需额外的同步就可以通过公共的接口随意访问它。

4、被守护对象:被守护对象只能通过获取特定的锁来访问。

如上通过不可变对象、线程封闭、同步容器、并发容器总结而来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值