Java 基础之多线程



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


      线程是指程序在执行过程中,能够执行程序代码的一个执行单元。在 Java 语言中线程有 4 种运行状态:运行、就绪、挂起和结束。

       进程是指一段正在执行的程序。而线程有时也被称为轻量级的进程,各个线程之间共享程序的内存空间(代码段、数据段和堆空间)及一些进程级的资源(例如打开文件),但是各个线程拥有自己的栈空间

      在操作系统级别上,程序的执行都是以进程为单位的,而每个进程中通常都会有多个线程互不影响地并发执行,那么为什么要使用多线程?其实,多线程的使用为程序的研发带来了巨大的便利,具体而言有以下几个方面的内容:

1)使用多线程可以减少程序的响应时间,在单线程的情况下,如果某个操作很耗时,或者陷入长时间的等待(如等待网络响应),此时程序将不会响应鼠标、键盘等操作,使用多线程后,可以把这个耗时的线程分配到一个单独的线程中去执行,从而使的程序具备了更多的交互性

2)与进程相比,线程的创建和切换开销更小,由于启动一个新的线程必须给这个线程分配独立的地址空间,建立许多数据结构来维护线程代码段,数据段等信息,而运行于同一进程内的线程共享代码段和数据段,线程的启动,切换的开销比进程要少很多,同时多线程在数据共享方面效率非常高

3)多 CPU 或者多核计算机本身就具有执行多线程的能力,如果使用单个线程,将无法重复利用计算机资源,造成资源的浪费,因此在多 CPU 计算机上使用多线程能提高 CPU 的利用率

4)使用多线程能简化程序的结构,是程序便于理解和维护,一个非常复杂的进程可以分成多个线程来执行


二、同步和异步有什么区别?


      在多线程的环境中,经常会碰到数据的共享问题,即当多个线程需要访问同一个资源时,它们需要以某种顺序来确保该资源在某一时刻只能被一个线程使用,否则,程序的运行结果将是不可预测的,在这种情况下就必须对数据进行同步,例如多个线程同时对同一数据进行写操作,即当线程 A 需要使用某个资源时,如果这个资源正在被线程 B 使用,同步机制就会让线程 A 一直等待下去,直到线程 B 结束对该资源的使用后,线程 A 才能使用这个资源,由此可见,同步机制可以保证资源的安全

      想要实现同步操作,必须要获得每一个线程的对象锁,获得它可以保证在同一时刻只有一个线程可以进入临界区(访问互斥资源的代码块),并且在这个锁被释放之前,其他的线程就不能再进入这个临界区,如果还有其它线程想要获得该对象的锁,只能进入等待队列等待,只有当拥有该对象锁的线程退出该临界区域时,锁才会被释放,等待队列中优先级最高的线程才能获得该锁,从而进入共享代码区

      Java 语言在同步机制中提供了语言级的支持,可以通过使用 synchronized 关键字来实现同步,但该方法并非万金油,它是以很大的系统开销为代价的,有时候可能造成死锁,所以同步控制并非越多越好,要尽量避免无谓的同步机制,实现同步的方式有两种:一种是利用同步代码块来实现,而另一种是利用同步方法来实现

      异步与非阻塞类似,由于每个线程都包含了运行时所需要的数据或方法,因此,在进行输入输出处理时,不必关心其它线程的状态和行为,也不必等到输入输出处理完毕才返回,当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,异步能够提高程序的效率


三、如何实现 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 {  
    @Override  
    public void run() {  
        //处理具体的逻辑  
    }  
}

那么如何启动这个线程呢?只需要 new 出 MyThread 的实例,然后调用它的 start() 方法,这样 run() 方法中的代码就会运行在子线程,如下:new MyThread().start();

2)实现 Runnable 接口,并实现该接口的 run() 方法

/** 
 * 实现 Runnable 接口创建子线程 
 */  
class MyThread implements Runnable {  
    @Override  
    public void run() {  
        //处理具体的逻辑  
    }  
}

如果使用这种写法,启动线程的方法也需要相应的改变,如下:

MyThread myThread = new MyThread();  
new Thread(myThread).start();

      Thread 构造函数接受一个 Runnable 参数,我们 new 出的 MyThread 正是一个实现了 Runnable 接口的对象,所以可以直接将它传入到 Thread 的构造函数里,接着就和上面的一样了,调用 Thread 的 start() 方法,run() 方法中的代码就会在子线程当中运行了

      其实,不管是通过继承 Thread 类还是通过使用 Runnable 接口来实现多线成的方法,最终还是通过 Thread 的对象的 API 来控制线程的

3)实现 Callable 接口,重写  call 方法

      Callable 接口实际是属于 Executor 框架中的功能类,Callable 接口与 Runnable 接口的功能类似,但提供了比 Runnable 更强大的功能,主要表现为以下 3 点:

  • Callable 可以在任务结束后提供一个返回值,Runnable 无法提供这个功能
  • Callable 中的 call 方法可以抛出异常,而 Runnable 的 run 方法不能抛出异常
  • 运行 Callable 可以拿到一个 Future 对象,Future 对象表示异步计算的结果,它提供了检查计算是否完成的方法,由于线程属于异步计算模型,因此无法从别的线程中得到函数的返回值,在这种情况下,就可以使用 Future 来监视目标线程调用 call() 方法的情况,当调用 Future 的 get() 方法以获取结果时,当前线程就会阻塞以,直到 call() 方法结束返回结果
public class CallableAndFuture {
    /**
     * 创建线程类
     */
    public static class CallableTest implements Callable<String> {

        @Override
        public String call() throws Exception {
            return "Helle World";
        }
    }

    public static void main(String[] args){
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        //启动线程
        Future<String> future = executorService.submit(new CallableTest());
        //等待多线程结束,并获取返回结果
        try {
            System.out.println("waiting thread to finish");
            System.out.println(future.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

      以上代码只供参考,以上 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 thread = new Thread(new Test());
        thread.start();
    }
}

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

public class Test extends Thread implements Runnable {

    @Override
    public void run() {
        System.out.println("this is run");
    }

    public static void main(String args[]) {
        Thread thread = new Thread(new Test());
        thread.start();
    }
}


四、run() 方法与 start() 方法有什么区别?


     通常,系统通过调用线程类的 start() 方法启动一个线程,此时该线程处于就绪状态,而非运行状态,也就意味着这个线程可以被 JVM 来调度执行,在调度过程中,JVM 通过调用线程类的 run() 方法来完成实际的操作,当 run() 方法结束后,线程就会终止

      如果直接调用线程类的 run() 方法,这会被当作一个普通的函数,程序中仍然只有主线程这一个线程,也就是说 start() 方法能够异步的调用 run() 方法,但是直接调用 run() 方法却是同步的,因此也就无法达到多线程的目的,由此可见,只有通过调用线程类的 start() 方法才能达到多线程的目的


五、多线程同步实现的方法有哪些?


(1)synchronized 关键字

      在 Java 语言中,每个对象都有一个对象锁与之相关联,该锁表明对象在任何时候只容许被一个对象所拥有,当一个线程调用对象的一段 synchronized 代码时,需要先获取这个锁,然后去执行相应的代码,执行结束后释放锁

synchronized 关键字主要有两种用法(synchronized 方法和 synchronized 块),此外该关键字还可以作用于静态方法,或某个实例,但这都对程序的效率有一定的影响

  • synchronized 方法:在方法的声明前加入 synchronized 关键字,示例如下
public synchronized void Test(){}

       只要把多个线程对类需要被同步的资源的操作放到 Test() 方法中,就能保证这个方法在同一时刻只能被一个线程访问,从而保证了多线程访问的安全,然而当一个线程的方法体规模非常大时,把该方法声明为 synchronized 会大大影响程序的执行效率,为了提高程序的效率,Java 提供了 synchronized 块

  • synchronized 块:synchronized 块既可以把任意的代码段声明为 synchronized,也可以指定上锁的对象,有非常高的灵活性,其用法如下:
    synchronized (syncObject){
        //访问syncObject的代码
    }

(2)wait() 方法与 notify() 方法

      当使用某个 synchronized 来修饰某个共享资源时,如果线程 A1 在执行 synchronized 代码,另外一个线程 A2 也将要同时执行同一对象的同一个 synchronized 代码时,线程 A2 将要等到线程 A1 执行完成后,才能继续执行,在这种情况下可以使用 notify() 方法和 wait() 方法

      在 synchronized 代码被执行期间,线程可以调用对象的 wait() 方法,释放对象锁,进入等待状态,并且可以调用 notify() 方法或 notifyAll() 方法通知正在等待的其他线程,notify() 方法仅唤醒一个线程(等待队列中的第一个线程),并容许它去获得锁,notifyAll() 方法唤醒所有等待这个对象的线程,并允许他们去获得锁(并不是让所有唤醒线程都获得锁,而是让他们去竞争)。

(3)Lock

lock(): 以阻塞方式获取锁,也就是说如果获取到了锁,立即返回,如果别的线程持有锁,当前线程等待,直到获取锁后返回

tryLock(): 以非阻塞方式获取锁,只是尝试性的去获取一下锁,如果获取到锁,立即返回 true,否则返回 false


六、sleep() 方法与 wait() 方法有什么区别?


      sleep() 方法与 wait() 方法都是使线程暂停执行一段时间的方法,具体而言,sleep() 方法与 wait() 方法的区别主要表现在如下几个方面:

1)原理不同

      sleep() 方法是 Thread 类的静态方法,是线程用来控制自身流程的,它会使此线程暂停执行一段时间,而把执行机会让给其他线程,等到计时时间一到,次线程会自动苏醒,而 wait() 方法是 Object 类的方法,用于线程间的通信,这个方法会使当前拥有该对象锁的进程等待,直到其他线程调用 notify() 方法(或 notifyAll() 方法)时才醒来,不过开发人员也可以给他指定一个时间,自动醒来

2)对象锁处理机制不同

      由于 sleep() 方法的主要作用是让线程暂停执行一段时间,时间一到则自动恢复,不涉及线程间的通信,因此调用 sleep() 方法并不会释放锁,而 wait() 方法则不同,当调用 wait() 方法后,线程会释放掉它所占用的锁,从而使线程所在对象中的其他synchronized 数据可被别的线程使用

3)使用区域不同

      由于 wait() 方法的特殊意义,因此它必须放在同步控制方法或者同步语句块中使用,而 sleep() 方法可以在任何地方使用

最后由于 sleep() 方法不会释放锁标志,容易导致死锁问题的发生,因此一般情况下不推荐使用,而推荐使用 wait() 方法


七、终止线程的方法有哪些?


       在 Java 语言中,可以使用 stop() 方法与 suspend() 方法来终止线程的执行,当用 Thread.stop() 来终止线程时,它会释放已经锁定的所有监视资源,调用 suspend() 方法容易发生死锁(死锁指的是两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,如果无外力作用,它们都将无法推进),由于调用 suspend() 方法不会释放锁,这就会导致一个问题:如果用一个 suspend 挂起一个有锁的线程,那么在锁恢复之前将不会被释放,如果调用 suspend() 方法,线程将试图取得相同的锁,程序就会发生死锁


八、什么是守护线程?


       Java 提供了两种线程:守护线程与用户线程,守护线程又被称为“服务进程”或“后台线程”是指在程序运行时,在后台提供一种通用服务的线程,这种线程并不属于程序中不可或缺的部分,通俗点将,任何一个守护线程都是整个 JVM 中所有非守护线程的“保姆”

     用户线程和守护线程几乎一样,唯一的不同之处就在于如果用户线程已经全部退出运行,只剩下守护线程存在,JVM 也就退出了,因为当所有非守护程序结束时,没有了被守护者,守护线程也就没有工作可做了,也就没有运行程序的必要,程序也就终止了,同时会杀死所有守护线程





评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值