文章目录
并发基础概述
并行和并发有什么区别?
区别 | 并行 | 并发 |
---|---|---|
意思不同 | 两个或多个事件在同一时刻发生 | 两个或多个事件在同一时间间隔发生 |
侧重不同 | 不同实体上的多个事件 | 同一个实体上的多个事件 |
处理不同 | 多台处理器上同时处理多个任务 | 一台处理器上同时处理多个任务 |
守护线程是什么?
守护线程(Daemon thread),是个服务线程,即服务其他的线程。
线程状态及转换?
Thread类有个State的枚举类型,定义了6种状态:
状态 | 说明 |
---|---|
new 新建 | 初始状态,线程被构建,但是还没有调用start方法 |
runnable 可运行 | 运行状态,java线程将操作系统中就绪和运行两种状态笼统地称为“运行中” |
blocked 阻塞 | 阻塞状态,表示线程正在等待同步锁 |
waiting 等待 | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知 或 中断) |
time waiting 定时等待 | 超时等待状态,不同于waiting 可以在指定的时间自动返回 |
terminated 终止 | 终止状态,当前线程已经执行完毕 |
线程状态
线程
sleep() 和 wait()的区别?
区别 | sleep() | wait() |
---|---|---|
概念 | 让正在执行的线程主动让出CPU,并不会让出同步资源锁,在sleep指定时间后cpu再回到该线程继续执行 | 当前线程暂停,让出CPU并且让出同步资源锁,以便其他等待该资源的线程可以得到资源继续执行,只有调用了notify方法,该线程才会结束wait状态。调用notify方法并不会给线程分配任务,只是让该线程有权限重新参与线程调度 |
使用场景 | 任何地方使用 | 只能在同步方法或同步块中使用 |
所属类 | 是线程类(Thread)的方法,调用会暂停此线程指定的时间,但是不会释放对象锁,到时间自动恢复 | 是Object类方法,调用会放弃对象锁,进入等待队列,调用notify或notifyAll才会唤醒进程参与线程调度,再次获得对象锁才会进入运行状态 |
线程的run() 和 start()有什么区别?
run()和start()的关系:
每个线程都通过特定Thread对象所对应的run方法来完成具体的操作。
run方法称为线程体,通过Thread类的start方法来启动一个线程。
区别
区别 | run() | start() |
---|---|---|
方法调用 | 主线程调用run方法 = 执行了Thread类中的run方法,无法达到多线程目的 | 线程调用start方法启动线程,轮到执行自动调用run方法 |
调用次数 | 一个线程可以多次调用run方法 | 一个线程只能调用一次start方法,多次调用会报IllgealThreadStateException异常 |
总结
Thread类
方法名 | 说明 |
---|---|
static void sleep(long milis) | 使当前指定的线程停留指定的毫秒数 释放CPU资源,不释放同步锁 |
void join() | 等待这个线程死亡 一个线程调用了该方法,其余线程必须等待该线程执行完毕之后再执行 |
void setDaemon(boolean on) | 标记此线程为守护线程,如果将所有的线程设置为守护线程,那么JVM会退出 |
Object类
方法 | 说明 |
---|---|
void wait() | 使当前线程等待,直到别的线程调用对象的notify或者notifyAll方法 |
void notify() | 唤醒等待对象监视器的单个线程 |
void notifyAll() | 唤醒等待对象监视器的所有线程 |
(三大特性) Java 怎么保证多线程的运行安全?
多线程的运行安全体现在:原子性、可见性、有序性。
原子性:线程切换导致
提供互斥访问,同一时刻只能有一个线程对数据进行操作;
一个或者多个操作在CPU执行过程中不被中断的特性。
解决:JDK Atomic开头的原子类、synchronized、Lock
可见性:缓存导致
一个线程对主内存的修改可以及时被其他线程看到;
一个线程对共享变量的修改,另一个线程能够立刻看到;
解决:synchronized、volatile、LOCK
有序性 :编译优化导致
程序执行的顺序按照代码的先后顺序执行。
解决:Happens-Before规则
Java中main方法启动的是一个线程也是一个进程。
解释:
一个Java程序启动后它就是一个进程,进程相当于一个空盒,它只提供资源装载的空间,具体的调度不是由进程实现的,而是由线程实现的。
一个Java程序从main开始之后,进程启动,为整个程序提供各种资源,此时会启动一个线程,被称之为主线程,它将调度资源进行具体的操作。
Thread、Runnable开启的线程是主线程下的子线程,是父子关系,此时的java程序即多线程,这些线程共同进行资源的调度和执行。
创建线程的几种方式?
一共四种方式,分别是 类继承Thread类、实现Runnable接口、使用Callable接口和FutureTask类、线程池创建。
方式1:类继承Thread类
1 创建继承Thread类的子类
public class 子类 extends Thread{}
2 子类中重写Thread类的run方法
@Override
public void run(){
// 线程具体执行的操作;
}
3 创建子类的具体实现类对象
子类 对象名 = new 子类();
4 启动线程
对象名.start();
方式2:实现Runnable接口
1 创建实现Runnable接口的子类
public class SonR implements Runnable{}
2 重写Runnable接口中的run方法
@Override
public void run(){
线程执行的操作。
如果想获取线程的名称,不能使用getName()方法,因为没有继承Thread类。
可以使用Thread.currentThread.getName();
}
3 创建Runnable接口实现类的对象
SonR obj = new SonR();
4 创建线程 Thread(Runnable r)
Thread 对象名th = new Thread(obj);
5 启动线程
对象名th.start();
方式3:通过Callable接口和FutureTask类创建线程
总结:
创建接口Callable的实现类,重写call方法;
创建FutrueTask对象,使用其构造方法(参数是Callable接口 = 需要接口的实现类对象);
创建Thread类对象,使用其构造方法(参数是FutureTask对象)。
1 创建类实现Callable接口的类
public class CalSon implements Callable{}
2 重写Callable类中的call()方法
@Override
public Object call() throws Exception{
所执行的操作
return 返回值;
}
3 创建子类对象(Callable接口的实现类)
CalSon objCal = new CalSon();
4 创建FutureTask类的对象,并将Callable对象封装
FutureTask ft = new FutureTask(objCal);
5 创建线程,通过Thread(Runnable target),并启动线程
由于FutureTask实现了RunnableFuture接口,该接口又继承自 Runnable接口 和 Future接口
所以可以使用FutureTask对象作为target,创建线程
new Thread(ft).start();
6 获取Call方法的返回值,通过FutureTask对象.get()方法获得
ft.get();
方式4:通过线程池创建线程
有两种类型:Executor 和 ThreadPoolExecutor
Executor有四种方式:newCachedthreadPool、newFixedThreadPool、newScheduleThreadPool、newSingleThreadExecutor。(java多线程)
线程池
说下对线程池的理解?为什么要使用线程池?
- 概念:
线程池是一种多线程处理形式,同样提供了一种限制和管理资源的方式。处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。 - 使用线程池的原因:
对于线程,普通线程的创建采用即建即用的方式,没有线程数量的上限,这样就会忽略执行环境的性能。
线程池的大小 = (物理CPU数量 × CPU核数)+1 - 好处:
优点 | 说明 |
---|---|
降低资源消耗,提高响应速度 | 由于线程可以重复利用,重复利用已创建的线程可以减少线程创建和销毁造成的资源浪费; 当任务到达时,由于不需要等待线程创建,从而提高了任务响应速度 |
统筹内存和CPU的使用 | 根据配置和任务数量灵活地控制线程数量,避免线程太多导致内存溢出和太少导致CPU资源浪费。 |
可以统一管理 | 可以统一管理任务队列和线程,统一开始或结束任务,易于管理且有利于数据统计 |
创建线程池有哪些参数?ThreadPoolExecutor
ThreadPoolExecutor构造方法有7个参数:
参数名 | 介绍 |
---|---|
int corePoolSize | 核心线程数量 要保留在池中的线程数,即使它们处于空闲状态,除非设置了allowCoreThreadTimeOut |
int maximuPoolSize | 最大线程数量 |
long keepAliveTime | 存活时间 当线程数大于核心线程数时,多于的空闲线程在终止之前等待新任务的最大时间 |
TimeUnit unit | 时间单位 |
BlockingQueue< Runnable > workQueue | 阻塞队列 执行任务之前用于保存任务的队列, |
ThreadFactory threadFactory | 拒绝策略 执行程序创建线程时使用的工厂 |
RejectedExecutionHandler handler | 执行被阻止时使用的处理程序,也就是说队列和线程池都满了,线程池处于一种饱和状态,需要采取的一种策略处理提交的新任务。 默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。 |
ThreadPoolExecutor参数介绍
参数名 | 参数值说明 |
---|---|
TimeUnit unit | 配合 keepAliveTime使用,参数值:TimeUnit.xxx其中xxx = DAYS HOURS MINUTES SECONDS MILLISECONDS MICROSECONDS MANOSECONDS |
BlockingQueue< Runnable > workQueue | ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue、DelayQueue、LinedTransferQueue、LinkedBlockingDeque |
BlockingQueue参数具体说明:
阻塞队列名字 | 说明 |
---|---|
ArrayBlockingQueue | 数组结构组成的有界阻塞队列 |
LinkedBlockingQueue | 链表结构组成的有界阻塞队列 |
LinkedTransferQueue | 链表结构组成的无界阻塞队列,和SynchronousQueue类似,还含有非阻塞方式 |
LinkedBlockingDeque | 链表解耦组成的双向阻塞队列 |
SynchronousQueue | 不存储元素的阻塞队列,即直接提交给线程不保持它们 |
PriorityBlockingQueue | 支持优先级排序的无界阻塞队列 |
DelayQueue | 使用优先级队列实现的无界阻塞队列,只有在延时期满时才能从中提取元素 |
RejectExecutionHandler参数介绍
策略 | 说明 |
---|---|
AbortPolicy 默认 | 直接抛出异常 |
CallerRunsPolicy | 只用调用者所在线程来运行任务 线程调用运行该任务的execute本身 |
DiscardPolicy | 不能执行的任务将被删除 |
DiscardOldestPolicy | 丢弃队列里面最近的一个任务,并执行当前任务 如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序 |
如何创建线程池?Executor 和 ThreadPoolExecutor
创建线程有两种形式:Executor 和 ThreadPoolExecutor
Executor
- newCachedThreadPool,一个可缓冲线程池
1 通过newCachedThreadPool创建ExecutorService对象,Executors.newCachedThreadPool()方法
ExecutorService es = Executors.newCachedThreadPool();
2 通过ExecutorService对象的execute(Runnable target)方法创建对象
es.execute(new Runnable(){//采用匿名内部类的形式
@Override
public void run(){
要执行的操作;
}
});
- newFixedThreadPool,一个定长线程池
1 通过newFixedThreadPool创建ExecutorService对象,Executors.newFixedThreadPool()方法
ExecutorService es = Executors.newFixedThreadPool();
2 通过ExecutorService对象的execute(Runnable target)方法创建对象
es.execute(new Runnable(){//采用匿名内部类的形式
@Override
public void run(){
要执行的操作;
}
});
- newScheduledThread,一个定长线程,支持定时和周期性执行
1 通过newScheduledThread创建ScheduledExecutorService对象,Executors.newScheduledThread()方法
ScheduledExecutorService ses = Executors.newFixedThreadPool();
2 通过ScheduledExecutorService对象的schedule(Runnable target)方法创建对象
ses.schedule(new Runnable(){//采用匿名内部类的形式
@Override
public void run(){
要执行的操作;
}
},3,TimeUnit.SECONDS)};
参数介绍:
ScheduledFuture<?> ScheduledExecutorService对象.schedule(Runnable command,long delay,TimeUnit unit) 创建并执行在给定延迟后启动的单次操作。
Runnable command:要执行的任务;long delay:从现在开始延迟执行的时间;TimeUnit unit:延迟参数的时间单位。
- newSingleThreadExecutor,一个单线程化的线程池,只会用唯一的工作线程来执行任务,保证所有任务按照顺序执行
1 通过newSingleThreadExecutor 创建ExecutorService 对象,Executors.newSingleThreadExecutor()方法
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
2 通过ExecutorService对象的execute(Runnable target)方法创建对象
es.execute(new Runnable(){//采用匿名内部类的形式
@Override
public void run(){
要执行的操作;
}
});
ThreadPoolExecutor
1 通过ThreadFactory创建线程工厂,重写 Thread newThread(Runnable r) 方法
ThreadFactory threadfactoryxhj = new ThreadFactory(){
@Override
public Thread newThread(Runnable r){ }
};
2 通过ThreadPoolExecutor创建线程池
ThreadPoolExecutor threadPoolExecutorxhj = new threadPoolExecutor(corePoolsize核心线程数,maximumPoolSize最大线程数,keepAliveTime等待时间,unit等待时间的单位,workQueue阻塞队列,threadfactoryxhj创建线程的工厂);
3 使用自定义的线程工厂,使用匿名构造类的形式,submit参数是Runnable接口,run里面内容是对于创建的线程具体执行什么
threadPoolExecutorxhj.submit( new Runnable(){
@Override
public void run(){}
};
线程池中线程数一般怎么设置?需要考虑那些问题?
线程数的设置
一般针对任务类型而有不同的设置值。多线程执行的任务类型可以分为CPU密集型、I/O密集型、混合型。
任务类型 | 线程数 | 说明 |
---|---|---|
CPU密集型 | 线程数 = CPU核心数(CPU核心数+1) | 这种任务主要消耗的是CPU资源,CPU核心数+1 其中+1为了防止线程偶发的缺页中断以及其他原因导致的任务暂停而带来的影响。一旦任务暂停,CPU就会处于空闲状态,这样多出来的一个线程就可以充分利用CPU资源。 |
I/O密集型 | 线程数 = 2*CPU核心数(CPU核心数/(1-阻塞系数)) | 这种任务系统的大部分时间都是用来处理I/O交互,线程在处理I/O的时间段是不会占用CPU来处理,这是可以将CPU交出给其他线程使用。因此在此任务中,可以多配置一些线程,可以是2N |
混合型 | 线程数 = (线程等待时间/线程CPU时间+1)*CPU核心数 | 即包含CPU密集型又包含I/O密集型 |
扩展
阻塞系数 = 阻塞时间/(阻塞时间 + 计算时间),取值为0-1之间。
所需考虑问题
- 线程池中线程执行的任务的性质
任务类型是CPU密集型则线程数一般等于或略大于CPU核心数;但I/O密集型时间消耗主要是在I/O等待上,CPU压力不大,则线程数一般设置较大2N。 - CPU使用率
可以通过CPU使用率和CPU负载来判断线程是否合理。线程数设置太大,线程的初始化、切换、销毁会消耗CPU资源;任务短时间会迅速执行,任务的集中执行会给CPU造成较大的压力,并且CPU使用率会呈锯齿状。CPU的使用率应该持续稳定在一个合理、平均的数值范围内。 - 内存使用率
线程数过多和队列的大小都会影响内存的使用率。队列的大小可以通过前期计算线程池任务的条数来合理设置。不易太小,太小容易溢出,溢出会走拒绝策略,会影响性能并且会增加复杂度。 - 下游系统抗并发能力
多线程给下游系统造成的并发等于设置的线程数。比如:多线程访问数据库,就需要考虑数据库连接池大小设置,数据库并发太多影响其QPS;下游系统都是接口,考虑是否可以抗住这么多线程数的并发量。
扩展
QPS = 每秒查询率,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。
QPS = 并发量 / 平均相应时间。
执行execute() 和 submit() 方法的区别是什么呢?
区别 | execute() | submit() |
---|---|---|
java描述不同 | 在Executor接口中定义 public void execute(Runnable command) 在将来某个时间执行给定任务 | 在ExecutorService接口中定义 public Future< ? > submit(Runnable task) public Future submit(Callable task)提交一个Runnable任务用于执行,返回一个表示该任务的Future,该Future的get方法在成功完成时将会返回null |
提交任务不同 | 只能提交Runnable类型的任务 | 可以提交Runnable类型和Callable类型的任务 |
异常处理不同 | 会直接抛出任务执行时的异常,可以使用try、catch来捕获,和普通线程的处理方式一致 | 会吃掉异常,通过Future的get方法将任务执行时的异常重新抛出 |
是否有返回值 | 否 | 是 |
Future的get方法
使用get方法会阻塞当前线程直到任务完成;
get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时有可能任务没有执行完。
同步
java线程同步的几种方法?
java的线程同步 = 多个线程在同一个时间内,只能有一个线程执行,其他的线程都等此线程执行完之后才可以继续执行。
有6种方法:synchronized修饰方法或代码块、wait()和notify()方法、特殊变量volatile、可重入锁ReentrantLock、阻塞队列LinkedBlockingQueue、信号量Semaphore。
方式1:采用synchronized关键词修饰的方法/代码块
案例: idea_face-com.javaface4.test1
原始会出问题的类代码:
// idea_face - com.javaface4.test1
public class MethodThread implements Runnable{
// 原始内容
private int ticket = 5;
@Override
public void run() {
for(int i = 0; i < ticket; i++) {
if(ticket >0){
try {
Thread.sleep(300);
}catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "买票:ticket= " + ticket-- );
}
}
}
}
修改后的代码:
// synchronized修饰方法
public class MethodThread implements Runnable{
// synchronized修饰方法
private int ticket = 5;
@Override
public void run() {
for(int i = 0; i< 5 ;i++){
this.sale();
}
}
// synchronized 同步方法
public synchronized void sale(){
if(ticket > 0){
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "买票:ticket = " + ticket--);
}
}
}
// synchronized修饰代码块
public class MethodThread implements Runnable{
// synchronized修饰代码块
private int ticket = 5;
@Override
public void run() {
for (int i = 0; i < 5; i++) {
// synchronized 同步代码块
synchronized (this) {
if(ticket >0){
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "买票:ticket=" + ticket--);
}
}
}
}
}
测试代码:
public static void main(String[] args) {
MethodThread th = new MethodThread();
new Thread(th).start();
new Thread(th).start();
new Thread(th).start();
}
注意
-
由于java的每个对象都有一个内置锁,当用synchronized关键词修饰方法时,内置锁会保护整个方法。在调用此方法之前,需要获得内置锁,否则就处于阻塞状态。
-
语法:
修饰符 synchronized 返回值类型 方法名(){}
- synchronized关键字可以修饰静态方法,如果调用该静态方法,将会锁住整个方法。
- synchronized修饰方法和修饰代码块的区别:
synchronized修饰方法:粗粒度的并发控制,某一时刻只能有一个线程执行该synchronized方法;
synchronized修饰代码块:细粒度的并发控制,只会将代码块中的代码同步,代码块之外的内容可以被其他线程访问。
方式2:wait和notify
// idea_face com.javaface4.test1
public class method2 {
public static void main(String[] args) {
final Object object = new Object();
Thread t1 = new Thread(new Thread() {
@Override
public void run() {
synchronized (object) {
System.out.println("T1 start");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("T1 end");
}
}
});
// 采用匿名内部类的形式,new Thread类表示 该对象是继承Thread类的子类对象。
Thread t2 = new Thread(new Thread(){
@Override
public void run() {
synchronized (object){
System.out.println("T2 start");
object.notify();
System.out.println("T2 end");
}
}
});
t1.start();
t2.start();
// 结果分析:
// T1 start
//T2 start
//T2 end
//T1 end
// 两个线程使用了两个不同的类 和 run方法,
// 执行流程:T1启动,占用了锁→由于执行了wait方法,让出CPU、锁,
// T2获得CPU(锁)→启动→使用notify唤醒休眠线程,此时T1被唤醒等待启动→T2继续执行,
// T2执行完毕,T1获得CPU和锁继续执行。
}
}
注意
wait() 和 notify() 两个方法都属于Object类,不是属于线程的。他们使用在synchronized语句块中。
线程同步 表示 同一对象是不能同时被两个线程用来进入synchronized中的。
wait作用:让出CPU,让正在使用object对象监视器的线程进入等待状态;
notify作用:唤醒调用wait方法进入等待的线程。
wait之后如果没有其他线程将它notify,线程是绝对不可能重新启动的。
- 案例分析1:
四个线程,分别是t1 wait— t2 notify—t3 notify — t4 wait
线程的启动顺序和代码的执行先后顺序理论上是没有关系的。
结果会存在两种情况:
T1 start→T2 start →T2 end → T1 end → T4 start →T3 start → T3 end → T4 end 该情况下 四个线程都执行了。
T1 start→T2 start →T2 end → T1 end →T3 start → T3 end → T4 start 该情况下,线程4会一直等待下去。
方式3:使用特殊域变量volatile实现线程同步
案例书写分析
①使用Runnable接口创建线程,之前的形式和本次不是很一样,多了一个类,在该类中使用volatile修饰成员变量。
实现Runnable接口的实现类 具有私有变量就是多的这个类。
之前的形式:
public class RunCla implements Runnable{
@Override
public void run(){}
}
RunCla objRun = new RunCla();
Thread t1 = new Thread(objRun);
Thread t2 = new Thread(objRun);
t1.start();
t2.start();
之后的形式:
public class volClass{
private volatile 数据类型 value = 值;
}
public class RunCla implements Runnable{
private volClass objVol;
public void RunCla(volCalss objVol){
this.objVol = objVol;
}
@Override
public void run(){
涉及对volClass类中volatile修饰变量的修改。
}
}
public class method{
public static void main(String[] args){
VolClass objVol = new VolClass();
RunCla objRun = new 类Thread(objVol);
Thread t1 = new Thread(objRun);
Thread t2 = new Thread(objRun);
t1.start();
t2.start();
}
② 所有的代码都是一个类中的书写,而不是将各个类分别在不同的文件中书写。
代码:
// idea_face-com.javaface4.test1
public class method3 {
// 一个.java文件中,只能有一个public修饰的class类,但是可以有多个非public修饰的class类
class Bank{
private volatile int account = 10;
public int getAccount() {
return account;
}
public void setAccount(int money){
account += money;
System.out.println("account:" + account);
}
}
// 采用实现Runnable接口的方式创建线程,要求对象所属的类实现Runnable接口,类的成员变量是一个对象,对象的其中一个成员变量被volatile修饰。
class VolatileThread implements Runnable {
private Bank bank;
public VolatileThread(Bank bank){
this.bank = bank;
}
@Override
public void run() {
for(int i = 0;i < 10; i++){
bank.setAccount(10);
System.out.println(Thread.currentThread().getName() + "-->第" + i + "次当前账户余额" + bank.getAccoutac());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 线程的创建
public void userVolatileThread(){
Bank bank = new Bank();
VolatileThread vt1 = new VolatileThread(bank);
Thread t1 = new Thread(vt1);
Thread t2 = new Thread(vt1);
System.out.println("线程1:");
t1.start();
System.out.println("线程2:");
t2.start();
}
public static void main(String[] args) {
method3 mode3 = new method3();
mode3.userVolatileThread();
}
}
注意
volatile关键字为域变量的访问提供了一种免锁机制;
使用Volatile 相当于告诉虚拟机该域可能会被其他线程更新,所以每次使用该域就要重新计算,而不是使用寄存器中的值;
volatile不会提供任何原子操作,它也不能用来修饰final类型的变量;
java规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只有当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。而volatile关键词就是提示JVM,对于该成员变量不能保存它的私有拷贝,而应该直接与共享成员变量交互。
方式4:使用可重入锁实现线程同步ReentrantLock
思路:
1 锁相关的内容在Runnable接口的实现类中书写
public class RunnableClass implements Runnable{}
2 创建ReentrantLock对象
ReentrantLock reentrant = new ReentrantLock();
3 对可能出现问题的代码加锁,结合finally 实现锁的释放
try{
reentrant.lock();
可能出现问题的代码;
}finally{
reentrant.unlock();
}
代码:
//311-test1
public class SellTicket implements Runnable {
private int ticket = 100;
private Lock lock = new ReentrantLock();
@Override
public void run() {
while(true) {
try {
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// Thread.currentThread().getName() 获取当前正在运行的线程的名字
System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "张票");
ticket--;
}
}finally {
lock.unlock();
}
}
}
}
注意
可重入锁指的是:Lock接口的实现类 ReentrantLock():创建一个ReentrantLock的实例。
通过 对象.lock() 方法 获得锁,对象.unlock() 释放锁;
方式5:使用阻塞队列实现线程同步LinkedBlockingQueue
思路
1 创建阻塞队列
LinkedBlockingQueue< E > lbq = new LinkedBlockingQueue< E >();
2 使用Runnable接口创建线程
public class RunCla implements Runnable{
@Override
public void run(){
// 案例生产者 消费者 ,仓库就是共享区域。
if(生产者){
// 生产者需要将产品放入仓库中
lbq.put(产品);
// 然后将线程暂停1000毫秒
Thread.sleep(1000);
}
else(消费者){
// 消费者从仓库中去商品
lbp.take();
// 然后将线程暂停1000毫秒
Thread.sleep(1000);
}
}
}
代码:
// idea_face -com.javaface4.test1
public class method4 {
// 定义一个阻塞队列
private LinkedBlockingDeque<Integer> queue = new LinkedBlockingDeque<Integer>();
// 定义生产商品个数
private static final int size = 10;
// 定义启动线程的标志,为0,启动生产商品的线程;为1,启动消费商品的线程。
private int flag = 0;
// 一个class类中可以定义多个类,但是只能定义一个public类
private class LinkedBlockThread implements Runnable {
@Override
public void run() {
int new_flag = flag++;
System.out.println("线程启动:"+ new_flag);
if(new_flag == 0){//表示启动生产商品的线程
for(int i =0 ; i < size ; i++){
int b = new Random().nextInt(10);
// 产生一个0-10之间的随机数
System.out.println("生产商品:" + b + "号");
try {
queue.put(b);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("仓库中还有商品:" + queue.size() + "个");
System.out.println("生产--------");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}else{// 此时消费者线程启动
for(int i = 0; i< size/2 ;i ++){
// 这里为什么是size/2不理解? 2022/4/28 表示消费商品数量为size/2个,也就是5个。
try {
int n = queue.take();
System.out.println("消费者买了" + n + "号产品");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("仓库中还有商品:" + queue.size() + "个");
System.out.println("消费----------");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args){
// 内部类的访问
method4 mode =new method4();
LinkedBlockThread lbt = mode.new LinkedBlockThread();
Thread th1 = new Thread(lbt);
Thread th2 = new Thread(lbt);
th1.start();
th2.start();
}
}
结果分析:
线程启动:0 // 先启动的是 生产者线程
线程启动:1 // 后启动的是 消费者线程
生产商品:5号 // if中的try
消费者买了5号产品 // else中的try
仓库中还有商品:0个
生产--------
仓库中还有商品:0个
消费----------
生产商品:6号
消费者买了6号产品
仓库中还有商品:0个
生产--------
仓库中还有商品:0个
消费---------- //前面的几种都是:生产者 刚生产了产品,消费者紧接着就购买了产品。
生产商品:0号
仓库中还有商品:1个
生产--------
消费者买了0号产品
仓库中还有商品:0个
消费----------
生产商品:3号
仓库中还有商品:1个
生产--------
消费者买了3号产品
仓库中还有商品:0个
消费----------
生产商品:2号
仓库中还有商品:1个
生产--------
消费者买了2号产品
仓库中还有商品:0个
消费---------- // 这一段是,生产者生产产品后 统计了仓库数量,消费者才进行商品的购买。
生产商品:6号
仓库中还有商品:1个
生产--------
生产商品:1号
仓库中还有商品:2个
生产--------
生产商品:4号
仓库中还有商品:3个
生产--------
生产商品:7号
仓库中还有商品:4个
生产--------
生产商品:7号
仓库中还有商品:5个
生产--------
// 由于代码指定是消费5个商品,所以后面没有再消费商品了。
- 使用LinkedBlockingQueue< E > 实现线程同步。
LinkedBlockingQueue< E > 是一个基于已连接节点的,范围任意的阻塞队列。
该队列按照先进先出的原则排序元素。
队列的头部是在队列中时间最长的元素,队列的尾部是在队列中时间最短的元素,新元素插入到队列的尾部。
获取队列元素的操作只会获取头部元素,如果队列满了或者为空会进入阻塞状态。 - 常用方法:
方法名 | 说明 |
---|---|
LinkedBlockingQueue() | 创建一个容量为Integer.MAX_VALIE的LinkedBlockingQueue |
put( E e ) | 在队尾添加一个元素,如果队列满则阻塞当前线程,直到队列有空位 |
size() | 返回列表中的元素个数 |
take() | 移除并返回头部元素,如果队列空则阻塞当前线程,直到取到元素为止 |
方式6:使用信号量Semaphore
理解
允许多个线程同时访问同一个资源。
思路:
1 创建线程池
ExecutorService exec = Executors.newCachedThreadPool();
2 设置Semaphore信号量
final Semaphore semp = new Semaphore(2);
semp.availablePermits() 获取当前可用的许可数。
semp.acquire() 获取许可
semp.release() 释放许可
exec.execute(Runnable runnable) 在将来某个时间执行给定的命令
exec.shutdown() 启动有序关闭,先前提交的任务被执行,但不接收任何新的任务
代码:
// idea_face -com.javaface4.test1
public class method5 {
public static void main(String[] args){
// 创建线程池 使用newCachedThreadPool
ExecutorService exec = Executors.newCachedThreadPool();
// 设置Semaphore信号量
final Semaphore semp = new Semaphore(2);
// 模拟5个客户端同时访问
for(int i=0;i<5;i++){
final int number = i;
Runnable run = new Runnable() {
@Override
public void run() {
try{
if(semp.availablePermits() >0){
System.out.println(number + "线程启动");
}else{
System.out.println(number + "线程启动,等待排队");
}
// 获取许可
semp.acquire();
System.out.println(number + "线程执行");
// 模拟业务逻辑
Thread.sleep((long)(Math.random()*1000));
// 释放许可
semp.release();
System.out.println(number + "线程释放");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
exec.execute(run);
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
exec.shutdown();
}
}
结果分析
1线程启动
1线程执行
0线程启动
0线程执行 // 由于Semaphore设置允许并发线程数是2 则只有两个线程启动执行 (0 1)
2线程启动,等待排队
3线程启动,等待排队
4线程启动,等待排队
1线程释放 // 线程1 执行结束
2线程执行 // 线程2 执行 (运行线程是0 和 2 )
4线程执行 // 没有得到权限怎么执行的?2022/4/28 可能是因为0线程执行release方法之后,还没有来的急输出“0线程释放”字样时,4线程已经执行acquire方法且输出“4线程执行”
0线程释放 // 运行线程: 4 2
3线程执行 // 此时可能是线程2已经执行了release方法,但是还没有执行输出“2线程释放”
// 同样因为上述原因 3线程执行。 此时执行线程是 3 4
2线程释放 // 线程2 输出“2线程释放”
4线程释放
3线程释放
注意
Semaphore是一个计数信号量,用于控制资源能够被并发访问的线程数量,以保证多个线程能够合理的使用特定资源。
Semaphore 在构造时设置一个许可数量,该数使用AQS.state来记录。
使用Semaphore时有个容易犯错的地方,即先release再acquire会导致Semphore管理的虚拟许可额外新增一个。
常用的方法:
方法 | 说明 |
---|---|
acquire() | 获取许可 获取到许可之后才能继续访问共享资源,且AQS.state减1 获取不到,线程阻塞等待其他线程归还许可 |
release() | 许可归还 AQS.state 加1 归还后,唤醒AQS队列中阻塞的线程获取许可 |
int availablePermits() | 获取当前可用的许可数 |
Semaphore(int number) | 创建Semaphore计数信号量 |
void execute(Runnable command) Executor接口的方法 | 在将来的某个时间执行给定的命令 |
void shutdown() | 启动有序关闭,其中先前提交的任务将被执行,但不会接收任何新任务 |
扩展
AQS:AbstractQueueSynchronizer(抽象队列同步器),用来实现各种锁,各种同步组件。包含了state变量、加锁线程、等待队列并发中的核心组件。常用的ReentrantLock、CountDownLatch等基础库都是基于AQS实现的。
AQS核心思想:如果被请求的核心资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就通过一个基于双向链表的队列阻塞等待。
AQS同步状态——State,由volatile修饰,用于表示当前临界资源获取锁的情况。
访问State字段的方法:
方法名 | 说明 |
---|---|
protected final in getState() | 获取state的值 |
protected final void setState(int newState) | 设置State的值 |
protected final boolean compareAndSetState(int expect,int update) | 使用CAS方式更新State |
信号量 Semaphore 和 线程池 的区别
区别 | Semaphore | 线程池 |
---|---|---|
概念 | 创建了多少线程,实际就会有多少线程执行,只是同时执行的线程数量会受到影响 | 不管创建多少线程,实际可执行的线程数是一定的 |
实际工作线程 | 由开发者自己创建 | 由线程池创建 |
并发线程控制 | 必须手动通过acquire和release方法实现 | 由线程池自动管理 |
设置超时和实现异步访问 | 不支持 | 支持,通过提交callable对象获得Future,从而在需要结果时调用Future的get方法获得线程执行的结果,同时也可以实现超时 |
总结
同步方法、同步块、使用特殊域变量volatile、使用可重入锁、使用局部变量实现同步锁,都是在底层实现的线程同步,但在实际开发中应该尽量远离底层结构。
Thread.interrupt()方法的工作原理是什么?
Thread.interrupt()方法目的
一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。
Thread.interrupt()的作用:不是中断线程,而是通知线程应该中断。
Thread.interrupt()方法调用后结果:
在java中的线程中断interrupt()只是改变了线程的中断状态,具体中断状态改变后带来的结果无法确定。
可以实现让暂停的线程继续执行,或者 暂停线程。
一个对象上调用interrupt()方法,真正有影响的是wait、join、sleep方法,包括它们的的重载方法。wait是Object类的方法,其余是Thread方法。
如果一个线程被调用interrupt()后,它的状态是阻塞的,这个状态是由于正在执行的wait、join、sleep的线程导致的,那么是会改变线程的运行结果。
具体对于wait join sleep的影响是什么
-
wait方法
处于wait状态的等待notify、notifyAll唤醒的线程,其实这个线程已经“暂停”执行,如果它的中断状态被改变,那么它就会抛出异常。这个InterruptedException异常不是线程抛出的,而是wait方法。也就是对象的wait方法内部会不断检查在此对象上休息的线程的状态,如果发现哪个线程的状态被置为已中断,则会抛出InterruptedException,意思就是这个线程不能再等待了,其意义就等同于唤醒它,然后执行catch中的代码。
使用interrupt()方法 和 使用notify和notifyAll方法唤醒线程的区别是:
被notify或notifyAll唤醒的线程,会继续执行wait后面的代码;
而在wait中被中断的线程 则将控制权交给catch语句,不再继续执行wait后面的语句; -
join方法
调用interrupt()方法是唯一手段改变它的中断状态,使它从join中将控制权转到处理异常的catch语句中,然后再由catch中的处理转到正常的逻辑。 -
sleep方法
调用interrupt()方法是唯一手段改变它的中断状态,使它从sleep中将控制权转到处理异常的catch语句中,然后再由catch中的处理转到正常的逻辑。
wait join sleep介绍
wait、join、sleep都会抛出InterruptedException。
源码介绍:
public final native void wait(long timeout) throws InterruptedException;
final = 表示wait是最终方法不能被重写,不能被继承;native = 表示wait将具体执行移交给本地系统的函数库
public static native void sleep(long millis) throws InterruptedException;
static = 表示sleep是静态方法;native = 表示sleep将具体的执行移交给本地系统的函数库;
public final synchronized void join(long millis) throws InterruptedException{}
final = 表示join是最终方法不能被重写,synchronized = 表示join是同步方法。
Thread类
Thread类中有的方法:run() 、start()、getPriority()。