Java基础知识--多线程

1.多线程介绍及运行原理

进程:

-正在运行的程序。确切的来说当一个程序进入内存运行即变成一个进程,进程是处于运行过程中的 程序,并且具有一定独立功能,进程是系统进行资源分配和调度的一个独立单位。
-进程是正在运行的程序,进程负责给程序分配内存空间,而每一个进程都是由程序代码组成的,这些代码在进程中执行的流程就是线程。

线程:

-线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进 程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程,但至少有一个线程。

多线程

即就是一个程序中有多个线程在同时执行

多线程运行原理

大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个任务。

例:

我们一边使用浏览器,一边使用QQ,同时还开着画图板,dos窗口等软件。感觉这些软件好像在同时运行着。其实这些软件在某一时刻,只会运行一个进程。

解释例子:

-这是由于CPU(中央处理器)在做着高速的切换而导致的。
对于CPU而言,它在某个时间点上,只能执行一个程序,即就是说只能运行一个进程,CPU不断地在这些进程之间切换。
因为CPU的执行速度相对 我们的感觉实在太快了,虽然CPU在多个进程之间轮换执行,但我们自己感到好像多个进程在同时执行。 
如果我们开启的程序过多,CPU切换到每一个进程的时间也会变长,我们也会感觉机器运行变慢。

-所以合理的使用多线程可以提高效率,但是大量使用,并不能给我们带来效率上的提高。

2.主线程及如何创建线程

主线程介绍:

-我们在运行写过的代码时,当我们在dos命令行中输入java空格类名回车后,启动JVM,并且加载对应的class文件。
-虚拟机并会从main方法开始执行我们的程序代码,一直把main方法的代码执行结束。
如果,在执行过程遇到循环时间比较长的代码,那么在循环之后的其他代码是不会被执行的。

如下代码演示:

class Demo {         
	String name;         
	Demo(String name) {                 
 		this.name = name;         
    }         
    void show() {               
   		for (int i=1;i<=20 ;i++ )  {                       			    				
   			System.out.println("name="+name+",i="+i);               
        }        
     } 
 }   

class ThreadDemo {        
 	public static void main(String[] args)         {                 
 		Demo d = new Demo("小强");         
 		Demo d2 = new Demo("旺财");                 
		d.show();                         
	    d2.show();                 
	    System.out.println("Hello World!");         
    }
 } 

解释代码:
-若在上述代码中show方法中的循环执行次数很多,这时书写在d.show();下面的代码是不会执行的,并且在dos窗口会看到不停的输出name=小强,i=值,这样的语句。
-原因是:jvm启动后,必然有一个执行路径(线程)从main方法开始的。一直执行到main方法结束。这个线程在java中称之为主线程。当主线程在这个程序中执行时,如果遇到了循环而导致程序在指定位置停留时间过长,无法执行下面的程序。
-可不可以实现一个主线程负责执行其中一个循环,由另一个线程负责其他代码的执行。实现多部分代码同时执行。这就是多线程技术可以解决的问题

创建线程方式一继承Thread类

创建线程的步骤:

定义一个类继承Thread。
重写run方法。 
创建子类对象,就是创建线程对象。 
调用start方法,开启线程并让线程执行,同时还会告诉jvm去调用run方法

在这里插入图片描述
在这里插入图片描述

线程对象调用run方法不开启线程。仅是对象调用方法。线程对象调用start开启线程,并让jvm调用run方 法在开启的线程中执行。

继承Thread类原理

继承Thread类:
	因为Thread类描述线程事物,具备线程应该有功能。 

Thread t1 = new Thread(); 
t1.start();//这样做没有错,但是该start调用的是Thread类中的run方法,而这个run方法没有做什么事情,更重要的是这个run方法中并没有定义我们需要让线程执行的代码(所以不能直接创建Thread类的对象)

-创建线程的目的
	是为了建立单独的执行路径,让多部分代码实现同时执行。也就是说线程创建并执行需要给定的代码(线 程的任务)。

-自定义线程需要执行的任务都定义在run 方法中。Thread类中的run方法内部的任务并不是我们所需要,只有重写这个run方法,既然Thread类已经定 义了线程任务的位置,只要在位置中定义任务代码即可。所以进行了重写run方法动作 

3.多线程的内存图解及获取线程名称

多线程内存图解

以上个程序为例

在这里插入图片描述
图解解释:

-多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈
-当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了 

获取线程名称

开启的线程都会有自己的独立运行栈内存

如何线程的名字

Thread.currentThread()获取当前线程对象 
Thread.currentThread().getName();获取当前线程对象的名称

在这里插入图片描述
代码解释:

原来主线程的名称: main 
自定义的线程: Thread-0  线程多个时,数字顺延。Thread-1...... 
进行多线程编程时不要忘记了Java程序运行时从主线程开始,main方法的方法体就是主线程的线程执行体

多线程的异常信息

在这里插入图片描述
当运行上面的程序会看到有如下的异常信息
在这里插入图片描述
以前我们讲过被用蓝色线标注出来的是异常的名称。

我们修改程序继承运行看看
在这里插入图片描述
继承运行上面的程序发现有点小变化

-发现被黄色框选中的部分,有些变化,分别在说明每个异常发生具体的哪一个线程上。
并且线程任务中的 System.out.println("over");语句并没有打印出来。是因为没有对这个异常进行处理,此时这个功能会中止执行,所以不会打印出over这个字符串。
- 我们发现main方法中的Hello World!字符串却打印出来了,是因为在这个程序中有三个线程,分别是主线程,Thread-0,Thread-1这三个线程。
异常发生在Thread-0,Thread-1线程上,而 main线程并没有发生异常,所以主线程正常运行完成。 

注意:

-当主线程执行完成了,并不代表程序就结束,如果此时还有其他线程正常执行,程序仍然在执行过程中。
-当任何一个线程出现了异常,其他线程还是会继续运行的。异常只会影响到异常所属的那个线程。

创建线程方式二实现Runnable接口

Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为 run 的无参数方法。

创建线程的第二种方式:实现Runnable接口。

1. 定义类实现Runnable接口。
2.覆盖接口中的run方法。 
2. 创建Thread类的对象 
3. 将Runnable接口的子类对象作为参数传递给Thread类的构造函数。 
5 .调用Thread类的start方法开启线程

代码演示:
在这里插入图片描述
输出结果:
在这里插入图片描述

实现Runnable的原理

继承Thread类和实现Runnable接口有啥区别

-实现Runnable接口,避免了继承Thread类的单继承局限性。覆盖Runnable接口中的run方法,将线程任务代码定义到run方法中。
-创建Thread类的对象,只有创建Thread类的对象才可以创建线程。线程任务已被封装到Runnable接口的run方法中,而这个run方法所属于Runnable接口的子类对象,所以将这个子类对象作为参数传递给Thread的构造函数,这样,线程对象创建时就可以明确要运行的线程的任务。

实现Runnable的好处

-实现Runnable接口避免了单继承的局限性,所以较为常用。实现Runnable接口的方式,更加的符合面向对象,
-线程分为两部分,一部分线程对象,一部分线程任务。
  继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。
  实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦

多线程联系——售票
大家都去过火车站购买车票,来模拟窗口售票。售票的动作需要同时执行,所以使用多线程技术
在这里插入图片描述
如果我们创建一个线程对象,多次调用其start()方法,多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
多次开启同一个线程会发生IllegalThreadStateException (指示线程没 有处于请求操作所要求的适当状态时抛出的异常。)

4.多线程状态图
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190312153040319.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2luZmVybm9fZGV2aWw
=,size_16,color_FFFFFF,t_70)
5.线程的安全问题

上述买票的例子,继续进行分析发现,当四个线程都开启之后,CPU会在这四个线程之间随机切换,切换 过程中会发生如下图所述的问题。
在这里插入图片描述
当四个线程把票卖的只剩下后一张时,发生了上图的情况,这时会发现输出的票出现0号票、-1号票和-2号票。对于多线程操作怕的就是出现线程安全问题
在这里插入图片描述
线程安全问题发生的原因
对上述代码进行图解说明发现,得出发生问题的原因

在这里插入图片描述
问题产生的原因;

1.线程任务中在操作共享的数据。 
2. 线程任务操作共享数据的代码有多条(运算有多个) 

解决问题:

- 只要让一个线程在执行线程任务时将多条操作共享数据的代码执行完,在执行过程中,不要让其他线程参与运算。
- 解决这个问题Java中给我们提供相应的独立代码块,这段代码块需要使用关键字synchronized来标识其 为一个同步代码块。 

synchronized关键字

synchronized关键字在使用时需要个对象作为标记,当任何线程进入synchronized标识的这段代码时, 首先都会先判断目前有没有线程正在使用synchronized标记对象,若有线程正在使用这个标记对象, 那么当前这个线程就在synchronized标识的外面等待,直到获取到这个标记对象后,这个线程才能执行同步代码块。

在这里插入图片描述
同步的好处和弊端

同步好处:解决多线程安全问题。这里举例(火车上的卫生间)说明同步锁机制。 
同步弊端:降低了程序的性能。每个线程都要去判断锁机制,那么会增加程序运行的负担,同时只要做判 断,CPU都要处理,那么也会消耗CPU的资源。即就是加同步会降低程序的性能。

在这里插入图片描述
在这里插入图片描述

当线程任务代码只会有一个线程执行时,加不加同步都可以。
当线程任务代码会有被多个线程执行时,这时需要加同步,但是加同步时一定要保证多个线程使用的是同一把锁。
上述代码发生的安全问题就是因为每个线程都有自己的Object对象作为自己的锁。

同步的前提:

必须保证多个线程在同步中使用的是同一个锁。

注意:

当多线程安全问题发生时,加入了同步后,问题依旧,就要通过这个同步的前提来判断同步是否写正确。 

多线程安全问题练习

两个客户到一个银行去存钱,每个客户一次存100,存3次
在这里插入图片描述
在这里插入图片描述
说明多线程的随机性造成了安全问题发生。哪的问题啊?

1.既然是多线程的问题,问题发生在线程任务内。
2. 任务代码中是否有共性数据呢?有的,bank对象的中的sum。 
3. 是否有对sum进行多次运算呢?有! 

加同步锁就搞定
在这里插入图片描述

6.死锁示例

同步的另一个弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易 引发一种现象:死锁。这种情况能避免就避免掉
在这里插入图片描述
在这里插入图片描述

7.生产者消费者问题

在生活中经常会遇到两方都在处理某一资源,而处理的方式不同。比如:水池中注水和排水,煤场中往进 运煤和往出拉煤。这些操作处理的资源都相同,只是他们操作的方式有所不同。这类操作就多线程中另外一种 高级应用,即多生产和多消费

生产者消费者代码
多线程中为常见的应用案例:生产者消费者问题。

举例:

-生产者在生产商品,而消费者在消费生产的商品。
生产者把生产的商品放进容器中,而消费者从容 器中取出商品进行消费。
可是在整个过程中,如果容器装满了,那么生产者应该停止生产,如果容器中没有商 品了,消费应该停止消费。
这就是一个典型的多生产,多消费的案例。 

-在学习过程中,为了代码简单明了,大家很容易看懂,就把上述的多生产和多消费进行简化,要求生产者 生产一个商品,消费者消费一个商品,然后生产者继续生产,消费者进行消费,以此类推下去。 

分析案例:

生产和消费同时执行,需要多线程。但是执行的任务却不相同,处理的资源确实相同的:线程间的通信。 

思路:

1. 描述一下资源。
2. 描述生产者,具备着自己的任务。 
3. 描述消费者,具备着自己的任务 

在这里插入图片描述
在这里插入图片描述

上述代码进行运行时发现有严重的问题。

 问题1:数据错误:已经被生产很早期的商品,才被消费到。 
       出现线程安全问题,加入了同步解决。使用同步函数。
       问题已解决:不会在消费到之前很早期的商品

在这里插入图片描述

加入同步后又有新的问题产生了。

问题2:发现了连续生产却没有消费,同时对同一个商品进行多次消费。希望的结果应该是生产一个商 品,就被消费掉。生产下一个商品。
	搞清楚几个问题?生产者什么时候生产呢?消费者什么时候应该消费呢? 

    当容器中没有面包时,就生产,如果有了面包,就不要生产。 
    当容器中已有面包时,就消费,如果没有面包,就不要消费。 

8.等待唤醒机制

生产者生产了商品后应该告诉消费者来消费。这时的生产者应该处于等待状态。消费者消费了商品后,应 该告诉生产者,这时消费者处于等待状态

等待:wait();
通知:notify();//唤醒
问题解决:实现生产一个消费一个。
在这里插入图片描述
等待/唤醒机制

wait(): 会让线程处于等待状态,其实就是将线程临时存储到了线程池中。

notify():会唤醒线程池中任意一个等待的线程。

notifyAll():会唤醒线程池中所有的等待线程。

记住:这些方法必须使用在同步中,因为必须要标识wait,notify等方法所属的锁。同一个锁上的 notify,只能唤醒该锁上的被wait的线程。

为什么这些方法定义在Object类中呢?

-因为这些方法必须标识所属的锁,而锁可以是任意对象,任意对象可以调用的方法必然时Object类中的方法多生产多消费问题以及解决方案 
上述程序只是一个生产和一个消费者,其实就是所谓的单生产和单消费,可是我们都知道生活中经常会有 多个生产者和消费者,把代码改为多个生产者或多个消费者。 

在这里插入图片描述
把生产者和消费者改为多个时,又有新的问题发生了

问题1:

生产了商品没有被消费,同一个商品被消费多次。 
Thread-0......生产者....面包2499//没有被消费。 Thread-	1......生产者....面包2500 
Thread-3....消费者....面包2500 

问题原因:

被唤醒的线程没有判断标记,造成问题1的产生。 

解决:

只要让被唤醒的线程必须判断标记就可以了。将if判断标记的方式改为while判断标记 

记住:

多生产多消费,必须时while判断条件

在这里插入图片描述
当把if改为while之后又出现问题了。

问题2:

发现while判断后,死锁了。 

原因:

本方唤醒了本方 既是生产方唤醒了线程池中生产方的线程 

解决:

希望本方要唤醒对方,没有对应的方法,所以只能唤醒所有 

在这里插入图片描述
9.Lock接口

lock():获取锁
unlock():释放锁;

提供了一个更加面对对象的锁,在该锁中提供了更多的显示的锁操作。 使用Lock接口,以及其中的lock()方法和unlock()方法替代同步。
在这里插入图片描述
在这里插入图片描述

10. Condition接口

-上述代码虽然把锁换成了显示的锁,可是使用的等待和唤醒还是Object中的wait和notify,这就会导致锁和等待唤醒机制
处于两个对象上,而我们想要的应该那个锁下的线程等待和唤醒,也就是等待和唤醒和锁有一定的关联关系。

-已经将旧锁替换成新锁,那么锁上的监视器方法(wait,notify,notifyAll)也应该替换成新锁的监视器
方法。而jdk1.5中将这些原有的监视器方法封装到了一个Condition对象中。想要获取监视器方法,需要先获取 Condition对象。
 
-Condition对象的出现其实就是替代了Object中的监视器方法。 

await(); 
signal(); 
signalAll();   

在这里插入图片描述
在这里插入图片描述

将所有的监视器方法替换成了Condition。功能和之前的老程序的功能一样,仅仅是用新的对象。改了写 法而已。但是问题依旧;效率还是低。

** 11. 解决多生产多消费问题**

上述程序低效的原因是本方可能唤醒的是本方,而我们希望本方可以唤醒对方中的一个。
老程序中可以通过两个锁嵌套完成,但是容易引发死锁。新程序中,就可以解决这个问题,只用一个锁, 在JDK1.5之后可以在一个锁上加上多个监视器对象。
在这里插入图片描述
在这里插入图片描述
12. Condition范例

前面的程序出现的问题是,多个生产者,生产同一个商品,或者多个消费者消费同一个商品,其实在生活 中我们会到多个生产者同时生产同类以商品,而多个消费者消费这同一类商品。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
13.多线程细节

sleep和wait的区别

相同点:
	可以让线程处于冻结状态。 
不同点:        
	 1、sleep必须指定时间。            
	   wait可以指定时间,也可以不指定时间。         
	 2、sleep时间到,线程处于临时阻塞或者运行。            				  
	   wait如果没有时间,必须要通过notify或者notifyAll唤醒 一般放在逻辑的后。         
	 3、sleep不一定非要定义在同步中。           
	   wait必须定义在同步中。         
	 4、sleep和wait都定义在同步中时,线程执行到sleep,不会释放锁。线程执行到wait,会释放锁。 

线程停止&interrupt方法

-stop方法过时了,看API描述发现,有其他解决方案。 
-线程结束:就是让线程任务代码执行完,run方法结束。run方法结束呢?run方法中通常都定义循环, 只要控制住循环就可以了。 
       
注意:万一线程在任务中处于了冻结状态,那么它还能去判断标记吗?不能在去判断标记,那怎么停止 呢?通过查阅stop方法的描述,
发现提供了一个解决方法:如果目标线程等待很长时间,则应使用 interrupt  方法来中断该等待。所谓的中断并不是停止线程。interrupt的功能是将线程的冻结状态清除,让线程恢复到的运行状态(让线程重新具备cpu的执行资格)。       
因为是强制性的所以会有异常InterruptedException发生, 可以在catch中捕获异常,在异常处理中,改变标记让循环结束,让run方法结束。 

在这里插入图片描述
在这里插入图片描述
守护线程&线程优先级&线程组

守护线程:

-也可以理解为后台线程,之前创建的都是前台线程。只要线程调用了setDaemon(true);就可以把线程标 记为守护线程。
前台后台线程运行时都是一样的,获取CPU的执行权执行。只有结束的时候有些不同。前台线 程要通过run方法结束,线程结束。
后台线程也可以通过run方法结束,线程结束,还有另一种情况,当进程中所有的前台线程都结束了,这时无论后台线程处于什么样的状态,
都会结束,从而进程会结束。进程结束依赖 的都是前台线程。  

线程的优先级:

用数字标识的,1到10,其中默认的初始优先级时5 明显的三个优先级 1,5,10。 
 setPriority(Thread.MAX_PRIORITY); 

线程组:

ThreadGroup:可以通过Thread的构造函数明确新线程对象所属的线程组。线程组的好处,可以对多个 同组线程,进行统一的操作。默认都属于main线程组。 

在这里插入图片描述
在这里插入图片描述

join方法和yield方法
在这里插入图片描述

线程的匿名内部类使用

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值