一、 什么是线程

我们用一个通俗易懂的例子来去看什么叫线程。

首先我们需要理解,线程是包含在进程中的。进程可以简单理解为操作系统上正在跑的程序。比如,你打开的浏览器、Idea编辑器、或者是游戏等等,都是操作系统中的进程。

如果你是Windows操作系统,你可以按下 Ctrl-Alt-Del 呼叫任务管理器,你可以看到当前操作系统上运行的全部进程。

光有进程还不够,我们可以认为进程只是管理者,而真正去执行命令的,其实是包含在进程中的线程(工人)。

我们用一个 Hello World 程序来简单建立起对线程的理解

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

这个 Hello World 程序运行的过程如下:

  • 操作系统接收到运行指令,创建一个新的进程,并在进程内创建一个线程来开始执行任务。
  • 线程读取到对应机器指令,在控制台上输出Hello World。
  • 任务完成,线程释放,进程退出。

现在你应该可以理解什么是线程了,说白了就是计算机中的“底层工人”。

二、 线程的阻塞

我们现在来讲解一下线程中的一个重要概念——阻塞。

所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性。

再看一个简单的输入输出程序例子:

import java.util.Scanner;
public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String next = sc.next();
        System.out.println("RECEIVED: " + next);
    }
}

这个输入输出程序运行的过程如下:

  • 操作系统接收到运行指令,创建一个新的进程,并在进程内创建一个线程来开始执行任务。
  • 线程读取到对应机器指令,创建sc变量,构建Scanner对象,并将对象地址存入sc变量中。
  • 线程读取到对应机器指令,停下工作并等待用户输入(Scanner中的next()方法造成线程阻塞)。
  • (读取到用户输入时)线程继续工作,并将刚才得到的用户输入放到next变量中。
  • 线程读取到对应机器指令,将next变量中的内容与RECEIVED:字符串进行拼接,并输入到控制台。
  • 任务完成,线程释放,进程退出。

你会发现,当程序等待用户输入的时候,程序似乎“停止了运行”,等到用户完成输入并按下回车以后,程序才继续执行,这里其实就是因为Scanner中的next()方法需要等待,所以它阻塞了线程,等到自己完成以后,才会取消阻塞。

三、Java 中的线程

概念介绍

从刚才的例子来看,似乎都只是普通的 Java 代码执行,都只涉及到了一个工人,也就是主线程(Main Thread),是由 JVM 帮助我们创建的。

但事实上,为了完成更加复杂的任务(尤其是涉及到 GUI 的任务),通常一个程序会需要多个线程同时工作,那么这个时候,就需要我们自己来创建新的线程,并为线程分配任务。

首先还是看 Hello World 程序:

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

我们都知道,一个 Java 程序必须要指定一个主类(在这里就是Main类),并拥有主方法(void main(String[])方法)才可以开始执行。事实上,这就相当于是告诉主线程:你的任务目标就是执行Main类中的void main(String[])方法,执行完了,你就可以放假了。

同样的,如果要手动创建一个新的线程,就必须要准备好一个特定的类和一个特定的方法,随后我们让新的线程来执行我们定义好的方法。Java 为了便于统一进行管理,已经为我们准备好了两种方式,来去定义我们这个“任务”:

  • 方法1:建立的类实现Runnable接口,重写run()方法。
  • 方法2:建立的类继承自Thread类,重写run()方法。

首先需要解释一下,Thread类就是我们的“工人类”,实例化出来的Thread对象就是就是一个线程,我们可以通过Thread类型的变量来操作对应的线程开始(start)、休眠(sleep)、停止(interrupt)等。

在这里,我们给出一个使用 Java 多线程的例子:

public class Main {
    public static void main(String[] args) {
        WorkerThread thread = new WorkerThread();
        thread.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("Main Thread working...");
        }
    }
}

class WorkerThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("Child Thread working...");
        }
    }
}

运行该程序,你会发现在有些地方,Main Thread working...Child Thread working...交替出现,这表明主线程和我们创建出来的线程是交替运行的,给我们一种“它们两个正在同时运行”的样子。(不过由于 CPU 的调度十分复杂,线程之间的交替没有规律,因此我们一般可以认为这两个线程就是同时运行的)

不过值得注意的是,一个进程要退出的话,必须是所有线程全部完成任务后才能退出。哪怕是主线程已经完成任务,只要有子线程还在运行,整个进程就不会退出(除非正在运行的子进程是守护线程(Daemon))。

创建一个线程

1. 继承Thread类

public class CreateThread_1 {

    public static void main(String[] args) {
        WorkerThread thread = new WorkerThread();
        thread.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("Main Thread working...");
        }
    }
}

class WorkerThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("Child Thread working...");
        }
    }
}

像这样,直接新建一个类继承自Thread类,相当于直接在Thread中指定任务。

2. 实现Runnable接口后传递给Thread

public class CreateThread_2 {
    public static void main(String[] args) {
        WorkerTask task = new WorkerTask();
        Thread thread = new Thread(task);
        thread.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("Main Thread working...");
        }
    }
}

class WorkerTask implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("Child Thread working...");
        }
    }
}

这样相当于在外面定义一个“任务类”,然后把任务对象给Thread。

3. 利用Lambda表达式传递“任务”

public class CreateThread_3 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                System.out.println("Child Thread working...");
            }
        });
        thread.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("Main Thread working...");
        }
    }
}

本质上来讲,使用Lambda表达式和实现Runnable接口的本质是一样的,这是 Java 8(JDK 1.8)加入的新特性,可以用于代替函数式接口。

开始线程 start()

线程被定义后并不会自动开始,需要调用start()方法。

注意,调用run()方法并不会开始一个线程,只是相当于你在当前线程里进行了一次方法调用而已。

停止线程 interrupt()

你可以使用stop()来完全停止一个线程,但是注意,这个方法已经过时,且非常不安全。

使用interrupt()方法可以为一个线程标记停止。不过请注意,interrupt()方法并不会直接结束,而是单纯打上标记。

  • interrupt():将调用该方法的对象所表示的线程标记一个停止标记,并不是真的停止该线程。

要想真正停止某个线程,你需要配合使用isInterrupted()或interrupted()在合适的地方退出线程。
这两者的区别为

  • interrupted():获取当前线程的中断状态,并且会清除线程的状态标记。是一个是静态方法。
  • isInterrupted():获取调用该方法的对象所表示的线程,不会清除线程的状态标记。是一个实例方法。
import static java.lang.Thread.interrupted;

public class InterruptThread {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true) {
                System.out.println("Child Thread working...");
                if (interrupted()) return;
            }
        });
        thread.start();
        // Despite interrupt immediately, you can still see one output.
        thread.interrupt();
    }
}

休眠线程 sleep()

通过sleep(int)方法可以使得线程休眠,其中参数填写的是休眠的毫秒数。

import static java.lang.Thread.sleep;
public class SleepThread {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Child Thread working...");
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
    }
}

在这里,**sleep(int)将会在受到interrupt()**之后抛出异常。不过这个异常结束后,如果不停止整个线程,则该线程的interrupt状态会重新变成false。

使得其他线程加入 join()

join()方法可以将目标线程融入到当前线程中来(假设在A线程中调用了B.join()),等B线程执行完后,A才会继续执行。

public class JoinThread {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Child Thread working...");
            }
        });
        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Last part of Main Thread");
    }
}

在这里,join()方法被主方法(也就是主线程)调用,因此主线程会等待子线程执行结束。

注意,这里的**join()也会在受到interrupt()**后抛出异常。与sleep()一样,如果异常后没有结束程序,那么线程的interrupt状态将会重新变为false。

变更线程守护模式 setDaemon(bool)

用个比较通俗的比如,任何一个守护线程都是整个 JVM 中所有非守护线程的保姆。

在前面的时候我们说过,只要有一个子线程正在运行,那么整个程序都不会退出执行。但是如果我们把子线程设置为守护线程,那么在主线程退出后,整个程序也会退出。

JVM 停止的条件:剩余的线程中没有非守护线程。由于 Main 线程是非守护线程,所以至少 Main 线程要被执行完毕。尤其是对于一些长期阻塞的线程,你可以设置它们为守护线程,这样主线程退出后,你的程序不会因为子线程的阻塞而持续运行。

public class DaemonThread {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true) {
                System.out.println("Child Thread working...");
            }
        });
        thread.setDaemon(true);
        thread.start();
        System.out.println("Last part of Main Thread");
    }
}

不过如果你运行上述程序,你会发现以下结果:

Last part of Main Thread
Child Thread working...
Child Thread working...
Child Thread working...
Child Thread working...
Child Thread working...
Child Thread working...
Child Thread working...
Child Thread working...

似乎是因为 JVM 对于守护进程的检查并不是实时的,而是有时间间隔的,不过子线程在继续执行了一小段时间后就被终止了。

线程之间的资源共享(锁)

有时候,你创建的子线程之间(或者与主线程)要共享一些变量数据,那么这时候就需要注意数据的有效性。

一个人存钱或者取钱的三条操作是一个不可分割的操作,我们称它为原子操作。那么在 Java 编程中也是一样的,我们常常会碰到原子操作不可分割的情况,怎么办呢?

我们有以下几种方法能够确保原子操作。

1. 为方法添加 synchronized 修饰

当我们为某个方法添加了 synchronized 修饰以后,当某个对象调用该方法时,该对象将会被“锁住”,直到该方法执行完毕。被锁住的对象将无法在其他线程里被操作。

我们将会用一个例子展示 synchronized 方法:

public class Synchronized_Method {
    public static void main(String[] args) {
        SharedObject shared = new SharedObject();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) shared.addTen();
        });
        Thread thread2 = new Thread(() -> {
            while (true) System.out.println(shared.getNum());
        });
        thread2.setDaemon(true);
        thread1.start();
        thread2.start();
    }
}

class SharedObject {
    private int sharedInt = 0;
    public synchronized void addTen() {
        for (int i = 0; i < 100; i++) sharedInt++;
    }
    public int getNum() {
        return sharedInt;
    }
}

2. 为指定代码块锁住对象

你可以指定某块代码执行的过程中需要锁住某个对象。

public class Synchronized_Block {
    public static void main(String[] args) {
        SharedObject2 shared = new SharedObject2();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                synchronized (shared) {
                    shared.addTen();
                }
            }
        });
        Thread thread2 = new Thread(() -> {
            while (true) System.out.println(shared.getNum());
        });
        thread2.setDaemon(true);
        thread1.start();
        thread2.start();
    }
}

class SharedObject2 {
    private int sharedInt = 0;
    public void addTen() {
        for (int i = 0; i < 100; i++) sharedInt++;
    }
    public int getNum() {
        return sharedInt;
    }
}

上述代码第6~8行,在执行addTen()方法时,锁住了shared变量所指向的对象,防止一个线程引用时被另一个线程插队。

3. 线程死锁问题

在编写多线程程序的时候,一定要避免线程死锁的问题。

什么是线程死锁?用一个例子带你简单看一下:

两个对象A,B,两个线程1,2:

  • 线程1先锁住A对象,然后请求B对象的使用权;
  • 线程2先锁住B对象,然后请求A对象的使用权;
  • 最终,谁也无法同时拿到两个对象的使用权。

这就会导致线程 1、2 始终处在等待资源的状态,而对象A、B也一直处在占用状态。

下面是Java中线程死锁的一个例子:

public class DeadLock {
    static final SharedObject3 sharedA = new SharedObject3();
    static final SharedObject3 sharedB = new SharedObject3();
    static Thread thread1 = new Thread(() -> {
        synchronized (sharedA) {
            System.out.println("SharedA locked by thread1.");
            try { DeadLock.thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); }
            synchronized (sharedB) {
                System.out.println("Thread 1 complete.");
                // Never going to reach.
            }
        }
    });
    static Thread thread2 = new Thread(() -> {
        synchronized (sharedB) {
            System.out.println("SharedB locked by thread2.");
            try { DeadLock.thread1.join(); } catch (InterruptedException e) { e.printStackTrace(); }
            synchronized (sharedA) {
                System.out.println("Thread 2 complete.");
                // Never going to reach.
            }
        }
    });
    public static void main(String[] args) {
        thread2.setDaemon(false);
        thread1.setDaemon(false);
        thread1.start();
        thread2.start();
    }
}

class SharedObject3 {

}

转载自https://www.chorg.top/2020/03/21/java-thread/

启动线程的三种方式

1:Thread 2: Runnable 3:Executors.newCachedThread

public class HowToCreateThread {
    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("Hello MyThread!");
        }
    }

    static class MyRun implements Runnable {
        @Override
        public void run() {
            System.out.println("Hello MyRun!");
        }
    }

    public static void main(String[] args) {
        new MyThread().start();
        new Thread(new MyRun()).start();
        new Thread(() -> {
            System.out.println("Hello Lambda!");
        }).start();
    }
}

yield,join

  • yield
    Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。

yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

static void testYield() {
    new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            System.out.println("A" + i);
            if (i % 10 == 0) Thread.yield();
        }
    }).start();

    new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            System.out.println("------------B" + i);
            if (i % 10 == 0) Thread.yield();
        }
    }).start();
}
  • join
    使主线程进入等待池并等待t线程执行完毕后才会被唤醒。
static void testJoin() {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            System.out.println("A" + i);
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });

    Thread t2 = new Thread(() -> {

        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        for (int i = 0; i < 100; i++) {
            System.out.println("B" + i);
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t1.start();
    t2.start();
}

线程的状态

fd5gf5nhgfb.jpg

1.初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2.运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3.阻塞(BLOCKED):表示线程阻塞于锁。
4.等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
5.定时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
6.终止(TERMINATED):表示该线程已经执行完毕。

  • 1.初始状态
    实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。

  • 2.1. 就绪状态
    就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
    调用线程的start()方法,此线程进入就绪状态。
    当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
    当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
    锁池里的线程拿到对象锁后,进入就绪状态。

  • 2.2. 运行中状态
    线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。

  • 3.阻塞状态
    阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。

  • 4.等待
    处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。

  • 5.超时等待
    处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

  • 6.终止状态
    当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
    在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

public class ThreadState {
    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println(this.getState());

            for(int i=0; i<10; i++) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

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

    public static void main(String[] args) {
        Thread t = new MyThread();
        System.out.println(t.getState());
        t.start();

        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(t.getState());
    }
}

hhhhh