Java学习笔记-《Java程序员面试宝典》-第四章基础知识-4.10多线程(4.10.1-4.10.4)

4.10.1什么是线程?它与进程有什么区别?为什么要使用多线程

线程是指在程序执行过程中,能够执行程序代码的一个执行单元。在Java语言中,线程有4种状态:运行、就绪、挂起和结束。
进程是指一段正在执行的程序。而线程有时也被称为轻量级进程,它是程序执行的最小单元,一个进程可以拥有多个线程,各个线程之间共享程序的内存空间(代码段、数据段和堆空间)及一些进程级的资源(例如打开的文件),但是各个线程拥有自己的栈空间,进程与线程的对比关系如下图所示:
Java学习笔记-《Java程序员面试宝典》-第四章基础知识-4.10多线程(4.10.1-4.10.4)_第1张图片
在操作系统级别上,程序的执行都是以进程为单位的,而每个进程中通常会有多个进程互不影响地并发执行,那么为什么要使用多线程呢?其实,多线程的使用为程序研发带来了极大地便利,具体而言,有以下几个方面的内容:
1>使用多线程可以减少程序的响应时间。在单线程(单线程指的是程序执行过程中只有一个有效操作的序列,不同操作之间都有明确的执行先后顺序)的情况下,如果某个操作很耗时,或者陷入长时间的等待(如等待网络响应),此时程序将不会响应鼠标和键盘等操作,使用多线程后,可以把这个耗时的线程分配到一个单独的线程去执行,从而使程序具备了更好的交互性。
2>与进程相比,线程的创建和切换开销更小。由于启动一个新的线程必须给这个线程分配独立的地址空间,建立许多数据结构来维护线程代码段、数据段等信息,而运行于同一进程内的多个线程共享代码段、数据段,线程的启动或切换的开销比进程要少很多。同时多线程在数据共享方面效率非常高。
3>多CPU或多核计算机本身就有执行多线程的能力,如果使用单个线程,将无法重复利用计算机资源,造成资源的巨大浪费。因此在多CPU计算机上使用多线程能提高CPU的利用率。
4>使用多线程能简化程序的结构,使程序便于理解和维护。一个非常复杂的进程可以分为多个线程来执行。

4.10.2同步和异步有什么区别

在多线程的环境中,经常会碰到数据的共享问题,即当多个线程需要访问同一个资源时,他们需要以某种顺序来确保该资源在某一时刻只能被一个线程使用,否则,程序的运行结果将会是不可预料的,在这种情况下就必须对数据进行同步,例如多个线程同时对同一数据进行写操作,即当线程A需要使用某个资源时,如果这个资源正在被线程B使用,同步机制就会让线程A一直等待下去,直到线程B结束对该资源的使用后,线程A才能使用这个资源,由此可见,同步机制能够保证资源的安全。
要想实现同步操作,必须要获得每一个线程对象的锁。获得它可以保证在同一时刻只有一个线程能够进入临界区(访问互斥资源的代码块),并且在这个锁被释放之前,其他线程就不能再进入这个临界区。如果还有其他进程想要获得该对象的锁,只能进入等待队列等待。只有当拥有该对象锁的进程退出临界区时,锁才会被释放,等待队列中优先级最高的线程才能获得该锁,从而进入共享代码区。
Java语言在同步机制中提供了语言级的支持,可以通过使用synchronized关键字来实现同步,但该方法并非”万金油”,它是以很大的系统开销作为代价的,有时候甚至可能造成死锁,所以,同步控制并非越多越好,要尽量避免无谓的同步控制。实现同步的方式有两种:一种是利用同步代码块来实现同步;另一种是利用同步方法来实现同步。
异步与非阻塞类似,由于每个线程都包含了运行时自身所需要的数据或方法,因此,在进行输入输出处理时,不必关心其他线程的状态或行为,也不必等到输入输出处理完毕才返回。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,异步能够提高程序的效率。
举个生活中的简单例子区分同步和异步。同步就是你喊我去吃饭,如果听到了,我就和你去吃饭;如果我没听到,你就不停的喊,直到我告诉你我听到了,我们才一起去吃饭。异步就是你喊我,然后你自己去吃饭,我得到消息后可能立即走,也可能等到下班才去吃饭。

4.10.3如何实现Java多线程

Java虚拟机允许应用程序并发的运行多个线程。在Java预言中国,多线程的实现一般有以下3种方法,其中前两种为最常用的方法。
1>继承Thread类,重写run()方法
Thread本质上也是实现了Runnable接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一办法就是通过Thread类的start()方法。start()方法是一个native(本地)方法,它将启动一个新线程,并执行run()方法(Thread中提供的run()方法是一个空方法)。这种方式通过自定义直接extend Thread,并重写run()方法,就可以直接启动新线程并执行自己定义的run()方法。需要注意的是,调用start()方法后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行多线程代码是由操作系统决定的。下例给出了Thread的使用方法:

class MyThread extends Thread{ 
    public void run(){
        System.out.println("Thread body");//线程的函数体
    }
}
public class Test{
    public static void main(String[] args){
        MyThread thread = new MyThread();
        thread.start();    //开启线程
    }
}

2>实现Runnable接口,并实现该接口的run()方法
以下是主要步骤:
1)自定义类并实现Runnable接口,实现run()方法。
2)创建Thread对象,用实现Runnable接口的对象作为参数实例化该Thread对象。
3)调用Thread的start()方法。

class MyThread implements Runnable{//创建线程类
    public void run(){
        System.out.println("Thread body");
    }
}
public class Test{
    public static void main(String[] args){
        MyThread thread = new MyThread();
        Thread t = new Thread(thread);
        t.start(); //开启线程
    }
}

其实,不管是通过继承Thread类还是通过使用Runnable接口来实现多线程的方法,最终还是通过Thread的对象的API来控制线程的。
3>实现Callable接口,重写call()方法
Callable接口实际是属于Executor框架中的功能类,Callable接口与Runnable接口的功能类似,但提供了比Runnable更强大的功能,主要表现为以下3点:
1)Callable可以在任务结束后提供一个返回值,Runnable无法提供这个功能。
2)Callable中的call()方法可以抛出异常,而Runnable的run()方法不能抛出异常。
3)运行Callable可以拿到一个Future对象,Future对象表示异步计算的结果,它提供了检查计算是否完成的方法。由于线程属于异步计算模型,因此无法从别的线程中得到函数的返回值,在这种情况下就可以用Future来监视目标线程调用call()方法的情况,当调用Future的get()方法以获取结果时,当前线程就会阻塞,直到call()方法结束返回结果。

import java.util.concurrent.*;

public class CallableAndFuture {

    //创建线程类
    public static class CallableTest implements Callable<String>{

        @Override
        public String call() throws Exception {
            // TODO Auto-generated method stub
            return "Hello World";
        }

    }

    public static void main(String[] args){

        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        //启动线程
        Future future = threadPool.submit(new CallableTest());

        try{

            System.out.println("waiting thread to finish");
            System.out.println(future.get()); //等待线程结束,并获取返回值
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

运行结果:
waiting thread to finish
Hello World

以上3种方式中,前两种方式线程执行完后都没有返回值,只有最后一种是带返回值。当需要实现多线程时,一般推荐实现Runnable接口的方式,其原因是:首先,Thread类定义了多种方法可以被派生类使用或重写。但是只有run()方法是必须被重写的,在run()方法中实现这个线程的主要功能。这当然是实现Runnable接口所需的方法。其次,很多Java开发人员认为,一个类仅在他们需要被加强或修改时才会被继承。因此,如果没有必要重写Thread类中的其他方法,那么通过继承Thread的实现方式与实现Runnable接口的效果相同,在这种情况下最好通过实现Runnable接口的方式来创建线程。

引申:一个类是否可以同时继承Thread与实现Runnable接口?
答:可以,实例如下:

public class Test extends Thread implements Runnable{
    public static void main(String[] args){
        Thread t = new Thread(new Test());
        t.start();
    }
}

从上例中可以看出,Test类实现了Runnable接口,但是并没有实现接口的run()方法,但是这并不会导致编译错误,可以通过编译并运行。因为Test类从Thread类中继承了run()方法,这个继承的run()方法可以被当做对Runnable接口的实现,因此这段代码可以通过编译运行。当然也可以不使用继承的run()方法,而是需要通过在Test类中重写run()方法来实现Runnable接口中的run()方法,示例如下:

public class Test extends Thread implements Runnable{

    public void run(){
        System.out.println("this is run");
    }
    public static void main(String[] args){
        Thread t = new Thread(new Test());
        t.start();
    }
}

运行结果:
this is run

4.10.4 run()方法与start()方法有什么区别

通常,系统通过调用线程类的start()方法来启动一个线程,此时该线程处于就绪状态,而非运行状态,也就意味着这个线程可以被JVM来调度执行。在调度过程中,JVM通过调用线程类的run()方法来完成实际的操作,当run()方法结束后,此线程就会终止。
如果直接调用线程类的run()方法,这会被当作一个普通的函数调用,程序中仍然只有主程序这一线程,也就是说,strat()方法能够异步地调用run()方法,但是直接调用run()方法确实同步的,因此也就无法达到多线程的目的。
由此可知,只有通过调用线程类的start()方法才能达到多线程的目的。下面通过一个例子来说明run()方法与start()方法的区别。


class ThreadDemo extends Thread{

    @Override
    public void run(){
        System.out.println("ThreadDemo begin");

        try{
            Thread.sleep(1000);
        }catch(InterruptedException e){
            e.printStackTrace();
        }

        System.out.println("ThreadDemo end");
    }
}
public class TestThread {

    public static void test1(){
        System.out.println("test1 begin");
        Thread t1 = new ThreadDemo();
        t1.start();
        System.out.println("test1 end");
    }

    public static void test2(){
        System.out.println("test2 begin");
        Thread t1 = new ThreadDemo();
        t1.run();
        System.out.println("test2 end");
    }

    public static void main(String[] args){
        test1();

        try{
            Thread.sleep(5000);
        }catch(InterruptedException e){
            e.printStackTrace();
        }

        System.out.println();
        test2();
    }
}

运行结果:
test1 begin
test1 end
ThreadDemo begin
ThreadDemo end

test2 begin
ThreadDemo begin
ThreadDemo end
test2 end

从test1的运行结果可以看出,线程t1是在test1方法结束后才执行的(System.out.println(“test1 end”)语句不需要等待t1.start()运行结果就可以执行了),因此在test1中调用start()方法是异步的,所以main线程与t1线程是异步执行的。从test2的运行结果可以看出,调用t1.run()是同步的调用方法,因为System.out.println(“test2 end”)只有等t1.run()调用结束后才能执行。

你可能感兴趣的:(Java学习笔记-《Java程序员面试宝典》-第四章基础知识-4.10多线程(4.10.1-4.10.4))