黑马程序员——多线程2:应用

------- android培训java培训、期待与您交流! ----------

1. 创建并执行线程

       在前面的内容中我们说到,主线程在执行的过程中,还会自动创建一个线程去负责垃圾回收,那么我们也可以手动创建一个进程去执行我们指定的代码。

1) 创建线程的第一种方法

       Java类库中表示线程的类称为Thread,存在于java.lang包当中。API文档中对它的描述是:该类表示程序中的执行线程。Java虚拟机允许应用程序并发地运行多个执行线程。API文档还告诉我们,创建新执行线程有两种方法,这里我们先说第一种方法:将类声明为Thread类的子类。该子类应重写Thread类的run方法。这一方式类似于异常类的定义——如果想要定义为异常类,就要继承Exception,或者RuntimeException。我们通过下面的代码,演示如何定义线程类。

代码1:

//定义线程类需要继承Thread类,并复写run方法
class SubThread extends Thread
{
	//复写run方法
	public void run()
	{
		System.out.println("SubThreadrun");
	}
}
public class ThreadDemo
{
	public static void main(String[] args)
	{
		//创建线程,并执行其中的run方法。
		SubThreadst= newSubThread();
		//调用线程对象的start方法,启动该线程
		st.start();
	}
}
运行结果为:

SubThread run

 

上述代码就是对定义并创建线程的一个简单演示,其步骤可以简单总结为:

第一步:定义一个类并继承Thread类。通常为了简化代码书写,可以使用匿名内部类的方式。

第二步:复写Thread类中的run方法。

第三步:调用线程对象的start方法——启动该线程,并执行run方法。

 

在这里我们对第二步和第三步进行一些简单的解释:

        关于第二步:之所以我们要创建并执行一个线程,就是为了使其在主线程执行的同时,帮助我们执行另外的一些代码,以达到优化程序的目的。那么Thread类作为对线程这一类事物的描述,很自然其内部就会定义某个方法来存储需要被执行的代码——run方法。与此相对应的是,主线程需要执行的代码存放在main方法中。但是Java标准类库中的Thread类其run方法并没有定义任何内容,所以需要我们自定义一个类,继承Thread,并复写run方法,而这个子类的run方法中的内容就是我们指定其将要执行的代码。

        关于上述内容,这里我们可以通过Thread类的源代码进行说明。

代码2:

//Thread类run方法源代码
@Override
public voidrun(){
       if (target != null) {
		target.run();
	}
}
由以上代码可知,Thread类的run方法非常简单,如果target变量不为空则那么该线程所要执行的代码就是target变量所指向对象(Runnable接口实现类对象,下面会讲到)的run方法,否则什么都不执行。这也就是为什么需要复写run方法的原因——若直接调用Thread对象的run方法不会有任何执行效果。有关Runnable接口实现类对象涉及到创建的线程的第二种方法,因此将在后面的内容中介绍。

        关于第三步:可能大家会对调用线程对象的start方法而不调用run方法有所疑惑。查阅Thread类的API文档,其中对start方法的说明为:使该线程开始执行;Java虚拟机调用该线程的run方法。也就是说,通过new关键字仅仅是创建了一个线程对象,但该对象还并未作为一个线程运行,换句话说如果代码2中我们调用的是SubThread对象的run方法,那么这与调用一个对象的普通方法没有区别——因为这是单线程的;只有调用该对象的start方法才使该线程作为主线程以外的线程独立运行起来,并执行定义在run方法中的指定代码。

2) 多线程的运行

为了演示多线程的运行效果,我们对代码1做一些改动,改动如下,

代码3:

//定义线程类需要继承Thread类,并复写run方法
class SubThread extends Thread
{
	//复写run方法
	public void run()
	{
		//循环打印
		for(int x = 0; x<50; x++)
			System.out.println("SubThread----------"+x);
	}
}
public class ThreadDemo2
{
	public static void main(String[] args)
	{
		//创建线程,并执行其中的run方法。
		SubThread st = new SubThread();
		st.run();
 
		//主线程也执行一段循环打印代码
		for(int x = 0; x<50; x++)
			System.out.println("PrimeThread-----"+x);
	}
}
运行结果内容过多,不再这里显示了,理论上应是主线程和子线程交替打印,如下所示:

SubThread----------0

PrimeThread-----0

SubThread----------1

PrimeThread-----1

 

我们通过下图来理解代码2的执行过程, 

 

        我们将代码2执行结果和上图结合起来:当主线程执行到“SubThreadst = new SubThread();”语句创建了一个子线程,然后调用子线程的start方法启动子线程。子线程启动以后,主线程继续“向下执行”for循环,同时子线程也执行自己的代码——for循环,因此从控制台的结果上看,两个线程在同时运行。此外,两个线程的打印结果是交替出现的(并不完全是),这是因为,CPU不仅在多个进程之间进行快速地切换,更具体来说,CPU在单个进程内又在多个线程之间进行快速切换,因此,从结果上看就是——执行一会儿主线程输出语句,再执行一会儿子线程输出语句,这样不断地进行交替。

        如果多次运行代码2,就会发现,每次运行的结果是不同的。这是因为,CPU在线程间进行切换时,具体更多地执行哪个线程是由CPU随机决定的,这也就体现了多线程的一个特点:随机性。

3) 多线程练习

        为了巩固创建和执行线程的过程,我们做一个简单的练习。

需求:创建两个线程,和主线程交替运行。

分析一:定义Thread类的子类,并复写run方法——循环语句;在主函数中创建两个线程对象,分别调用其start方法,接着在主函数中执行循环语句,最终在控制台中呈现连个子线程与主线程同时执行的效果,并交替打印指定内容。

代码:

代码4:

//定义Thread类的子类
class SubThread extends Thread
{
	//为方便区分两个子类为每个线程对象定义名称
	private String name;
	SubThread(String name)
	{
		this.name = name;
	}
	public void run()
	{
		//循环输出
		for(int x=0;x<50; x++)
			System.out.println(name+"------"+x);
	}
}
public class ThreadTest
{
	public static void main(String[] args)
	{
		// 主函数中创建两个线程对象,分别调用start方法
		SubThread st1 = newSubThread("No.1");
		SubThread st2 = newSubThread("No.2");
             
		st1.start();
		st2.start();
             
		//在主函数中同样执行输出循环,以呈现与子线程同时运行的效果
		for(int x=0;x<50; x++)
			System.out.println("MainThread----------"+x);
	}
}
注意:这里我们还是要强调,创建线程对象以后,一定要调用其start方法,而不是run方法,否则运行效果与单线程是没有区别的——st1指向的线程对象执行完其run方法中的代码之前其他线程无法运行。 

分析二:实际上为了方便区分多个线程,Thread类中已经定义了私有成员变量name,以及获取线程名称的方法getName(),因此可以在对象内部通过“this.getName()”语句获取到当前正在执行的线程对象的默认名称。当然,如有需要也可以通过setName()方法设置自定义线程名称。不过,如果想获取到主线程对象的引用就不能使用this关键了,因为执行main方法的主线程对象并非是main方法所在类的实例对象,为此Thread类还对外提供了一个静态方法,专门用于返回指向当前线程对象的引用,包括主线程——currentThread()。那么最终就可以通过“Thread.currentThread().getName()”语句来获取主线程名称。该方法对于普通线程对象的作用等同于“this”。

代码:

代码5:

//对代码1进行简单的修改
public class SubThread extends Thread
{
	//由于Thread类已经定义了name成员变量,不必重复定义
	//private String name;
	//方便创建默认名称线程而手动定义空参构造方法
	SubThread(){}
	SubThread(String name)
	{
		//Thread类中已经定义了name的初始化动作,通过super()调用即可
		super(name);
	}
	public void run()
	{
		//循环输出
		for(int x=0; x<50; x++)
			//通过this获取当前线程对象的引用
			System.out.println(this.getName()+"---"+x);
	}
}
public classThreadTest2
{
       public static void main(String[] args)
	{
		SubThread st1 = new SubThread();
		SubThread st2 = new SubThread();
		st1.start();
		st2.start();
             
		for(int x = 0; x<50; x++)
			//通过currentThread获取对于主线程的引用
			System.out.println(Thread.currentThread().getName()+"----------"+x);
	}
}

        虽然通过this关键字可以获取到手动创建线程对象的引用,但是并不推荐在实际开发时这样做。一方面,主线程对象是无法通过这一方法获取到的,如果使用this关键字习惯了就容易出错;另一方面,当我们通过第二种方式创建线程时,也不能通过this关键字获取当前线程对象的引用(下面会讲到),体现了this关键的局限性。

2. 线程的状态

1) 线程正常运行的前提条件

        一个线程的正常运行需要同时满足两个条件:(a) 具有被CPU处理的资格;(b) 同时还要具有执行权,否则,即使线程被创建并被调用start方法也是不能运行的。这两个条件之间的关系是:只有满足(a)才能有可能满足(b),也就是说不满足(a),肯定不满足(b)。资格是线程运行的根本前提条件,这是由人为控制的,如果不需要该线程运行,就通过调用一些方法来“剥夺”其资格;然而,仅具有资格是不够的,还需要满足直接前提条件——执行权。执行权是由CPU分配的,当有多个线程具有资格时,CPU执行哪个线程,该线程就具备执行权,从而真正运行起来。

了解了上述两个前提条件,我们就来说一说除创建和运行以外,线程其他的三个状态。

2) 状态一:冻结

条件:不具备资格;不具备执行权。

说明:暂停线程的运行,但该线程并未“死亡”或者“消失”。换句话说,当条件满足的时候——达到规定时间或者人为唤醒——该线程可以被人为赋予资格,并在CPU赋予其执行权后继续运行。

实现方式:

方法一:调用sleep(long millis)方法,并指定冻结时长。当达到规定时长以后,线程自动唤醒,并继续运行。

方法二:调用wait()方法,使运行中的线程冻结。使用wait方法冻结的线程是无法自动唤醒的(只限于无参wait重载方法),只能手动唤醒该线程——通过调用该线程的notify()方法,notify的意思就是唤醒。这里要提一下,如果某个进程中的线程一直处于等待状态,那么这个进程就不会结束。

 

小知识点1:

我们经常会通过任务管理器中的“结束进程”按钮,来强制关闭某个卡死的进程。实际上,这个按钮的作用就是强制“消灭”该进程中所有正在运行的线程,以达到结束进程的目的。

 

3) 状态二:消亡

条件:不具备资格,不具备执行权。

说明:顾名思义,就是结束该线程。被“结束”的线程等同于没有指向的对象,是不能再被“唤醒”的。当然,处于“冻结”状态的线程也是可以被“杀死”,而消亡的。

实现方式:

方法一:调用stop()方法,在线程运行过程中强制使其消亡。

方法二:线程执行完run方法中的代码,自行消亡。

4) 状态三:阻塞(临时)

条件:具备资格,但不具备执行权。

说明:举个例子,当在主函数中创建了四个子线程,并分别调用它们的start方法,启动这些线程,但是在某一个时刻,真正在运行的线程其实只有一个(因为CPU只能处理一个),那么在一个时刻,其他三个线程就处于阻塞状态——具有被CPU处理的资格,但是没有执行权。换句话说,只有某个线程被赋予了执行权(通常由CPU决定)才能真正被运行起来。

方法:该状态无法人为控制,CPU分配。


我们通过下图来加深对线程各个状态及其相互关系的理解,


3. 定义线程类的第二种方法

在前面的内容中我们说明了定义线程类的第一种方法:继承Thread类,现在我们说一说定义线程类的第二种方法,并通过一个小例子来体现这种方法的应用。

1) 定义方式

        我们还是回过头来看一看Thread类的API文档,其中提到了定义线程类的第二种方法:定义一个实现Runnable接口的类,并复写该接口的的run方法。然后创建该类的实例对象,最后在创建一个Thread对象时将该对象作为一个参数来传递,并通过调用Thread对象的start方法来启动这个线程。下面是通过这种方法定义、创建和启动线程的代码格式:

定义线程类的代码格式:

代码6:

//实现Runnable接口
class PrimeRun implements Runnable
{
	//复写run方法
	public void run()
	{
		//自定义需要子线程执行的代码
	}
}
创建并启动线程的代码格式:

代码7:

//创建代码3中定义的PrimeRun类对象
PrimeRun p = new PrimeRun();
//创建Thread对象,并将PrimeRun对象作为参数传入,然后启动该线程
new Thread(p).start();

总结起来分为五个步骤:

第一步:定义实现Runnable接口的子类;

第二步:覆盖Runnable接口中的run方法;

第三步:创建Thread对象,一个Thread对象表示一个线程;

第四步:将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法——Thread(Runnable target);

第五步:调用Thread对象的start放阿飞开启线程,并执行Runnable子类对象run方法中的代码。

        此时我们再来看Thread类源代码中的run方法(代码2),其中的target变量就是指向的传递到Thread对象中的Runnable实现类对象,那么当该变量不为空时,启动线程就将执行Runnable实现类对象的run方法。实际上targetThread类的一个Runnable类型的私有成员变量,在通过Thread类的构造方法创建Thread对象时,将在构造方法中调用一个名为init的私有方法,该方法的作用之一就是接收传递到Thread构造方法的Runnable实现类对象,并将其赋值给target成员变量。更为详细的原理大家可以参考Thread类的源代码。

        那么当我们通过为Thread对象初始化一个Runnable实现类对象的方式创建一个线程时,能否在Runnable实现类的run方法中通过this关键字获取当前线程对象的引用呢?答案是否定的,因为此时的this指向的是Runnable实现类对象,而非Thread对象,因此此时只能通过调用currentThread方法。

2) 演示示例

需求:简单的卖票程序。

分析:首先,通常在火车站的售票大厅有多个售票窗口在同时卖票,以此来提高卖票效率。这里我们假设只有4个卖票窗口,那么对应的就是4个线程。其次,我们应该定义一个资源类——“车票”,并创建车票类对象,该对象内部定义了车票总数。然后将车票对象传入卖票窗口内,最终4个窗口同时对其进行循环减法运算——卖票。这么做可以使得4个窗口共享一个车票资源,而不会导致重票的产生。

实现方式:定义实现了Runnable接口的Tickets类,内部定义表示车票总数的成员变量count初始化值为100,并复写run方法——当count大于0时进行循环减法——模拟卖票过程。在主函数内创建一个Tickets对象,并在创建4个Thread对象时将Tickets对象作为参数传递。最后分别开启4个线程。

代码:

代码8:

//定义实现Runnable接口的Tickets类
public class Tickets implements Runnable
{    
	//定义车票总数
	private int count = 100;
      
	//复写run方法
	public void run()
	{
		//对车票数进行循环减法,直到车票数为0
		while(true)
		{
			if(count > 0)
			//打印线程名称和当前票数
				System.out.println(Thread.currentThread().getName()+"-----"+count--);
		}
	}
}
public class ThreadDemo3
{
	public static void main(String[] args)
	{
		Tickets ts = new Tickets();
             
		//创建4个线程对象,并将Tickets对象作为参数进行传递
		Thread t1 = new Thread(ts);
		Thread t2 = new Thread(ts);
		Thread t3 = new Thread(ts);
		Thread t4 = new Thread(ts);
             
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}
补充:

        在上述分析部分中,我们提到了共享车票资源避免重票的产生。可能有朋友会想到另一种方法:定义Tickets类的时候还是继承Thread类,并在该类内部定义个一个静态成员变量表示车票总数,这样也可以做到多个Tickets对象共享车票数据的目的。但是这样做的后果就是导致数据的生命周期过长,不利于内存的优化。因此不建议使用这一方法。

注意:

(a)    实现Runnable接口的类并不是线程类,因为它与Thread类不存在继承关系。在任何情况下,只有Thread类及其子类对象才是线程对象。

(b)    上述方法中我们将需要子线程执行的代码定义在了Runnable子类的run方法总,而不是Thread子类,因此应该使得Runnable子类对象和Thread对象之间产生一定的联系——通过Thread类的Thread(Runnable target)构造方法,多态地接受Runnable接口的子类对象,这也就是为什么要做前述第四步的原因。产生了这一联系以后在调用Thread对象的start方法时,首先执行Thread对象的run方法,该run方法最终又去执行了Runnable子类对象的run方法。

(c)    关于上面提到的Runnable接口,其API文档中有这样的说明:大多数情况下,如果只想重写run方法,而不重写其他Thread方法,那么应使用Runnable接口。这很重要,因为除非程序员打算修改或增强Thread类的基本行为,否则不应为该类创建子类。所以,从实际开发的角度来说,通过实现Runnable接口的方式定义线程类的方式更为常用,大家应重点掌握。

3) 两种定义线程类方法的区别——Runnable的设计目的

        区别一:这里最关键的问题是——Java语法中不允许进行直接的多继承。举个例子,如果一个类声明为Thread类的子类,那么它就不能再继承其他类了,这在很大程度上限制了该类在后期功能上的扩展。从另一个角度来说,假如某个类是通过继承其他类来定义的,那么它就不能再去继承Thread类而使用多线程技术了,同样不便于功能扩展。因此,设计Runnable接口的目的就是为了提高代码的扩展性,这也是为什么实际开发中更多地使用这一方法的原因。

        区别二:两种方法中,需要子线程单独执行的代码的存放位置不同。继承Thread类:直接存储在Thread子类run方法中;实现Runnable接口:存放在Runnable接口子类run方法中。那么通过后者,更能体现面向对象的思想——将某个线程将要执行的代码专门存储到一个对象中,降低了线程与执行代码之间的耦合性。
如果同时使用创建线程的两种方法创建一个线程——不仅定义Thread类的子类复写run方法,而且定义Runnable接口的实现类复写run方法,那么启动线程后究竟执行那个run方法的代码呢?先阅读以下代码。

代码9:

new Thread(new Runnable() {
	//Runnable实现类的run方法
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("Runnable :"+Thread.currentThread().getName() + " : "
				+Calendar.getInstance().get(Calendar.SECOND));
		}
	}
           
}){
	//Thread子类的run方法
	@Override
	public void run(){
		for (int i = 0; i < 10; i++) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("Thread :"+Thread.currentThread().getName() + " : "
				+Calendar.getInstance().get(Calendar.SECOND));
		}
	}
}.start();
执行结果为:

Thread :Thread-0 : 2

Thread :Thread-0 : 3

Thread :Thread-0 : 4

Thread :Thread-0 : 5

Thread :Thread-0 : 6

Thread :Thread-0 : 7

Thread :Thread-0 : 8

Thread :Thread-0 : 9

Thread :Thread-0 : 10

Thread :Thread-0 : 11

代码说明:

(1)  以上代码中无论是Thread类的子类对象,还是Runnable实现类对象,均通过匿名内部类的方式创建。

(2)  两者run方法中执行的代码是一样的——每秒打印当前时间的秒值大小,共打印十秒钟。

(3)  从执行结果来看,执行的是Thread子类中的run方法。这是因为Thread类的子类直接复写了父类的run方法,因此也就不会进行target变量是否为空的判断,以及调用target对象的run方法的操作,而是直接执行子类run方法的内容。

4. 实际开发中的简单应用

如果某个程序需要处理多个数据,并且相互之间是独立的,可以将匿名内部类和多线程结合起来实现功能,不仅便于代码书写,而且可以提高程序运行效率。举个例子,代码如下,

代码10:

class ThreadDemo4
{
	public static void main(String[] args)
	{
		//Thread类的子类对象
		newThread()
		{
			public void run()
			{
				//定义数据处理代码,这里仅以循环作为演示
				for(int x = 0; x<50; x++)
					System.out.println(Thread.currentThread().getName()+"-----"+x);
			}
		}.start();
 
		//Runnable接口的子类对象
		Runnable run = new Runnable()
		{
			publicv oid run()
			{
				//定义数据处理代码,这里仅以循环作为演示
				for(int x = 0; x<50; x++)
					System.out.println(Thread.currentThread().getName()+"-----"+x);
			}
		};
		new Thread(run).start();
             
		//主函数中再单独处理一部分数据
		for(int x = 0; x<50; x++)
			System.out.println(Thread.currentThread().getName()+"-----"+x);
	}
}
上述代码中通过匿名内部类的方式,分别采用两种不同的线程定义方法,实现了在主函数内同时处理3部分数据的功能。

内容概要:本文围绕六自由度机械臂的人工神经网络(ANN)设计展开,重点研究了正向与逆向运动学求解、正向动力学控制以及基于拉格朗日-欧拉法推导逆向动力学方程,并通过Matlab代码实现相关算法。文章结合理论推导与仿真实践,利用人工神经网络对复杂的非线性关系进行建模与逼近,提升机械臂运动控制的精度与效率。同时涵盖了路径规划中的RRT算法与B样条优化方法,形成从运动学到动力学再到轨迹优化的完整技术链条。; 适合人群:具备一定机器人学、自动控制理论基础,熟悉Matlab编程,从事智能控制、机器人控制、运动学六自由度机械臂ANN人工神经网络设计:正向逆向运动学求解、正向动力学控制、拉格朗日-欧拉法推导逆向动力学方程(Matlab代码实现)建模等相关方向的研究生、科研人员及工程技术人员。; 使用场景及目标:①掌握机械臂正/逆运动学的数学建模与ANN求解方法;②理解拉格朗日-欧拉法在动力学建模中的应用;③实现基于神经网络的动力学补偿与高精度轨迹跟踪控制;④结合RRT与B样条完成平滑路径规划与优化。; 阅读建议:建议读者结合Matlab代码动手实践,先从运动学建模入手,逐步深入动力学分析与神经网络训练,注重理论推导与仿真实验的结合,以充分理解机械臂控制系统的设计流程与优化策略。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值