Java的多线程机制-1

妖狐艹你老母 2022-02-09 14:31 255阅读 0赞

线程的状态与生命周期

在这里插入图片描述

如何理解多线程

这里的同时运行实际上只是一种几个事件同时发生的错觉,因为我们的计算机在任何给定的时刻只能执行进程中的一个。为了建立这些进程正在同步执行的感觉,Java虚拟机快速地把控制从一个线程切换到另一个线程。这些线程将被轮流执行,使每个线程都有机会使用CPU资源。
在main方法中如果创建了其他的进程,那么JVM就要在主线程和其他线程之间切换,JVM一直要等到Java应用程序中的所有线程都结束之后,才能结束Java引用程序。
这是传统意义上操作系统提供的功能,是单CPU在分时复用机制下的多线程,随着硬件技术的发展,硬件本身就能支持多线程,比如多核和超线程技术,实现了真正意义上的“同时运行”。

进程的创建

下面是一个狗叫和猫叫的程序

  1. public class example {
  2. public static void main(String args[]) {
  3. Cat cat = new Cat();
  4. cat.start();
  5. for(int i=1;i<=10;i++){
  6. System.out.println(i+"汪#");
  7. }
  8. }
  9. }
  10. class Cat extends Thread {
  11. public void run() {
  12. for(int i=1;i<=10;i++){
  13. System.out.println(i+"喵~");
  14. }
  15. }
  16. }

狗叫程序在主类中,而猫叫程序作为一个单独的进程出现不仅要在创建猫叫类的时候继承一个线程父类“extends Thread”,还要在主类中声明miaow.start();
这个程序运行结果会出现猫叫和狗叫交替的情况,而且每次运行的结果都不一定相同;
但是如果把两部分调换一下:

  1. public class example {
  2. public static void main(String args[]) {
  3. for(int i=1;i<=10;i++){
  4. System.out.println(i+"汪#");
  5. }
  6. Cat cat = new Cat();
  7. cat.start();
  8. }
  9. }
  10. class Cat extends Thread {
  11. public void run() {
  12. for(int i=1;i<=10;i++){
  13. System.out.println(i+"喵~");
  14. }
  15. }
  16. }

就会出现先把狗先把二十声叫完,猫才敢叫的情况。这说明java主类的程序是按顺序运行的,而如果猫叫程序在先声明,表示这个进程已经在JVM中存在了,JVM就要让两个线程轮流使用CPU资源,实现了“同时运行”。

上述的方法是使用Thread子类创建线程,其优点是:
可以在子类中增加新的成员变量,使线程具有某种属性,也可以在子类中新增加方法,使线程具有某种功能。
但是,由于Java不支持多继承(即一个子类只能有一个父类),Thread类的子类不能再扩展其他的类。

创建线程的另一个途径就是用Thread类直接创建线程对象;
然后给此线程对象传递一个实现Runnable接口类的实例作为它的目标对象;

当线程调用start()方法后,一旦轮到它来享用CPU资源,目标对象就会自动调用接口中的run()方法。
下面我们同样运用之前的例子来运用这种方法:

  1. public class example {
  2. public static void main(String args[]){
  3. Thread cat;//声明线程
  4. CatSpeak miaow;//声明目标对象miaow
  5. miaow = new CatSpeak();//创建目标对象
  6. cat = new Thread(miaow);//创建线程,其目标对象是miaow
  7. cat.start();
  8. for(int i=1;i<=10;i++){
  9. System.out.println(i+"汪#");
  10. }
  11. }
  12. }
  13. class CatSpeak implements Runnable{
  14. //创建实现Runnable接口的CatSpeak类
  15. public void run(){
  16. for(int i=1;i<=10;i++){
  17. System.out.println(i+"喵~");
  18. }
  19. }
  20. }

事实上我们可以把声明和创建连在一起写:
这次我们把狗的线程也以这种方式创建:

  1. public class example {
  2. public static void main(String args[]){
  3. CatSpeak miaow = new CatSpeak();//声明并创建目标对象miaow
  4. Thread cat = new Thread(miaow);//声明并创建线程cat
  5. DogSpeak bark = new DogSpeak();//声明并创建目标对象bark
  6. Thread dog = new Thread(bark);//声明并创建线程dog
  7. cat.start();
  8. dog.start();
  9. }
  10. }
  11. class CatSpeak implements Runnable{
  12. //创建实现Runnable接口的CatSpeak类
  13. public void run(){
  14. for(int i=1;i<=10;i++){
  15. System.out.println(i+"喵~");
  16. }
  17. }
  18. }
  19. class DogSpeak implements Runnable{
  20. //创建实现Runnable接口的DogSpeak类
  21. public void run(){
  22. for(int i=1;i<=10;i++){
  23. System.out.println(i+"汪#");
  24. }
  25. }
  26. }

cat和dog是具有相同目标对象的两个线程,所以run()方法启用运行了两次,分别运行在不同的线程中,即运行在不同的的时间片内。

注意:一个已经运行的线程在没有进入死亡状态时,不要再给线程分配实体,由于线程只能引用最后分配的实体,先前的实体就会成为“垃圾”,并且不会被垃圾收集器收集掉。
在这里插入图片描述

目标对象与线程

关于目标对象与线程的关系,我们还需要进一步的理解:

  1. public class example {
  2. public static void main(String args[]){
  3. Speak speak = new Speak();
  4. Thread cat = new Thread(speak);
  5. Thread dog = new Thread(speak);
  6. cat.setName("汤姆猫");
  7. dog.setName("史努比");
  8. cat.start();
  9. dog.start();
  10. }
  11. }
  12. class Speak implements Runnable{
  13. int count;
  14. public void run(){
  15. String name=Thread.currentThread().getName();
  16. while(true){
  17. if(name.equals("史努比")){
  18. System.out.println("汪#汪#汪#");
  19. count=count+3;
  20. }
  21. else if(name.equals("汤姆猫")){
  22. System.out.println("喵~喵~");
  23. count=count+2;
  24. }
  25. if(count>=10){
  26. return;
  27. }
  28. }
  29. }
  30. }

在这段代码中,线程dog和cat两个线程的目标对象相同,都是speak。

目标对象与线程的关系分为两种:
这段代码中是属于目标对象和线程完全解耦;
下面给出目标对象组合线程,即若耦合的情形:

  1. public class example {
  2. public static void main(String args[]) {
  3. CourtyardSpeak speak = new CourtyardSpeak();
  4. speak.dog.start();
  5. speak.cat.start();
  6. }
  7. }
  8. class CourtyardSpeak implements Runnable {
  9. int count;
  10. Thread dog,cat;
  11. CourtyardSpeak(){
  12. dog = new Thread(this);// 当前CourtyardSpeak类的对象作为线程的目标对象
  13. cat = new Thread(this);
  14. }
  15. public void run() {
  16. while(true) {
  17. Thread t=Thread.currentThread();
  18. if(count>=10){
  19. return;
  20. }
  21. else if(t==dog){
  22. System.out.println("汪#汪#汪#");
  23. count=count+3;
  24. }
  25. else if(t==cat){
  26. System.out.println("喵~喵~");
  27. count=count+2;
  28. }
  29. }
  30. }
  31. }

若耦合可以浅显的理解为目标对象和线程绑定在一起了。
完全解耦关系下,目标对象需要通过线程对象再获得线程的名字:

  1. String name = Thread.currentThread().getName();

而弱耦合关系下,目标对象可直接获得线程对象的引用:

  1. Thread t = Thread.currentThread();

线程调度器与优先级

JVM中的线程调度器负责管理线程,它把线程的优先级分为10个级别,可以通过setPriority(int grade)方法调整,如果没有明确设置,则默认优先级为5;
优先级高的线程先执行,直到进入死亡状态后,执行其他进程。
同等优先级的进程,在采用时间片的系统中,都有机会获得对CPU的使用权,当线程使用CPU的资源时间到时,即使没有完成自己的全部操作,CPU的使用权也会被切换给下一个排队等待的线程,等待进入下一次轮回获得使用权后,从中断处继续执行。

Thread的常用方法

1.start()
用于启动线程,使之从新建状态进入就绪队列排队,等待享用CPU资源;
注意调用一次后就不必再调用,否则会导致IllegalThreadStateException异常,即只有处于新建状态的线程才可以调用start()方法
2.run()
用来定义线程对象被调度之后执行的操作,是系统自动调用而用户程序不得引用的方法。
系统的Thread类中,run()方法没有具体内容,所以继承的子类必须重写run()方法;
当run()方法执行完毕,此线程就进入了死亡状态,即线程释放分配给线程对象的内存。
3.sleep(int millsecond)
有时会出现优先级高的线程需要优先级低的线程做一些工作来配合它,或者优先级高的线程需要完成一些费时的操作;
在run()方法中调用sleep()可以使优先级高的线程暂时放弃对CPU资源的使用权,休眠一段时间,长短由sleep的参数决定;
注意,如果线程 在休眠时被打断,JVM就会抛出InterruptedException异常,因此,必须与try-catch语句配合使用。
4.interrupt()
经常用来“吵醒”休眠状态的线程。当一些线程调用sleep方法处于休眠状态是,一个占有CPU资源的线程可以让休眠的线程调用interrupt()方法“吵醒自己”,即导致休眠的线程发生InterruptedException异常,从而结束休眠,重新排队等待CPU资源。
5.isAlive()
用于验证进程是否进入死亡状态;
当线程的run()方法结束之前,调用isAlive()返回true,当线程的run()方法结束后,调用isAlive()返回false。
6.currentThread()
是Thread类中的类方法,可以用类名调用,该方法返回当前正在使用CPU资源的线程。

下面我们用一个例子来展示这些用法。

  1. public class example1 {
  2. public static void main(String args[]) {
  3. Courtyard courtyard = new Courtyard();
  4. courtyard.Duck.start();//鸭子叫的线程启动
  5. courtyard.Cat.start();//猫叫的线程启动
  6. for(int i=1;i<=10;i++){
  7. System.out.print(i+"汪#\n");
  8. while(i==10){
  9. System.out.println("我好困啊,"+courtyard.Duck.getName()+"你回去睡觉了吗?");//getName()方法返回线程的名字
  10. if(courtyard.Duck.isAlive()){
  11. System.out.println(courtyard.Duck.getName()+"说:不想睡我在再嗨一会");
  12. }//狗叫线程在执行完毕前用isAlive()询问鸭子叫线程是否执行完
  13. else{
  14. System.out.println(courtyard.Duck.getName()+"已经再次睡着了");
  15. }
  16. break;
  17. }
  18. }
  19. }
  20. }
  21. class Courtyard implements Runnable {
  22. Thread Cat,Duck;
  23. Courtyard() {
  24. Cat = new Thread(this);
  25. Duck = new Thread(this);
  26. Cat.setName("汤姆猫");//setName()方法设置线程的名字
  27. Duck.setName("可达鸭");
  28. }
  29. public void run() {
  30. //在主函数中会自动被调用,这里需要重写
  31. if(Thread.currentThread()==Duck){
  32. //currentThread()方法返回当前正在使用CPU资源的线程
  33. try{
  34. System.out.println(Duck.getName()+"正在睡觉");
  35. Thread.sleep(1000*60*60);//sleep()方法会使线程暂时进入“休眠状态”
  36. }
  37. catch(InterruptedException e) {
  38. System.out.println(Cat.getName()+"把"+Duck.getName()+"叫醒了");
  39. for(int i=1;i<=10;i++){
  40. System.out.println(i+"嘎%");
  41. try{
  42. Thread.sleep(10);//在鸭子每次叫完一声又会暂时进入休眠状态(因为它被猫叫醒,还“昏昏欲睡”)
  43. }
  44. catch(InterruptedException s){
  45. };
  46. }
  47. }
  48. }
  49. else if(Thread.currentThread()==Cat){
  50. for(int i=1;i<=3;i++) {
  51. System.out.println(Duck.getName()+"快醒醒,起来嗨呀");
  52. }
  53. Duck.interrupt();//在猫叫线程中调用鸭子叫线程的interrupt()方法对其进行唤醒,因为猫叫线程此时是有CPU资源使用权的线程
  54. for(int i=1;i<=10;i++){
  55. System.out.println(i+"喵~");
  56. }
  57. }
  58. }
  59. }

为了更好的运用线程,Thread类还提供了很多方法:

可以在官方文档的Thread类中找到:
https://docs.oracle.com/javase/10/docs/api/java/lang/Thread.html

发表评论

表情:
评论列表 (有 0 条评论,255人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Java线机制

    首先弄清楚什么是进程?什么是线程? 进程:正在运行的一个程序。如打开任务管理器时,会看到正在运行的QQ,360等应用,每一个正在运行的应用程序就是一个进程。 线程:线程在

    相关 1.Java线

    1. 进程 一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运