java内部支持多线程,在JDK1.0的时候就确定了基础的线程模型,在其后的多个版本都是在此基础上面的优化,多线程的实现由多种方式,最常用的是实现runnable接口创建线程
多线程
只有一个执行流程的程序被称为单线程程序,相对而言,拥有多个执行流程的程序,被称为多线程程序。
Java程序运行原理
Java使用Thread类代表线程,所有线程对象都是Thread类或者其子类的实例,new Thread()时JVM会创建一个新的线程(Thread类内部调用实现,反正就是新建了一个线程) ,调用线程对象的start()方法线程启动(java线程启动的固定实现方式,直接调用run()方法不会启动线程,只是普通的方法调用),main方法运行在主线程中,main线程也是守护线程,在程序完全运行结束后才结束,即从头跑到尾的线程,在Thread启动之前的所有程序都是单线程的。
java虚拟机是多线程的,因为除了主线程外,还有垃圾回收线程
线程分类
用户线程就是主线程,用户线程结束完JVM就会停止。
后台线程:指为其他线程提供服务的线程,也称为守护线程。JVM的垃圾回收线程就是一个后台线程。
用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束。
前台线程:是指接受后台线程服务的线程,其实前台后台线程是联系在一起,就像傀儡和幕后操纵者一样的关系。傀儡是前台线程、幕后操纵者是后台线程。由前台线程创建的线程默认也是前台线程。可以通过isDaemon()和setDaemon()方法来判断和设置一个线程是否为后台线程。
当前线程:这个是容易混淆的概念。一般指通过Thread.currentThread()来获取的进程。
线程的创建和启动
Java使用Thread类代表线程,所有线程对象都是Thread类或者其子类的实例。创建线程的方式有四种,分别是:
1)继承Thread类创建线程;
2)实现Runnable接口创建线程;
3)利用Runnable内部类创建线程;
4)使用Callable和Future创建线程。
以上四种方式均可以创建线程,不过它们各有优劣,我将在分别叙述完每一种创建线程的方式后总结概括。
3.1 继承Thread类创建线程
主要步骤为:
① 定义一个类并继承Thread类,此类中需要重写Thread类中的run()方法,这个run()方法就是多线程都需要执行的方法;整个run()方法也叫做线程执行体;
② 创建此类的实例(对象),这时就创建了线程对象;
③ 调用线程对象的start()方法来启动该线程。
举例说明:
public class MyThread1 extends Thread {
private int a;
@Override
public void run(){
for(;a<100;a++){
//获取线程的名称并输出
System.out.println("Thread线程创建示例1===="+getName());
}
}
public static void main(String[] args) {
new MyThread1().start();
new MyThread1().start();
}
}
上面通过一个简单的例子演示了创建线程的第一种方法(通过继承Thread类创建线程);通过运行以上代码发现有两个线程在并发执行,由于没有对线程进行显示的命名,所以系统默认这两个线程的名称为Thread-0和Thread-1。
那么在上述例子中一共有多少个线程在运行呢?答案是三个!
分别是main(主线程)、Thread-0和Thread-1;我们在多线程编程时一定不要忘记Java程序运行时默认的主线程,main()方法的方法体就是主线程的线程执行体;同理,run()方法就是新建线程的线程执行体。
在程序中如果想要获取当前线程对象可以使用方法:Thread.currentThread();
如果想要返回线程的名称,则可以使用方法:getName();
故如果想要获取当前线程的名称可以使用以上二者的搭配形式:Thread.currentThread().getName();
此外,还可以通过setName(String name)方法为线程设置名字;具体操作步骤是在定义线程后用线程对象调用setName()方法:
在讨论完设置线程名称及获取线程名称的话题后,我们来分析下变量的共享。从以上代码运行结果来看,线程Thread0和线程Thread1分别输出0-99,由此可以看出,使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量。
3.2 实现Runnable接口创建线程类
主要步骤为:
① 定义一个类并实现Runnable接口,重写该接口的run()方法,run()方法的方法体依旧是该线程的线程执行体;
② 创建定义的类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象;
③ 调用线程的start()方法来启动该线程。
举例说明:
public class MyThread2 implements Runnable {
public static void main(String[] args)
{
MyThread2 m1 = new MyThread2();
Thread t1 = new Thread(m1,"线程1");
Thread t2 = new Thread(m1,"线程2");
t1.start();
t2.start();
}
private int a;
public void run()
{
for ( ; a<100 ; a++ )
{
System.out.println(Thread.currentThread().getName()+" "+a);
}
}
}
运行上面的程序可以看出:两个子线程的i变量是连续的,也就是说采用Runnable接口的方式创建的两个线程可以共享线程类的实例属性,这是因为我们创建的两个线程共用同一个target(m1),即为两个线程持有同一个对象的锁。
通过对以上两种创建新线程的方法进行比较分析,可以知道两种创建并启动多线程方式的区别是:通过继承Thread类创建的对象即是线程对象,而通过实现Runnable接口创建的类对象只能作为线程对象的target。
3.3 Runnable匿名内部类实现thread
public class MyThread3 {
private static int a;
public static void main(String[] args) {
new Thread(new Runnable(){
@Override
public void run(){
for(;a<100;a++){
System.out.println("Thread线程创建示例3====");
}
}
}).start();
new MyThread3().start();
}
}
实现方式同3.2 只不过采用匿名的方式
3.4 通过Callable和Future创建线程
Callable接口是在Java5才提出的,它是Runnable接口的增强版;它提供了一个call()方法作为线程执行体,且call()方法比run()方法更为强大,主要体现在:
① call()方法可以有返回值;
② call()方法可以申明抛出异常。
Java5提供了Future接口来代表Callable接口里call()方法的返回值,并为Futrue接口提供一个FutureTask实现类,此实现类实现了Future接口,并且实现了Runnable接口,可以作为Thread类的target。不过需要提出的是,Callable接口有泛型限制,Callable接口里的泛型形参类型于call()方法返回值类型相同。
主要步骤为:(创建并启动有返回值的线程)
① 创建Callable接口的实现类,并实现call()方法作为线程的执行体,且该call()方法有返回值;
//不再是void
② 创建Callable接口实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;
③ 使用FutureTask对象作为Thread对象的target创建并启动新线程;
④ 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
举例说明:
public class MyThread4 implements Callable<Integer> {
public static void main(String[] args)
{
MyThread4 m1 = new MyThread4();//创建Callable对象
//使用FutureTask来包装Callable对象
FutureTask<Integer> task = new FutureTask<Integer>(m1);
Thread t1 = new Thread(task,"有返回值的线程");
t1.start();//启动线程
//获取线程返回值
try
{
System.out.println("子线程的返回值:"+task.get());
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
public Integer call()//返回值类型为Integer
{
int i = 0;
for ( ; i<100 ; i++ )
{
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;//call()可以有返回值
}
}
其实,创建Callable实现类与创建Runnable实现类没有太大区别,只是Callable的call()方法允许声明抛出异常,而且允许带返回值。
3.4 四种创建线程方法的对比
由于实现Runnable接口和实现Callable接口创建新线程方法基本一致,这里我们姑且把他们看作是同一类型;这种方式同继承Thread方式相比较,优劣分别为:
1.采用实现Runnable接口和Callable接口的方式创建多线程
① 优点:
1)实现类只是实现了接口,所以它还可以继承其他类;
2)多个线程可以共享一个target,所以适合多线程处理同一资源的模式,从而可以将CPU、代码和数据分开,较好的体现了面向对象的思想。
② 缺点:
1)编程比较复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。
2) 采用继承Thread类的方式来创建新线程
① 优点:
1)编写简单,如果需要访问当前线程,则只需要使用this即可。
② 缺点:
1)因为线程已经继承了Thread类,所以不能再继承其他类。
3.总结
① 综合分析,我们一般采用实现Runnable接口和实现Callable接口的方式来创建多线程,更多的时候可以采用Runnable匿名内部类方便的实现多线程。