# Java线程安全初识


什么是线程安全?

我的理解是由于程序使用多线程的方式运行，导致程序无法正确的得出我们期望的结果。

什么会导致线程安全问题，主要是可见性、原子性、有序性问题。详细可见：[Java并发编程-可见性、原子性、有序性问题引入](https://leanote.baofeidyz.com/blog/post/baofeidyz/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B-%E5%8F%AF%E8%A7%81%E6%80%A7%E3%80%81%E5%8E%9F%E5%AD%90%E6%80%A7%E3%80%81%E6%9C%89%E5%BA%8F%E6%80%A7%E9%97%AE%E9%A2%98%E5%BC%95%E5%85%A5)

# 线程安全的实现方式
## 可见性和有序性问题
主要是通过volatile、Happens-Before规则以及final关键字解决。详细可见[Java并发编程-如何解决可见性和有序性问题](https://leanote.baofeidyz.com/blog/post/baofeidyz/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B-%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E5%8F%AF%E8%A7%81%E6%95%88%E3%80%81%E5%8E%9F%E5%AD%90%E6%80%A7%E3%80%81%E6%9C%89%E5%BA%8F%E6%80%A7%E9%97%AE%E9%A2%98)
## 原子性问题
### 互斥锁
#### sychronized

# 死锁问题
产生死锁需要同时满足四个条件，即：

 1. 互斥：共享资源X和Y只能被一个线程占用
 2. 占有且等待：线程1已经取得共享资源x，在等待共享资源y时，不释放共享资源x
 3. 不可抢占：其他线程不可抢占线程1占有的资源
 4. 循环等待：线程1等待线程2占有的资源，线程2等待线程1占有的资源

解决死锁问题，就是解决上面四个条件的任一一个。

1. 对于“占用且等待”条件，我们可一次性申请所有资源。
2. 对于“不可抢占”条件，占用部分资源的线程进一步申请其他资源时，如果申请不到，可主动释放它占有的资源。
3. 对于“循环等待”条件，按序申请，即资源是有线性顺序的，申请的时候可以先申请资源序号小的，再申请序号大的。

> 针对于“不可抢占”条件，sychronized申请不到资源时会进入阻塞状态，无法释放已占有的资源，我们应使用JUC提供的Lock解决

# sychronized关键字的使用场景、作用范围
## 实现方法
利用Monitor，在使用sychronized关键字修饰的代码块，编译后自动生成相关加锁和解锁的代码，但仅支持一个条件变量，通过monitorenter和monitorexit实现。
## 修饰非静态方法
默认对当前实例对象`this`加锁
## 修饰静态方法
默认对当前类的Class对象加锁
# 管程Monitor

![极客时间-Java并发编程实战-javaMESA管程模型](https://cdn.hashnode.com/res/hashnode/image/upload/v1654188752202/W9I1rvhtL.png)

# 为什么局部变量是线程安全的？

## 调用栈和栈帧
CPU支持栈结构，这个栈与方法调用相关，被称为调用栈。
每个方法在调用栈中都有自己的独立空间，被称为栈帧，每个栈帧都有对应方法需要的参数和返回地址。

当调用方法时，会创建新的栈帧，并压入调用栈；当方法返回时，对应的栈帧会自动弹出，即栈帧和方法是同生共死的。
![极客时间-Java并发编程实现-调用栈结构](https://cdn.hashnode.com/res/hashnode/image/upload/v1654188753560/ej3V1Elq-.png)

## 局部变量存储位置
局部变量放到了调用栈里，如下图所示：
![极客时间-Java并发编程实战-保护局部变量的调用栈结构](https://cdn.hashnode.com/res/hashnode/image/upload/v1654188755514/nFfLzV04cn.png)

## 调用栈与线程
每个线程都有自己独立的调用栈
![极客时间-Java并发编程实战-线程和调用栈的关系图](https://cdn.hashnode.com/res/hashnode/image/upload/v1654188756916/-eh-EMm8y.png)

# JUC中锁的分类和用途
## 公平锁、非公平锁
公平锁，多个线程按照申请锁的顺序来获取锁。会判断当前线程是否处于等待队列的头部，即链表的头部，如果是的话就直接获取锁。
非公平锁，没有顺序。不会判断当前线程处于等待队列的具体位置。CAS操作成功则认定为获取到锁。
synchronized是非公平锁，ReentrantLock可通过构造函数决定是公平锁还是非公平锁。
## 可重入锁
线程可重复获取同一把锁。
ReentrantLock在获取锁时，判断当前线程是否是之前已获取锁的线程，如果是，则直接返回true表示锁获取成功。
## 互斥锁（独享锁）、读写锁（共享锁）
互斥锁（独享锁）指锁一次只能被一个线程持有，读写锁（共享锁）指该锁可被多个线程持有。
synchronized和ReentrantLock都是互斥锁（独享锁），ReadWriteLock的读锁是共享锁，写锁是独占锁。

## 乐观锁、悲观锁
乐观锁在更新数据时会不断尝试更新，认为不加锁的并发操作是没问题的。基于CAS实现。
悲观锁认为对一个共享变量的并发操作，这个共享变量是一定会发生修改的，采取加锁方式。

乐观锁适合读操作远远大于写操作的情景，悲观锁适合写操作非常多的场景。
## 分段锁
对于ConcurrentHashMap来说，在put操作时，通过hashcode判断将要put的元素需要放到哪个分段，然后对分段进行加锁。当put操作不同的分段时，就可以实现并发操作。
## 无锁、偏向锁、轻量级锁、重量级锁
![](https://cdn.hashnode.com/res/hashnode/image/upload/v1654188758233/g-8pdUPaf.png)
自1.6以后，java对synchronized进行了优化，当第一个线程获得了锁，锁状态变更新为偏向锁状态。

### 偏向锁
获取锁：当之前的线程再次获取锁时，无需再执行获取锁的过程。
锁撤销：原持有偏向锁的线程状态是非活动状态时，偏向锁撤销，锁状态更新为无锁状态。
### 轻量级锁
获取锁：如果每次申请锁的线程都是不相同的，则锁会升级为轻量级锁，指向栈中锁记录的指针。轻量级锁适用于线程交替执行同步块的场景。
释放锁：通过CAS操作，尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word，如果成功则完成解锁操作。如果失败则表明有其他线程获取该锁，此时锁膨胀为重量级锁。释放锁的同时，唤醒被挂起的线程。
### 重量级
当多个线程同时竞争锁，则轻量级锁会膨胀为重量级锁。指向互斥量的指针。未获取到锁的线程会阻塞。
## 自旋锁
循环检测锁标志位

## 可中断锁
Lock
```java
// 支持中断的 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
类似于[CopyOnWriteArrayList](#CopyOnWriteArrayList)
### ConcurrentSkipListSet
类似于[ConcurrentSkipListMap](#ConcurrentSkipListMap)
## Queue
### 单端阻塞队列
#### ArrayBlockingQueue
使用数组实现
#### LinkedBlockingQueue
使用链表实现
#### SynchronousQueue
不持有队列，入队操作必须要等到消费者线程的出队操作
#### LinkedTransferQueue
链表实现，入队操作必须要等到消费者线程的出队操作
#### PriorityBlockingQueue
支持按照优先级出队
#### DelayQueue
支持延时出队
### 双端阻塞队列
#### LinkedBlockingDeque
### 单端非阻塞队列
#### ConcurrentLinkedQueue
### 双端非阻塞队列
#### ConcurrentLinkedDeque
## 原子类
### 原子化的基本数据类型
#### AtomicBoolean
#### AtomicInteger
#### AtomicLong
### 原子化的对象引用类型
#### AtomicReference
#### AtomicStampedReference
#### AtomicMarkableReference
# 用锁的最佳实践
1. 永远只在更新对象的成员变量时加锁
2. 永远只在访问可变的成员变量时加锁
3. 永远不在调用其他对象的方法时加锁
