一、各种锁的概念
1.乐观锁与悲观锁
乐观锁
每次读取数据的时候都认为数据没有被修改过,读取数据的时候不加锁 , 但是在更新的时候会去对比一下原来的值,看有没有被别人更改过。适用于读多写少的场景
juc中atomic 使用的就是乐观锁,即CAS
悲观锁
每次读取数据的时候都认为数据已经被修改过,读取数据的时候也会加锁。别人想要拿到数据就要等待锁。适合写操作比较多的场景
synchronized 实现也是悲观锁
2.共享锁/独占锁
独占锁
独占锁指锁一次只能被一个线程占有
ReentrantLock 就是独占锁
共享锁
锁可以被多个线程持有
ReadWriteLock 中 Read 共享, write独占
3.可重入锁
如果当前线程持有obj对象的锁,而内部代码块中还需要获取obj锁,直接放行的方式就是可重入锁。
synchronized 和 ReentrantLock都是可重入锁
实现方式为为锁对象设置一个计数器和占有他的线程,每次获取锁的时候计数器加一,释放锁的时候计数器减一,当计数器为0的时候释放锁。
4.公平锁和非公平锁
公平锁
所有尝试获取锁的线程都会加入锁的等待队列,每次唤醒队列中的第一个线程。
常见于AQS
非公平锁
抢占锁时候判断锁是否被占有,没被占有直接抢占锁,如果被占有就加入等待队列
ReentrantLock使用非公平锁
非公平锁的性能比公平锁好,因为线程有机会不阻塞直接获得锁,公平锁需要唤醒阻塞队列中的线程,所以公平锁的CPU开销会比较大。
5.无锁、偏向锁、轻量级锁、重量级锁
偏向锁
仅有一个线程在使用锁,没有竞争线程,就是偏向锁,一旦有其他线程产生竞争,锁升级为轻量级锁
轻量级锁
当前有两个线程,一个持有锁,另一个会自旋等待锁;当再有一个线程(3个以上)同时竞争锁的时候,锁升级为重量级锁
重量级锁
其他线程试图获取锁的时候都会进入阻塞队列,只有当前线程释放锁的时候才会唤醒线程。
6.自旋锁
获取不到锁就一直循环试图获取锁。
7.互斥锁和读写锁
synchronized、ReentrantLock属于互斥锁;(都是独占锁)
ReadWriteLock 属于读写锁(读为共享锁,写为独占锁)
二、线程的实现方式
Java 中线程的实现方式主要有四种,利用Spring 注解@Asyn 也可以实现,这里不做详细讨论。它们分别是:
- 继承Thread类,重写run 方法
- 实现Runnable 接口,重写run 方法
- 实现Callable 接口,重写call()方法,创建FutureTask 对象,指定Callable 对象
- 利用线程池
1. 继承Thread 类
class TestThread extends Thread {
@Override
public void run() {
// do something ....
}
}
2. 实现Runnable 接口
class TestThread implements Runnable {
@Override
public void run() {
// do something ....
}
}
3. 实现Callable 接口,配合FutureTask
// 创建Callable 对象
Callable<String> stringCallable = () -> {
System.out.println("do something");
Thread.sleep(2000);
return "ok";
};
// 根据Callable 对象创建FutureTask 对象
FutureTask<String> stringFutureTask = new FutureTask<>(stringCallable);
// 创建线程并启动
new Thread(stringFutureTask).start();
// 阻塞当前线程直到FutureTask返回处理结果
String result = stringFutureTask.get();
4. 使用线程池
三、线程池详解
1. 为什么使用线程池
在开发过程中不建议使用直接使用继承Thread类或者直接实现Runnable 的方式来管理线程,当每接收一个请求创建一个线程,线程执行完毕再销毁的这种模式会引发如下事实:
- 频繁创建线程耗费资源
- 线程上下文切换问题
- 可能引发资源耗尽的风险
使用线程池后,优点如下:
- 加快响应时间
- 增加吞吐量
但是线程池使用不当也会有一些风险,比如:
- 死锁:线程池中的线程持有其他线程的锁
- 资源不足:假如不断向无限线程池中添加任务就会导致资源不足。
- 并发错误:wait 和 notify 使用不当
- 请求过载:QPS 极高的情况下,不可能为每个请求都分配一个线程,分配可能导致请求过载。
2. 线程池核心参数与工作原理
核心参数
定义:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
其中各个参数的含义如下:
- corePoolSize:核心线程数目,核心线程没有最长存活时间,及时线程终止也不会被回收。
- maximumPoolSize:线程池内 (核心线程+非核心线程)的数量
- keepAliveTime:非核心线程的最大存活时间
- unit:keepAliveTime的单位
- workQueue:等待执行的任务队列
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排
- LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
- SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
- PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
- threadFactory:线程工厂类
- handler:拒绝策略
阻塞队列
- ArrayBlockingQueue; //基于数组的先进先出队列,此队列创建时必须指定大小;
- LinkedBlockingQueue; //基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
- SynchronousQueue; //这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
- SynchronousQueue ; // 这个队列不持有任务,而是会直接递交给线程池。如果没有空闲工作线程,则提交失败。
拒绝策略
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
添加线程流程
- 提交任务给线程池
- 判断核心线程数是否已满,如果没有满直接创建线程。
- 如果核心线程已经满了,判断阻塞队列是否已经满了,如果阻塞队列没满,创建线程假如阻塞队列中。
- 如果此时阻塞队列也满了,判断线程池中所有的线程数目是否达到了线程池最大数目,如果达到最大数目执行拒绝策略。
- 如果没有超过最大线程数,创建线程并加入线程池,当有某线程的空闲时间超过keepAliveTime 的时候,该线程会销毁。
3. Java 中实现的线程池
Alibaba 开发手册明确规定不允许使用java 自带的线程池,现在对四种线程池做详细总结:
- Executors. newFixedThreadPool
- Executors. newCachedThreadPool
- Executors. newSingleThreadExecutor
- Executors. newScheduledThreadPool
newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0,
Integer.MAX_VALUE,
60L,
TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
关注点:核心线程数为0,最大线程数为最大值,阻塞队列选用SynchronousQueue。
缺点:来了任务就执行,而且可容纳的最大线程数很大,过期时间很长,很容易OOM。
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads,
nThreads,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
关注点:核心线程数为n,最大线程数为n,阻塞队列选用LinkedBlockingQueue。
缺点:同时执行n个线程,但是队列采用LinkedBlockingQueue,大小默认为Integer最大值,所以和CachedPool一样容易爆内存。
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(
new ThreadPoolExecutor( 1,
1,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
关注点:核心线程数为1,最大线程数为1,阻塞队列选用LinkedBlockingQueue。
缺点:一个一个任务顺序执行,阻塞队列还是无限的(Integer最大值约等于无限),容易OOM。
newScheduledThreadPool
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize,
Integer.MAX_VALUE,
0,
NANOSECONDS,
new DelayedWorkQueue());
}
关注点:核心线程数为corePoolSize,最大线程数为最大值,阻塞队列选用DelayedWorkQueue。
缺点:允许最大线程数为Integer最大值,大量任务到来的时候创建这么多线程完全是不可取的。可能导致OOM。
4. 监控线程池
想要监控线程池状态有两种方案可以实现
直接使用线程池参数
- taskCount:线程池任务总量。
- completedTaskCount:已完成任务数量。
- largestPoolSize:从创建开始到现在,线程池中最多同时持有多少个线程。
- getPoolSize: 此时线程池的线程数量。
扩展线程池
继承线程池并重写下列方法:
- beforeExecute
- afterExecute
- terminated
四、线程基本概念与方法
1. 线程的基本概念
线程的状态
- 新建:新建线程对象,还没调用start
- 就绪:随时可以获得CPU执行,只能转到运行状态。
- 阻塞:运行状态通过wait 或者sleep进入。被唤醒后加入就绪态
- 运行:就绪状态通过获得锁进入
- 死亡:线程run 结束终止。
2. 多线程常用方法
| 类 | 方法名 | 解释 |
|---|---|---|
| Object | wait | 等待Object 锁 |
| Object | notify | 通知所有等待该锁的线程中的其中一个 |
| Object | notifyAll | 通知所有等待该锁的线程 |
| Thread | join | A线程中调用B线程的Join 方法后,A会立即阻塞,等待线程B执行完成后再唤醒进入就绪状态。 |
| Thread | yield | 线程执行yield 方法后会放弃时间片,进入就绪队列,随时准备再次执行。但是不会释放锁。 |
| Thread | interrupt | 中断线程,仅仅是给线程状态设置为中断,并不打断线程的运行 |
| Thread | isInterrupted | 判断线程状态是否为中断状态 |
| Thread | setDaemon | 设置当前线程为后台线程 |
| Thread | sleep | 阻塞当前线程一段时间 |
| Thread | suspend | 挂起线程,不释放锁,容易死锁 |
| Thread | stop | 终止线程,释放锁。 |
五、线程通信方式
在Java 中实现多线程通信有多种方式(这里先使用两种,如果有其他想法可以评论下)
- 使用synchronized 配合wait 和 notifyAll 使用
- 使用ReentrantLock 配合 Condition 使用(JUC 包)
- CountDownLatch 实现线程通信
这里通过一个示例来展示每种方式的用法, 新建两个线程交替打印1-100之间的数字,打印完以后主线程输出打印完毕。
1. 使用Synchronized
使用Synchronized 来配合wait 和 notifyAll 来使用,其中
- synchronized 来获得一个对象的锁
- 使用notifyAll 通知其他等待此锁的对象可以竞争该锁。
- 使用wait 表示等待该对象释放锁。
题解代码:
class TestThread implements Runnable {
private static Object lock = new Object();
// JUC 包中的类,初始设置一个值,使用countDownLatch.await()的时候会阻塞当前线程,直到初始值减小到0为止
public static CountDownLatch countDownLatch = new CountDownLatch(2);
private static volatile int increNum = 1;
private static volatile int currentThreadNum = 0;
private static final int MAX_NUM = 100;
private volatile int threadNum;
public TestThread(int threadNum) {
this.threadNum = threadNum;
}
@Override
public void run() {
try {
while (increNum < MAX_NUM) {
synchronized (lock) { // 获得锁
while (this.threadNum == currentThreadNum) {
lock.wait(); // 交替打印实现,释放锁
}
System.out.println(String.format("Thread %d : %d" , this.threadNum , increNum));
++ increNum;
currentThreadNum = this.threadNum;
lock.notifyAll(); // 通知所有线程可以抢锁了
}
}
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
new Thread(new TestThread(1)).start();
new Thread(new TestThread(2)).start();
TestThread.countDownLatch.await();
System.out.println("print over");
}
}
2. 使用ReentrantLock
使用ReentrantLock 配合 Condition 实现线程通信,相关方法:
- ReentrantLock 可以看成一把锁,谁获得锁谁可以访问资源。
- Condition 也是一把锁,可以看成和ReentrantLock 相关联的锁,获得ReentrantLock 锁的线程可以操作Condition 这把锁,用来实现与其他线程的交互功能。
初始化
ReentrantLock lock = new ReentrantLock(); Condition lockCondition = lock.newCondition();方法:
lock.lock(); // 尝试获得锁,获取不到阻塞当前线程,直到获得锁 lock.tryLock(); // 尝试获得锁一次,返回获取成功或者失败 lock.unlock(); // 释放锁,要在finally 中使用 lockCondition.signalAll(); // 通知所有等待此condition的线程 lockCondition.signal(); // 通知其他等待此condition 的一个线程,发出信号 lockCondition.await(); // 等待其他线程对此Condition 做signal 操作,否则阻塞当前线程,可以设置最长等待时间。
public class Main {
public static void main(String[] args) throws InterruptedException {
new Thread(new TestThread(1)).start();
new Thread(new TestThread(2)).start();
while (true) {
try {
TestThread.lock.lock();
TestThread.lockCondition.await();
System.out.println("print over");
break;
} finally {
TestThread.lock.unlock();
}
}
}
}
class TestThread implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static Condition lockCondition = lock.newCondition();
private static volatile int increNum = 1;
private static volatile int currentThreadNum = 0;
private static final int MAX_NUM = 100;
private volatile int threadNum;
public TestThread(int threadNum) {
this.threadNum = threadNum;
}
@Override
public void run() {
while (increNum < MAX_NUM) {
try {
lock.lock();
while (this.threadNum != currentThreadNum) {
System.out.println(String.format("Thread %d : %d", this.threadNum, increNum));
++increNum;
currentThreadNum = this.threadNum;
}
if (increNum >= MAX_NUM) {
lockCondition.signalAll();
break;
}
} finally {
lock.unlock();
}
}
}
}
六、理论与源码解析
1. AQS
AQS是ReentrantLock的核心组件,内部设置state变量,持有锁线程,等待队列等信息。每次加锁给state+1(前提判断持有线程为当前线程),如果不是当前线程抢占锁则加入等待队列。state为0的时候再从队列中取出线程执行。
2. CAS
原理简介
CAS 包含如下三个重要属性
- 内存地址V
- 旧的预期值 A
- 新的预期值 B
CAS 每次进行更新操作时,先从比较内存地址V中的数据和A是否相等,如果相等则更新内存地址V的值为B,否则什么都不做。
流程如下:
- 从V中获取值A
- 根据A计算目标值B
- 原子操作-----判断V中的值是否为A,如果为A则将V中的值改为B
存在的问题
- 自旋时间长,开销比较大
- 只能保证一个变量的原子操作
- 会出现ABA问题
ABA问题以及解决方案
ABA问题:
CAS的过程:
- 从V中获取值A
- 根据A计算目标值B
- 原子操作-----判断V中的值是否为A,如果为A则将V中的值改为B
因为只有第三步是原子操作,所以线程X获取值A,线程Y在此期间将值改为B,然后线程Z将值改为A,最后X线程执行第三步的时候对比值A还是原来的哪个值,所以会产生问题。
解决方案
- 加版本号
- 使用JUC中的 AtomicStampedReference
- AtomicReference == AtomicInteger
- AtomicReference 会给对象加一个时间戳
3. ReentrantLock 和 Synchronized 的区别
- synchronized 发生异常时,会主动释放锁;
- ReentrantLock 发生异常不会释放锁,容易造成死锁。
- Lock是可中断锁
4. notify 和 notifyAll的区别
notify 只会唤醒一个线程,而notifyAll 会唤醒等待此Object 锁的所有线程。
5. volatile 关键字有什么特点
保证可见性
直接从内存读取数据,而不是从寄存器中读取数据。
禁止指令重排序
指令重排是jvm 优化代码的一种方式,但是再有volatile的地方会禁止指令重排,比如:
int a = 1; int b = 1; a++; b++; volatile int c = a + b; a++; b++;这里前4行可能指令重排,比如先做b++在做a++;
后两行数据也可能颠倒顺序,但不影响程序执行。
如果大家还有什么想补充的请再评论区告诉我
资源共享,共同进步!
本文深入探讨了Java多线程的各种锁的概念,包括乐观锁、悲观锁、可重入锁等,并介绍了线程的实现方式,如继承Thread、实现Runnable和Callable接口,以及线程池的使用。此外,还讲解了线程池的核心参数、工作原理及监控,以及线程的基本概念和通信方式。最后,分析了AQS、CAS、ReentrantLock与synchronized的区别等并发编程的关键知识点。

1387

被折叠的 条评论
为什么被折叠?



