目录
认识线程(Thread)
1 线程是什么?
- 一个线程就是一个 "执行流"。
- 每个线程之间都可以按照顺序执行自己的代码. 多个线程之间 "同时" 执行着多份代码。
- 将一个大任务分解成不同小任务,交给不同执行流分别排队执行。
2 为什么要有线程
但是在服务器开发场景上,一般情况下,客户端,服务器都在一台计算机上;但是我们可以把客户端和服务器分别放在两台电脑上。
此时我们可以通过网络的方式,远程访问mysql服务器,这也是以后工作中,最典型的一种场景
在这种服务器的情况下,涉及一个非常关键的问题,一个服务器程序,同一时刻,是需要给多个客户端提供服务的:
客户端按照并发的方式,发送请求到服务器,服务器就要能对这些请求进行处理。怎么处理?
总结:
引入线程,也可以解决并发编程,提高效率,同时节省开销。
但是,线程不是引入越多越好,适量的引用可以提高效率,引入太多,会因为线程调度的开销过大,反而拖慢程序的性能。
多个线程共享同一份资源,可能会产生冲突,导致线程安全问题,甚至进一步的,如果某一个线程抛出异常没有得到及时的处理,可能会带走整个进程。
3 进程和线程的区别
区别一
进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
windows 的任务管理器中,我们无法看到进程内部的线程。需要借助一些其他调试工具(VS的调试器,Windbg.....),才可以看到每个进程中的线程
区别二
进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间
区别三
进程是系统分配资源的最小单位,线程是系统调度的最小单位。
为什么不能说线程共享CPU资源呢?
区别四
一个进程挂了一般不会影响到其他进程. 但是一个线程挂了, 可能把同进程内的其他线程一起带走(整个进程崩溃).
进程与进程之间被虚线隔开,互不影响;但是在同一个进程内的多个线程,可以彼此影响。如果一个线程出现异常,当异常没有被及时捕获,而提交给JVM时,当前线程被迫终止;甚至影响多个线程,整个进程都会崩溃:
这就好比刚刚的滑稽老师吃🐔,🐔是要处理的问题。而滑稽老师扮演线程,桌子扮演进程。
对于桌子上的十只🐔,适量的请几位滑稽老师来解决即可。
请的滑稽老师太多了,会因为大家都得吃🐔,又不得不彼此谦让,最后导致吃🐔效率降低。
因为线程引入过多,导致不但线程的调度开销变大,还是得效率降低。
不同桌子的🐔,由相应的滑稽老师们解决,桌子与桌子之间互不影响。
进程与进程之间资源独立,互不打扰。
线程与线程共享内存资源。
一个桌子的两个或者多个滑稽老师,抢同一只🐔,可能导致其中一个滑稽老师红温,掀桌子,导致这个桌子的滑稽老师都吃不到🐔了。
如果提前发现红温,想要掀桌子的滑稽老师,并及时劝阻,大家又能继续一起吃🐔。
4. Java的线程和操作系统线程的关系
线程是操作系统中的概念。操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API, 供用户使用(例如 Linux 的 pthread 库)
通俗地讲,API就是,Java的开发大佬写了一些类或者函数,我们拿过来直接用即可。如:
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.
5.创建第一个多线程程序
• 每个线程都是一个独立的执行流
• 多个线程之间(在一个进程内)是 "并发" 执行的
引入Thread类
引入Thread类,创建除主线程(main)外的另一个线程 t 。
在用Thread类的引用时,是不需要import任何包的, 因为Thread类,这个由标准库提供的API,是在java.lang文件目录底下的,idea在创建新项目时,默认已经导入了java.lang的包
重写run()
我们在MyThread类中,重写父类Thread中的run方法,用自己的逻辑,替代Thread类中,run()原有的逻辑:
通过两个while循环,我们可以直观的感受到多线程和单线程的区别
start()与run()区别
在完善好当前多线程程序后,屏蔽t.start(),调用t.run();对于后续的打印结果进行分析:
此时,run()相当于回调函数,我们不需要关心 run() 方法的调用,线程建好后,JVM会负责调用run() 。还学过的回调函数有,优先级队列。我们只需要为PriorityQueue传入比较器Comparator,只需要把比较的逻辑,写入比较器中的方法即可,不需要关心比较器里面的方法什么时候被调用。
降低多线程对CPU的占用率
此时,main 线程 和 t 线程 并发执行,会比单线程运行效率更高,CPU会以更高的频率(功率)工作,产生更多的热量;
此时CPU为了散热,会降低运行频率,从而CPU运行效率降低。
在两个while循环中,我们调用Thread类提供的一个静态方法sleep(),参数为毫秒;
调用类的静态属性,不需要通过类名实例化一个对象,再通过对象的引用调用属性;而是直接通过类名调用静态属性;
而通过Thread类,两次调用sleep(),可以让当前线程暂时放弃CPU,降低CPU占用率。
处理sleep()异常
再来分析两处地方,对sleep()进行调用报错的原因。
报错原因,是因为我们并没有捕获异常
对于这两处关于调用sleep()的报错:
第一处报错的解决方案,只有通过try...catch来捕获异常,而不是在run()后面throw异常;
因为run(),是Thread类的run()的重写方法;在Thread类中的run,并没有在方法名后throw异常;
重写的方法的方法名,方法参数,抛出的异常都得和父类中的一致,因为这些限制,我们不能在run() 后 throws 异常,但是main方法可以。
但是,上述两种处理sleep()异常的方式,并没有对异常本身的问题进行任何的处理,在实际开发中,我们处理异常的方式如下:
线程的随机调度
观察程序运行结果,对于先打印“hello thread”还是先打印“hello main ”,并不是一定的:
6.使用 jconsole 命令观察线程
可以借助这个工具,查看这个程序有多少个线程;
因为一个进程中,如果线程多了,可能会对程序运行产生一定的影响。因为线程调度过多,会消耗更多资源。
我们通过这个图,查看某一个时间的线程是不是特别多的问题:
这就是我们看到的线程的详细信息,主要还是因为当前代码比较简单,也没有复杂的对应关系,所以看起来也没有太多的信息,但是实际上这些信息非常关键;
因为在实际工作中,我们涉及到线程会有很多个,而且里面线程的对应关系也可能很复杂。
所以当我们去排查一些问题的时候,首当其冲的,就是要先去查看,这个进程里面有什么线程,以及每个线程正在干什么。
这些都是我们排查问题,找出bug的重要依据。