juc概述
- wait 和 sleep的区别 ```xml sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用 sleep不会释放锁,它也不需要占用锁.wait会释放锁,但调用它的前提是当前线程占有锁 他们都可以被interrupted方法中断
原子性:即一个操作或多个操作,要么全部执行并且执行过程中不会被任何因素打断,要么就都不执行
2. 并发和并行
```xml
串行模式: 多个任务依次执行
并行模式
并发: 同一时刻多个线程在访问同一个资源,多个线程对一点
并行: 多项工作一起执行,之后再汇总
- 管程 ```xml Monitor: 监视器(锁) 是一种同步机制,保证同一时间,只有一个线程访问被保护的数据(或代码)
jvm同步基于进入和退出,使用管程对象实现的
4. 用户线程和守护线程
```xml
用户线程: 自定义线程
守护线程: 比如垃圾回收(运行在后台)
主线程结束了,用户线程还在运行,jvm还在存活状态
没有用户线程,都是守护线程,jvm就会停止运行
public class test {
public static void main(String[] args) {
Thread aa = new Thread(() -> {
//isDaemon():判断该线程是否是守护线程 true是
System.out.println(Thread.currentThread().getName()+"::"+Thread.currentThread().isDaemon());
while (true){
//让该线程一级一直执行
}
}, "aa");
aa.start();
System.out.println(Thread.currentThread().getName()+"over");
}
}
Lock接口
- Synchronized关键字
```xml
多线程编程步骤:
- 创建资源类,在资源类中创建属性和操作方法(高内聚)
- 在资源类操作方法中 a.判断 b.干活 c.通知
- 创建多个线程,调用资源类的操作方法
- 防止虚假唤醒
一个对象里面如果有多个Synchronized方法,某一时刻内,只要一个线程去调用其中的一个Synchronized方法了, 其他的线程都只能等待,换句话说,某一时刻内,只有唯一一个线程去访问这些Synchronized方法 锁的是当前对象this,呗锁定后,其他的线程都不能进入到当前对象的其他Synchronized方法
所有的非静态同步方法用的都是同一把锁-实例对象本身 Synchronized实现同步的基础:java中的每一个对象都可以作为锁 具体表现有三种形式
1. 对于普通同步方法,锁是当前实例对象
2. 对于静态同步方法,锁的是当前类的Class对象
3. 对于同步方法块,锁的是Synchronized括号里配置的对象
当一个线程试图访问同步代码块时,他首先必须得到锁,退出或异常的时候必须释放锁 所有静态同步方法用的是同一把锁—类对象本身 这两把锁(this,Class)是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞争条件的 但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁之后才能获取锁 不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之就按,只要他们同一个类的实例对象
2. Look接口
```xml
Lock实现提供了比使用synchronized方法和语句可获得更广泛的锁定操作
可重入锁: 可重复使用
Lock和Synchronized区别
1. Lock不是java语言内置的,synchronized是java语言的关键字,因此是内置特性,Lock是一个类
通过这个类可以实现同步访问.
2. Lock和synchronized有一点非常大的不同,采用synchronized不需要用户手动去释放锁,当synchronized
方法或者synchronized代码块执行完成之后,系统会自动让线程释放对锁的占用,Lock则必须要用户手动释放锁
如果没有手动释放锁,会出现死锁现象.
3. synchronized在发生异常的时,会自动释放线程占有的锁,因此不会导致死锁现象;而Lock在发生异常时,如果没有
主动通过unLock()去释放锁,则很可能造成死锁现象,因此在使用Lock的时候需要在finally中释放锁
4. Lock可以让等待的线程响应中断,而synchronized却不行,使用synchronized时,等待线程会一直等待下去,不能够响应中断
5. 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到
6. Lock可以提高多个线程进行读操作的效率
在性能上来说,如果竞争资源不激烈,两者性能差距是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized
线程间通信
class Share{
private int number = 0;
public synchronized void incr() throws InterruptedException {
//如果number=0 就+1
if (number!=0){
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"::"+number);
//通知其他线程起来干活
this.notifyAll();
}
public synchronized void decr() throws InterruptedException {
//如果number=1 就-1
if (number!=1){
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"::"+number);
//通知其他线程起来干活
this.notifyAll();
}
}
- 虚假唤醒
```xml
wait()在哪里睡在哪里醒
AA,CC线程做+1操作
BB,DD线程做-1操作
当值为0时 AA线程抢到资源对资源进行+1 资源变为1 此时CC线程抢到资源
此时AA线程再次抢到资源 在CC线程相同的位置睡 当DD/BB线程抢到资源的时候满足if (number!=0){
this.wait();//此处睡着
}
this.notifyAll(); 唤醒其他线程 AA,CC线程被唤醒后都进行+1操作,导致资源变成2if (number!=1){
this.wait();
}
可以使用while(number!=0) 解决这个问题
<a name="Em7FX"></a>
## 线程间的定制通信
1. 举例
```xml
启动三个线程,按照如下要求
1. AA打印5次,BB打印10次,CC打印15 次
2. AA打印5次,BB打印10次,CC打印15 次
进行10轮
集合的线程安全
1.list集合
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i <50 ; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
Exception in thread "8" java.util.ConcurrentModificationException
使用Vector解决ArrayList线程安全问题(不推荐)
// Vector中的add方法
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
使用Collections.synchronizedList解决(不推荐)
List<String> list = Collections.synchronizedList(new ArrayList<>());
使用CopyOnWriteArrayList解决
List<String> list = new CopyOnWriteArrayList<>();
写实复制技术
1. 并发读取
2. 独立写:写操作时将之前的集合复制一份后写入新的内容
3. 写操作完成后,合并两个集合.新的读操作来读取新的集合
CopyOnWrite容器即写时复制的容器.往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行copy
复制出一个新的容器Object[] newElements,然后新的容器Object[] newElements里添加元素,添加完元素之后,
再讲原容器的引用指向新的容器
这样做的好处就是可以对CopyOnWrite容器进行并发读取,而不需要加锁,因为不会向当前容器添加任何元素
,所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器
CopyOnWrite见解
/**
* 写时复制技术
* copyOnWrite
* 写入的时候,创建一个新的长度为原数组长度+1的新数组 然后将新数据写到新数组中
* 再用新数组替换就数组
* 删除的时候,创建一个新的长度为原数组长度-1的新数组 然后通过数组复制的方式实现删除
* 然后替换原来的数组
*
* 写时复制技术最终目的:为了在读多写少的场景下,通过写时复制技术,让大量读请求在无需加锁
* 消耗性能的情况下,保证多线程并发读写的情况下线程安全
*/
CopyOnWrite主要基于等效不可变思想
将数组设计成是不可变的(每次都是新的数组替换就数组,不存在对数组修改的情况)
数组中的对象是可变的
这种现象叫做等效不可变
public boolean add(E e) {
final ReentrantLock lock = this.lock;//获取锁
lock.lock();//上锁
try {
Object[] elements = getArray();//获取原来的数组(Array)
int len = elements.length;//计算数组的长度
Object[] newElements = Arrays.copyOf(elements, len + 1);//复制一个长度+1的新数组
newElements[len] = e;//将新加入的数据放在新数组的尾部
setArray(newElements);//将新数组复制给Array
return true;
} finally {
lock.unlock();//解锁
}
}
- hashSet和hashMap ```xml CopyOnWriteArraySet 解决set ConcurrentHashMap 解决Map
HashSet底层是HashMap
<a name="0xKjR"></a>
## 多线程锁
1. synchronized锁
```xml
1. 对于普通方法,锁是当前实例对象
2. 对于静态同步方法,锁的当前类的class对象
3. 对于同步方法块,锁是synchronized括号里配置的对象
公平锁和非公平锁
非公平锁:线程饿死,效率高
公平锁:阳光普照,效率相对低
可重入锁
死锁
产生死锁的原因
1. 系统资源不足
2. 进程运行推进顺序不合适
3. 资源分配不当
验证是否是死锁
jps 类似linux中的ps -ef
jstack jvm自带的堆栈跟踪工具
Callable
callable和runnable接口的区别
1.callable接口有返回值
2.callable接口有异常
3.落地方法不一样callable接口是call方法
线程执行计数器
CountDownLatch
CountDownLatch主要有两个方法,当一个或多个线程调用await()方法时,这些线程会阻塞
其他线程调用countDown方法会将计数器-1(调用countDown方法的线程不会阻塞)
当计数器的值变成0时,因await()方法阻塞的线程会被唤醒,继续执行
CountDownLatch countDownLatch = new CountDownLatch(6);//设置计数器技术次数
for (int i = 0; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"同学走了");
countDownLatch.countDown(); //计数器-1
},String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println("关门");
CyclicBarrier
设置一个限制数,当等待进程数量达到限制数,执行指定线程.并唤醒被阻塞的线程
```java CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()-> System.out.println(“召唤神龙“)); for (int i = 1; i <= 7; i++) {
final int ii = i;
new Thread(()->{
System.out.println("找到了第"+ ii+"颗龙珠");
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("第"+ ii+"颗龙珠飞走了");
},String.valueOf(i)).start();
}
找到了第1颗龙珠
找到了第5颗龙珠
找到了第4颗龙珠
找到了第3颗龙珠
找到了第2颗龙珠
找到了第7颗龙珠
找到了第6颗龙珠
***召唤神龙***
第6颗龙珠飞走了
第1颗龙珠飞走了
第4颗龙珠飞走了
第5颗龙珠飞走了
第7颗龙珠飞走了
第2颗龙珠飞走了
第3颗龙珠飞走了
3. Semaphore
```xml
限制某个资源最大同时访问(信号灯)
在信号量上定义两种操作:
1. acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量-1)
要么一直等待下去,知道有线程释放信号量,或超时
2. release(释放) 实际上会将信号量的值+1,然后唤醒等待的线程
信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制
Semaphore semaphore = new Semaphore(4); //现在有4个停车位
for (int i = 1; i <= 6; i++) {
new Thread(()->{
try {
semaphore.acquire(); //位置-1
System.out.println(Thread.currentThread().getName()+"抢占了车位");
//if (semaphore.tryAcquire())
TimeUnit.SECONDS.sleep(4); //线程睡4s
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName()+"开走了");
semaphore.release(); //让出位置
}
},String.valueOf(i)).start();
}
1抢占了车位
4抢占了车位
3抢占了车位
2抢占了车位
1开走了
4开走了
5抢占了车位
3开走了
2开走了
6抢占了车位
5开走了
6开走了
读写锁
ReaderWriteLock
多个线程同时读一个资源类没有任何问题,所以为了满足并发量.读取共享资源应该同时进行
但是如果有一个线程想去写共享资源,就不应该再有其他的线程可以对资源进行读或写
读-读能共存
读-写不能共存
写-写不能共存
```java private volatile Map
map = new HashMap<>(); private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); public void put(String key,Object value){ readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"开始写入数据");
map.put(key, value);
System.out.println(Thread.currentThread().getName()+"写入完成");
} finally {
readWriteLock.writeLock().unlock();
}
} public void get(String key){
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"开始读取数据");
map.get(key);
System.out.println(Thread.currentThread().getName()+"读取完成");
} finally {
readWriteLock.readLock().unlock();
}
}
1开始写入数据
1写入完成
2开始写入数据
2写入完成
3开始写入数据
3写入完成
5开始写入数据
5写入完成
4开始写入数据
4写入完成
4开始读取数据
1开始读取数据
1读取完成
5开始读取数据
2开始读取数据
2读取完成
4读取完成
5读取完成
3开始读取数据
3读取完成
<a name="hc22G"></a>
## 阻塞队列
1. BlockingQueue
```xml
当队列是空的,从队列中获取元素的操作将会被阻塞
当队列是满的,向队列中添加元素的操作将会被阻塞
试图从空的队列中获取元素的线程将会被阻塞,知道其他线程往空的队列插入新的元素
试图向已满的队列中添加新的元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空
使队列变得空闲起来并后续新增
用处:
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会被自动唤醒
为什么需要BlockingQueue
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切都被BlockingQueue一手包办了
在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,
尤其还要兼顾效率和线程安全,而这给我们的程序带来了不小的复杂度
- BlockingQueue核心方法
抛出异常:
当阻塞队列满时,再往队列里add插入元素会抛出IllegalStateException: Queue full
当阻塞队列空时,再从队列里remove移除元素会抛出NoSuchElementException
特殊值 :
插入方法,成功true失败false
移除方法,成功返回出队列的元素,队列里没有就返回空
一直阻塞:
当阻塞队列满时,生产者线程继续王队列里put元素,队列会一直阻塞直到put数据or响应中断退出
当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用
超时退出:
当阻塞队列满时,队列会阻塞生产者线程一定 时间,超过限时后生产者线程会退出
线程池
为什么要用线程池
线程池的优势
线程池做的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务.
如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行
主要特点 ```xml 线程复用 控制最大并发数 管理线程
降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提高响应速度.当任务到达时,任务可以不需要等待线程创建就能立即执行
提高线程的可管理性.线程是稀缺资源,如果无线制的创建,不进会消耗系统资源,还会降低系统的稳定性 使用线程池可以进行统一的分配,调优和监控 ```
线程池的使用
线程池的分类 ```xml
- Executors.newFixedThreadPool(int);指定线程数量的线程池 执行长期任务性能好,创建一个线程池,一池有N个固定的线程(有固定线程数的线程)
- Executors.newSingleThreadExecutor();单个线程的线程池
Executors.newCachedThreadPool();可扩容
```java
ExecutorService service = Executors.newFixedThreadPool(5);
try {
for (int i = 1; i <= 10; i++) {
service.execute(()->{
System.out.println(Thread.currentThread().getName()+"办理业务");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
} finally {
service.shutdown();
}
pool-1-thread-1办理业务
pool-1-thread-5办理业务
pool-1-thread-4办理业务
pool-1-thread-3办理业务
pool-1-thread-2办理业务
pool-1-thread-4办理业务
pool-1-thread-3办理业务
pool-1-thread-1办理业务
pool-1-thread-5办理业务
pool-1-thread-2办理业务
线程池的重要参数
参数介绍(源代码中的注释)
源代码中注释:(谷歌翻译)
使用给定的初始参数创建一个新的ThreadPoolExecutor 。
参数:
corePoolSize – 要保留在池中的线程数,即使它们处于空闲状态,除非设置了allowCoreThreadTimeOut
maximumPoolSize – 池中允许的最大线程数
keepAliveTime – 当线程数大于核心数时,这是多余空闲线程在终止前等待新任务的最长时间。
unit – keepAliveTime参数的时间单位
workQueue – 用于在执行任务之前保存任务的队列。 这个队列将只保存execute方法提交的Runnable任务。
threadFactory – 执行程序创建新线程时使用的工厂
handler – 执行被阻塞时使用的处理程序,因为达到了线程边界和队列容量
抛出:
IllegalArgumentException – 如果以下情况之一成立: corePoolSize < 0 keepAliveTime < 0 maximumPoolSize <= 0 maximumPoolSize < corePoolSize
NullPointerException – 如果workQueue或threadFactory或handler为 null
参数解析 ```xml
- corePoolSize:常驻线程数 线程池中常驻核心线程数
- maximumPoolSize:最大线程数 线程池中能容纳同时执行的最大线程数,此值必须大于等于1
- keepAliveTime:多余线程等待时间 多余的空闲线程的存活时间,当前线程池中线程数量超过corePoolSize时,当空闲时达到keepAliveTime时, 多余线程会被销毁,直到线程数量等于corePoolSize
- unit:时间单位 keepAliveTime的单位
- workQueue:阻塞队列 任务队列,被提交但是尚未执行的任务
- threadFactory:工厂 表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认即可
handler:拒绝策略 表示当线程队列满时,并且最大线程数大于等于maximumPoolSize时如何来拒绝请求执行的runnale的策略
<a name="BfljT"></a>
## 线程池的底层工作原理
![image.png](https://cdn.nlark.com/yuque/0/2021/png/21477905/1629474689538-e2b25e3d-63e8-4492-b8fb-9707187e6507.png#align=left&display=inline&height=352&margin=%5Bobject%20Object%5D&name=image.png&originHeight=352&originWidth=509&size=115354&status=done&style=none&width=509)
```xml
流程描述:
1. 在创建线程池后,开始等待请求
2. 当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
2.1 如果正在运行的线程数量小于corePoolSize,那么马上创建(分配)线程运行这个任务
2.2 如果正在运行的线程数大于或等于corePoolSize,那么将这个线程放入队列
2.3 如果此时队列满了且运行的线程数小于maxinummPoolSize,那么就创建非核心(临时)线程运行队列中的任务
2.4 如果队列满了且正在运行的线程数量大于等于maxinumPoolSize,那么线程池会启动饱和拒绝策略
3. 当一个线程完成任务时,他会从队列中取下一个任务来执行
4. 当一个线程无事可做超过一定时间(keepAliveTime)时,线程会判断:
4.1 如果当前运行的线程数大于corePoolSize,那么这个线程会被停掉
4.2 在线程池的所有任务完成后,它最终会缩小到corePoolSize的大小
线程池的选择,生产中设置合理参数
都不用!!!
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,
这样的处理方式让写的同学更加明确线程池的运行规则,避免资源耗尽的风险
自定义线程池
线程池的拒绝策略 ```xml 等待队列已经排满了,再也塞不下新任务了 同时,线程池中的max线程也达到了,无法继续为新任务服务 这个时候我们就需要拒绝策略机制合理的处理这个问题
AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
- CallerRunsPolicy:”调用者运行”一种调节机制,该策略即不会抛弃任务,也不会抛出异常, 而是将某些任务回退到调用者,从而降低新任务的流量
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然而把当前任务加入队列中尝试再次提交当前任务
DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常,如果允许任务丢失,这是最好的策略 ```
创建线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2, //核心线程数
5,//最大线程数
2L,//超时等待时间
TimeUnit.SECONDS,//等待时间单位
new LinkedBlockingDeque<>(3),//阻塞队列
Executors.defaultThreadFactory(),//线程创建工厂
new ThreadPoolExecutor.AbortPolicy());//线程拒绝策略
cpu密集型
最大线程数建议设置为cpu线程数+1
Runtime.getRuntime().availableProcessors() 获取cpu的线程数