• home > java > KeyConcepts >

    java并发编程(2):Java多线程-java.util.concurrent高级工具

    Author:zhoulujun Date:

    机伴随着多核CPU的出现,也就意味着不同的线程能被不同的 CPU 核得到真正意义的并行执行。Java 是最先支持多线程的开发的语言之一,Java1 5提供了一个非常高效实用的多线程包:java util concurrent

    高级多线程控制类

    Java1.5提供了一个非常高效实用的多线程包:java.util.concurrent, 提供了大量高级工具,可以帮助开发者编写高效、易维护、结构清晰的Java多线程程序。

    ThreadLocal类

    ThreadLocal类 用来保存线程的独立变量。对一个线程类(继承自Thread)

    当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。常用于用户登录控制,如记录session信息。

    实现:每个Thread都持有一个TreadLocalMap类型的变量(该类是一个轻量级的Map,功能与map一样,区别是桶里放的是entry而不是entry的链表。功能还是一个map。)以本身为key,以目标为value。

    主要方法是get()和set(T a),set之后在map里维护一个threadLocal -> a,get时将a返回。ThreadLocal是一个特殊的容器。

    原子类(AtomicInteger、AtomicBoolean……)

    如果使用atomic wrapper class如atomicInteger,或者使用自己保证原子的操作,则等同于synchronized

    AtomicInteger.compareAndSet(int expect,int update)//返回值为boolean

    AtomicReference

    对于AtomicReference 来讲,也许对象会出现,属性丢失的情况,即oldObject == current,但是oldObject.getPropertyA != current.getPropertyA。

    这时候,AtomicStampedReference就派上用场了。这也是一个很常用的思路,即加上版本号

    Lock类 

    lock: 在java.util.concurrent包内。共有三个实现:

    1. ReentrantLock

    2. ReentrantReadWriteLock.ReadLock

    3. ReentrantReadWriteLock.WriteLock

    主要目的是和synchronized一样, 两者都是为了解决同步问题,处理资源争端而产生的技术。功能类似但有一些区别。

    区别如下:

    1. lock更灵活,可以自由定义多把锁的枷锁解锁顺序(synchronized要按照先加的后解顺序)

    2. 提供多种加锁方案,lock 阻塞式, trylock 无阻塞式, lockInterruptily 可打断式, 还有trylock的带超时时间版本。

    3. 本质上和监视器锁(即synchronized是一样的)

    4. 能力越大,责任越大,必须控制好加锁和解锁,否则会导致灾难。

    5. 和Condition类的结合。

    6. 性能更高,synchronized和Lock性能对比,如下图:

    java.util.concurrent lock与synchronized 性能对比


    ReentrantLock的使用

    可重入的意义在于持有锁的线程可以继续持有,并且要释放对等的次数后才真正释放该锁。

    private java.util.concurrent.locks.Lock lock = new ReentrantLock();
    public void method() {	
    	try {		
    		lock.lock();		//获取到锁lock,同步块
    	} finally {		
    		lock.unlock();//释放锁lock
    	}
    }


    • ReentrantLock 比 synchronized 功能更强大,主要体现:

    • ReentrantLock 具有公平策略的选择。

    • ReentrantLock 可以在获取锁的时候,可有条件性地获取,可以设置等待时间,很有效地避免死锁。

    • 如 tryLock() 和 tryLock(long timeout, TimeUnit unit)

    • ReentrantLock 可以获取锁的各种信息,用于监控锁的各种状态。

    • ReentrantLock 可以灵活实现多路通知,即Condition的运用。

    公平锁与非公平锁

    ReentrantLock 默认是非公平锁,允许线程“抢占插队”获取锁。公平锁则是线程依照请求的顺序获取锁,近似FIFO的策略方式。

    锁的使用

    1. lock() 阻塞式地获取锁,只有在获取到锁后才处理interrupt信息

    2. lockInterruptibly() 阻塞式地获取锁,立即处理interrupt信息,并抛出异常

    3. tryLock() 尝试获取锁,不管成功失败,都立即返回true、false,注意的是即使已将此锁设置为使用公平排序策略,tryLock()仍然可以打开公平性去插队抢占。如果希望遵守此锁的公平设置,则使用 tryLock(0, TimeUnit.SECONDS),它几乎是等效的(也检测中断)。

    4. tryLock(long timeout, TimeUnit unit)在timeout时间内阻塞式地获取锁,成功返回true,超时返回false,同时立即处理interrupt信息,并抛出异常。

    如果想使用一个允许闯入公平锁的定时 tryLock,那么可以将定时形式和不定时形式组合在一起:

    if (lock.tryLock() || lock.tryLock(timeout, unit) ) { ... }

    private java.util.concurrent.locks.ReentrantLock lock = new ReentrantLock();
    public void testMethod() {	
    	try {		
    		if (lock.tryLock(1, TimeUnit.SECONDS)) {
    			//获取到锁lock,同步块
    		} else {		
    			//没有获取到锁lock
    		}
    	} catch (InterruptedException e) {
    		e.printStackTrace();
    	} finally {
    		if (lock.isHeldByCurrentThread())
    		//如果当前线程持有锁lock,则释放锁lock
    			lock.unlock();
    	}
    }


    条件Condition的使用

    条件Condition可以由锁lock来创建,实现多路通知的机制。

    具有await、signal、signalAll的方法,与wait/notify类似,需要在获取锁后方能调用。

    private final java.util.concurrent.locks.Lock lock = new ReentrantLock();
    private final java.util.concurrent.locks.Condition condition = lock.newCondition();
    public void await() {	
    	try {		
    		lock.lock();		//获取到锁lock
    		condition.await();//等待condition通信信号,释放condition锁
    		//接到condition通信
    	} catch (InterruptedException e) {
    		e.printStackTrace();
    	} finally {
    		lock.unlock();//释放对象锁lock
    	}
    }


    ReentrantReadWriteLock的使用

    ReentrantReadWriteLock是对ReentrantLock 更进一步的扩展,实现了读锁readLock()(共享锁)和写锁writeLock()(独占锁),实现读写分离。读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

    读锁示例:

    private final java.util.concurrent.locks.ReadWriteLock lock = new ReentrantReadWriteLock();
    public void method() {	
    	try {		
    		lock.readLock().lock();//获取到读锁readLock,同步块
    	} finally {		
    		lock.readLock().unlock();//释放读锁readLock
    	}
    }

    写锁示例:

    private final java.util.concurrent.locks.ReadWriteLock lock = new ReentrantReadWriteLock();
    public void method() {	
    	try {		
    		lock.writeLock().lock(); //获取到写锁writeLock,同步块
    	} finally {
    		lock.writeLock().unlock(); //释放写锁writeLock
    	}
    }


    容器类

    同步容器与异步容器概览

    同步容器

    包括两部分:

    • 一个是早期JDK的Vector、Hashtable;

    • 一个是它们的同系容器,JDK1.2加入的同步包装类,使用Collections.synchronizedXxx工厂方法创建。

    Map<String, Integer> hashmapSync = Collections.synchronizedMap(new HashMap<>());

    同步容器都是线程安全的,一次只有一个线程访问容器的状态

    在某些场景下可能需要加锁来保护复合操作

    复合类操作如:新增、删除、迭代、跳转以及条件运算。

    这些复合操作在多线程并发的修改容器时,可能会表现出意外的行为

    最经典的便是ConcurrentModificationException,

    原因是当容器迭代的过程中,被并发的修改了内容,这是由于早期迭代器设计的时候并没有考虑并发修改的问题。

    其底层的机制无非就是用传统的synchronized关键字对每个公用的方法都进行同步,使得每次只能有一个线程访问容器的状态。这很明显不满足我们今天互联网时代高并发的需求,在保证线程安全的同时,也必须有足够好的性能。

    并发容器

    与Collections.synchronizedXxx()同步容器等相比,util.concurrent中引入的并发容器主要解决了两个问题:

    1. 根据具体场景进行设计,尽量避免synchronized,提供并发性

    2. 定义了一些并发安全的复合操作,并且保证并发环境下的迭代操作不会出错

    util.concurrent中容器在迭代时,可以不封装在synchronized中,可以保证不抛异常,但是未必每次看到的都是"最新的、当前的"数据。

    Map<String, Integer> concurrentHashMap = new ConcurrentHashMap<>();

    ConcurrentHashMap 替代同步的Map即(Collections.synchronized(new HashMap()))。

    众所周知,HashMap是根据散列值分段存储的,同步Map在同步的时候会锁住整个Map,而ConcurrentHashMap在设计存储的时候引入了段落Segment定义,同步的时候只需要锁住根据散列值锁住了散列值所在的段落即可,大幅度提升了性能。ConcurrentHashMap也增加了对常用复合操作的支持,比如"若没有则添加":putIfAbsent(),替换:replace()。这2个操作都是原子操作。注意的是ConcurrentHashMap 弱化了size()和isEmpty()方法,并发情况尽量少用,避免导致可能的加锁(当然也可能不加锁获得值,如果map数量没有变化的话)。

    CopyOnWriteArrayList和CopyOnWriteArraySet分别代替List和Set,主要是在遍历操作为主的情况下来代替同步的List和同步的Set,这也就是上面所述的思路:迭代过程要保证不出错,除了加锁,另外一种方法就是"克隆"容器对象。---缺点也明显,占有内存,且数据最终一致,但数据实时不一定一致,一般用于读多写少的并发场景。

    • ConcurrentSkipListMap可以在高效并发中替代SoredMap(例如用Collections.synchronzedMap包装的TreeMap)。

    • ConcurrentSkipListSet可以在高效并发中替代SoredSet(例如用Collections.synchronzedSet包装的TreeMap)。

    • ConcurrentLinkedQuerue是一个先进先出的队列。它是非阻塞队列。注意尽量用isEmpty,而不是size();

    CountDownLatch闭锁的使用

    管理类

    管理类的概念比较泛,用于管理线程,本身不是多线程的,但提供了一些机制来利用上述的工具做一些封装。

    了解到的值得一提的管理类:ThreadPoolExecutor和 JMX框架下的系统级管理类 ThreadMXBean

    ThreadPoolExecutor

    如果不了解这个类,应该了解前面提到的ExecutorService,开一个自己的线程池非常方便:

    ExecutorService e = Executors.newCachedThreadPool();
        ExecutorService e = Executors.newSingleThreadExecutor();
        ExecutorService e = Executors.newFixedThreadPool(3);
        // 第一种是可变大小线程池,按照任务数来分配线程,
        // 第二种是单线程池,相当于FixedThreadPool(1)
        // 第三种是固定大小线程池。
        // 然后运行
        e.execute(new MyRunnableImpl());

    该类内部是通过ThreadPoolExecutor实现的,掌握该类有助于理解线程池的管理,本质上,他们都是ThreadPoolExecutor类的各种实现版本。


    参考文章:

    Java多线程并发编程一览笔录 https://www.cnblogs.com/yw0219/p/10597041.html

    Java 中的多线程你只要看这一篇就够了 https://juejin.im/entry/57339fe82e958a0066bf284f



    转载本站文章《java并发编程(2):Java多线程-java.util.concurrent高级工具》,
    请注明出处:https://www.zhoulujun.cn/html/java/KeyConcepts/8476.html