1.线程,进程与程序
1.程序:
是为了完成某项特定任务,用某种语言编写的一组指令的集合;
即一段静态的代码,**静态**对象
2.进程
是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程
3.线程
进程可进一步细化为线程,是一个程序内部的一条执行路径
进程中要同时干几件事时,每一件事的执行路径称为一个线程
即线程是一个比进程更小的执行单位,能够完成进程中的一个功能
*** 注意点: ***
1.进程是系统分配资源的最小单位
2.线程是系统调度的最小单位
3.一个进程内的线程之间是可以共享资源的
4.每一个进程中至少有一个线程存在,即主线程
5.线程与进程不同的是:同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多
4.为什么每个线程需要有自己的程序计数器,虚拟机栈和本地方法栈?
1.程序计数器
首先程序计数器是一块比较小的内存区域,可以看做当前线程所指向的字节码的行号指示器
以为线程之间是时间片轮转调度的,当时间片运行回来时程序计数器会告诉程序从第几行开始运行
2.java虚拟机栈(jvm)
每个方法执行的同时都会创建一个栈帧用于储存局部变量表,操作数栈,动态链接,方法出口等信息.
每一个方法从调用到执行完成的过程,就对应一个栈帧在虚拟机中入栈和出栈操作
不同线程内,即使运行同一个方法也是处于不同的内存
即使是同一个线程,递归调用某个方法 ,每次调用都会生成该次方法的方法栈
3.本地方法栈:
本地方法栈和jvm虚拟机栈的作用完全一样,区别是本地方法栈为虚拟机native方法服务
而虚拟机栈jvm为执行的Java方法服务
2.线程调度的方法
1.抢占式:(高优先级的线程优先抢占CPU)
同级别的线程组成先进先出,使用时间片策略
对于高优先级使用抢占式策略,优先抢占CPU
2.时间片轮转
就是说一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行
任务执行的一小段时间就叫时间片,任务正在执行时的状态叫做运行状态.
任务执行一段时间被强制暂停去执行下一个任务,被暂停的任务就处于就绪态
3.分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
3.并发与并行
1.并发:
多个进程在一个CPU下采用时间片轮转的方式,在一段时间之内,让多个进程都得以推进,称为并发
2.并行:
多个进程在多个CPU下分别同时进行运行,称为并行
4.进程中的上下文
上下文简单来说就是一个环境,进程在时间片轮转切换时,由于每个进程运行环境不同,就涉及到转换前后的上下文环境的切换.
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到加载的过程就是一次上下文切换。上下文切换会影响多线程的执行速度。
4.1如何减少上下文切换,提高操作系统效率
5.进程状态
1.就绪态:
进程处于可运行状态,但是时间片还没轮转到该进程,则该进程就处于就绪态
2.运行态:
进程处于可运行状态,但是时间片已经轮到该进程,该进程正在执行代码,则该进程就处于就绪态
3.阻塞态
进程不具备运行条件.正在等待某个时间完成
6.多线程
6.1什么是多线程?
指一个进程中有多个执行路径(线程)正在执行,即为多线程
6.2为什么要使用多线程
1.首先,一个程序中有很多操作是比较耗时的,若使用单线程则需要等待这些操作执行完,这样容易造成阻塞
2.使用多线程可以提高效率,发挥多核CPU的优势
操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上,从而可以提高程序运行效率
3.方便线程之间的通信
对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。
6.3多线程的缺点
了解了多线程的好处以后,就要了解应该在什么样的情况下使用多线程技术。因为并不是说所有情况下用多线程都是好事
1.首先使用太多线程是很耗费系统资源的
2.影响性能,
因为操作系统需要在不同线程间来回切换,这个操作则会影响我们的性能
CPU还要花时间去维护
等候使用共享资源时造成程序的运行速度变慢
6.4线程的应用场景
例如:
1.任务量较多,较大.或者说执行时间比较长
2.同时处理多个任务,让阻塞代码不影响后续代码执行
例如:单线程不停地处理多个客户端http请求响应.会导致输入输出流导致代码阻塞.之后的代码执行不了
7.创建一个线程
public class Create {
//创建线程的方法
public static void main(String[] args) {
Solution1();
Solution2();
Solution3();
Solution4();
}
//使用继承Thread类,创建线程
private static void Solution1() {
MyThread t=new MyThread();
t.start();
}
static class MyThread extends Thread{
@Override
public void run() {
System.out.println("使用继承Thread类,创建线程");
}
}
//实现Runnable接口
private static void Solution2() {
Thread t=new Thread(new MyRunnable());
t.start();
}
static class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("使用实现Runnable接口,创建一个线程");
}
}
//使用匿名类创建Runnable子类
private static void Solution3() {
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使用匿名类创建Runnable子类,创建线程");
}
});
t1.start();
}
//使用匿名类创建线程
private static void Solution4() {
Thread t=new Thread(){
@Override
public void run() {
System.out.println("使用匿名类创建Thread子类对象");
}
};
t.start();
}
}
8.Thread的常见属性
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否中断 | isInterrupted() |
9.Thread常见API
Modifier and Type | Method | Description |
---|---|---|
static int | activeCount() | 返回当前线程的thread group及其子组中活动线程数的估计 |
static Thread | currentThread() | 返回对当前正在执行的线程对象的引用 |
void | interrupt() | 中断对象关联线程,如果线程正在阻塞,则以异常方式通知 |
static boolean | interrupted() | 判断当前线程的中断标志位是否设置,调用后清除标志位 |
boolean | isInterrupted() | 判断当前线程的中断标志位是否设置,调用后不清除标志位 |
void | join(long millis) | 等待这个线程死亡最多 millis毫秒 |
void | run() | 如果这个线程使用单独的Runnable运行对象构造,则调用该Runnable对象的run方法; 否则,此方法不执行任何操作并返回。 |
static void | sleep(long millis) | 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行) |
void | sleep | 导致此线程开始执行 |
static void | yield() | 对调度程序的一个让步,即当前线程愿意让出CPU,但并不会改变当前状态 |
9.1 run和start方法的区别
start方法的作用:
1.启用当前线程
2.调用当前线程的run方法
run方法的作用:
在主线程中调用以后,则在主线程中调用该线程的run()方法,并不创建新线程
9.2中断一个线程
目前常见的方法有两个:
1.通过共享的标记来进行沟通
2.调用interrupt()方法来通知
注意:线程初始是标志位=false
interrupt方法
1.通过thread对象调用interrupt()方法通知该线程停止运行
2.如果线程调用了wait/sleep/join 等方法而阻塞挂起,则以抛出interruptedException异常的方法通知,并清除中断标志位
3.如果没有阻塞挂起,则只是内部的一个中断标志位被设置.thread可以通过.
thread.isinterrupted() 判断指定线程的中断被设置,并不清除中断标志
thread.interrupted() 判断当前线程的中断标志位被设置,并清除中断标志
10.线程的状态
注意我们之前学过的isAlive()方法可以认为除了New和TERMINATED的状态都是活着的
阻塞状态:
竞争锁失败后,jvm将竞争失败的线程全部都放入同步队列中
释放对象锁后,jvm查询同步队列竞争这个对象失败的线程全部唤醒.让他们重新竞争
等待/超时等待:
jvm将等待的线程全部放入等待队列.满足一定条件后唤醒线程让它继续执行
yield()方法
yield()方法只是让出CPU,并不会改变自己的状态
synchronize关键字影响的代码块
竞争失败的线程不停地在阻塞态和被唤醒态之间切换(如果这样子的线程数量很多.则会对性能有很大影响)
如何确定线程数
(1)计算公式: CPU核数/(1-阻塞系数)
某线程执行任务的总时间=执行任务总的阻塞时间+真正执行任务的时间(非阻塞)
阻塞系数=真正执行任务的时间/总时间
(2)计算密集型任务:
CPU核数/CPU核数+1 作为线程数
11.线程安全问题
11.1线程安全的概念
首先我们认为如果多线程环境下代码的运行结果和我们在单线程环境下的结过相同.则我们说这个程序是安全的
11.2线程不安全的原因
1.原子性
是指一个操作是不可中断的。即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。当一个线程执行操作的时候,不要被其他线程加塞。
一条java语句不一定是原子的,也不一定只是一条指令
比如说n++,其实是由从内存把数据读取到CPU,进行数据更新,把数据写回到CPU三步进行操作的
如果我们不保证原子性在多线程的情况下,如果一个线程正在对一个变量进行操作.中途其他线程插入进来了.这个操作就被打断了.最终这个结果可能是错误的
2.可见性
为了提高效率JVM在执行过程中会尽可能将数据在工作内存中执行.这样就会导致共享变量在多线程之间不能及时看到改变.
执行过程:
1.从主内存中能够将数据复制到工作内存中
2.在工作内存中修改变量
3.将数据从线程工作内存写回主内存
主内存是共享的.工作内存则是每个线程都有一个.并不是共享的
为什么要有工作内存???
数据读取顺序的优先级:寄存器>高速缓冲区>内存
线程计算时原始数据来源于内存.在计算过程中该数据可能被重复利用(读取).这些数据被存放在寄存器和高速缓冲区则会提高效率
3.顺序性
如果是在单线程情况下,JVM和CPU指令集会对代码进行优化.例如
我们new操作可以分为三步
1.分配初始化内存空间
2.new对象
3.赋值给共享变量
但是经过优化步骤可能变成1->3->2
这种叫做指令的重排序
synchronize关键字
volatile关键字
wait(),notify(),notifyAll()
wait()方法是让当前线程进入等待状态.同时wait()方法会是释放掉当前线程所持有的锁.
直到其他线程调用此对象的notify()或者notifyAll()方法
notify和notifyAll的区别
相同点:都是唤醒当前对象上等待的线程
不同点:notify是唤醒单个线程(随机唤醒),而notifyAll方法则是唤醒全部wait()阻塞的线程
notify方法后,当前线程不会马上释放锁.要等到notify方法的线程将程序执行完,也就是退出同步代码块后才会释放锁
wait和sleep的区别
相同点:让线程放弃执行一段时间
不同点:
1.wait方法执行时需要会先释放锁.等到被唤醒时再重新请求锁
而sleep方法是无视锁的.
单例模式
class Singleton{
private static Singleton instance=new Singleton();
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
instance =new Singleton();
}
return instance;
}
}
Callable 和 Runnable 的不同之处
- 方法名,Callable 规定的执行方法是 call(),而 Runnable 规定的执行方法是 run();
- 返回值,Callable 的任务执行后有返回值,而 Runnable 的任务执行后是没有返回值的;
- 抛出异常,call() 方法可抛出异常,而 run() 方法是不能抛出受检查异常的;
- 和 Callable 配合的有一个 Future 类,通过 Future 可以了解任务执行情况,或者取消任务的执行,还可获取任务执行的结果,这些功能都是 Runnable 做不到的,Callable 的功能要比 Runnable 强大。
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable=new Callable() {
@Override
public Integer call() throws Exception {
System.out.println("call");
return 123;
}
};
//Thread使用callable
FutureTask<Integer> task=new FutureTask<>(callable);
new Thread(task).start();
Integer x=task.get();//当前线程阻塞等待,知道线程执行完毕(join效果差不多),但是可以获取线程的返回值
System.out.println(x);
//线程池使用callable
ExecutorService executorService=Executors.newFixedThreadPool(4);
Future<Integer> future=executorService.submit(callable);
x= future.get();//阻塞等待
System.out.println(x);
}
}
CAS
什么是CAS
CAS全称“compare and swap” 字面意思 交换并比较
当多个线程同时对一个资源进行CAS操作时,只有一个线程可以操作成功,但是并不会阻塞其他线程,其他线程只会收到一个操作失败的信号,可见CAS是一个乐观锁。
技术背景
线程执行的任务量比较小的时候,线程安全需要使用synchronized(多线程同时竞争对象锁)加锁,效率比较低
(竞争失败的线程会在阻塞态和被唤醒状态间来回切换 ,状态转换是非常浪费资源的)
使用场景
代码块执行速度非常快,例如对变量修改保证线程安全
目的
保证效率很高的 线程安全操作
原理
使用CAS是不进行线程阻塞的(线程是一直处于运行状态的),自旋尝试赋值的操作
自旋的实现
(1)循环死等
(2)可中断方法---->interrupt
(3)设置循环次数,达到次数就退出
(4)设置循环最大时间,达到最大时间就退出
具体:给定三或者四个参数(原始值(旧值),期望值(主存中存放的值),修改值,有可能有版本号)——>
首先对比主存中的值和原始值是否相等,如果相等则证明没有被其他线程更改过,就可以把要更新的值赋值给原始值。
如果不相等则证明改制已经被其他线程修改过了,则证明原始值不是最新的值,所以不能直接复制。所以进入循环,等待
存在的问题:ABA问题
ABA问题
产生问题的原因
当线程拷贝主存值到工作内存进行修改的时间段,其他线程把主存中变量值由A改成B再次改成B(A–>B–>A)。
相当于被其他线程修改过了,但是本身不知道,还在进行修改
解决方案
采用乐观锁的设计,引入版本号。版本号一致可以进行操作,不一致不可以进行操作
乐观锁
乐观锁假设认为数据一般情况下不会发生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误信息,让用户决定如何去做。线程安全上采取版本号来控制(用户自己判断版本号,并处理)
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以在每次拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到他拿到锁
自旋锁
按照之前的学习,线程在进行抢锁失败后进入阻塞状态,放弃CPU,需要过很久才能再次被调度。但是经过实际测算,实际生活中大部分情况下,虽然当前抢锁失败了,但用不了多久锁就会被释放。基于这个事实,自旋锁诞生了
简单的理解,自旋锁就是下面的代码
while(抢锁(Lock)== 失败){
//循环
}
只要没抢到锁就死等
但是这种情况缺点也很明显,就是如果之前的假设不成立(锁没有被很快释放),则线程就是光在消耗CPU资源,长期在做无用功。或者线程数量比较多
实现方式
(1)循环死等
(2)可中断方法---->interrupt
(3)设置循环次数,达到次数就退出
(4)设置循环最大时间,达到最大时间就退出
synchronized背后的原理
实现原理:通过对象头加锁操作,monitor锁机制:编译为字节码时,生成monitorenter和monitorexit
JVM将synchronized分为无锁,偏向锁,轻量级锁,重量级锁状态。会根据情况一次升级
synchronized锁只能升级不能降级
无锁
没有对资源进行锁定,所有线程都能访问并修改同一个资源,但是同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功
偏向锁
对象的代码一直被同一个线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。
偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁,只有当其他线程尝试竞争偏向锁才被会释放
(针对同一个线程再次申请已持有的对象锁)(是四种状态中最乐观的一把锁:从始至终只有一个线程请求一把锁)
轻量级锁
大概率在同一个时间点,只有一个线程申请对象锁
轻量级锁是指当锁 是偏向锁的时候,被第二个线程B所访问,此时偏向锁就会升级为轻量级锁,线程B就会通过自旋的方式尝试获取锁,线程不会阻塞,以提高性能
实现原理:CAS
重量级锁
指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态
使用场景:同一个时间点,频繁出现多个线程竞争同一个对象锁。
**缺点:**线程之间状态切换频繁,切换成本非常高
其他优化方案
锁粗化
锁粗化就是讲多次连接在一起的加锁,解锁操作合并为一次,将多个连续的锁扩展为一个更大的范围。例如
public class Test {
public static void main(String[] args) {
StringBuffer sb=new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
}
}
每次调用stringbuffer.append方法都要加锁和解锁。如果虚拟机检测到有一系列连串的对同一个对象进行加锁和解锁的操作,就会将其合并成一次范围更大的加锁和解锁操作。即在第一次append方法是进行加锁,最后一次append进行解锁
锁消除
锁消除即删除不必要的加锁操作,根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么认为这段代码是线程安全的,不必加锁。
死锁
死锁是这样一种情形:多个线程同时被阻塞,他们中的一个或者全部都在等某个资源被释放。由于线程被无限期的阻塞,因此程序不可能正常终止。
本质:一个线程等待另一个线程执行完毕后才可以继续执行,但是如果现在相关的几个线程彼此之间都在等待着,就会形成死锁
后果
线程阻塞等待,无法继续进行
死锁产生的四个必要条件
(1)互斥使用,当资源被一个线程使用时,别的线程不能使用
(2)不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能右资源占有者主动释放
(3)请求和保持,当资源请求者在请求其他的资源的同时保持对原有资源的占用
(4)循环等待,及存在一个等待队列:a1等待a2的资源,a2等待a3的资源,a3在等待a1的资源,这样就形成了等待环路
解决方案
破坏上述任何一种条件就可以解决死锁问题
(1)资源一次性分配
(2)可剥夺资源:在线程满足某些条件时,释放已占有的资源
(3)资源有序分配:系统为每类资源赋予一个编号,每个线程按照编号递请求资源,释放则相反
- 能够响应中断。synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程
就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线
程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能够唤醒它,那
它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。
- 支持超时。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错
误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
- 非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也
有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
// 支持中断的 API
void lockInterruptibly() throws InterruptedException;
// 支持非阻塞获取锁的 API
boolean tryLock();
// 支持超时的 API
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
Lock
锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源。
在Lock接口出现之前,java程序主要是靠synchronized关键字实现锁功能的,而 JDK5 之后,并发包中
增加了lock接口,它提供了与synchronized一样的锁功能。虽然它失去了像synchronize关键字隐式加
锁解锁的便捷性,但是却拥有了锁获取和释放的可操作性,可中断的获取锁以及超时获取锁等多种
synchronized关键字所不具备的同步特性。
Lock lock = new ReentrantLock();
lock.lock();
try{
.......
}finally{
lock.unlock();
}
需要注意的是synchronized同步块执行完成或者遇到异常是锁会自动释放,而lock必须调用unlock()
方法释放锁,因此在finally块中释放锁。
Lock锁的实现原理
AQS(AbstractQueuedSynchronizer):队列式同步器
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
AQS的大致实现思路
AQS内部维护了一个CLH队列来管理锁。线程会首先尝试获取锁,如果失败就将当前线程及等待状态等信息包装成一个node节点加入到同步队列sync queue里。 接着会不断的循环尝试获取锁,条件是当前节点为head的直接后继才会尝试。如果失败就会阻塞自己直到自己被唤醒。而当持有锁的线程释放锁的时候,会唤醒队列中的后继线程。
Lock接口API
下面来看看Lock接口中定义了哪些方法
void lock(); // 获取锁
void lockInterruptibly() throws InterruptedException; // 获取锁的过程能够响应中断
boolean tryLock(); // 非阻塞式响应中断能立即返回,获取锁返回true反之为false
boolean tryLock(long time,TimeUnit unit);// 超时获取锁,在超时内或未中断的情况下能获取锁
Condition newCondition(); // 获取与lock绑定的等待通知组件,当前线程必须先获得了锁才能等待,等待会释放锁,再次获取到锁才能从等待中返回。
Lock锁的特点
1.提供公平锁和非公平锁
是否按照入队的顺序设置线程的同步状态----多个线程申请加锁操作是,是否按照时间顺序来加锁
2.AQS提供的独占式,共享式设置同步状态
独占式:只允许一个线程获取到锁
共享式:一定数量的线程共享式获取锁
3.带Reentrant关键字的Lock包下的API:可重入锁
允许多次获取同一个Lock对象的锁
Lock体系中提供的读写锁API:ReentranReadWriteLock
读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞
使用场景:多线程执行某个操作时,允许读-读并发/并行执行,不允许读-写,写-写并发/并行执行,如多线程读写文件
读锁和写锁之间锁只能降级不能升级(写锁—>读锁)
public class ReadWriteTest {
private static ReadWriteLock LOCK = new ReentrantReadWriteLock();
private static Lock READLOCK = LOCK.readLock();
private static Lock writeLock = LOCK.writeLock();
public static void readFile(){
try {
READLOCK.lock();
//IO读文件
} finally {
READLOCK.unlock();
}
}
public static void writeFile(){
try {
writeLock.lock();
//IO写文件
} finally {
writeLock.unlock();
}
}
public static void main(String[] args) {
//20个线程读文件
for(int i = 0;i < 20;i++){
//
}
//20个线程写文件
for(int i = 0;i < 20;i++){
//
}
}
}
优势:针对读读并发执行,提高运行效率
Condition
condition作用:线程间通信
怎么使用:
(1)通过Lock对象.newCondition()获取Condition对象
(2)调用Condition对象.await()让当前线程阻塞等待,并释放锁(==synchronized.wait())
(3)调用Condition对象.signal()/signalAll()通知之前await阻塞的线程(相当于synchronized锁对象.notify()/notifyAll())
public static Lock LOCK = new ReemtrantLock();
public static Condition CONDITION = LOCK.newCondition();
public static void t3(){
try{
LOCK.lock()//=synchronized()加锁的代码
while(库存达到上限){
CONDITION.wait();//=wynchronized锁的对象.wait()
}
System.out.println("t3");
CONDITION.signal();//=synchronized锁对象.notify();
CONDITION.signalAll();//=synchronized锁对象.notifyAll()
}finally{
LOCK.unlock();
}
}
CountDownLatch
就好像跑步比赛,10位选手依次就位,哨音响才同时出发;所有选手都冲过终点才公布成绩
使用场景:
在某个线程A某个地方,阻塞等待。直到一组线程执行完毕后,在执行A后续的代码
注意事项:只提供了计数器递减的操作
public class CountDownLatchTest {
//10位选手依次就位,哨音响才同时出发;所有选手都冲过终点才公布成绩
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch=new CountDownLatch(10);//计数器给定初始值
Runnable r=new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep((long) (Math.random()*10000));
latch.countDown();//计数器的值-1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i=0;i<10;i++){
new Thread(r).start();
}
latch.await();//当前线程阻塞等待,直到计数器的值=0
System.out.println("比赛结束");
}
}
Semaphore
一个计数信号量,主要用于控制多线程对共同资源库访问的限制
使用场景:
(1)CountDownLatch一样的地方
(2)用于多线程有限资源的访问
public class SemaphoreTest {
//最多5个坑
private static final Semaphore available=new Semaphore(5);
public static void main(String[] args) {
ExecutorService pool= Executors.newFixedThreadPool(10);
Runnable r=new Runnable() {
@Override
public void run() {
try {
//acquire(int i)
//当前线程获取Semaphore对象中的给定数量的资源
//如果获取成功资源数量减少,当前线程往下进行
//如果获取不到,当前线程阻塞等待,直到有足够的资源量
available.acquire();
Thread.sleep(10000);
System.out.println(Thread.currentThread().getName());
//release(int i)
//释放给定数量的许可证,将其返回到信号量
available.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i=0;i<10;i++){
pool.execute(r);
}
pool.shutdown();
}
}
模拟服务端接收客户端的http请求:只有1000个并发
在一个时间点,客户端任务数达到1000,再有客户端请求,将阻塞等待
public class SimulationHttp {
//模拟服务端接收客户端的http请求:只有1000个并发
//在一个时间点,客户端任务数达到1000,再有客户端请求,将阻塞等待
public static void main(String[] args) {
Semaphore semaphore=new Semaphore(1000);
while(true){
Thread t=new Thread(new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();
//模拟每个线程处理客户端http请求任务
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
}
});
t.start();
}
}
}
ThreadLocal详情
概念
ThreadLocal用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其他线程里的变量。也就是说
ThreadLocal可以为每个线程创建一个单独的变量副本,相当于线程的private static类型变量
ThreadLocal的作用和同步机制有些相反,同步机制是为了保证多线程环境下数据的一致性,而ThreadLocal是保证了多线程环境下数据的独立性
使用场景:隔离线程间的变量,保证每个线程是使用自己线程内的变量副本
常见API
get() | 返回当前线程的此线程局部变量的副本中的值。 |
---|---|
set() | 将当前线程的此线程局部变量的副本设置为指定的值。 |
remove() | 删除此线程局部变量的当前线程的值。 |
public class ThreadLocalTest {
private static ThreadLocal<String> threadLocal=new ThreadLocal<>();
public static void main(String[] args) {
try {
threadLocal.set("abc");
for (int i=0;i<20;i++){
String finalI = String.valueOf(i) ;
int finalI1 = i;
new Thread(new Runnable() {
@Override
public void run() {
try {
threadLocal.set(finalI);
if (finalI1 ==5){
Thread.sleep(100);
System.out.println(threadLocal.get());
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
threadLocal.remove();
}
}
}).start();
}
while(Thread.activeCount()>1){
Thread.yield();
}
System.out.println(threadLocal.get());
} finally {
threadLocal.remove();
}
}
}
定义类变量:static ThreadLocal<保存的数据类型> threadLocal=new ThreadLocal<>();
建议:当有线程设置值的时候,在线程结束前进行remove