转载请注明http://www.cnblogs.com/devtrees/p/4395477.html
多线程的学习
一.多线程概述
(一).进程与线程
在说多线程或者线程之前,让我们来了解一下更显而易见的进程概念。那什么是进程呢?
进程就是正在进行中的程序。
Windows操作系统中在任务栏处右击,弹出的菜单中可看见任务管理器,打开它,就可以看见当前运行的程序和进程列表。
进程:是一个正在执行中的程序。
每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。
线程:就是进程中的一个独立的控制单元。
线程在控制进程的执行。
一个进程中至少有一个线程。
无论是qq还是迅雷,只要他们启动程序,就会在内存中开辟一段空间,产生地址值,进程就是用于标注这段空间的,用于封装里面的控制单元。
当出现’’java.lang.NoClassDefFoundError:com/sun/tools/javac/main’’错误的时候,证明java程序找不到tools.jar文件,也就是说环境变量有可能配置不对,或源文件丢失。
Java VM启动时就会有一个进程java.exe,该进程中至少有一个线程负责java程序的执行,而且这个线程运行的代码存在于main方法中。该线程称之为主线程。
扩展:其实更细节说明jvm,jvm不止一个线程,还有负责垃圾回收机制的线程。
有多条执行路径的程序,我们就称之为多线程程序。
多线程存在的意义。
同时进行,提高效率。
如何在我们的程序中自定义多线程程序。
通过对api的查找,java已经提供了对线程这类事物的描述。
Java.lang包下有一个Thread类,用于创建程序中的执行线程。就是用于描述控制单元这样d额一个对象。jVM允许应用程序并发地运行多个执行线程。
创建方式:
一种是将类声明称Thread的子类(继承Thread),该类应重写Thread类的run方法。
步骤:
1.自定义一个类继承Thread.
2.复写Thread类中的run()方法.
3.创建线程对象,调用start()方法启动线程。
//自定义一个类继承Thread
写个小demo:
1 class Demo extends Thread{ 2 3 //复写Thread类中run()方法 4 5 public void run(){ 6 7 for (int x=0;x<60;x++) { 8 9 System.out.println("demo run"+x); 10 11 } 12 13 } 14 15 } 16 17 18 19 class ThreadDemo{ 20 21 public static void main(String[] args) { 22 23 //创建对象调用方法 24 25 //创建自定义的线程对象实际上就是创建了线程 26 27 Demo d = new Demo(); 28 29 //调用start()方法,启动线程;jvm调用线程对象的run()方法,执行内部代码 30 31 d.start(); 32 33 for (int x=0;x<60;x++) { 34 35 System.out.println("hello world!"+x); 36 37 } 38 39 } 40 41 }
执行结果:
大家会看到执行的结果是交替进行的,并且还是不规则的交替打印。其原理是这样的:
上图解释了,两个线程同时进行,那么不规则如何解释呢?
也就是说:
因为多个线程都在获取cpu的执行权,cpu执行到谁,谁就运行。明确一点,在某一个时刻,只能有一个程序在运行。(多核除外)cpu在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象的吧多线程的运行形容为在互相抢夺cpu的执行权。
这就是多线程的一个特性:随机性。
谁抢到谁执行,至于执行多长时间,cpu说的算。
接下来思考:我们为什么要复写这个run()方法呢?
Thread类用于描述线程。该类就定义了一个功能,用于存储线程要运行的代码。该存储功能就是run()方法。
解释:
既然是用于描述线程,Thread类里面就会有很多功能用于操作线程。我们创建线程的目的其实是为了让线程执行一些代码,那么线程就需要在描述过程中定义这些代码存放的位置。线程要启动,要运行,要运行什么,这个什么在哪啊?线程提供了一个存储空间。这个空间就是run()方法。
也就是说,Thread类中的run()方法,用于存储线程要运行的代码。同理,主线程要运行的代码,放在main()方法中,这个是jvm定义的。
这是从run()方法本身说,那么从start()方法说,就是:
我们在主线程main()方法中创建Thread类对象,也就是创建了一个线程,这个线程对象通过调用start()方法,将线程启动。而这个start()方法又会调用Thread类中的run()来运行里面的的代码,如果我们不复写run(),直接创建线程对象,直接start(),我们不会得到我们想要的结果,因为父类Thread类中的run()方法中可能什么也没有写,start()方法调用run()也是白调用。所以,我们需要复写run()方法,里面写上我们自定义的代码,这样,run()方法才有意义。我们自定义的线程类继承了Thread类,调用start()方法,而start()方法会调用run()父类方法,因为我们复写了父类中的方法,父类会找子类,所以实际会调用我们写的run(0方法,这样我们自定义的代码才会运行。
因此,复写Thread类中的run()方法
目的:将自定义代码存储在run()方法中,让线程运行。
注意:在主线程中线程对象.start()方法和主线程中线程对象直接.run()方法的区别(面试)
d.start();//开启线程并执行该线程的run()方法
//d.run();仅仅是对象调用方法,在主线程中执行,而线程创建了,并没有运行。即还是单线程程序。
小练习:
创建两个线程,和主线程交替运行。
1 class Test extends Thread{ 2 3 private String name; 4 5 Test(String name){ 6 7 this.name = name; 8 9 } 10 11 public void run(){ 12 13 for (int x=0;x<60;x++) { 14 15 System.out.println(name + "test....run"+x); 16 17 } 18 19 } 20 21 } 22 23 24 25 class ThreadDemo{ 26 27 public static void main(String[] args) { 28 29 //创建对象调用方法 30 31 //创建自定义的线程对象实际上就是创建了线程 32 33 Demo d = new Demo(); 34 35 //调用start()方法,启动线程;jvm调用线程对象的run()方法,执行内部代码 36 37 //d.start();//开启线程并执行该线程的run()方法 38 39 //d.run();仅仅是对象调用方法,在主线程中执行,而线程创建了,并没有运行。即还是单线程程序。 40 41 //测试小练习 42 43 Test t1 = new Test("one"); 44 45 Test t2 = new Test("two"); 46 47 t1.start(); 48 49 t2.start(); 50 51 for (int x=0;x<60;x++) { 52 53 System.out.println("hello world!"+x); 54 55 } 56 57 } 58 59 }
线程运行状态(线程的生命周期)
线程在运行过程中,有几种状态是咱们必须掌握的。只有掌握这几种状态,我们才知道线程是怎样运作的。
获取线程对象以及名称
线程也有自己的名称,怎么获取呢?
找线程对象的方法吧。线程名称应该是定义在线程这类事物中。所以怎样获取这名称,是不是线程最熟悉?
Void setName(String name)
String getName()
线程都有自己默认的名称:Thread-编号,该编号从0开始。
1 class Test extends Thread{ 2 3 private String name; 4 5 Test(String name){ 6 7 this.name = name; 8 9 } 10 11 public void run(){ 12 13 for (int x=0;x<60;x++) { 14 15 System.out.println(this.getName() + "test....run"+x); 16 17 } 18 19 } 20 21 } 22 23 24 25 class ThreadDemo{ 26 27 public static void main(String[] args) { 28 29 //创建对象调用方法 30 31 //创建自定义的线程对象实际上就是创建了线程 32 33 Demo d = new Demo(); 34 35 //调用start()方法,启动线程;jvm调用线程对象的run()方法,执行内部代码 36 37 //d.start();//开启线程并执行该线程的run()方法 38 39 //d.run();仅仅是对象调用方法,在主线程中执行,而线程创建了,并没有运行。即还是单线程程序。 40 41 //测试小练习 42 43 Test t1 = new Test("one"); 44 45 Test t2 = new Test("two"); 46 47 t1.start(); 48 49 t2.start(); 50 51 for (int x=0;x<60;x++) { 52 53 System.out.println("hello world!"+x); 54 55 } 56 57 } 58 59 }
1 class Test extends Thread{ 2 3 //private String name; 4 5 Test(String name){ 6 7 //this.name = name; 8 super(name); 9 } 10 11 public void run(){ 12 13 for (int x=0;x<60;x++) { 14 15 System.out.println(this.getName() + "test....run"+x); 16 17 } 18 19 } 20 21 } 22 23 24 25 class ThreadDemo{ 26 27 public static void main(String[] args) { 28 29 //创建对象调用方法 30 31 //创建自定义的线程对象实际上就是创建了线程 32 33 Demo d = new Demo(); 34 35 //调用start()方法,启动线程;jvm调用线程对象的run()方法,执行内部代码 36 37 //d.start();//开启线程并执行该线程的run()方法 38 39 //d.run();仅仅是对象调用方法,在主线程中执行,而线程创建了,并没有运行。即还是单线程程序。 40 41 //测试小练习 42 43 Test t1 = new Test("one"); 44 45 Test t2 = new Test("two"); 46 47 t1.start(); 48 49 t2.start(); 50 51 for (int x=0;x<60;x++) { 52 53 System.out.println("hello world!"+x); 54 55 } 56 57 } 58 59 }
这里博主本来有两个疑惑来着,后来经过博主苦思冥想,终于破解:
1.第一个既然打印的this.getName(),为什么获取到的不是one,two?
2.都已经super(name);了,为什么获取到的还是new对象时的name?
回答:
1.this是代表本类对象的引用,这个大家都知道,所以this.getName()是在本类中调用getName()方法,本类有吗?没有。那么他就会去父类Thread类中找,父类有吗?有,获取的是什么呢?由于我们没有创建父类的对象,更没有创建父类创建线程名称的构造函数,更没有调用父类setName()的方法,所以,父类的getName()方法,返回的是默认的setName()方法的值,也就是Thread-编号。
2.super(name)是在构造方法里,调用的是父类的构造方法,而父类的该方法又被子类对象复写,所以,又会去找子类,所以相当于饶了父类这一个弯子。
其实在1中也还有个小疑问没有解开,就是当set(),get()方法组合与构造函数同时操作同一事物时,哪一个会好使。
Thread类中还有个静态方法:
Static Thread currentThread() 返回对当前正在执行的线程对象。
也可以获取当前线程的名称,而且比this更通用。
总结一下:
获取线程名称:
Static Thread currentThread():获取当前的线程对象
getName()
设置线程名称
setName()或者构造函数。
小案例:
需求:简单的卖票程序,多个窗口同时卖票。
代码如下:
1 class Ticket extends Thread{ 2 3 private int tick = 100; 4 5 public void run(){ 6 7 while(true){ 8 9 if(tick>0){ 10 11 System.out.println("sale:" + tick--); 12 13 } 14 15 } 16 17 } 18 19 } 20 21 class TicketDemo{ 22 23 public static void main(String[] args){ 24 25 Ticket t1 = new Ticket(); 26 27 Ticket t2 = new Ticket(); 28 29 Ticket t3 = new Ticket(); 30 31 Ticket t4 = new Ticket(); 32 33 t1.start(); 34 35 t2.start(); 36 37 t3.start(); 38 39 t4.start(); 40 41 } 42 43 }
但结果我们发现,每一个线程都会从100打印到1,也就是说每一个线程都卖了100张票,怎么解决呢?
让四个线程共享这100张票就可以了。
既然是共享的,没有特有的数据参与运算,我们就可以把100设为静态就搞定了。
但我们一般不这样,因为设为static后,它的生命周期过长。于是,我们就只创建一个对象,让它执行四次start()方法,结果发现也是可行的,只不过报了一个异常:
Java.lang.IllegalThreadStateExceptionThread-0…sale:99
这是无效的线程状态异常。已经运行的程序是不需要在开启的。那么显然,第一种创建方式,已经不行了,接下来我们来介绍一下第二种创建线程的方法。
创建线程的另外一种方法是声明实现Runnable接口的类。然后实现run方法。然后可以创建该类的实例,在创建时,作为一个参数来传递并启动。
Runnable接口里只有一个run()方法。
Thread类有一个构造方法可以接受Runnable接口类型的对象。
1 class Ticket implements Runnable{//extends Thread{ 2 3 private int tick = 100; 4 5 public void run(){ 6 7 while(true){ 8 9 if(tick>0){ 10 11 System.out.println("sale:" + tick--); 12 13 } 14 15 } 16 17 } 18 19 } 20 21 class TicketDemo{ 22 23 public static void main(String[] args){ 24 25 // Ticket t1 = new Ticket(); 26 27 // Ticket t2 = new Ticket(); 28 29 // Ticket t3 = new Ticket(); 30 31 // Ticket t4 = new Ticket(); 32 33 // t1.start(); 34 35 // t2.start(); 36 37 // t3.start(); 38 39 // t4.start(); 40 41 Ticket t = new Ticket();// 42 43 //记住,开启线程的只能是Thread类或者Thread类子类的对象 44 45 Thread t1 = new Thread(t);// 46 47 Thread t2 = new Thread(t); 48 49 Thread t3 = new Thread(t); 50 51 Thread t4 = new Thread(t); 52 53 t1.start(); 54 55 t2.start(); 56 57 t3.start(); 58 59 t4.start(); 60 61 62 63 } 64 65 }
创建线程的第二种方法,实现Runable接口
步骤:
1.定义类实现Runnable接口
2.覆盖Runnable接口中的run()方法
3通过Thread类建立线程对象
4将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数
5调用Thread类的start方法开启线程(让其调用Runnable接口子类的run()方法)。
其中复写Runnable接口中的run()方法我们前文已经分析过了,将线程要运行的代码存放在该run()方法中。
那么,为什么要将Runnable接口的子类对象传递给Thread的构造函数?
因为,自定义的run()方法所属的对象是Runnable接口的子类对象,所以要让线程去执行指定对象的run()方法。就必须明确该run()所属对象。
那么,创建线程的第二种实现方式和第一种继承方式有什么区别呢?(面试)
实现方式好处:避免了单继承的局限性。
在定义线程时,建议使用实现方式。
继承Thread线程代码存放在Thread子类run()方法中
实现Runnable,线程代码存放在接口的子类的run()方法中。