1、ThreadLocal
ThreadLocal提供了线程的局部变量,每个线程都可以通过set()
和get()
来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离~。
1.1、管理Connection
**最典型的是管理数据库的Connection:**当时在学JDBC的时候,为了方便操作写了一个简单数据库连接池,需要数据库连接池的理由也很简单,频繁创建和关闭Connection是一件非常耗费资源的操作,因此需要创建数据库连接池~
ThreadLocal能够实现当前线程的操作都是用同一个Connection,保证了事务!
ThreadLocalMap是ThreadLocal的一个内部类。用Entry类来进行存储
值都是存储到这个Map上的,key是当前ThreadLocal对象!
Thread为每个线程维护了ThreadLocalMap这么一个Map,而ThreadLocalMap的key是LocalThread对象本身,value则是要存储的对象
1.2、ThreadLocal原理总结
- 每个Thread维护着一个ThreadLocalMap的引用
- ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
- 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象
- 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
- ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
1.3、安全发布对象
安全发布对象有几种常见的方式:
-
在静态域中直接初始化 :
public static Person = new Person()
; -
- 静态初始化由JVM在类的初始化阶段就执行了,JVM内部存在着同步机制,致使这种方式我们可以安全发布对象
-
对应的引用保存到volatile或者AtomicReferance引用中
-
- 保证了该对象的引用的可见性和原子性
-
由final修饰
-
- 该对象是不可变的,那么线程就一定是安全的,所以是安全发布~
-
由锁来保护
-
- 发布和使用的时候都需要加锁,这样才保证能够该对象不会逸出
1.4、解决线程安全性的办法
在Java中,我们一般会有下面这么几种办法来实现线程安全问题:
-
无状态(没有共享变量)
-
使用final使该引用变量不可变(如果该对象引用也引用了其他的对象,那么无论是发布或者使用时都需要加锁)
-
加锁(内置锁,显示Lock锁)
-
使用JDK为我们提供的类来实现线程安全(此部分的类就很多了)
-
- 原子性(就比如上面的
count++
操作,可以使用AtomicLong来实现原子性,那么在增加的时候就不会出差错了!) - 容器(ConcurrentHashMap等等…)
- ……
- 原子性(就比如上面的
-
…等等
2、原子性
原子性就是执行某一个操作是不可分割的,
- 比如上面所说的count++
操作,它就不是一个原子性的操作,它是分成了三个步骤的来实现这个操作的~
- JDK中有atomic包提供给我们实现原子性操作~
3、可见性
对于可见性,Java提供了一个关键字:volatile给我们使用~
- 我们可以简单认为:volatile是一种轻量级的同步机制
volatile经典总结:volatile仅仅用来保证该变量对所有线程的可见性,但不保证原子性
我们将其拆开来解释一下:
-
保证该变量对所有线程的可见性
-
- 在多线程的环境下:当这个变量修改时,所有的线程都会知道该变量被修改了,也就是所谓的“可见性”
-
不保证原子性
-
- 修改变量(赋值)实质上是在JVM中分了好几步,而在这几步内(从装载变量到修改),它是不安全的。
使用了volatile修饰的变量保证了三点:
- 一旦你完成写入,任何访问这个字段的线程将会得到最新的值
- 在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。
- volatile可以防止重排序(重排序指的就是:程序执行的时候,CPU、编译器可能会对执行顺序做一些调整,导致执行的顺序并不是从上往下的。从而出现了一些意想不到的效果)。而如果声明了volatile,那么CPU、编译器就会知道这个变量是共享的,不会被缓存在寄存器或者其他不可见的地方。
一般来说,volatile大多用于标志位上(判断操作),满足下面的条件才应该使用volatile修饰变量:
- 修改变量时不依赖变量的当前值(因为volatile是不保证原子性的)
- 该变量不会纳入到不变性条件中(该变量是可变的)
- 在访问变量的时候不需要加锁(加锁就没必要使用volatile这种轻量级同步机制了)
4、不变性
final仅仅是不能修改该变量的引用,但是引用里边的数据是可以改的!
就好像下面这个HashMap,用final修饰了。但是它仅仅保证了该对象引用hashMap变量
所指向是不可变的,但是hashMap内部的数据是可变的,也就是说:可以add,remove等等操作到集合中~~
- 因此,仅仅只能够说明hashMap是一个不可变的对象引用
Map<Person> hashMap = new HashMap<>();
不可变的对象引用在使用的时候还是需要加锁的
- 或者把Person也设计成是一个线程安全的类~
- 因为内部的状态是可变的,不加锁或者Person不是线程安全类,操作都是有危险的!
5、Synchronized
synchronized是Java的一个关键字,它能够将代码块(方法)锁起来
- 它使用起来是非常简单的,只要在代码块(方法)添加关键字synchronized,即可以实现同步的功能~
synchronized是一种互斥锁
- 一次只能允许一个线程进入被锁住的代码块
synchronized是一种内置锁/监视器锁
- Java中每个对象都有一个内置锁(监视器,也可以理解成锁标记),而synchronized就是使用**对象的内置锁(监视器)**来将代码块(方法)锁定的!
5.1、Synchronized有什么用?
- synchronized保证了线程的原子性。(被保护的代码块是一次被执行的,没有任何线程会同时访问)
- synchronized还保证了可见性。(当执行完synchronized之后,修改后的变量对其他的线程是可见的)
Java中的synchronized,通过使用内置锁,来实现对变量的同步操作,进而实现了对变量操作的原子性和其他线程对变量的可见性,从而确保了并发情况下的线程安全。
5.2、Synchronized如何使用
1、修饰普通方法
用的锁是Java3y对象(内置锁)
public class Java3y {
// 修饰普通方法,此时用的锁是Java3y对象(内置锁)
public synchronized void test() {
// 关注公众号Java3y
// doSomething
}
}
2、修饰代码块
用的锁是Java3y对象(内置锁)—>this
public class Java3y {
public void test() {
// 修饰代码块,此时用的锁是Java3y对象(内置锁)--->this
synchronized (this){
// 关注公众号Java3y
// doSomething
}
}
}
3、修饰静态方法
获取到的是类锁(类的字节码文件对象):Java3y.class
public class Java3y {
// 修饰静态方法代码块,静态方法属于类方法,它属于这个类,获取到的锁是属于类的锁(类的字节码文件对象)-->Java3y.class
public synchronized void test() {
// 关注公众号Java3y
// doSomething
}
}
5.3、类锁与对象锁
synchronized修饰静态方法获取的是类锁(类的字节码文件对象),synchronized修饰普通方法或代码块获取的是对象锁。
结果证明:类锁和对象锁是不会冲突的!
6、AQS
-
AQS其实就是一个可以给我们实现锁的框架
-
内部实现的关键是:先进先出的队列、state状态
-
定义了内部类ConditionObject
-
拥有两种线程模式
-
- 独占模式
- 共享模式
-
在LOCK包中的相关锁(常用的有ReentrantLock、 ReadWriteLock)都是基于AQS来构建
-
一般我们叫AQS为同步器
-
juc包中很多可阻塞的类都是基于AQS构建的
-
- AQS可以说是一个给予实现同步锁、同步器的一个框架,很多实现类都在它的的基础上构建的
-
在AQS中实现了对等待队列的默认实现,子类只要重写部分的代码即可实现(大量用到了模板代码)
7、LOCK
- Lock方式来获取锁支持中断、超时不获取、是非阻塞的
- 提高了语义化,哪里加锁,哪里解锁都得写出来
- Lock显式锁可以给我们带来很好的灵活性,但同时我们必须手动释放锁
- 支持Condition条件对象
- 允许多个读线程同时访问共享资源
7.1、ReentrantReadWriteLock
- 读锁不支持条件对象,写锁支持条件对象
- 读锁不能升级为写锁,写锁可以降级为读锁
- 读写锁也有公平和非公平模式
- 读锁支持多个读线程进入临界区,写锁是互斥的
ReentrantReadWriteLock比ReentrantLock锁多了两个内部类(都是Lock实现)来维护读锁和写锁,但是主体还是使用Syn:
7.2、总结
- AQS是ReentrantReadWriteLock和ReentrantLock的基础,因为默认的实现都是在内部类Syn中,而Syn是继承AQS的~
- ReentrantReadWriteLock和ReentrantLock都支持公平和非公平模式,公平模式下会去看FIFO队列线程是否是在队头,而非公平模式下是没有的
- ReentrantReadWriteLock是一个读写锁,如果读的线程比写的线程要多很多的话,那可以考虑使用它。它使用state的变量高16位是读锁,低16位是写锁
- 写锁可以降级为读锁,读锁不能升级为写锁
- 写锁是互斥的,读锁是共享的。
8、线程池
我们可以简单认为:Callable就是Runnable的扩展。
- Runnable没有返回值,不能抛出受检查的异常,而Callable可以!
newFixedThreadPool
一个固定线程数的线程池,它将返回一个corePoolSize和maximumPoolSize相等的线程池。
newCachedThreadPool
非常有弹性的线程池,对于新的任务,如果此时线程池里没有空闲线程,线程池会毫不犹豫的创建一条新的线程去处理这个任务。
SingleThreadExecutor
使用单个worker线程的Executor
8.1、构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- 指定核心线程数量
- 指定最大线程数量
- 允许线程空闲时间
- 时间对象
- 阻塞队列
- 线程工厂
- 任务拒绝策略
线程数量要点:
- 如果运行线程的数量少于核心线程数量,则创建新的线程处理请求
- 如果运行线程的数量大于核心线程数量,小于最大线程数量,则当队列满的时候才创建新的线程
- 如果核心线程数量等于最大线程数量,那么将创建固定大小的连接池
- 如果设置了最大线程数量为无穷,那么允许线程池适合任意的并发数量
线程空闲时间要点:
- 当前线程数大于核心线程数,如果空闲时间已经超过了,那该线程会销毁。
排队策略要点:
- 同步移交:不会放到队列中,而是等待线程执行它。如果当前线程没有执行,很可能会新开一个线程执行。
- 无界限策略:如果核心线程都在工作,该线程会放到队列中。所以线程数不会超过核心线程数
- 有界限策略:可以避免资源耗尽,但是一定程度上减低了吞吐量
拒绝任务策略:
- 直接抛出异常
- 使用调用者的线程来处理(从哪里来回哪里去)
- 直接丢掉这个任务
- 丢掉最老的任务
8.2、线程池关闭
区别:
- 调用shutdown()后,线程池状态立刻变为SHUTDOWN,而调用shutdownNow(),线程池状态立刻变为STOP。
- shutdown()等待任务执行完才中断线程,而shutdownNow()不等任务执行完就中断了线程。
8.3、死锁
发生死锁的原因主要由于:
-
线程之间交错执行
-
- 解决:以固定的顺序加锁
-
执行某方法时就需要持有锁,且不释放
-
- 解决:缩减同步代码块范围,最好仅操作共享变量时才加锁
-
永久等待
-
- 解决:使用
tryLock()
定时锁,超过时限则返回错误信息
- 解决:使用
8.4、常用的辅助类
CountDownLatch—减法计数器
- count初始化CountDownLatch,然后需要等待的线程调用await方法。await方法会一直受阻塞直到count=0。而其它线程完成自己的操作后,调用
countDown()
使计数器count减1。当count减到0时,所有在等待的线程均会被释放 - 说白了就是通过count变量来控制等待,如果count值为0了(其他线程的任务都完成了),那就可以继续执行。
CyclicBarrier—加法计数器
CyclicBarrier允许一组线程互相等待,直到到达某个公共屏障点。叫做cyclic是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用(对比于CountDownLatch是不能重用的)
使用说明:
- CountDownLatch注重的是等待其他线程完成,CyclicBarrier注重的是:当线程到达某个状态后,暂停下来等待其他线程,所有线程均到达以后,继续执行。
Semaphore—信号量
Semaphore(信号量)实际上就是可以控制同时访问的线程个数,它维护了一组**“许可证”**。
- 当调用
acquire()
方法时,会消费一个许可证。如果没有许可证了,会阻塞起来 - 当调用
release()
方法时,会添加一个许可证。 - 这些"许可证"的个数其实就是一个count变量罢了~
总结
Java为我们提供了三个同步工具类:
-
CountDownLatch(闭锁)
-
- 某个线程等待其他线程执行完毕后,它才执行(其他线程等待某个线程执行完毕后,它才执行)
-
CyclicBarrier(栅栏)
-
- 一组线程互相等待至某个状态,这组线程再同时执行。
-
Semaphore(信号量)
-
- 控制一组线程同时执行。
9、Atomic 原子类
count++
并不是原子操作。因为count++
需要经过读取-修改-写入
三个步骤。举个例子:
-
如果某一个时刻:线程A读到count的值是10,线程B读到count的值也是10
-
线程A对
count++
,此时count的值为11 -
线程B对
count++
,此时count的值也是11(因为线程B读到的count是10) -
所以到这里应该知道为啥我们的结果是不确定了吧。
-
基本类型:
-
- AtomicBoolean:布尔型
- AtomicInteger:整型
- AtomicLong:长整型
-
数组:
-
- AtomicIntegerArray:数组里的整型
- AtomicLongArray:数组里的长整型
- AtomicReferenceArray:数组里的引用类型
-
引用类型:
-
- AtomicReference:引用类型
- AtomicStampedReference:带有版本号的引用类型
- AtomicMarkableReference:带有标记位的引用类型
-
对象的属性:
-
- AtomicIntegerFieldUpdater:对象的属性是整型
- AtomicLongFieldUpdater:对象的属性是长整型
- AtomicReferenceFieldUpdater:对象的属性是引用类型
-
JDK8新增DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder
-
- 是对AtomicLong等类的改进。比如LongAccumulator与LongAdder在高并发环境下比AtomicLong更高效。
9.1、ABA问题
使用CAS有个缺点就是ABA的问题,什么是ABA问题呢?首先我用文字描述一下:
- 现在我有一个变量
count=10
,现在有三个线程,分别为A、B、C - 线程A和线程C同时读到count变量,所以线程A和线程C的内存值和预期值都为10
- 此时线程A使用CAS将count值修改成100
- 修改完后,就在这时,线程B进来了,读取得到count的值为100(内存值和预期值都是100),将count值修改成10
- 线程C拿到执行权,发现内存值是10,预期值也是10,将count值修改成11
上面的操作都可以正常执行完的,这样会发生什么问题呢??线程C无法得知线程A和线程B修改过的count值,这样是有风险的。
9.2、解决ABA问题
要解决ABA的问题,我们可以使用JDK给我们提供的AtomicStampedReference和AtomicMarkableReference类。
AtomicStampedReference:简单来说就是在给为这个对象提供了一个版本,并且这个版本如果被修改了,是自动更新的。