Java---多线程入门

前置知识

什么是进程,什么又是线程?咱不是讲系统,简单说下,知道个大概就好了。

进程:一个可执行文件执行的过程。
线程:操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务

什么是并行,什么是并发?这个也简单说下。

并行:cpu的两个核心分别执行两个线程。
并发:cpu的一个核心在两个(或多个)线程上反复横跳执行。

线程的创建

三种线程创建方式的优缺点以及适用场景。

继承Thread 实现Runnable 实现Callable(本文不涉及)
优点 编程简单
执行效率高(就一个子类)
面向接口编程
执行效率高(就一个实现类)
容器管理线程组
有返回值、有异常
缺点 单继承
无法有效关系线程组
无返回值、无异常
无法有效关系线程组
无返回值、无异常
执行效率相对较低(类间关系复杂)
编程麻烦
适用场景 不推荐使用,但需要了解 简单的多线程程序 复杂业务的多线程程序推荐使用
如,企业级应用

继承Thread

// 声明
class T extends Thread {
    public void run() {
        // do something
    }
}
// 使用
new T().start();

实现Runnable

// 声明
class T implements Runnable {
    public void run() {
        // do something
    }
}
// 使用
new Thread(new T()).start();

为什么是start,而不是run,其实run只是个很普通的方法,我们来看看start的源码。

public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    group.add(this);
    boolean started = false;
    try {
        start0(); // 这个才是开启线程
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
        }
    }
}
// start0的实现
private native void start0(); // 这是一个native方法,通常使用C/C++来实现

多线程机流程(从启动到终止)

我们通过一个案例来说明。

顺便说说sleep()

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        new Thread(new T0(), "T0").start();
        int cnt = 0;
        while (cnt < 50) {
            cnt ++;
            System.out.println(Thread.currentThread().getName());
            Thread.sleep(200); // 让当前线程停止200毫秒
        }
    }
}
class T0 implements Runnable {
    int cnt;
    @Override
    public void run() {
        while (cnt < 50) {
            cnt ++;
            System.out.println(Thread.currentThread().getName());
            try {
                Thread.sleep(200); // 让当前线程停止200毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

执行流程:

  1. 启动main方法(进程开启)
  2. 启动main线程
  3. 打印main/打印T0(根据线程调度执行)
  4. 其中一个线程结束
  5. 另一个线程结束
  6. 进程结束

线程常用方法

  • setName:设置线程名称,不设置则使用默认线程名称。

  • getName:获取线程名称。

  • start:开启线程。实际开启线程的方法为start0。

  • run:调用start后创建的新线程会调用run方法。单纯调用run方法无法达到多线程的效果,run方法只是个普通的方法。

  • setPriority:更改线程优先级。

  • getPriority:获取线程优先级。

    线程的优先级:
    public static final int MIN_PRIORITY = 1;
    public static final int NORM_PRIORITY = 5;
    public static final int MAX_PRIORITY = 10;
    
  • sleep:让线程休眠指定时间。

线程终止

虽然Thread中提供了一个stop方法用来停止线程,但目前已经被废弃。那么我们如何停止线程呢?我们来看看下面这个例子。

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        T0 t0 = new T0();
        new Thread(t0, "T0").start();
        int cnt = 0;
        while (cnt < 50) {
            cnt ++;
            System.out.println(Thread.currentThread().getName());
        }
        t0.flag = false; // 设置为false用以跳出T0线程的循环
        /*
        	在main线程执行了50次后退出T0线程,接着退出main线程
         */
    }
}
class T0 implements Runnable {
    boolean flag = true; // 定义一个标记,用来控制线程是否停止
    @Override
    public void run() {
        while (flag) {
            System.out.println(Thread.currentThread().getName());
        }
    }
}

通过定义一个标记flag让线程退出。

线程中断

Thread中有一个interrupt方法,这个方法不是说中断线程的运行,而是中断线程当前执行的操作。我们来看下样例。

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        T0 t0 = new T0();
        Thread thread0 = new Thread(t0, "T0");
        thread0.start();
        System.out.println("张三在划水。。。");
        Thread.sleep(2000);
        thread0.interrupt(); // 通过抛出一个InterruptedException异常来打断当前操作(sleep)
    }
}
class T0 implements Runnable {
    boolean flag = true;
    @Override
    public void run() {
        System.out.println("李四在打盹。。。");
        while (flag) {
            try {
                Thread.sleep(20000); // 2秒后被interrupt中断
            } catch (InterruptedException e) {
                flag = false;
                System.out.println("老板来了,张三摇醒了李四。。。"); // 由于main线程调用了interrupt,实际过了2秒就输出了
            }
        }
    }
}

输出结果:

张三在划水。。。
李四在打盹。。。
老板来了,张三摇醒了李四。。。

线程让步

Thread类中提供了yield方法,用来礼让cpu资源。礼让了资源就会执行其他线程吗?我们看看这个例子

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        T0 t0 = new T0();
        Thread thread0 = new Thread(t0, "T0");
        thread0.start();
        int cnt = 0;
        while (cnt < 5) {
            cnt ++;
            System.out.println(Thread.currentThread().getName());
            Thread.yield(); // 放弃当前的cpu资源,让cpu重新分配资源。
        }
    }
}
class T0 implements Runnable {
    int cnt;
    @Override
    public void run() {
        int cur = 0; // 连续吃的包子数
        while (cnt < 5) {
            cnt ++;
            System.out.println(Thread.currentThread().getName());
            Thread.yield(); // 放弃当前的cpu资源,让cpu重新分配资源。
        }
    }
}

输出结果:

main
T0
main
main
main
main
T0
T0
T0
T0

可以看到两个线程互相礼让,如果yield方法会强制执行其他线程的话,那线程应该会交替执行,而不是有连续执行同一个线程的情况。所以证明了yield并不是强制礼让。

线程插队

Thread类提供了join方法,可以指定一个线程优先执行。

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread0 = new Thread(new T0(), "T0");
        thread0.start();
        Thread thread1 = new Thread(new T0(), "T1");
        thread1.start();
        int cnt = 0;
        while (cnt < 2) {
            cnt ++;
            System.out.println(Thread.currentThread().getName());
            thread0.join(); // 让线程thread0插队,执行完thread0的所有任务后回到当前线程。
        }
    }
}
class T0 implements Runnable {
    int cnt;
    @Override
    public void run() {
        int cur = 0;
        while (cnt < 2) {
            cnt ++;
            System.out.println(Thread.currentThread().getName());
        }
    }
}

输出结果:

main
T1
T1
T0
T0
main

总体上main线程确实被T0插队了,但为啥T1在T0的前面被执行?因为当前是多核CPU的环境,其它的核心在执行剩下的线程,执行T1线程的核心比T0的快所以T1在T0之前被输出。不止有并发,还有并行。在单纯并发的条件下,就变成了T0的所有任务都执行完毕后,才会执行其他线程。当前的main与T0是并发的,与T1是并行。

守护线程

Thread类提供setDaemon方法,可以设置目标线程为当前线程的守护线程,当前线程终止时,目标(守护)线程也随之终止。

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread0 = new Thread(new T0(), "T0");
        thread0.setDaemon(true);
        thread0.start();
        System.out.println("张三 --> 新一天的工作开始了");
        int time = 0;
        while (time < 8) { // 张三每天工作八个小时。。。
            time ++;
        }
        System.out.println("张三 --> 下班了,回家吃老婆做的饭咯");
        System.out.println("小红 --> 我老公张三下班了,今天就到这了");
        System.out.println("小红 --> 守护线程YYDS");
    }
}
class T0 implements Runnable {
    int cnt;
    @Override
    public void run() {
        System.out.println("小红 --> 李四来我家甜蜜双排王者荣耀");
        while (true);
    }
}

输出结果:

张三 --> 新一天的工作开始了
小红 --> 李四来我家甜蜜双排王者荣耀
张三 --> 下班了,回家吃老婆做的饭咯
小红 --> 我老公张三下班了,今天就到这了
小红 --> 守护线程YYDS

当main线程终止时,T0线程也终止。

线程的状态

5种状态是OS的线程状态。而6种则说的是JVM的线程状态。以下是状态图。

线程状态

OS的线程状态为粗体
JVM的线程状态为英文

线程同步机制

想看个经典问题。

多线程售票问题

我们来看看案例。

public class ThreadTest {
    public static void main(String[] args) {
        T0 t0 = new T0();
        Thread thread0 = new Thread(t0, "张三");
        Thread thread1 = new Thread(t0, "李四");
        thread0.start();
        thread1.start();
    }
}
class T0 implements Runnable {
    int ticket = 100;
    @Override
    public void run() {
        while (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.printf("%s买了一张票,还剩%d张%n", Thread.currentThread().getName(), -- ticket);
        }
    }
}

输出结果:

...
张三买了一张票,还剩3张
张三买了一张票,还剩2张
李四买了一张票,还剩1张
张三买了一张票,还剩0张
李四买了一张票,还剩0张

出现了重复卖票,造成这种现象的原因是线程不安全。那如何让线程安全呢。这就是接下来要介绍的互斥锁。

互斥锁

java提供了synchronized关键字用以开启锁。看看如何使用锁解决上面的线程安全问题。

public class ThreadTest {
    public static void main(String[] args) {
        T0 t0 = new T0();
        Thread thread0 = new Thread(t0, "张三");
        Thread thread1 = new Thread(t0, "李四");
        thread0.start();
        thread1.start();
    }
}
class T0 implements Runnable {
    int ticket = 100;
    @Override
    public synchronized void run() { // 我们将锁加在run方法上
        while (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.printf("%s买了一张票,还剩%d张%n", Thread.currentThread().getName(), -- ticket);
        }
    }
}

输出结果:

...
张三买了一张票,还剩4张
张三买了一张票,还剩3张
张三买了一张票,还剩2张
张三买了一张票,还剩1张
张三买了一张票,还剩0张

所有的输出都在线程张三上,这显然不是我们想要的。
首先,为什么会有这种现象发生?其实,被synchronized修饰的方法或代码块会被上锁,并发环境下先进入该方法或者代码块的线程将获得锁并执行这部分代码,而其他线程则处于阻塞状态直到获得锁的线程执行完被上锁的所有代码后,其他线程才有机会去争夺锁。

上述现象的原因是synchronized修饰了整个方法,所以当张三拿到锁时会执行完所有的循环后释放锁,这时李四就什么都输出不了了,和单线程一样,除了多了个一直阻塞的线程,性能低下。

那么如何保证线程安全的前提下,保证并发的性能呢?第一次尝试解决。

public class ThreadTest {
    public static void main(String[] args) {
        T0 t0 = new T0();
        Thread thread0 = new Thread(t0, "张三");
        Thread thread1 = new Thread(t0, "李四");
        thread0.start();
        thread1.start();
    }
}
class T0 implements Runnable {
    int ticket = 100;
    @Override
    public /*synchronized*/ void run() { // 我们将锁加在run方法上
        while (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized(this) { // 存在两个线程执行时都满足ticket>0,仍然有线程安全问题  
                System.out.printf("%s买了一张票,还剩%d张%n", Thread.currentThread().getName(), -- ticket);
            }
        }
    }
}

输出结果:

...
李四买了一张票,还剩3张
李四买了一张票,还剩2张
张三买了一张票,还剩1张
张三买了一张票,还剩0张
李四买了一张票,还剩-1张

虽然两个线程恢复了并发,但线程安全问题也随之出现。

第二次尝试解决。

public class ThreadTest {
    public static void main(String[] args) {
        T0 t0 = new T0();
        Thread thread0 = new Thread(t0, "张三");
        Thread thread1 = new Thread(t0, "李四");
        thread0.start();
        thread1.start();
    }
}
class T0 implements Runnable {
    int ticket = 100;
    @Override
    public /*synchronized*/ void run() { // 我们将锁加在run方法上
        while (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized(this) {
                if (ticket <= 0) return; // 在同步代码块中再次判断以确保线程安全
                System.out.printf("%s买了一张票,还剩%d张%n", Thread.currentThread().getName(), -- ticket);
            }
        }
    }
}

输出结果:

...
李四买了一张票,还剩4张
李四买了一张票,还剩3张
张三买了一张票,还剩2张
张三买了一张票,还剩1张
李四买了一张票,还剩0张

通过在同步代码块中再次判断以达到线程安全。

死锁

并发编程中不只有线程安全问题,还有死锁问题。

public class ThreadTest {
    public static void main(String[] args) {
        String lock1 = "A";
        String lock2 = "B";
        String lock3 = "C";
        Thread t0 = new Thread(new T0(lock1, lock2));
        Thread t1 = new Thread(new T0(lock2, lock3));
        Thread t2 = new Thread(new T0(lock3, lock1));
        t0.start();
        t1.start();
        t2.start();
    }
}
class T0 implements Runnable {
    String lock1;
    String lock2;
    T1(String lock1, String lock2) {
        this.lock1 = lock1;
        this.lock2 = lock2;
    }
    @Override
    public void run() {
        synchronized(lock1) {
            System.out.println("获取锁: " + lock1);
            synchronized(lock2) {
                System.out.println("获取锁: " + lock2);
            }
            System.out.println("释放锁: " + lock2);
        }
        System.out.println("释放锁: " + lock1);
    }
}

输出结果:

获取锁:B
获取锁:A
获取锁:C

可以看出三个线程分别持有一把锁,相互锁住不能释放,形成死锁。
为了不写出死锁的并发代码,我们需要学习释放锁的时机。

释放锁

  • run执行完毕,释放锁。

  • wait执行,释放锁。

  • sleep执行,不会释放锁。

  • join执行,不会释放锁,而是挂起当前线程。

  • notify执行,不会释放锁。

    public class ThreadTest {
        public static void main(String[] args) {
            String lock = "A";
            Thread thread0 = new Thread(new T0(lock), "T0");
            Thread thread1 = new Thread(new T1(lock), "T1");
            thread0.start();
            thread1.start();
        }
    }
    class T0 implements Runnable {
        String lock;
        public T0(String lock) {
            this.lock = lock;
        }
        @Override
        public void run() {
            synchronized(lock) {
                System.out.println(Thread.currentThread().getName() + "获取锁");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + "释放锁");
            }
        }
    }
    class T1 implements Runnable {
        String lock;
        public T1(String lock) {
            this.lock = lock;
        }
        @Override
        public void run() {
            try {
                Thread.sleep(5000);
                synchronized(lock) {
                    System.out.println(Thread.currentThread().getName() + "获取锁");
                    lock.notify();
                    Thread.sleep(5000);
                    System.out.println(Thread.currentThread().getName() + "释放锁");
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
    

    输出结果:

    T0获取锁
    T1获取锁
    T1释放锁
    T0释放锁
    

    证明notify并不会释放锁,只是通知一个wait的线程:Waiting → Runnable(Ready),接着在调用notify的线程执行完毕后释放锁。

posted @ 2022-09-05 22:43  spoonb  阅读(223)  评论(0编辑  收藏  举报