Java基础知识(十) 多线程

上一篇:Java基础知识(九) 容器

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

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

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

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

  1. 使用多线程可以减少程序的响应时间。在单线程(单线程指的是程序执行过程中只有一个有效操作的序列,不同操作之间都有明确的执行先后顺序)的情况下,如果某个操作很耗时,或者陷入长时间的等待(如等待网络响应),此时程序将不会响应鼠标和键盘等操作,使用多线程后,可以把这个耗时的线程分配到一个单独的线程去执行,从而使程序具备了更好的交互性。
  2. 与进程相比,线程的创建和切换开销更小。由于启动一个新的线程必须给这个线程分配独立的地址空间,建立许多数据结构来维护线程代码段、数据段等信息,而运行于同一进程内的线程共享代码段、数据段,线程的启动或切换的开销比进程要少很多。同时多线程在数据共享方面效率非常高。
  3. 多 CPU 或多核计算机本身就具有执行多线程的能力,如果使用单个线程,将无法重复利用计算机资源,造成资源的巨大浪费。因此在多 CPU 计算机上使用多线程能提高 CPU 的利用率。
  4. 使用多线程能简化程序的结构,使程序便于理解和维护。一个非常复杂的进程可以分
    成多个线程来执行。

2. 同步和异步有什么区别

在多线程的环境中,经常会碰到数据的共享问题,即当多个线程需要访问同一个资源时,它们需要以某种顺序来确保该资源在某一时刻只能被一个线程使用,否则,程序的运行结果将会是不可预料的,在这种情况下就必须对数据进行同步,例如多个线程同时对同—数据进行写操作,即当线程 A 需要使用某个资源时,如果这个资源正在被线程 B 使用,同步机制就会让线程 A 一直等待下去,直到线程 B 结束对该资源的使用后,线程 A 才能使用这个资源,由此可见,同步机制能够保证资源的安全。要想实现同步操作,必须要获得每一个线程对象的锁。获得它可以保证在同一时刻只有一个线程能够进入临界区(访问互斥资源的代码块),并且在这个锁被释放之前,其他线程就不能再进入这个临界区。如果还有其他线程想要获得该对象的锁,只能进入等待队列等待。只有当拥有该对象锁的线程退出临界区时,锁才会被释放,等待队列中优先级最高的线程才能获得该锁,从而进入共享代码区。

Java 语言在同步机制中提供了语言级的支持,可以通过使用 aynchronized 关键字来实现同步、但该方法并非“万金油”,它是以很大的系统开销作为代价的,有时候甚至可能造成死锁,所以,同步控制并非越多越好,要尽量避免无谓的同步控制。实现同步的方式有两种:一种是利用同步代码块来实现同步;另一种是利用同步方法来实现同步。异步与非阻塞类似,由于每个线程都包含了运行时自身所需要的数据或方法,因此,在进行输入输出处理时,不必关心其他线程的状态或行为,也不必等到输人输出处理完毕才返回。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,异步能够提高程序的效率。

举个生活中的简单例子就可以区分同步与异步了。同步就是你喊我去吃饭,如果听到了,我就和你去吃饭;如果我没有听到,你就不停地喊,直到我告诉你听到了,我们才一起去吃饭。异步就是你喊我,然后自己去吃饭,我得到消息后可能立即走,也可能等到下班才去吃饭。

3. 如何实现Java多线程

Java中实现多线程一般有三种方式:

  1. 继承Thread类,重写run()方法
/**
 * 描述:测试多线程
 *
 * @author Ye
 * @version 1.0
 * @date 2021/8/13 10:15
 */
public class TestThread1 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}
class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println(" 线程体0 " + this.getName());
    }
}

运行截图:
在这里插入图片描述

  1. 实现Runnable接口,并实现该接口的run()方法
/**
 * 描述:多线程测试
 *
 * @author Ye
 * @version 1.0
 * @date 2021/8/13 10:30
 */
public class TestThread2 {
    public static void main(String[] args) {
        MyThread1 thread = new MyThread1();
        Thread thread1 = new Thread(thread);
        thread1.run();
    }
}

class MyThread1 implements Runnable{
    @Override
    public void run() {
        System.out.println("线程体: " + this.getClass().getName());
    }
}

运行截图:
在这里插入图片描述

  1. 实现Callable接口,重写call()方法
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
 * 描述:测试多线程
 *
 * @author Ye
 * @version 1.0
 * @date 2021/8/13 10:44
 */
public class TestThread3 {

    public static class CallableTest implements Callable<String>{
        @Override
        public String call() throws Exception {
            return "线程回传信息!";
        }
    }

    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        // 启动线程
        Future<String> future = threadPool.submit(new CallableTest());

        try{
            System.out.println("等待线程完成");
            // 等待线程结束,并获取返回结果
            System.out.println(future.get());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

运行截图:
在这里插入图片描述

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

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

由此可知,只有通过调用线程类的start()方法才能真正达到多线程的目的。下面通过一个例子来说run()方法与 start()方法的区别。

/**
 * 描述:测试多线程
 *
 * @author Ye
 * @version 1.0
 * @date 2021/8/13 11:14
 */
public class TestThread4 {

    /**
    *  start方法
    * */
    public static void test1(){
        System.out.println("test1: 开始");
        Thread thread = new ThreadDemo();
        thread.start();
        System.out.println("test1: 结束");
    }
    /**
     *  run方法
     * */
    public static void test2(){
        System.out.println("test2: 开始");
        Thread thread = new ThreadDemo();
        thread.run();
        System.out.println("test2: 结束");
    }


    public static void main(String[] args) {
        test1();
        try{
            Thread.sleep(3000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println();
        test2();
    }
}

class ThreadDemo extends Thread{
    @Override
    public void run(){
        System.out.println("线程例子: 开始");
        try{
            Thread.sleep(1000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("线程例子: 结束");
    }

}

运行截图:
在这里插入图片描述

5. 多线程同步的实现方法有哪些

Java主要提供了3种实现同步机制的方 法:

1. synchronized关键字

在Java中,每个对象都有一个对象锁与之关联,该锁表明对象在任何时候只允许被一个线程所拥有,当调用synchronized代码时,需要获取锁,执行代码,执行结束后释放锁。

  1. synchronized 方法。在方法的声明前加入synchronized关键字,示例如下:
public synchronized void mutiThreadAccess();
  1. synchronized块。synchronized块既可以把任意的代码段声明为synchronized,也可以指定上锁的对象,有非常高的灵活性。其用法如下:
    synchronized(syncObject) {
        // 访问syncObject的代码
    }

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

当使用 synchronized 来修饰某个共享资源时,如果线程 A1 在执行 synchronized 代码,另外一个线程 A2 也要同时执行同一对象的同— synchronized 代码时,线程 A2 将要等到线程 A1 执行完成后,才能继续执行。在这种情况下可以使用 wait()方法和 notify()方法。在 synchronized 代码被执行期间,线程可以调用对象的 wait()方法,释放对象锁,进入等待状态,并且可以调用 notify()方法或 noifyAll()方法通知正在等待的其他线程。notify()方法仅唤醒一个线程(等待队列中的第一个线程)并允许它去获得锁,noifyAlI()方法唤醒所有等待这个对象的线程并允许它们去获得锁(并不是让所有唤醒线程都获取到锁,而是让它们去竞争)。

3. Lock

JDK 5 新增加了 Lock 接口以及它的一个实现类 ReentranLock (重人锁),Lock 也可以用来实现多线程的同步,具体而言,它提供了如下一些方法来实现多线程的同步:

  1. look()。以阻塞的方式获取锁,也就是说,如果获取到了锁,立即返回;如果别的线程持有锁,当前线程等待,直到获取锁后返回。
  2. tryLock()。以非阻塞的方式获取锁。只是尝试性地去获取一下锁,如果获取到锁,立即返回 true,否则,立即返回 false。
  3. tryLock(long timeout, TimeUnit unit)。如果获取了锁,立即返回 true,否则会等待参数给定的时间单元,在等待的过程中,如果获取了锁,就返回 true,如果等待超时,返回 false。
  4. lockInterruptibly()。如果获取了锁,立即返回;如果没有获取锁,当前线程处于休眠状态,直到获得锁,或者当前线程被别的线程中断(会收到 IntermuptedException 异常)。它与lock()方法最大的区别在于如果 lock()方法获取不到锁,会一直处于阻塞状态,且会忽略 interrupt()方法

6. sleep()方法与wait()方法有什么区别

sleep()是使线程暂停执行一段时间的方法。wait()也是一种使线程暂停执行的方法,例如,当线程交互时,如果线程对一个同步对象x 发出一个 wait()调用请求,那么该线程会暂停执行,被调对象进入等待状态,直到被唤醒或等待时间超时。具体而言,sleep()方法与 wait()方法的区别主要表现在以下几个方面:

  1. 原理不同。sleep()方法是 Thread 类的静态方法,是线程用来控制自身流程的,它会使此线程暂停执行一段时间,而把执行机会让给其他线程,等到计时时间一到,此线程会自动“苏醒”,例如,当线程执行报时功能时,每一秒钟打印出一个时间,那么此时就需要在打印方法前面加上一个 sleep()方法,以便让自己每隔 1s 执行一次,该过程如同闹钟一样。而 wait()方法是 Object 类的方法,用于线程间的通信这个方法会使当前拥有该对象锁的进程等待,直到其他线程调用 notify()方法(或 notifyAll 方法)时才“醒”来,不过开发人员也可以给它指定一个时间,自动“醒”来。与 wait()方法配套的方法还有 notify()方法和 notifyAll()方法。
  2. 对锁的处理机制不同。由于 sleep()方法的主要作用是让线程暂停执行一段时间,时间一到则自动恢复,不涉及线程间的通信,因此,调用 sleep()方法并不会释放锁。而 wait()方法则不同,当调用 wait()方法后,线程会释放掉它所占用的锁,从而使线程所在对象中的其他synchronized数据可被别的线程使用。
  3. 使用区域不同。由于 wait()方法的特殊意义,因此它必须放在同步控制方法或者同步语句块中使用,而 sleep()方法则可以放在任何地方使用。sleep()方法必须捕获异常,而 wait()、notity()以及 notifyall()不需要捕获异常。在 sleep的过程中,有可能被其他对象调用它的 interrupt(),产生 InterruptedException 异常。由于 sleep 不会释放“锁标志”,容易导致死锁问题的发生,因此,一般情况下,不推荐使用sleep()方法,而推荐使用 wait()方法。

7. 终止线程的方法有哪些

在Java 语言中,可以使用 stop()方法与 suspend()方法来终止线程的执行。当用Thread.stop()来终止线程时,它会释放已经锁定的所有监视资源。如果当前任何一个受这些监视资源保护的对象处于一个不一致的状态,其他线程将会“看”到这个不一致的状态,这可能会导致程序执行的不确定性,并且这种问题很难被定位。调用 suspend()方法容易发生死锁(死锁指的是两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,如果无外力作用,它们都将无法推进)。由于调用 suspend()方法不会释放锁,这就会导致一个问题:如果用一个 suspend 挂起一个有锁的线程,那么在锁恢复之前将不会被释放。如果调用 suspend()方法,线程将试图取得相同的锁,程序就会发生死锁,例如,线程 A 已经获取到了互斥资源 M 的锁,此时线程 A 通过 suspend()方法挂起线程 A 的执行,接着线程 B也去访问互斥资源 M,这时候就造成了死锁。鉴于以上两种方法的不安全性,Java 语言已经不建议使用以上两种方法来终止线程了。

那么,如何才能终止线程呢?一般建议采用的方法是让线程自行结束进入 Dead 状态。一个线程进入 Dead 状态,即执行完 run()方法,也就是说,如果想要停止一个线程的执行,就要提供某种方式让线程能够自动结束 run()方法的执行。在实现时,可以通过设置一个 flag 标志来控制循环是否执行,通过这种方法来让线程离开 run()方法从而终止线程。

/**
 * 描述:测试多线程
 *
 * @author Ye
 * @version 1.0
 * @date 2021/8/13 16:41
 */
public class TestThread6 implements Runnable{
    private volatile Boolean flag;

    public void stop(){
        flag = false;
    }

    @Override
    public void run() {
        while (flag){
            ; //do something
        }
    }
}

当线程发生I/O阻塞,可以使用interrupt()方法安全退出

/**
 * 描述:测试多线程
 *
 * @author Ye
 * @version 1.0
 * @date 2021/8/13 16:16
 */
public class TestThread5 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(" 线程准备休眠 ");
                try{
                    // 用休眠来模拟线程被阻塞
                    Thread.sleep(5000);
                    System.out.println(" 线程完成 ");
                }catch (Exception e){
                    System.out.println(" 线程中断 ");
                }
            }
        });

        thread.start();
        thread.interrupt();
    }
}

运行截图:
在这里插入图片描述

8. synchronized与Lock有什么异同

Java 语言提供了两种锁机制来实现对某个共享资源的同步:synchronized 和 Lock。其中synchronized 使用 Object 对象本身的 notify、wait、noityAIl 调度机制,而 Lock 可以使用 Condition 进行线程之间的调度,完成 synchronized 实现的所有功能。

具体而言,二者的主要区别主要表现在以下几个方面的内容:

  1. 用法不一样。在需要同步的对象中加人 synchronized 控制,synchronized 既可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。而 Lock 需要显式地指定起始位置和终止位置。synchronized 是托管给 JVM 执行的,而 Look 的锁定是通过代码实现的,它有比synchronized 更精确的线程语义。
  2. 性能不一样。在 JDK 5 中增加了一个 Lack 接口的实现类 ReentrantLock。它不仅拥有synchronized 相同的并发性和内存语义,还多了锁投票、定时锁、等候和中断锁等。它们的性能在不同的情况下会有所不同:在资源竞争不是很激烈的情况下,synchronized 的性能要优于Reentrantlock,但是在资源竞争很激烈的情况下,synchronized 的性能会下降得非常快,而ReentrantLock 的性能基本保持不变。
  3. 锁机制不一样。synchronized 获得锁和释放的方式都是在块结构中,当获取多个锁时,必须以相反的顺序释放,并且是自动解锁,不会因为出了异常而导致锁没有被释放从而引发死锁。而 Lock 则需要开发人员手动去释放,并且必须在 finally 块中释放,否则会引起死锁问题的发生。此外,Lock 还提供了更强大的功能,它的 tryLock()方法可以采用非阻塞的方式去获取锁。

虽然 synchronized 与 Lock都可以用来实现多线程的同步,但是,最好不要同时使用这两种同步机制,因为 ReetrantLock 与 synchronized 所使用的机制不同,所以它们的运行是独立的,相当于两种类型的锁,在使用时互不影响。

示例如下:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 描述:测试锁机制
 *
 * @author Ye
 * @version 1.0
 * @date 2021/8/13 17:15
 */
public class TestThread7 {
    public static void main(String[] args) {
        final  SyncTest syncTest = new SyncTest();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0;i<5;i++){
                    syncTest.addValueSync();
                    try{
                        Thread.sleep(20);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        });

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0;i<5;i++){
                    syncTest.addValueLock();
                    try{
                        Thread.sleep(20);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        });

        thread.start();
        thread1.start();
    }
}


class SyncTest{
    private int value = 0;
    Lock lock = new ReentrantLock();
    public synchronized void addValueSync(){
        this.value++;
        System.out.println(Thread.currentThread().getName() + ": " + value);
    }

    public void addValueLock(){
        try{
            lock.lock();
            value ++;
            System.out.println(Thread.currentThread().getName() + ": " + value);
        }finally {
            lock.unlock();
        }
    }
}

运行截图:
在这里插入图片描述

9. 什么是守护线程

Java提供了两种线程:守护线程与用户线程。守护线程又被称为“服务进程”“精灵线程”或“后台线程”,是指在程序运行时在后台提供一种通用服务的线程,这种线程并不属于程序中不可或缺的部分。通俗点讲,任何一个守护线程都是整个 JVM 中所有非守护线程的“保姆”。用户线程和守护线程几乎一样,唯一的不同之处就在于如果用户线程已经全部退出运行,只剩下守护线程存在了,JVM 也就退出了。因为当所有非守护线程结束时,没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了,程序也就终止了,同时会“杀死”所有守护线程。也就是说,只要有任何非守护线程还在运行,程序就不会终止。

在 Java 语言中,守护线程一般具有较低的优先级,它并非只由 JVM 内部提供,用户在编写程序时也可以自己设置守护线程,例如,将一个用户线程设置为守护线程的方法就是在调用start()方法启动线程之前调用对象的 setDaemon(true)方法,若将以上参数设置为 false,则表示的是用户进程模式。需要注意的是,当在一个守护线程中产生了其他线程,那么这些新产生的线程默认还是守护线程,用户线程也是如此,示例如下:

/**
 * 描述:测试多线程
 *
 * @author Ye
 * @version 1.0
 * @date 2021/8/13 19:19
 */
public class TestThread8 {
    public static void main(String[] args) {
        System.out.println("******** 开始 **********");
        Thread thread = new ThreadDemo1();
        thread.setDaemon(true);
        thread.start();
        System.out.println("******** 结束 **********");
    }
}

class ThreadDemo1 extends Thread{
    @Override
    public void run(){
        System.out.println(Thread.currentThread().getName() + ": 开始");
        try{
            Thread.sleep(1000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ": 结束");
    }
}

运行截图:
在这里插入图片描述

10. join()方法的作用是什么

在 Java 语言中,join()方法的作用是让调用该方法的线程在执行完run()方法后,再执行join 方法后面的代码。简单点说,就是将两个线程合并,用于实现同步功能。具体而言,可以通过线程 A 的 join()方法来等待线程 A 的结束,或者使用线程 A 的 join(2000)方法来等待线程 A 的结束,但最多只等待2s,示例如下:

/**
 * 描述:测试多线程
 *
 * @author Ye
 * @version 1.0
 * @date 2021/8/13 19:40
 */
public class TestThread9 {
    public static void main(String[] args) {
        Thread thread = new Thread(new ThreadImp());
        thread.start();
        try{
            thread.join(1000);
            if (thread.isAlive()){
                System.out.println(" 线程没有结束 ");
            }else {
                System.out.println(" 线程完成 ");
            }
            System.out.println(" 加入完成 ");
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}


class ThreadImp implements Runnable{

    @Override
    public void run() {
        try{
            System.out.println(" 开始线程 ");
            Thread.sleep(5000);
            System.out.println(" 结束线程 ");
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

运行截图:
在这里插入图片描述

参考:《Java程序员面试笔试宝典》 何昊、薛鹏、叶向阳 编著

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值