Java多线程

并发编程的优点:
利用多核CPU的计算能力,利用好多线程机制可以大大提高系统的并发能力及性能;面对复杂的业务模型,并行程序会比串行程序更适应业务需求,而并发编程更适合这种业务拆分。
缺点:内存泄漏,线程安全,死锁等

并发编程的三要素:
原子性:原子是不可再分割的最小单元,原子性是指一个或多个操作要么全部执行成功,要么全部执行失败。
可见性:一个线程对共享变量的修改,另一个线程能看到(synchronized,volatile)
有序性:程序的执行顺序按照代码的先后顺序
线程安全的问题原因有:

  1. 线程切换带来的原子性问题
  2. 缓存导致的可见性问题
  3. 编译优化带来的有序性问题
    解决方案:
    JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
    synchronized、volatile、LOCK,可以解决可见性问题
    Happens-Before 规则可以解决有序性问题
    ==================================================
    多线程的优劣:
    可以提高CPU的利用率
    缺点:
    线程也是程序,线程也需要占内存,线程多内存也占的也多。
    多线程需要协调和管理,所以需要CPU跟踪线程。
    线程之间共享资源的访问会相互影响,必须解决禁用共享资源的问题
    ===================================================
    线程死锁:
    由于竞争资源或由于彼此通信造成的一种堵塞的现象,若无外力的作用下,都将无法推进,此时的系统处于死锁状态。
    如图,线程A拥有的资源2,线程B拥有的资源1,此时线程A和线程B都试图去拥有资源1和资源2,但是它们的🔒还在,因此就出现了死锁。

===================================================
形成死锁的四个必要条件:
1、互斥条件:线程(进程)对所分配的资源具有排它性,即一个资源只能被一个进程占用,直到该进程被释放。
2、请求与保持条件:一个进程(线程)因请求被占有资源而发生堵塞时,对已获取的资源保持不放。
3、不剥夺条件:线程(进程)已获取的资源在未使用完之前不能被其他线程强行剥夺,只有等自己使用完才释放资源。
4、循环等待条件:当发生死锁时,所等待的线程(进程)必定形成一个环路,死循环造成永久堵塞。

如何避免死锁:
我们只需破坏形参死锁的四个必要条件之一即可。
破坏互斥条件:无法破坏,我们的🔒本身就是用来对线程(进程)产生互斥
破坏请求与保持条件:一次申请所有资源
破坏不剥夺条件:占有部分资源的线程尝试申请其它资源,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件:按序来申请资源。

什么是上下文的切换:
当前任务执行完,CPU时间片切换到另一个任务之前会保存自己的状态,以便下次再切换会这个任务时可以继续执行下去,任务从保存到再加载执行就是一次上下文切换。

创建线程的四种方式:
1、继承Thread类
2、实现Runnable接口
3、实现Callable接口
4、Executors( 沃丝炊 )工具类创建线程池

Runnable接口和Callable接口有何区别:
相同点:
Runnable和Callable都是接口
都可以编写多线程程序
都采用Thread.start()启动线程
不同点:
Runnable接口run方法无返回值,Callable接口call方法有返回值,是个泛型,和Futrue和FutureTask配合用来获取异步执行结果。
Runable接口run方法只能抛出运行时的异常,且无法捕获处理;Callable接口call方法允许抛出异常,可以获取异常信息。
注:Callable接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会堵塞主线程继续往下执行,如果不调用就不会堵塞。

run()方法和start()方法有和区别:
每个线程都是通过某个特定的Thread对象对应的run()方法来完成其操作的,run方法称为线程体,通过调用Thread类的start方法来启动一个线程。
start()方法用于启动线程
run()方法用于执行线程的运行代码
run()可以反复调用
而start()方法只能被调用一次
调用start()方法无需等待run()方法体代码执行结束,可以直接继续执行其它的代码;
调用start()方法线程进入就绪状态,随时等该CPU的调度,然后可以通过Thread调用run()方法来让其进入运行状态,run()方法运行结束,此线程终止,然后CPU再调度其它线程。

为什么调用start()方法会执行run()方法,为什么不能直接调用run()方法:
new Thread,线程进入了新建的状态,start方法的作用是使线程进入就绪的状态,当分配到时间片后就可以运行了。start方法会执行线程前的相应准备工作,然后在执行run方法运行线程体,这才是真正的多线程工作。
如果直接执行了run方法,run方法会被当作一个main线程下的普通方法执行,并不会在某个线程中去执行它,所以这并不是多线程工作。
小结:调用start方法启动线程可使线程进入就绪状态,等待运行;run方法只是thread的一个普通方法调用,还是在主线程里执行。

什么是Callable和Future:
Callable接口也类似于Runnable接口,但是Runnable不会接收返回值,并且无法抛出返回结果的异常,而Callable功能更强大,被线程执行后,可有返回值,这个返回值可以被Future拿到,也就是说Future可以拿到异步执行任务的返回值。
Future接口表示异步任务,是一个可能没有完成的异步任务结果,所以说Callable用于产生结果,Future用于接收结果。

什么是FutureTask:
FutureTask是一个异步运算的任务,FutureTask里面可以可以传入Callable实现类作为参数,可以对异步运算任务的结果进行等待获取,判断是否已经完成,取消任务等操作。只有当结果完成之后才能取出,如果尚未完成get方法将堵塞。一个Future对象可以调用Callable和Runable的对象进行包装,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。
线程状态和基本操作========
线程声明周期的6种状态:

新创建:又称初始化状态,这个时候Thread才刚刚被new出来,还没有被启动。
可运行状态:表示已经调用Thread的start方法启动了,随时等待CPU的调度,此状态又被称为就绪状态。
被终止:死亡状态,表示已经正常执行完线程体run()中的方法了或者因为没有捕获的异常而终止run()方法了。
计时状态:调用sleep(参数)或wait(参数)后线程进入计时状态,睡眠时间到了或wait时间到了,再或者其它线程调用notify并获取到锁之后开始进入可运行状态。另一种情况,其它线程调用notify没有获取到锁或者wait时间到没有获取到锁时,进入堵塞状态。
无限等待状态:获取锁对象后,调用wait()方法,释放锁进入无限等待状态
锁堵塞状态:wait(参数)时间到或者其它线程调用notify后没有获取到锁对象都会进入堵塞状态,只要一获取到锁对象就会进入可运行状态。
堵塞状态的详解:

================================================
Java用到的线程调度算法是什么?
计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获取到CPU的使用权才能执行指令,所谓多线程的并发运行,其实从宏观上看,各线程轮流获取CPU的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待CPU的调度,JVM有一项任务就是负责CPU的调度,线程调度就是按照特定的机制为多个线程分配CPU的使用权。
有两种调度模型:分时调度 和 抢占式调度
分时调度就是让所有的线程轮流获得CPU的使用权,并且平均分配到各个线程占有CPU的时间片。
抢占式调度:Java虚拟机采用抢占式调度模型,是指优先让线程池中优先级高的线程首先占用CPU,如果线程池中优先级相同,那么随机选择一个线程,使其占有CPU,处于这个状态的CPU会一直运行,优先级高的分的CPU的时间片相对会多一点。

Java线程调度策略
线程调度优先选择优先级高的运行,但是如果出现一下情况,就会终止运行(不是进入死亡状态):
1、线程调用了yield方法让出CPU的使用权,线程进入就绪状态。
2、线程调用sleep()方法,使其进入计时状态
3、线程由于IO受阻
4、另一个更高的优先级线程出现
5、在支持的时间片系统中,该线程的时间片用完。

什么是线程调度(Thread Scheduler)和时间分片(Time Slicing )
线程调度是一个操作系统服务,它负责为储在Runnable状态的线程分配CPU时间片,一旦我们创建一个线程并启动它,它的执行便依赖线程调度器的实现。
时间分片是指CPU可用时间分配给Runnable的过程,分配的时间可以根据线程优先级或线程等待时间。

Java线程同步和线程调度的相关方法
1、wait():调用后线程进入无限等待状态,并释放所持对象的锁
2、sleep():使一个线程进入休眠状态(堵塞状态),带有对象锁,是一个静态方法,需要处理InterruptException异常。
3、notify():唤醒一个处于等待状态的线程(无限等待或计时等待),如果多个线程在等待,并不能确切的唤醒一个线程,与JVM确定唤醒那个线程,与其优先级有关。
4、notityAll():唤醒所有处于等待状态的线程,但是并不是将对象的锁给所有的线程,而是让它们去竞争,谁先获取到锁,谁先进入就绪状态。

sleep()和wait()有什么区别
两者都可以使线程进入等待状态
类不同:sleep()是Thread下的静态方法,wait()是Object类下的方法
是否释放锁:sleep()不释放锁,wait()释放锁
用处不同:wait()常用于线程间的通信,sleep()常用于暂停执行。
用法不同:wait()用完后,线程不会自动执行,必须调用notify()或notifyAll()方法才能执行,sleep()方法调用后,线程经过过一定时间会自动苏醒,wait(参数)也可以传参数使其苏醒。
它们苏醒后还有所区别,因为wait()会释放锁,所以苏醒后没有获取到锁就进入堵塞状态,获取到锁就进入就绪状态,而sleep苏醒后之间进入就绪状态,但是如果cpu不空闲,则进入的是就绪状态的堵塞队列中。

如何调用wait()方法的,使用if还是循环
处以等待状态的线程可能会收到错误警告或伪唤醒,如果不在循环中检查等待条件,程序可能会在没有满足条件的时候退出。

===================================================
为什么线程通信方法wait(),notify(),notifyAll()要被定义到Object类中
Java中任何对象都可以被当作锁对象,wait(),notify(),notifyAll()方法用于等待获取唤醒对象去获取锁,Java中没有提供任何对象使用的锁,但是任何对象都继承于Object类,所以定义在Object类中最合适。

为什么线程通信方法wait(),notify(),notifyAll()要在同步代码块或同步方法中被调用?
wait(),notify(),notifyAll()方法都有一个特点,就是对象去调用它们的时候必须持有锁对象。
如对象调用wait()方法后持有的锁对象就释放出去,等待下一个线程来获取。
如对象调用notifyAll()要唤醒等待中的线程,也要讲自身用于的锁对象释放,让就绪状态中的线程竞争获取锁。
由于这些方法都需要线程持有锁对象,这样只能通过同步来实现,所以它们只能在同步块或同步方法中被调用。

Thread的yiele方法有什么作用?
让出CPU的使用权,使当前线程从运行状态进入就绪状态,等待CPU的下次调度。

为什么Thread的sleep和yield是静态的?
Thread类的sleep()和yield()方法将在当前正在运行的线程上工作,所以其它处于等待状态的线程调用它们是没有意义的,所以设置为静态最合适。

线程sleep和yield方法有什么区别
线程调用sleep()方法进入堵塞状态,醒来(因为没有释放锁)后直接进入了就绪状态,运行yield后也没有释放锁,于是进入了就绪状态。
sleep()方法使用时需要处理InterruptException异常,而yield没有。
sleep()执行后进入堵塞状态(计时等待),醒来后进入就绪状态(可能是堵塞队列),而yield是直接进入就绪状态。

如何停止一个正在运行的线程?
使用stop方法终止,但是这个方法已经过期,不被推荐使用。
使用interrupt方法终止线程
run方法执行结束,正常退出

如何在两个线程间共享数据?
两个线程之间共享变量即可实现共享数据。
一般来说,共享变量要求变量本身是线程安全的,然后在线程中对变量使用。

同步代码块和同步方法怎么选?
同步块是更好的选择,因为它不会锁着整个对象,当然也可以让它锁住整个对象。同步方法会锁住整个对象,哪怕这个类中有不关联的同步块,这通常会导致停止继续执行,并等待获取这个对象锁。
同步块扩展性比较好,只需要锁住代码块里面相应的对象即可,可以避免死锁的产生。
原则:同步范围越小越好。

什么是线程安全?Servlet是线程安全吗?
线程安全是指某个方法在多线程的环境下被调用时,能够正确处理多线程之间的共享变量,程序能够正确完成。
Servlet不是线程安全的,它是单实例多线程的,当多个线程同时访问一个方法时,不能保证共享变量是安全的。
Struts2是多实例多线程的,线程安全,每个请求过来都会new一个新的action分配这个请求,请求完成后销毁。
springMVC的controller和Servlet一样,属性单实例多线程的,不能保证共享变量是安全的。
Struts2好处是不用考虑线程安全问题,springMVC和Servlet需要考虑。
如果想既可以提升性能又可以不能管理多个对象的话建议使用ThreadLocal来处理多线程。

Java中是如何保证多线程安全的?
1、使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
2、使用自动锁,synchronized锁
3、Lock lock = new ReentrantLock(),使用手动锁lock .lock(),lock.unlock()方法

线程同步和线程互斥的区别
线程同步:当一个线程对共享数据进行操作的时候,在没有完成相关操作时,不允许其它的线程来打断它,否则就会破坏数据的完整性,必然会引起错误信息,这就是线程同步。
线程互斥:而线程互斥是站在共享资源的角度上看问题,例如某个共享资源规定,在某个时刻只能一个线程来访问我,其它线程只能等待,知道占有的资源者释放该资源,线程互斥可以看作是一种特殊的线程同步。
实现线程同步的方法:
1、同步代码块:sychronized(对象){} 块
2、同步方法:sychronized修饰的方法
3、使用重入锁实现线程同步:reentrantlock类的锁又互斥功能,Lock lock = new ReentrantLock(); Lock对象的ock和unlock为其加锁

对乐观锁和悲观锁的理解
乐观锁:每个去拿数据的时候都认为别人不会修改,所以都不会上锁,但是在更新的时候会判断一下在此期间有没有去更新这个数据。所以乐观锁使用了多读的场合,这样可以提高吞吐量,像数据库提供的类似write_condition机制,都是用的乐观锁,还有那个原子变量类,在java.util.concurrent.atomic包下
悲观锁:总是假设最坏的情况,每次去拿数据的时候都会认为有人会修改,所以每次在拿数据的时候都会上锁。这样别的对象想拿到数据,那就必须堵塞,直到拿到锁。传统的关系型数据库用到了很多这种锁机制,比如读锁,写锁,在操作之前都会先上锁,再比如Java的同步代码块synchronized/方法用的也是悲观锁。

有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?
可以使用 join() 方法实现,
thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。
比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
t.join(); //调用join方法,等待线程t执行完毕
t.join(1000); //等待 t 线程,等待时间是1000毫秒。
//新建线程
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(“线程:”+Thread.currentThread().getName()+“执行完成”);
}
};
Thread t1 = new Thread(runnable, “t1”);//线程进入了新建的状态
Thread t2 = new Thread(runnable, “t2”);//线程进入了新建的状态
Thread t3 = new Thread(runnable, “t3”);//线程进入了新建的状态
try {
//T2在T1执行完后执行,T3在T2执行完后执行 使用join()方法实现
t2.start();//线程进入就绪的状态,等待获取CPU的执行权…
t2.join();
t1.start();//线程进入就绪的状态,等待获取CPU的执行权…
t1.join();
t3.start();//线程进入就绪的状态,等待获取CPU的执行权…
t3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
=线程池==
什么是线程池
java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池
多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。
一个线程池包括以下四个基本组成部分:
1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。

常见线程池
Executors( 沃丝炊 )类里面提供了一些静态工厂,生成一些常用的线程池
①newSingleThreadExecutor
单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务
②newFixedThreadExecutor(n)
固定数量的线程池,没提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行
③newCacheThreadExecutor(推荐使用)
可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。
④newScheduleThreadExecutor
大小无限制的线程池,支持定时和周期性的执行线程

为什么不建议使用 Executors静态工厂构建线程池
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让实现的人更加明确线程池的运行规则,规避资源耗尽的风险
Executors返回的线程池对象的弊端如下:
1:FixedThreadPool 和 SingleThreadPool:
允许的请求队列(底层实现是LinkedBlockingQueue)长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
2:CachedThreadPool 和 ScheduledThreadPool
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
创建线程池的正确姿势:
避免使用Executors创建线程池,主要是避免使用其中的默认实现,那么我们可以自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。

或者是使用开源类库:开源类库,如apache和guava

线程池常用参数

corePoolSize:核心线程数量,会一直存在,除非allowCoreThreadTimeOut设置为true
maximumPoolSize:线程池允许的最大线程池数量
keepAliveTime:线程数量超过corePoolSize,空闲线程的最大超时时间
unit:超时时间的单位
workQueue:工作队列,保存未执行的Runnable 任务
threadFactory:创建线程的工厂类
handler:当线程已满,工作队列也满了的时候,会被调用。被用来实现各种拒绝策略。

常用开源库的使用

================================================
new Thread的弊端

每次new Thread新建对象性能差。
线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
缺乏更多功能,如定时执行、定期执行、线程中断。 相比new Thread,Java提供的四种线程池的好处在于:
重用存在的线程,减少对象创建、消亡的开销,性能佳。
可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
提供定时执行、定期执行、单线程、并发数控制等功能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星空 | 永恒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值