看完了,发现对你有用的话点个赞吧! 持续努力更新学习中!!多线程其他的部分点击我的头像查看更多哦!
知识点
标注:在学习中需要修改的内容以及笔记全在这里 www.javanode.cn,谢谢!有任何不妥的地方望纠正
线程创建
1. 创建方式
- 继续Thread类
- 实现Runable接口
- 实现Callable接口,并与Future、线程池结合使用,
1. 继承Thread
Thread thread = new Thread(){
@Override
public void run() {
System.out.println("this is new thread");
}
};
thread.start();
2. 实现runable接口
Thread thread1 = new Thread(new Runnable() {
public void run() {
System.out.println("impl runnable thread");
}
});
thread1.start();
3. 实现Callable接口
/**
* 3.实现callable接口,提交给ExecutorService返回的是异步执行的结果
*/
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future submit = executorService.submit(new Callable() {
public String call() throws Exception {
return "three new callable thread";
}
});
String returnString = submit.get();
System.out.println(returnString);
2. 总结
- 实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
4):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
线程状态切换
新建状态(New):新创建了一个线程对象。
就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
- 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
线程调度
Java线程的实现:Java线程模型是基于操作系统原生线程模型来实现的;
线程模型只对线程的并发规模和操作成本产生影响,对Java程序的编写和运行过程来说,并没有什么不同。
1. 线程优先级
时分形式是现代操作系统采用的基本线程调度形式
,操作系统将CPU资源分为一个个的时间片,并分配给线程,线程使用获取的时间片执行任务,时间片使用完之后,操作系统进行线程调度,其他获得时间片的线程开始执行;那么,一个线程能够分配得到的时间片的多少决定了线程使用多少的处理器资源,线程优先级则是决定线程可以获得多或少的处理器资源的线程属性
;
可以通过设置线程的优先级,使得线程获得处理器执行时间的长短有所不同,但采用这种方式来实现线程获取处理器执行时间的长短并不可靠(因为系统的优先级和Java中的优先级不是一一对应的,有可能Java中多个线程优先级对应于系统中同一个优先级);Java中有10个线程优先级,从1(Thread.MIN_PRIORITY)到10(Thread.MAX_PRIORITY),默认优先级为5;因此,程序的正确性不能够依赖线程优先级的高低来判断;
2. 线程调度分类
线程调度是指系统为线程分配处理器使用权的过程;主要调度方式有:抢占式线程调度、协同式线程调度
;
2.1 抢占式线程调度
每个线程由系统来分配执行时间,线程的切换不由线程本身决定;Java默认使用的线程调度方式是抢占式线程调度
;我们可以通过Thread.yield()使当前正在执行的线程让出执行时间,但是,却没有办法使线程去获取执行时间;
2.2 协同式线程调度
每个线程的执行时间由线程本身来控制,线程执行完任务后主动通知系统,切换到另一个线程上;
2.3 两种线程调度方式的优缺点
协同式的优点:实现简单,可以通过对线程的切换控制避免线程安全问题;
协同式的缺点:一旦当前线程出现问题,将有可能影响到其他线程的执行,最终可能导致系统崩溃;
抢占式的优点:一个线程出现问题不会影响到其他线程的执行(线程的执行时间是由系统分配的,因此,系统可以将处理器执行时间分配给其他线程从而避免一个线程出现故障导致整个系统崩溃的现象发生)
2.4 结论
Java中,线程的调度策略主要是抢占式调度策略,正是因为抢占式调度策略,导致多线程程序执行过程中,实际的运行过程与我们逻辑上理解的顺序存在较大的区别,也就是多线程程序的执行具有不确定性,从而会导致一些线程安全性问题的发生;
3. 调度方式
3.1 调度的方式
- 线程睡眠:Thread.sleep(long millis)方法,使
线程转到阻塞状态
。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。 - 线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。
- 线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
- 线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
- 线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。
3.2 深入理解(重要)
sleep()
sleep(long millis): 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
join()
join():指等待t线程终止。
join是Thread类的一个方法,启动线程后直接调用,即join()的作用是:等待该线程终止,也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。
案例:
在很多情况下,主线程生成并启动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。
代码:
package cn.javanode.thread.joinUse;
/**
* @author xgt(小光头)
* @version 1.0
* @date 2021-1-10 9:52
*/
public class JoinUseRunnableThread {
static class joinThrad implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +" 线程运行开始!");
for (int i = 0; i < 5; i++) {
System.out.println("子线程"+Thread.currentThread().getName() +"运行 : "+i);
try {
Thread.sleep((int)Math.random()*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
System.out.println("main方法的线程开启");
Thread joinThread = new Thread(new joinThrad());
joinThread.setName("JoinThread");
joinThread.start();
//添加join 子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。
try {
joinThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main方法的线程结束");
}
}
yield()
yield():暂停当前正在执行的线程对象,并执行其他线程
yield()做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。
package cn.javanode.thread.yieldUse;
/**
* @author xgt(小光头)
* @version 1.0
* @date 2021-1-10 10:57
*/
public class ThreadYieldDemo {
static class yieldThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName()+"runing time="+i);
if(i==30){
Thread.yield();
}
}
}
}
public static void main(String[] args) {
//yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。
Thread yt1 = new Thread(new yieldThread());
yt1.setName("ytthread1");
Thread yt2 = new Thread(new yieldThread());
yt2.setName("ytthread2");
yt1.start();
yt2.start();
}
}
3.3 补充
sleep()和yield()的区别
-
- sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;
- yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
补充:
sleep 方法使当前运行中的线程睡眠一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程
-
- sleep 方法允许较低优先级的线程获得运行机会,
- yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程时获得 CPU 占有权。
补充:
在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。
wait和sleep区别
共同点:
多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。
如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。
需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException 。
不同点:
-
所属对象不同
:Thread类的方法:sleep(),yield()等 。Object对象的方法:wait()和notify()等 -
是否释放锁
:每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。sleep()睡眠时,保持对象锁,仍然占有该锁;wait()睡眠时,释放对象锁。但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。 -
使用的位置不同
wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用 -
异常捕获
:sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
补充:
sleep()方法
sleep()使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;
sleep()是Thread类的Static(静态)的方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法时,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。
wait())方法
wait()方法是Object类里的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);其他线程可以访问;wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。
wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。
wait()和notify()、notifyAll()
这三个方法用于协调多个线程对共享数据的存取
,所以必须在synchronized语句块内使用
。synchronized关键字用于保护共享数据,阻止其他线程对共享数据的存取,但是这样程序的流程就很不灵活了,如何才能在当前线程还没退出synchronized数据块时让其他线程也有机会访问共享数据呢?此时就用这三个方法来灵活控制。wait() 方法使当前线程暂停执行并释放对象锁标示,让其他线程可以进入synchronized数据块,当前线程被放入对象等待池中。当调用notify()方法后,将从对象的等待池中移走一个任意的线程并放到锁标志等待池中,只有锁标志等待池中线程能够获取锁标志;如果锁标志等待池中没有线程,则notify()不起作用。notifyAll() 从对象等待池中移走所有等待那个对象的线程并放到锁标志等待池中。(下面的线程间通信部分会细说
)
wait,notify 和notifyAll 这些方法为什么不在 thread类里面
Java提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程来获得 由于 wait notify和notifyAll 都是锁级别的的操作,所以把他们定义在Object类中因为锁属于对象。
线程间通信(重要)
如果你的多线程程序仅仅是每个线程独立完成各自的任务,相互之间并没有交互和协作,那么,你的程序是无法发挥出多线程的优势的,只有有交互的多线程程序才是有意义的程序,否则,还不如使用单线程执行多个方法实现程序来的简单、易懂、有效!
1. java等待通知机制
场景:线程A修改了对象O的值,线程B感知到对象O的变化,执行相应的操作,这样就是一个线程间交互的场景;可以看出,这种方式,相当于线程A是发送了消息,线程B接收到消息,进行后续操作,是不是很像生产者与消费者的关系?我们都知道,生产者与消费者模式可以实现解耦,使得程序结构上具备伸缩性;
- 一种简单的方式是,线程B每隔一段时间就轮询对象O是否发生变化,如果发生变化,就结束轮询,执行后续操作;
缺点: 这种方式不能保证对象O的变更及时被线程B感知,同时,不断地轮询也会造成较大的开销;分析这些问题的症结在哪?其实,可以发现状态的感知是拉取的,而不是推送的,因此才会导致这样的问题产生
- Java内置的经典的等待/通知机制
那就是wait()/notify()/notifyAll(),重要 便于理解例子
/**
如果在调用了此方法之后,其他线程调用notify()或者notifyAll()方法之前,线程被中断,则会清除中断标志并抛出异常
* 当前线程必须拥有对象O的监视器,调用了对象O的此方法会导致当前线程释放已占有的监视器,并且等待
* 其它线程对象O的notify()或者notifyAll()方法,当其它线程执行了这两个方法中的一个之后,并且
* 当前线程获取到处理器执行权,就可以尝试获取监视器,进而继续后续操作的执行
*/
public final void wait() throws InterruptedException {
wait(0);
}
/**
唤醒等待在对象O的监视器上的一个线程,如果多个线程等待在对象O的监视器上,那么将会选择其中的一个进行唤醒
* 被唤醒的线程只有在当前线程释放锁之后才能够继续执行.
* 被唤醒的线程将会与其他线程一同竞争对象O的监视器锁
* 这个方法必须在拥有对象O的监视器的线程中进行调用
* 同一个时刻,只能有一个线程拥有该对象的监视器
*/
public final native void notify();
/**
*唤醒等待在对象O的监视器上的所有线程
* 被唤醒的线程只有在当前线程释放锁之后才能够继续执行.
* 被唤醒的线程将会与其他线程一同竞争对象O的监视器锁
* 这个方法必须在拥有对象O的监视器的线程中进行调用
* 同一个时刻,只能有一个线程拥有该对象的监视器
*/
public final native void notifyAll();
2. 经典的等待/通知机制代码
package cn.javanode.thread.JavaWaitAndConsumer;
public class WaitAndNotify {
//轮询标志位
private static boolean stop = false;
//监视器对应的对象
private static Object monitor = new Object();
//等待线程
static class WaitThread implements Runnable{
@Override
public void run() {
synchronized(monitor){
//循环检测标志位是否变更
while(!stop){
try {
//标志位未变更,进行等待 锁释放,整个线程等待
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//被唤醒后获取到对象的监视器之后执行的代码
System.out.println("1Thread "+Thread.currentThread().getName()+" is awakened at first time");
stop = false;
}
//休眠1秒之后,线程角色转换为唤醒线程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//与上述代码相反的逻辑
synchronized(monitor){
while(stop){
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
monitor.notify();
stop = true;
System.out.println("2Thread "+ Thread.currentThread().getName()+" notifies the waitted thread at first time");
}
}
}
//通知线程
static class NotifyThread implements Runnable{
@Override
public void run() {
synchronized (monitor){
while(stop){
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
stop = true;
monitor.notify();
System.out.println("3Thread "+ Thread.currentThread().getName()+" notifies the waitted thread at first time");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (monitor){
while(!stop){
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("4Thread "+Thread.currentThread().getName()+" is awakened at first time");
}
}
}
public static void main(String[] args){
Thread waitThread = new Thread(new WaitThread());
waitThread.setName("waitThread");
Thread notifyThread = new Thread(new NotifyThread());
notifyThread.setName("notifyThread");
waitThread.start();
notifyThread.start();
}
}
通过上述代码,可以提炼出等待通知机制的经典模式:
等待方实现步骤:
- 加锁同步
- 条件不满足,进入等待,被唤醒之后,继续检查条件是否满足(循环检测)
- 条件满足,退出循环,继续执行后续代码
synchronized(obj){
while(condition不满足){
obj.wait();
}
//后续操作
}
通知方实现步骤:
- 加锁同步
- 条件不满足,跳过循环检测
- 设置条件并唤醒线程
synchronized(obj){
while(condition不满足){
obj.wait();
}
更新condition
obj.notify();
//后续操作
}
3. 生产者消费者代码
package cn.javanode.thread.JavaWaitAndConsumer;
public class ProducerAndConsumer {
//商品库存
private static int storeMount = 0;
//监视器对应的对象
private static Object monitor = new Object();
//生产者线程
static class ProducerThread implements Runnable{
@Override
public void run() {
try {
produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void produce() throws InterruptedException {
while(true){
synchronized(monitor){
//循环检测库存是否大于0,大于0表示还有商品可以消费,线程等待消费者消费商品
while(storeMount > 0){
monitor.wait();
}
//被唤醒后获取到对象的监视器之后执行的代码
System.out.println("Thread "+Thread.currentThread().getName()+" begin produce goods");
//生产商品
storeMount = 1;
//唤醒消费者
monitor.notify();
Thread.sleep(1000);
}
}
}
}
//消费者线程
static class ConsumerThread implements Runnable{
@Override
public void run() {
try {
consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void consume() throws InterruptedException {
while(true){
synchronized (monitor){
//检测库存是否不为0,如果不为0,那么有商品可供消费,否则等待生产者生产商品
while(storeMount == 0){
monitor.wait();
}
//消费商品
storeMount = 0;
//唤醒生产者线程
monitor.notify();
System.out.println("Thread "+Thread.currentThread().getName()+" begin consume goods");
Thread.sleep(1000);
}
}
}
}
public static void main(String[] args){
Thread producerThread = new Thread(new ProducerThread());
producerThread.setName("producerThread");
Thread consumerThread = new Thread(new ConsumerThread());
consumerThread.setName("consumerThread");
producerThread.start();
consumerThread.start();
}
}
上述代码示例演示了一个生产者生产商品和一个消费者消费商品的场景,对于一个生产者多个消费者、多个生产者一个消费者、多个生产者多个消费者等场景,只需要将唤醒的方法换为notifyAll()即可,否则,会出现饥饿现象!
4. 总结
以上就是本文叙述的所有内容,本文首先对于给出Java中线程调度形式,引出多线程编程中需要解决的线程安全问题,并分析线程安全问题,给出解决线程安全问题的常用手段(加锁同步),最后,结合Java内置的等待通知机制,进行了样例代码的展示以及分析,给出了经典的等待通知机制的编程范式,最后,基于等待通知机制给A出了生产者消费者模式的实现样例,希望本文能给想要学习多线程编程的朋友一点帮助,如有不正确的地方,还望指出,十分感谢!
注意细节(了解)
- 线程分类
- 用户线程:大多数线程都是用户线程,用于完成业务功能
- 守护线程:支持型线程,主要用于后台调度以及支持性工作,比如GC线程,当JVM中不存在非守护线程时,JVM将会退出
- Thread.setDaemon(true)来设置线程属性为守护线程,该操作必须在线程调用start()方法之前执行
- 守护线程中的finally代码块不一定会执行,因此不要寄托于守护线程中的finally代码块来完成资源的释放
- 线程交互的方式
- join
- sleep/interrupt
- wait/notify
- 启动线程的方式
- 只能通过线程对象调用start()方法来启动线程
- start()方法的含义是,当前线程(父线程)同步告知虚拟机,只要线程规划期空闲,就应该立即启动调用了start()方法的线程
- 线程启动前,应该设置线程名,以便使用Jstack分析程序中线程运行状况时,起到提示性作用
- 终止线程的方式
- 中断检测机制
- 线程通过调用目标线程的interrupt()方法对目标线程进行中断标志,目标线程通过检测自身的中断标志位(interrupted()或isInterrupted())来响应中断,进行资源的释放以及最后的终止线程操作;
- 抛出InterruptedException异常的方法在抛出异常之前,都会将该线程的中断标志位清除,然后抛出异常
- suspend()/resume()(弃用)
- 调用后,线程不会释放已经占有的资源,容易引发死锁问题
- stop()(弃用)
- 调用之后不一定保证线程资源的释放
- 中断检测机制
- 锁释放的情况:
- 同步方法或同步代码块的执行结束(正常、异常结束)
- 同步方法或同步代码块锁对象调用wait方法
- 锁不会释放的情况:
- 调用Thead类的静态方法yield()以及sleep()
- 调用线程对象的suspend()