Java多线程

1.线程安全

当多个线程访问某个类时,无论运行时环境采用哪种线程线程调度策略或者这些线程如何交替执行,而且在主调代码中无需任何额外的同步或协同,该类总是可以表现出正确的行为,那么我们称该类是线程安全的。

注:在线程安全类中封装了必要的同步机制,因此客户端无需进一步采取同步措施

影响线程安全的因素:

  • 共享
  • 可变

实现线程安全的方式:

  1. 不在线程间共享状态变量,特例:无状态的对象一定是线程安全的
  2. 状态变量不可变
  3. 对状态变量应用同步机制

1.1 不在线程间共享状态变量

如果仅在单线程内访问数据,那么就不需要同步机制,也能保证线程安全。这种技术也被称为线程封闭。

当某个对象封闭在一个线程中,这种用法将自动实现线程安全性,即使被封闭的对象本身不是线程安全的!!!

线程封闭主要有三种实现:

  1. Ad-hoc封闭,即维护线程封闭的职责完全由程序实现来承担,不推荐使用
  2. 栈封闭:在栈封闭中,只能通过局部变量访问对象。在维护栈封闭时,程序员需要多做一些工作,以保证被引用的对象不会逃逸【Escape】。
  3. Thread-Local类:ThreadLocal提供了线程本地变量,使得线程中的某个值与保存值的对象关联起来。ThreadLocal对象通常用于防止对可变的单例变量或全局变量进行共享。每个Thread对象内存在一个threadLocals变量,类型为ThreadLocal.ThreadLocalMap,ThreadLocal更像是一个Facade,帮助我们以一个一致的接口,操作Thread的threadLocals变量。

1.2 状态变量不可变

不可变对象一定是线程安全的!
当满足一下条件时,对象才是不可变的:

  1. 对象创建后其状态不能修改
  2. 对象的所有域都是Final的(Final类型的域是不能修改的)
  3. 对象是正确创建的(在对象的创建期,this引用没有逃逸)

1.3 同步机制

Java提供的同步机制:

  • 同步原语:synchronized
  • 显式锁:Lock
  • volatile
  • 原子变量:AtomicXxxx

1.3.1 同步原语:synchronized

通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有守护变量的锁,都能以独占的方式来访问这些变量,并且对变量的任何修改对随后获得这个锁的其他线程都是可见的。

1.3.2 显式锁:Lock

Java5引入的Lock机制,并不是替代内置锁的方法,而是当内置加锁机制不适当时,作为一种可选的高级功能。

与内置加锁机制不同,显式锁Lock提供了一种无条件的可轮询的可定时的可中断锁获取模式,所有加锁与解锁都是显式的。

Lock实现提供了与同步原语(内置锁)相同的内存可见性保证!!

public interface Lock {
	//获取锁
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

ReentrantLock实现了Lock接口,并提供与synchronized相同的互斥性内存可见性保证。

从内存语义的角度看:

  • 获取ReentrantLock = 进入同步代码块
  • 释放ReentrantLock = 退出同步代码块

ReentrantLock同样实现了synchronized所具备的可重入性

synchronized相对于显式锁的劣势:

  • 无法中断一个正在等待获取锁得线程
  • 性能有损耗

显式锁相对于synchronized的劣势:

  • 内置锁必须在代码块中显式的释放,lock & unlock
  • 无法实现非阻塞的加锁规则
无条件锁

Lock#lock()方法提供了无条件锁,它与同步原语基本一致,提供了互斥性与内存可见性。

轮询锁与定时锁

可定时的与可轮训的锁获取模式是由Lock#tryLock()方法实现的。
内置锁中,死锁是一个严重的问题,而恢复程序的唯一方式就是重启,而防止死锁的唯一方法就是在构造程序时避免出现不一致的锁顺序

可定时的且可轮训的锁中,仅当锁在调用时处于空闲状态时才获取锁。当获取锁时,锁如果可用,那么获取锁并返回true;而如果锁不可用,那么直接返回false。

    public static <T> Optional<Future<T>> action(Lock l1,
                                      Lock l2,
                                      long timeOut,
                                      Callable<T> task){
        
        LocalTime endline = LocalTime.now().plusNanos(timeOut);

        while (true){
            try {
                if (l1.tryLock()){
                    try {
                        if (l2.tryLock()){
                            Future<T> future = pool.submit(task);
                            return Optional.of(future);
                        }
                    } finally {
                        l2.unlock();
                    }

                }
            } finally {
                l1.unlock();
            }

            if (LocalTime.now().isAfter(endline)){
                return Optional.empty();
            }
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

定时锁的使用,是使用Lock#tryLock(long time, TimeUnit unit)这个重载版本实现的。

定时锁同样具有可中断性,如果获取锁时,锁不可用,那么当前线程会进入休眠,直到下面三种事情之一发生:

  • 当前线程获取到锁
  • 其他线程通过Thread#interrupt中断了该线程
  • 指定的等待时间已经过了

如果锁被获取,则返回true,否则返回false,如果线程被中断,则抛出InterruptedException

    public static <T> Optional<Future<T>> action(Lock lock,
                                                 long timeOut,
                                                 TimeUnit unit,
                                                 Callable<T> task){
        try {
            if (!lock.tryLock(timeOut,unit)){
                return Optional.empty();
            }
        } catch (InterruptedException e){
            return Optional.empty();
        }
        // Acquired Lock
        try {
            return Optional.of(pool.submit(task));
        } finally {
            lock.unlock();
        }
    }
可中断锁

Lock#lockInterruptibly()实现了可中断的锁获取模式。

公平锁

ReentrantLock提供了公平策略,默认是非公平锁,可以通过构造器参数fair来配置自定义的公平策略。

公平锁:线程将按照他们发出请求的顺序来获取锁
非公平锁:允许插队

注:公平锁的性能显著低于非公平锁,因此如果不必要的话,不要为公平付出性能的代价

读写锁

互斥是一种保守的加锁策略,虽然可以避免“写-写”或者“写-读”冲突,但是他同样也避免了“读-读”冲突。

读写锁,一个资源可以被多个读操作访问,或者被一个写操作访问,但是不可以二者兼得!!!

public interface ReadWriteLock {
    /**
     * 返回用于读的锁.
     */
    Lock readLock();

    /**
     * 返回用于写的锁.
     */
    Lock writeLock();
}

Java默认的实现是ReentrantReadWriteLock,它在读写锁的基础上,又提供了可重入性。

StampedLock

在Java 8中引入了StampedLock。它还支持读锁和写锁。但是,锁获取的方法返回一个用于释放锁或检查锁是否仍然有效的戳记:

public class StampedLockDemo {
    Map<String,String> map = new HashMap<>();
    private StampedLock lock = new StampedLock();
 
    public void put(String key, String value){
        long stamp = lock.writeLock();
        try {
            map.put(key, value);
        } finally {
            lock.unlockWrite(stamp);
        }
    }
 
    public String get(String key) throws InterruptedException {
        long stamp = lock.readLock();
        try {
            return map.get(key);
        } finally {
            lock.unlockRead(stamp);
        }
    }
}

StampedLock提供的另一个特性是乐观锁。大多数情况下,读操作不需要等待写操作完成,因此不需要完全的读锁。

相反,我们可以升级到读锁:

public String readWithOptimisticLock(String key) {
	// 获取乐观锁,其实并没加锁,它假定不会有write操作
    long stamp = lock.tryOptimisticRead();
    String value = map.get(key);
 	// 验证stamp之后,是否有write操作
    if(!lock.validate(stamp)) {
    	// 由乐观锁升级到读锁
        stamp = lock.readLock();
        try {
            return map.get(key);
        } finally {
        	// 释放锁
            lock.unlock(stamp);               
        }
    }
    return value;
}

StampedLock还支持读锁升级写锁,即通过tryConvertToWriteLock方法,如果升级成功,则返回新的stamp,否则返回0

StampedLock性能很高,推荐使用

协作性

监视器需要支持两种线程同步:

  • ①互斥:Java虚拟机通过对象锁来支持互斥,以允许多个线程独立地操作共享数据,而不会相互干扰。
  • ②协作:在Java虚拟机中,通过Object类的wait和notify方法支持协作,这使线程能够一起工作,以实现一个共同的目标。

条件队列:它使得一组线程(称之为等待集)能通过某种方式,等待特定的条件变为True.
注:在传统的队列中,元素通常是数据,而在条件队列中的元素,却是一个个等待相关条件的线程

每个对象可以作为一个锁(对象锁),同时,每个对象还可以作为一个条件队列,且Java通过Object中声明的wait/notify方法,构成了内部条件队列的API。

注意:内置锁仅可以有一个相关联的条件队列,而一个条件队列可以支持多个条件谓词!!

当使用条件等待时,比如Object#wait或Condition#await:

  • 通常都存在一个条件谓词—其中包括对一些对象状态变量的验证,在线程执行正式逻辑代码前必须通过该谓词测试
  • 在调用wait之前测试条件谓词,并且从wait中返回时需要再次验证条件谓词,因此wait通常处于一个while循环中,while语句的条件表达式,即是条件谓词
  • 确保使用与条件队列相关联的锁,来保护构成条件谓词的各个状态变量
  • 当调用wait/notify/notifyAll时,必须持有与条件队列相关的锁
  • 在正式逻辑处理结束后再释放锁,不要在条件谓词之后以及逻辑处理之前释放锁
// 模拟有界的缓存
public class OuterPrediectQueueBuffer<V> extends BaseBoundedBuffer<V> {

    final Lock lock = new ReentrantLock();
    // 不得Empty,空了不得take
    final Condition nonEmptyCond = lock.newCondition();
    //不得Full,满了不得put
    final Condition nonFullCond = lock.newCondition();

    protected OuterPrediectQueueBuffer(int capacity) {
        super(capacity);
    }

    public synchronized void put(V v) throws InterruptedException {

        lock.lock();				// lock是条件队列相关联的锁
        try {
            while(isFull()){		// isFull是条件谓词,被唤醒后,while循环中再验证是否满足了条件谓词
                nonFullCond.wait();	// 条件等待
            }
            doPut(v);				// 正式的逻辑
            nonEmptyCond.signalAll();// 通知	
            return;
        } finally {
            lock.unlock();			// 逻辑处理完成后释放锁
        }
    }

    public synchronized V take() throws InterruptedException {

        lock.lock();
        try {
            while (isEmpty()){
                nonEmptyCond.wait();
            }
            V v = doTake();
            nonFullCond.signalAll();
            return v;
        } finally {
            lock.unlock();
        }
    }
}

Java5引入的Condition,解决了内置锁仅可以有一个相关联的条件队列的限制

一个Condition与一个Lock关联
一个条件队列与一个内置锁(对象锁)关联

对于每个Lock,它可以有人以数量的Coondition对象。而且Condiftion会继承Lock的公平策略,对于公平锁,线程会按照FIFO的顺序从Condition#await中释放。

Object与Condition中等待、通知的方法对应关系为:

Object Condition
wait await
notify signal
notifyAll signalAll

1.3.3 volatile

1.3.4 原子变量:AtomicXxxx

CAS(Compare-And-Swap)的缩写,比较并交换。CAS指令在Intel CPU上称为CMPXCHG指令,它的作用是将指定内存地址的内容与所给的某个值相比,如果相等,则将其内容替换为指令中提供的新值,如果不相等,则更新失败。

CAS是一种乐观锁的技术,它希望能成功的更新操作,并且如果另一个线程在最近一次检查后更新了该变量,那么CAS需要检测到这个错误。

CAS的典型使用模式:首先从内存取出值,并根据该值计算出新值,然后调用CAS更新。

CAS使用无锁的方式,实现了读-改-写操作序列。

自Java5之后,Java提供了AtomicXxx的,支持CAS操作。

CAS中的ABA问题:假设内存V中的值由A -> B -> A,在某些情况下,仍然认为发生了变化,当此时执行CAS应该失败。

解决ABA的最简单的方式,仍然是使用乐观锁,即并非更新某个引用的值,而是更新两个值,包括一个引用和一个版本号。

Java通过AtomicStampedReference 来解决了ABA问题。

2. 基础构建块

2.1 同步容器

HashTable、Vector是古老的线程安全类,他们通过对Map加锁,保证对所有的的操作都是线程安全的。

注:迭代会发生ConcurrentModificationException,这是Fail-Fast机制

2.2 并发容器

Java 5引入了ConcurrentHashMap,用来替换HashMap,引入了CopyOnWriteArrayList,用于在迭代为主的情况下替换List。

ConcurrentHashMap

ConcurrentHashMap也是一个基于散列的Map,它是它使用了一种完全不同的加锁策略来提供更高的并发性和伸缩性。

ConcurrentHashMap并不是像HashTable那样在每一个方法上加对象锁,而是使用了分段锁,来实现细粒度的加锁机制。

ConcurrentHashMap提供了弱一致性,通过牺牲了一点一致性,换得了并发性。

ConcurrentHashMap并不会出现Fail-Fast,因此对ConcurrentHashMap进行迭代不会触发ConcurrentModificationException,弱一致性的迭代器可以容忍并发修改。

PS:因为分段锁并不是基于Map对象的,因此我们无法通过客户端加锁,实现对ConcurrentHashMap的独占访问。

ConcurrentHashMap提供了很多有用的原子操作,比如putIfAbsent(key,value)、remove(key,value),他们是基于CAS的。

分段锁:ConcurrentHashMap通过其DEFAULT_CONCURRENCY_LEVEL指定了其初始的并发等级,也就是16。假设当前的容量大小为16.,那么以10个元素为一个段,每个段由一个特定于该段的锁进行互斥。此外,某些方法如size()和isEmpty()根本不受保护。虽然这允许更大的并发性,但这意味着它们不是强一致的(它们不会反映并发变化的状态)

CopyOnWriteArrayList

它是ArrayList的线程安全版本

仅仅当遍历操作远大于修改操作时,CopyOnWriteArrayList才是最高效的

CopyOnWriteArrayList的设计使用了一种有趣的技术,使它成为线程安全的,而不需要同步。当我们使用任何修改方法时——例如add()或remove()——CopyOnWriteArrayList的整个内容都会被复制到新的内部副本中。

由于这个简单的事实,我们可以以一种安全的方式遍历列表,即使并发修改正在发生。

当我们调用CopyOnWriteArrayList上的iterator()方法时,我们会得到一个由CopyOnWriteArrayList内容的不可变快照备份的迭代器。

当创建迭代器时,其实是创建了一个COWIterator类型的迭代器对象,该迭代器对象持有对当前ArrayList中array字段的引用。与此同时,即使其他线程向列表中添加或删除了某个元素,该修改先加锁,然后拷贝array,然后执行添加/删除操作,并将array的引用更新为指向该拷贝。

:CopyOnWriteArrayList中读不加锁,写加锁!!

这种数据结构的特点使得它在迭代/遍历比修改更频繁的情况下特别有用。如果在我们的场景中添加/修改元素是一种常见的操作,那么CopyOnWriteArrayList将不是一个好的选择——因为额外的拷贝肯定会导致性能变低。

Queue

Queue实现了非阻塞操作,并提供了几种实现,比如:

  • ConcurrentLinkedQueue,只是一个传统的先进先出的队列
  • PriorityQueue,这是一个非并发的优先队列

如果在Queuue上执行add或者offer,那么当队列满的时候,会立即执行返回fasle
如果在Queuue上执行poll,那么当队列为空的时候,会立即执行返回fasle

BlockingQueue

BlockingQueue拓展了Queuue,增加了可阻塞API:

  • 阻塞插入:put
  • 阻塞获取:take

BlockingQueue多用于生产者-消费者模式

2.3 同步工具类

2.3.1 闭锁 Latch

闭锁是一种同步工具类,可以延迟线程的进度直到其到达终止状态。当闭锁到达终态,将不会再改变状态。

闭锁可以用于确保某些活动直到其他活动完成后才继续执行,比如:

  • 确保某个计算在其需要的所有资源被初始化后才继续执行
  • 确保某个服务在其锁依赖的所有服务都启动之后才启动
CountDownLatch

CountDownLatch不可重用,不可逆

FutureTask

FutureTask也可以作为闭锁,它可以处于一下三种状态:

  1. 等待运行
  2. 正在运行
  3. 运行完成

当FutureTask抵达终态,就会永远停留在这个状态上

我们通过CAS + FutureTask,可以实现Go语言中提供的Once机制

public class Once<V> {

    private FutureTask<V> task;

    private Once(FutureTask<V> task) {
        this.task = task;
    }

    public static <V> Once of(Callable<V> callable){
        return new Once<V>(
                new FutureTask<V>(callable)
        );
    }
    public Pair<V,Boolean> get(){
        Pair<V,Boolean> vo = new Pair<>(null,false);
        try {
            vo =new Pair<>(task.get(),true);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return vo;
    }
}

2.3.2 屏障/栅栏

栅栏类似于闭锁,他能阻塞一组线程,直到某个时间发生。

栅栏与闭锁的区别在于:

  • 所有线程必须同时到达栅栏为止,才可以继续执行。
  • 闭锁无法重用,不可逆,而屏障/栅栏是可以重用的,且可逆

闭锁用于等待事件,而栅栏用于等待其他线程

eg:公司组织团建,并约定早上公司集合,那么只有当所有的同事集合后,大巴车才会触发去团建地点

CyclicBarrier

屏障/栅栏允许一组线程彼此等待到达一个共同的屏障点。

CyclicBarrier在包含固定大小的线程的程序中非常有用,这些线程有时必须彼此等待。
CyclicBarrier被称为循环屏障,因为它可以在等待的线程被释放后重新使用。所以在正常的使用中,一旦所有的线程都抵达屏障点,屏障将会被打破,然后它就会重置自己,可以再次被使用。

CyclicBarrier中有一个reset()方法,他会强制重置屏障为其初始状态。如果有线程在屏障点处await,他们将返回一个BrokenBarrierException异常。

因此,reset会导致当前正在等待的线程抛出一个BrokenBarrierException并立即唤醒。reset是当你想要“打破”屏障时才会使用。

Exchanger

Exchanger是一个特殊屏障,它是两个互相等待线程,在屏障点处交互数据,通俗而言,A线程(持有数据D1)与B线程(持有数据D2)在屏障点相互等待,当均抵达屏障点时,则通过exchange方法交换数据,交换后,A线程可以持有D2,而B则可以持有D1。

2.3.3 信号量

信号量用于控制同时地访问某个特定资源的操作数量,或者同时执行某个特定操作的数量。

Semaphore

Semaphore维护了一组虚拟的许可【permit】,许可的初始数量由构造器传入,在执行受限的操作时,需要先获取许可【permit】,操作结束后需要释放许可【permit】。

许可数为1的信号量,可以实现互斥的功能。

信号量是有界、阻塞的。

public class BoundedCache<K,V> {
    
    public ConcurrentHashMap<K,V> cache = new ConcurrentHashMap<>();
    
    public Semaphore semaphore;
    
    public static final int BOUND = 256;

    public BoundedCache() {
        this(BOUND);
    }
    public BoundedCache(Integer max) {
        semaphore = new Semaphore(max);
    }
    public boolean put(K key,V value) throws InterruptedException {
        boolean flag = false;
        try {
            semaphore.acquire();
            flag = true;
            cache.put(key,value);
            return true;
        } finally {
            if (!flag)
                semaphore.release();
        }
    }
    
    public V remove(K key){
        V removed = cache.remove(key);
        if (removed != null){
            semaphore.release();
        }
        return removed;
    }
    
}

3.线程池

任务:一组逻辑工作单元
线程:使任务异步执行的机制

虽然Executor是一个简单的接口,但是它为灵活且强大的异步任务执行框架提供了基础。它提供了一种标准的方法,将任务的提交任务的执行解耦。

Executor基于生产者-消费者模式,提交任务的操作相当于生产者(生产待完成的工作单元),执行任务的线程则相当于消费者(执行完成这些工作单元)。

public class ThreadPoolExecutor extends AbstractExecutorService {
    private volatile int corePoolSize;
    private volatile int maximumPoolSize;
	// 工作队列
    private final BlockingQueue<Runnable> workQueue;
    private final ReentrantLock mainLock = new ReentrantLock();
    // 线程池中的所有工作者线程,访问由mainLock互斥锁定
    private final HashSet<Worker> workers = new HashSet<Worker>();
    // 创建新线程的线程工厂
    private volatile ThreadFactory threadFactory;
    // 当Executor终止时或者工作队列饱和时,饱和策略开始发挥作用
    // Java提供了RejectedExecutionHandler 的各种实现,包括AbortPolicy、CallerRunsPolicy等
    private volatile RejectedExecutionHandler handler;
    private static final RejectedExecutionHandler defaultHandler =
        new AbortPolicy();
}

你可能感兴趣的:(Java,多线程,JVM)