Java并发(一) 线程基础

595 阅读5分钟
  • 什么是进程和线程?

进程: 进程是操作系统进行资源分配的最小单位,一个进程中可以存在一个或者多个线程,同一进程中的线程可以共享该进程中的系统资源,但是进程和进程之间是相互独立的,不同进程间传递消息需要使用跨进程通讯。

线程: 线程是CPU调度的最小单位,必须依赖进程而存在,同一进程中的多个线程共享进程数据。

举例: 进程就像是火车,而线程就是这个火车上的车厢,每个火车最少有一个车厢,同一个火车上的车厢可以共享这个火车上的资源。

  • 什么是并发和并行?

并发: 并发指应用能交替执行不同的任务,就像是单CPU核心执行多线程任务,并不是同时执行,而是以肉眼无法观察到的速度交替执行。

并行: 并行指应用能够同时执行不同的任务,就像是多CPU核心执行多线程任务,两个核心可以同时执行不同的任务。

举例: 并发是两个队列使用同一个咖啡机,只能交替使用,不能同时。 并行是两个队列使用两个咖啡机,可以同时使用,互不干扰。 在这里插入图片描述

  • 进入正题

前面说了这么多都是为了后面学习多线程的铺垫,而且也是面试很容易问到的点,因为很多人总是搞不清楚区别,下面正式进入多线程的学习。

  • 线程创建的三种方式

  • 1. 继承Thread类

public class ThreadDemo {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("线程执行了");
        }
    }
}

  • 2. 实现Runnable接口

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

    static class MyRunnable implements Runnable{
        @Override
        public void run() {
            System.out.println("线程执行了");
        }
    }
}
  • 3. 实现Callable接口

public class ThreadDemo {
    public static void main(String[] args) {
        MyCallable myCallable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask<>(myCallable);

        Thread thread = new Thread(futureTask);
        thread.start();

        //接收线程返回的值
        try {
            System.out.println(futureTask.get());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    static class MyCallable implements Callable<String>{
        @Override
        public String call() throws Exception {
            return "线程执行了,我是返回的值";
        }
    }
}
  • 线程怎么停止?

正常情况下,如果线程执行完成会自动销毁,但是如果我们线程有一个很耗时的操作,导致线程一直无法销毁,我们想终止应该怎么办?

  • 方法一(stop方法)

从图中可以看到,stop()方法已经被官方废弃了,因为stop方法会强行终止一个线程,无法保证线程的资源被释放,所以我们不建议这种方式。 在这里插入图片描述

  • 方法二(自定义标识符)

因为线程正常执行完就会停止,所以我们可以自定义一个标识符来判断,如果标识符状态改变了就停止线程中的任务,线程就可以正常销毁了,大多数人使用的都是这种方式。

public class ThreadDemo {
    private static boolean isRunning = true; //线程是否运行
    
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        myThread.start();

        //2s后停止线程
        Thread.sleep(2000);
        isRunning = false;
    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            while (isRunning) {
                System.out.println("线程正在执行");
            }
        }
    }
}
  • 方法三(interrupt方法) 推荐

interrupt与方法二非常类似,方法二需要我们自己定义一个标识符,其实官方已经给我们定义一个标识符了,我们直接使用就行。注意: 调用interrupt方法线程并不会停止,需要我们自己去获取interrupt的状态来决定线程是否继续执行。

public class ThreadDemo {

    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        myThread.start();

        //2s后停止线程
        Thread.sleep(2000);
        myThread.interrupt();
    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("线程正在执行");
            }
        }
    }
}
  • 线程生命周期

在这里插入图片描述

注意:线程调用start()方法并不会立即执行,而是进入就绪状态等待系统的调度。

  • 守护线程

线程分为用户线程和守护线程,守护线程会守护所有用户线程(老舔狗了),只要还有用户线程没有结束,守护线程就不会结束,当所有用户线程都结束了,守护线程就会自动结束。

public class ThreadDemo {

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        //将线程设置为守护线程
        myThread.setDaemon(true);
        myThread.start();
    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("线程正在执行");
            }
        }
    }
}
  • sleep()

sleep()会使当前调用的线程睡眠一段时间,也就是暂停执行一段时间,此时会让出CPU执行权进入阻塞状态,等待阻塞完毕会进入就绪状态,等待系统调度。

  • join()

join()方法会暂停除了调用线程以外的线程,保证调用线程执行完毕后再执行其他线程,如果我们现在创建三个线程,并执行:

public class ThreadDemo {

    public static void main(String[] args) throws InterruptedException {
        MyThread1 myThread1 = new MyThread1();
        MyThread2 myThread2 = new MyThread2();
        MyThread3 myThread3 = new MyThread3();

        myThread1.start();
        myThread2.start();
        myThread3.start();
    }

    static class MyThread1 extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 3; i++) {
                System.out.println("线程1执行了-> "+i);
            }
        }
    }

    static class MyThread2 extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 3; i++) {
                System.out.println("线程2执行了-> "+i);
            }
        }
    }

    static class MyThread3 extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 3; i++) {
                System.out.println("线程3执行了-> "+i);
            }
        }
    }
}

上面代码是没有加入join()代码的,所以执行结果会是随机的:

线程1执行了-> 0
线程3执行了-> 0
线程3执行了-> 1
线程2执行了-> 0
线程3执行了-> 2
线程1执行了-> 1
线程1执行了-> 2
线程2执行了-> 1
线程2执行了-> 2

如果我们给thread1设置join(),执行的时候就会先把thread1执行完毕,然后thread2和thread3再继续抢夺资源:

MyThread1 myThread1 = new MyThread1();
MyThread2 myThread2 = new MyThread2();
MyThread3 myThread3 = new MyThread3();

myThread1.start();
myThread1.join();
myThread2.start();
myThread3.start();

执行结果:
线程1执行了-> 0
线程1执行了-> 1
线程1执行了-> 2
线程2执行了-> 0
线程3执行了-> 0
线程2执行了-> 1
线程3执行了-> 1
线程2执行了-> 2
线程3执行了-> 2
  • yield()

yield()方法会使当前线程由运行状态变为就绪状态,此时CPU会随机调度就绪状态的线程,有可能再次调度到此线程,也可能会调度到别的线程,所以yield()方法有可能让出资源后会立即再次抢夺到资源。

  • wait()和notify()、notifyAll()

这三个方法都是Object类的方法,所以所有对象都可以调用这三个方法,由于这三个方法跟锁机制有关联,本篇没有讲到线程锁的概念,所以下篇会和锁机制一起详解。