Java基础-多线程-①线程的创建和启动
简单阐释进程和线程
对于进程最直观的感受应该就是“windows任务管理器”中的进程管理:
(计算机原理课上的记忆已经快要模糊了,简单理解一下):一个进程就是一个“执行中的程序”,是程序在计算机上的一次运行活动。程序要运行,系统就在内存中为该程序分配一块独立的内存空间,载入程序代码和资源进行执行。程序运行期间该内存空间不能被其他进程直接访问。系统以进程为基本单位进行系统资源的调度和分配。何为线程?线程是进程内一次具体的执行任务。程序的执行具体是通过线程来完成的,所以一个进程中至少有一个线程。回忆一下 HelloWrold 程序中main方法的执行,其实这时候,Java虚拟机会开启一个名为“main”的线程来执行程序代码。一个进程可以包含多个线程,这些线程共享数据空间和资源,但又分别拥有各自的执行堆栈和程序计数器。线程是CPU调度的基本单位。
多线程
一个进程包含了多个线程,自然就叫做多线程。拥有多个线程就可以让程序看起来可以“同时”处理多个任务,为什么是看起来呢?因为CPU也分身乏术,只能让你这个线程执行一会儿,好了你歇着,再让另一个线程执行一会儿,下次轮到你的时候你再继续执行。这里的“一会儿”实际上时间非常短,感觉上就是多个任务“同时”在执行。CPU就这样不停的切来切去…既然CPU一次也只能执行一个线程,为什么要使用多线程呢?当然是为了充分利用CPU资源。一个线程执行过程中不可能每时每刻都在占用CPU,CPU歇着的时候我们就可以让它切过来执行其他的线程。
比如QQ聊天的时候,跟一个人正聊着呢,另一个消息过来了。如果是单线程,不好意思,等我跟这一个聊完说拜拜之后再去理你吧。多线程呢,消息窗口全打开,这个窗口说完话了,总得等人家回吧,趁这个空闲时候,处理另一个窗口的消息。这样看起来不就是同时进行了么,每一个窗口的另一边都以为你只在跟他一个人聊天…
Java中的多线程
Java中启用多线程有两种方式:①继承Thread类;②实现Runnable接口。
There are two ways to create a new thread of execution. One is to declare a class to be a subclass of
Thread
. This subclass should override therun
method of classThread
. An instance of the subclass can then be allocated and started.The other way to create a thread is to declare a class that implements theRunnable
interface. That class then implements therun
method. An instance of the class can then be allocated, passed as an argument when creatingThread
, and started.
继承Thread类
创建一个类,继承java.lang.Thread,并覆写Thread类的run()方法,该类的实例就可以作为一个线程对象被开启。
/** * Dog类,继承了Thread类 * @author lt */ class Dog extends Thread { /* * 覆写run()方法,定义该线程需要执行的代码 */ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(i); } } }
线程创建好了,怎么让它作为程序的一个独立的线程被执行呢?创建一个该类的实例,并调用start()方法,将开启一个线程,并执行线程类中覆写的run()方法。
public class ThreadDemo { public static void main(String[] args) { Dog dog = new Dog(); dog.start(); } }
看不出什么端倪,如果我们直接调用实例的run()方法,执行效果是完全一样的,见上图。
public class ThreadDemo { public static void main(String[] args) { Dog dog = new Dog(); dog.run(); } }
如果一切正常,这时候程序中应该有两个线程:一个主线程main,一个新开启的线程。run()方法中的代码究竟是哪个线程执行的呢?Java程序中,一个线程开启会被分配一个线程名:Thread-x,x从0开始。我们可以打印当前线程的线程名,来看看究竟是谁在执行代码。
class Dog extends Thread { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("当前线程:" + Thread.currentThread().getName() + "---" + i); } } } public class ThreadDemo { public static void main(String[] args) { System.out.println("当前线程:" + Thread.currentThread().getName()); Dog dog = new Dog(); dog.start(); } }
可以看到,确实开启了一个新的线程:Thread-0,main()方法的线程名就叫main。
同一个实例只能调用一次start()方法开启一次,多次开启,将报java.lang.IllegalThreadStateException异常:
我们再创建一个实例,开启第三个线程:
public class ThreadDemo { public static void main(String[] args) { System.out.println("当前线程:" + Thread.currentThread().getName()); Dog dog = new Dog(); Dog dog2 = new Dog(); dog.start(); dog2.start(); } }
这时候我们已经能够看到多线程的底层实现原理:CPU切换处理、交替执行的效果了。
run和start
上面我们直接调用run()方法和调用start()方法的结果一样,现在我们在打印线程名的情况下再来看看:
public class ThreadDemo { public static void main(String[] args) { System.out.println("当前线程:" + Thread.currentThread().getName()); Dog dog = new Dog(); Dog dog2 = new Dog(); dog.run(); dog2.run(); } }
可以看到,这时候并没有开启新的线程,main线程直接调用执行了run()方法中的代码。所以start()方法会开启新的线程并在新的线程中执行run()方法中的代码,而run()方法不会开启线程。查看start()的源代码,该方法调用了本地方法 private native void start0();即调用的是操作系统的底层函数:
public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { /* do nothing. If start0 threw a Throwable then it will be passed up the call stack */ } } } private native void start0();
实现Runnable接口
第二种方式,实现Runnable接口,并覆写接口中的run()方法,这是推荐的也是最常用的方式。Runnable接口定义非常简单,就只有一个抽象的run()方法。
//Runnable接口源码 public interface Runnable { public abstract void run(); }
class Dog implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("当前线程:" + Thread.currentThread().getName() + "---" + i); } } }
这时候的Dog类看起来跟线程什么的毫无关系,也没有了start()方法,怎么样开启一个新的线程呢?直接调用run()方法?想想也不行。这时候我们需要将一个Dog类的实例,作为Thread类的构造函数的参数传入,来创建一个Thread类的实例,并通过该Thread类的实例来调用start()方法从而开启线程。
public class ThreadDemo { public static void main(String[] args) { System.out.println("当前线程:" + Thread.currentThread().getName()); Dog dog = new Dog(); Thread thread = new Thread(dog); thread.start(); } }
这时候如果要开启第三个线程,需要创建一个新的Thread类的实例,同时传入刚才的Dog类的实例(当然也可以创建一个新的Dog实例)。这时候我们就可以看到跟继承Thread类的方式的区别:多个线程可以共享同一个Dog类的实例。
public class ThreadDemo { public static void main(String[] args) { System.out.println("当前线程:" + Thread.currentThread().getName()); Dog dog = new Dog(); Thread thread = new Thread(dog); Thread thread2 = new Thread(dog); thread.start(); thread2.start(); } }
两种方式的比较
继承Thread类的方式有它固有的弊端,因为Java中继承的单一性,继承了Thread类就不能继承其他类了;同时也不符合继承的语义,Dog跟Thread没有直接的父子关系,继承Thread只是为了能拥有一些功能特性。而实现Runnable接口,①避免了单一继承的局限性,②同时更符合面向对象的编程方式,即将线程对象进行单独的封装,③而且实现接口的方式降低了线程对象(Dog)和线程任务(run方法中的代码)的耦合性,④如上面所述,可以使用同一个Dog类的实例来创建并开启多个线程,非常方便的实现资源的共享。实际上Thread类也是实现了Runnable接口。实际开发中多是使用实现Runnable接口的方式。