多线程【概述、原理、创建、获取】

多线程【概述、原理、创建、获取】

1、多线程概述

进程:正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能,进程是系统进行资源分配和调度的一个独立单位

进程是正在运行的程序,进程负责给程序分配内存空间,而每一个进程都是由程序代码组成的,这些代码在进程中执行的流程就是线程。

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

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

一个程序中有多个线程在同时执行。

2、多线程运行原理

大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个任务。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。感觉这些软件好像在同时运行着。

其实这些软件在某一时刻,只会运行一个进程。由于CPU(中央处理器)在做着高速的切换而导致的。对于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=值。

原因是:,必然有一个执行路径(线程)从main方法开始的**。一直执行到main方法结束。这个线程在java中称之为主线程。当主线程在这个程序中执行时,如果遇到了循环而导致程序在指定位置停留时间过长,无法执行下面的程序

多线程可以解决一个主线程负责执行其中一个循环,由另一个线程负责其他代码的执行

3、多线程内存图解

多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。

在这里插入图片描述

当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。

4、获取线程名称

Thread.currentThread()获取当前线程对象

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

class Demo extends Thread  //继承Thread
{
	String name;
	Demo(String name)
	{
		this.name = name;
	}
	//复写其中的run方法
	public void run()
	{
		for (int i=1;i<=20 ;i++ )
		{
			System.out.println("name="+name+","+Thread.currentThread().getName()+",i="+i);
		}
	}
}
class ThreadDemo 
{
	public static void main(String[] args) 
	{
		//创建两个线程任务
		Demo d = new Demo("小强");
		Demo d2 = new Demo("旺财");
		d2.start();//开启一个线程
		d.run();//主线程在调用run方法
	}
}

原来主线程的名称: main

自定义的线程: Thread-0 线程多个时,数字顺延。Thread-1......

进行多线程编程时不要忘记了Java程序运行时从主线程开始main方法的方法体就是主线程的线程执行体

5、创建线程两种方式(都要重写run()方法):

创建新执行线程有两种方法:

  1. 一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run() 方法。接下来可以分配并启动该子类的实例。
  2. 创建线程的另一种方法是声明实现 Runnable 接口。然后重写 run 方法。然后可以分配该类的实例,在创建 Thread 时作为一个参数来传递并启动。

5.1、方式一:继承Thread类,重写run()方法

创建线程的步骤:

  • 定义一个类继承Thread。
  • 重写run()方法。
  • 创建子类对象,就是创建线程对象。
  • 调用start()方法,开启线程并让线程执行,同时还会告诉jvm去调用run()方法。
class Demo extends Thread  //继承Thread
{
	String name;
	Demo(String name)
	{
		this.name = name;
	}
	//复写其中的run方法
	public void run()
	{
		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.run(); 这里仍然是主线程在调用run方法,并没有开启两个线程
		//d2.run();
		d2.start();//开启一个线程
		d.start();//主线程在调用run方法
	}
}

线程对象调用 run()方法和调用start()方法区别:

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

继承Thread类原理

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

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

创建线程的目的

建立单独的执行路径,让多部分代码实现同时执行。也就是说线程创建并执行需要给定的代码(线程的任务)。对于之前所讲的主线程,它的任务定义在main函数中。自定义线程需要执行的任务都定义在run方法中。Thread类中的run方法内部的任务并不是我们所需要,只有重写这个run方法,既然Thread类已经定义了线程任务的位置,只要在位置中定义任务代码即可。所以进行了重写run方法动作。

5.2、方式二:实现Runnable接口,重写run()方法

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

总结

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

  1. 定义类实现Runnable接口。
  2. 覆盖接口中的run方法。
  3. 创建Thread类的对象。
  4. Runnable接口的子类对象作为参数传递Thread类的构造函数。
  5. 调用Thread类的start方法开启线程。
class Demo implements Runnable
{
	private String name;
	Demo(String name)
	{
		this.name = name;
	}
	//覆盖了接口Runnable中的run方法。
	public void run()
	{
		for(int i=1; i<=20; i++)
		{			System.out.println("name="+name+"..."+Thread.currentThread().getName()+"..."+i);
		}
	}
}
class ThreadDemo2 
{
	public static void main(String[] args) 
	{
		//创建Runnable子类的对象。注意它并不是线程对象。
		Demo d = new Demo("Demo");
		//创建Thread类的对象,将Runnable接口的子类对象作为参数传递给Thread类的构造函数。
		Thread t1 = new Thread(d);
		Thread t2 = new Thread(d);
		//将线程启动。
		t1.start();
		t2.start();
		System.out.println(Thread.currentThread().getName()+"----->");
		System.out.println("Hello World!");
	}
}

在这里插入图片描述

5.2.1、实现Runnable的原理

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

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

5.2.2、实现Runnable的好处

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

6、多线程的异常信息

在前面学习过程,编写的程序有时会看到有相应的异常信息打印出来,当时上课我们只说了这个异常信息是什么,怎么去解决异常。可是每个异常的前面还有部分内容并没有给大家完全讲解它的含义。

class Demo extends Thread
{
	void show()
	{
		int[] arr = new int[3];
		System.out.println("arr[3]="+arr[3]);
	}
}
class ThreadDemo2 
{
	public static void main(String[] args) 
	{
		Demo d = new Demo();
		d.show();
		System.out.println("Hello World!");
	}
}

运行结果如下:

在这里插入图片描述

以前我们讲过被用蓝色线标注出来的是异常的名称。可是被黄色线标注出来的是什么东东呢?

我们修改程序继承运行看看。

class Demo extends Thread
{
	public void run()
	{
		int[] arr = new int[3];
		System.out.println("arr[3]="+arr[3]);
        System.out.println("over");
	}
}
class ThreadDemo2 
{
	public static void main(String[] args) 
	{
		Demo d = new Demo();
		Demo d2 = new Demo();
		d.start();
		d2.start();
		System.out.println("Hello World!");
	}
}

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

发现被黄色框选中的部分,有些变化,分别在说明每个异常发生具体的哪一个线程上。并且线程任务中的**System.out.println(“over”);**因为程序终止,所以不会打over字符串。

我们发现main方法中的Hello World!字符串却打印出来了。其实在这个程序中有三个线程,分别是主线程,Thread-0,Thread-1这三个线程。异常发生在Thread-0,Thread-1线程上,而main线程并没有发生异常,所以主线程正常运行完成。

注意:
当主线程执行完成了,并不代表程序就结束,如果此时还有其他线程正常执行,程序仍然在执行过程中。

当任何一个线程出现了异常,其他线程还是会继续运行的。异常只会影响到异常所属的那个线程。

7、多线程练习

7.1、多线程练习——售票

大家都去过火车站购买车票,我们来模拟窗口售票。售票的动作需要同时执行,所以使用多线程技术。

class Ticket implements Runnable
{
	//1、描述票的数量。
	private int tickets = 100;
	//2、售票的动作,这个动作需要被多线程执行,那就是线程任务代码。需要定义run方法中。
	//线程任务中通常都有循环结构。
	public void run()
	{
		while(true)
		{
			if(tickets>0)
			{
				//打印线程名称。
                System.out.println(Thread.currentThread().getName()+"....."+tickets--);
			}
		}
	}
}
class ThreadDemo3 
{
	public static void main(String[] args) 
	{
		//1,创建Runnable接口的子类对象。
		Ticket t = new Ticket();

		//2,创建四个线程对象。并将Runnable接口的子类对象作为参数传递给Thread的构造函数。
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		Thread t3 = new Thread(t);
		Thread t4 = new Thread(t);

		//3,开启四个线程。
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

如果我们创建一个线程对象,多次调用其start()方法,多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。多次开启同一个线程会发生IllegalThreadStateException - 如果线程已经启动。

8、线程状态图

查阅API关于IllegalThreadStateException这个异常说明信息发现,这个异常的描述信息为:指示线程没有处于请求操作所要求的适当状态时抛出的异常。

线程的状态:

在这里插入图片描述

1、新建状态(New): 新创建了一个线程对象。

2、就绪状态(Runnable): 线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

3、运行状态(Running): 就绪状态的线程获取了CPU,执行程序代码。

4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

  • 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
  • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
  • 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)

5、死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值