什么是线程安全?
我的理解是由于程序使用多线程的方式运行,导致程序无法正确的得出我们期望的结果。
什么会导致线程安全问题,主要是可见性、原子性、有序性问题。详细可见:Java并发编程-可见性、原子性、有序性问题引入
线程安全的实现方式
可见性和有序性问题
主要是通过volatile、Happens-Before规则以及final关键字解决。详细可见Java并发编程-如何解决可见性和有序性问题
原子性问题
互斥锁
sychronized
死锁问题
产生死锁需要同时满足四个条件,即:
- 互斥:共享资源X和Y只能被一个线程占用
- 占有且等待:线程1已经取得共享资源x,在等待共享资源y时,不释放共享资源x
- 不可抢占:其他线程不可抢占线程1占有的资源
- 循环等待:线程1等待线程2占有的资源,线程2等待线程1占有的资源
解决死锁问题,就是解决上面四个条件的任一一个。
- 对于“占用且等待”条件,我们可一次性申请所有资源。
- 对于“不可抢占”条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可主动释放它占有的资源。
- 对于“循环等待”条件,按序申请,即资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请序号大的。
针对于“不可抢占”条件,sychronized申请不到资源时会进入阻塞状态,无法释放已占有的资源,我们应使用JUC提供的Lock解决
sychronized关键字的使用场景、作用范围
实现方法
利用Monitor,在使用sychronized关键字修饰的代码块,编译后自动生成相关加锁和解锁的代码,但仅支持一个条件变量,通过monitorenter和monitorexit实现。
修饰非静态方法
默认对当前实例对象this
加锁
修饰静态方法
默认对当前类的Class对象加锁
管程Monitor
为什么局部变量是线程安全的?
调用栈和栈帧
CPU支持栈结构,这个栈与方法调用相关,被称为调用栈。 每个方法在调用栈中都有自己的独立空间,被称为栈帧,每个栈帧都有对应方法需要的参数和返回地址。
当调用方法时,会创建新的栈帧,并压入调用栈;当方法返回时,对应的栈帧会自动弹出,即栈帧和方法是同生共死的。
局部变量存储位置
局部变量放到了调用栈里,如下图所示:
调用栈与线程
每个线程都有自己独立的调用栈
JUC中锁的分类和用途
公平锁、非公平锁
公平锁,多个线程按照申请锁的顺序来获取锁。会判断当前线程是否处于等待队列的头部,即链表的头部,如果是的话就直接获取锁。 非公平锁,没有顺序。不会判断当前线程处于等待队列的具体位置。CAS操作成功则认定为获取到锁。 synchronized是非公平锁,ReentrantLock可通过构造函数决定是公平锁还是非公平锁。
可重入锁
线程可重复获取同一把锁。 ReentrantLock在获取锁时,判断当前线程是否是之前已获取锁的线程,如果是,则直接返回true表示锁获取成功。
互斥锁(独享锁)、读写锁(共享锁)
互斥锁(独享锁)指锁一次只能被一个线程持有,读写锁(共享锁)指该锁可被多个线程持有。 synchronized和ReentrantLock都是互斥锁(独享锁),ReadWriteLock的读锁是共享锁,写锁是独占锁。
乐观锁、悲观锁
乐观锁在更新数据时会不断尝试更新,认为不加锁的并发操作是没问题的。基于CAS实现。 悲观锁认为对一个共享变量的并发操作,这个共享变量是一定会发生修改的,采取加锁方式。
乐观锁适合读操作远远大于写操作的情景,悲观锁适合写操作非常多的场景。
分段锁
对于ConcurrentHashMap来说,在put操作时,通过hashcode判断将要put的元素需要放到哪个分段,然后对分段进行加锁。当put操作不同的分段时,就可以实现并发操作。
无锁、偏向锁、轻量级锁、重量级锁
自1.6以后,java对synchronized进行了优化,当第一个线程获得了锁,锁状态变更新为偏向锁状态。
偏向锁
获取锁:当之前的线程再次获取锁时,无需再执行获取锁的过程。 锁撤销:原持有偏向锁的线程状态是非活动状态时,偏向锁撤销,锁状态更新为无锁状态。
轻量级锁
获取锁:如果每次申请锁的线程都是不相同的,则锁会升级为轻量级锁,指向栈中锁记录的指针。轻量级锁适用于线程交替执行同步块的场景。 释放锁:通过CAS操作,尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word,如果成功则完成解锁操作。如果失败则表明有其他线程获取该锁,此时锁膨胀为重量级锁。释放锁的同时,唤醒被挂起的线程。
重量级
当多个线程同时竞争锁,则轻量级锁会膨胀为重量级锁。指向互斥量的指针。未获取到锁的线程会阻塞。
自旋锁
循环检测锁标志位
可中断锁
Lock
// 支持中断的 API
void lockInterruptibly()
throws InterruptedException;
// 支持超时的 API
boolean tryLock(long time, TimeUnit unit)
throws InterruptedException;
// 支持非阻塞获取锁的 API
boolean tryLock();
线程安全的集合
List
CopyOnWriteArrayList
在写操作的时候,会将共享变量复制一份出来,当写操作完成以后,再修改共享变量的内存引用地址。 不能使用迭代器删除数据,因为操作的是一个副本,不会修改到实际的共享变量。
Map
ConcurrentHashMap
- KEY和VALUE不允许为空
- KEY是无序的
ConcurrentSkipListMap
- KEY和VALUE不允许为空
- KEY是有序的
- 实现方案是使用SkipList(跳表)数据结构
Set
CopyOnWriteArraySet
类似于CopyOnWriteArrayListConcurrentSkipListSet
类似于ConcurrentSkipListMapQueue
单端阻塞队列
ArrayBlockingQueue
使用数组实现LinkedBlockingQueue
使用链表实现SynchronousQueue
不持有队列,入队操作必须要等到消费者线程的出队操作LinkedTransferQueue
链表实现,入队操作必须要等到消费者线程的出队操作PriorityBlockingQueue
支持按照优先级出队DelayQueue
支持延时出队双端阻塞队列
LinkedBlockingDeque
单端非阻塞队列
ConcurrentLinkedQueue
双端非阻塞队列
ConcurrentLinkedDeque
原子类
原子化的基本数据类型
AtomicBoolean
AtomicInteger
AtomicLong
原子化的对象引用类型
AtomicReference
AtomicStampedReference
AtomicMarkableReference
用锁的最佳实践
- 永远只在更新对象的成员变量时加锁
- 永远只在访问可变的成员变量时加锁
- 永远不在调用其他对象的方法时加锁