线程安全的机制
线程表示一条单独的执行流,每个线程有自己的执行计数器,有自己的栈,但可以共享内存,共享内存是实现线程协作的基础,但共享内存有两个问题:竞态条件和内存可见性。
synchronized
synchronized是一个关键字,既可以解决竞态问题,也可以解决内存可见性问题
synchronized保护的是对象,而不是代码,只有对同一个对象的synchronized方法调用,synchronized才能保证它们被顺序调用。对于实例方法,这个对象是this;对于静态方法,这个对象是类对象;对于代码块,需要指定哪个对象
synchronized不能尝试获取锁,也不能响应中断,还可能会死锁。相比于显式锁,synchronized简单易用,JVM也在不断优化它的实现
显式锁
显式锁是相当于synchronized隐式锁而言的,它可以实现synchronized同样的功能,但需要程序自己创建锁,调用锁相关接口,主要接口是Lock,主要实现类是ReentrantLock。
相比synchronized,显式锁支持以非阻塞方式获取锁,可以响应中断,可以限时,可以指定公平性,可以解决死锁问题,所以更加的灵活。
在一些读多写少、读操作可以完全并行的场景中,可以使用读写锁以提高并发度,读写锁的接口是ReadWriteLock,实现类是ReentrantReadWriteLock。
volatile
synchronized和显式锁都是锁,使用锁可以实现安全,但使用锁是有成本的,获取不到锁的线程还需要等待,会有线程的上下文切换开销等。
如果共享的对象只有一个,操作也只是进行最简单的get/set操作,set也不依赖于之前的值,那就不存在竞态条件问题,而只有内存可见性问题,这时,在变量的声明上加上关键字volatile就可以了。
volatile和synchronized的区别:
- volatile 仅能使用在变量级别; synchronized 则可以使用在实例方法、静态方法和代码块。
- volatile 仅能实现变量的修改可见性,并不能保证原子性;synchronized 则可以保证变量的修改可见性和原子性
- volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
- volatile 标记的变量不会被编译器优化; synchronized 标记的变量可以被编译器优化
原子变量和CAS
使用volatile,set的新值不能依赖于旧值,但很多时候,set的新值与原来的值有关,同时也不一定需要锁,这个时候就可以考虑原子变量。它们包含了一些以原子方式实现组合操作的方法。
原子变量的基础是CAS,一般的计算机系统都在硬件层次上直接支持CAS指令。相对于synchronized,它是乐观的,而synchronized是悲观的。
写时复制
之所有会有线程安全的问题,是因为多个线程并发读写同一个对象,如果每个线程读写的对象都是不同的,或者如果共享访问的对象是只读的,不能修改,那就不存在线程安全问题了。
写时复制就是将共享访问的对象变为只读的,写的时候再使用锁,保证只有一个线程写,写的线程不是直接修改原对象,而是新创建一个对象,对该对象修改完毕后,再原子性地修改共享访问的变量,让它指向新的对象。
ThreadLocal
ThreadLocal让每个线程对同一变量,都有自己的独有副本。每个线程实际访问的对象都是自己的,自然也就不存在线程安全问题。
线程的协作机制
常见的协作场景:生产者/消费者协作模式、主从协作模式、同时开始、集合点等
wait/notify
wait/notify与synchronized配合一起使用,是线程的基本协作机制。
每个对象都有一把锁和两个等待队列,一个是锁等待队列,放的是等待获取锁的线程;另一个是条件等待队列,放的是等待条件的线程。wait将自己加入条件等待队列,notify从条件队列上移除一个线程并唤醒,notifyAll移除所有线程并唤醒。
wait/notify方法只能在synchronized代码块内被调用,调用wait时,线程会释放对象锁,被notify/notifyAll唤醒后,需要重新竞争锁,获取到锁后才会从wait调用中返回。
显式条件
显式条件和显式锁配合使用,与wait/notify相比,可以支持多个条件队列,代码更为易读,效率更高。
线程中断
线程中断并不是强迫终止一个线程,它是一种协作机制,是给线程传递一个取消信号,但是由线程来决定如何以及何时退出,线程在不同状态和IO操作时对中断有不同的反应。
协作工具类
信号量Semaphore用于限制对资源的并发访问数
倒计时门闩CountDownLatch主要用于不同角色线程间的同步,比如:同时开始多个线程;主线程等待多个从线程的结果
循环栅栏CyclicBarrier用于同一角色线程间的协调一致,所有线程在到达栅栏后都需要等待其他线程,等所有线程都到达后再一起通过,它是可以循环的。
阻塞队列
阻塞队列封装了锁和条件,常用于生产者/消费者协作模式,只需要调用队列的入队/出队方法就可以了
- 无锁非阻塞并发队列:ConcurrentLinkedQueue和ConcurrentLinkedDeque
- 普通阻塞队列:基于数组的ArrayBlockingQueue,基于链表的LinkedBlockingQueue和LinkedBlockingDeque
- 优先级阻塞队列:PriorityBlockingQueue
- 延时阻塞队列:DelayQueue
- 其他阻塞队列:SynchronousQueue和LinkedTransferQueue
Future/FutureTask
Future是一个接口,主要实现类是FutureTask。
Future封装了调用线程和执行线程关于执行状态和结果的同步,对于调用线程而言,它只需要通过Future就可以查询异步任务的状态、获取最终结果、取消任务等。
在常见的主从协作模式中,主线程往往需要获取子线程的结果,就可以使用Future
容器类
线程安全的容器有两类:同步容器;并发容器
同步容器
Collections类中有一些静态方法,可以基于普通容器返回线程安全的同步容器
public static <T> Collection<T> synchronizedCollection(Collection<T> c);
public static <T> List<T> synchronizedList(List<T> list);
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
它们是给所有容器方法都加上synchronized来实现安全的。
同步容器的性能比较低,这里的线程安全针对的是容器对象,指的是当多个线程并发访问同一个容器对象时,不需要额外的同步操作。
并发容器
写时复制的List和Set
CopyOnWriteArrayList实现了List接口,它的用法与其他的List基本是一样的。CopyOnWirteArrayList的内部也是 一个数组,但这个数组是以原子方式被整体更新的。每次修改操作,都会新建一个数组,复制原数组的内容到新数组,在新数组上进行需要的修改,然后以原子方式设置内部的数组引用。
CopyOnWriteArraySet实现了Set接口,不包含重复的元素。 内部是通过CopyOnWriteArrayList实现的
CopyOnWriteArrayList和CopyOnWriteArraySet适用于读远多于写、集合不太大的场景。它们是以优化读操作为目标的,读不需要同步,性能很高。
ConcurrentHashMap
ConcurrentHashMap是HashMap的并发版本,通过细粒度锁和其他技术实现了高并发,读操作完全并行,写操作支持一定程度的并行,以原子方式支持一些复合操作,迭代不用加锁。
基于跳表的Map和Set
Java并发包中与TreeMap/TreeSet对应的并发版本是ConcurrentSkipListMap和ConcurrentSkipListSet。
TreeSet是基于TreeMap实现的,类似地,ConcurrentSkipListSet也是以及ConcurrentSkipListMap实现的。
ConcurrentSkipListMap是基于SkipList实现的,SkipList称为跳跃表或跳表,是一种数据结构。
并发队列
- 无锁非阻塞并发队列:ConcurrentLinkedQueue和ConcurrentLinkedDeque
- 普通阻塞队列:基于数组的ArrayBlockingQueue,基于链表的LinkedBlockingQueue和LinkedBlockingDeque
- 优先级阻塞队列:PriorityBlockingQueue
- 延时阻塞队列:DelayQueue
- 其他阻塞队列:SynchronousQueue和LinkedTransferQueue
无锁非阻塞是指,这些队列不实用锁,所有操作总是立即执行,主要通过循环CAS实现并发安全;
阻塞队列是指,这些队列使用锁和条件,很多操作都需要先获取锁或满足特点条件,获取不到锁或等待 条件时,会等待(阻塞),直到获取到锁或条件满足