Java多线程概述

后端 / 2020-10-22

线程简介

线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
一个进程可以有很多线程,每条线程并行执行不同的任务。

多任务
开车打电话
你可以一边开车一边打电话 咳咳 科目四学了这是不可以的奥
吃鸡
你可以一边吃鸡一边上厕所

本质
看似多个任务在同时做,实质上,你的大脑(cpu)只是在同一时间,依旧只做了一件事情。

进程与线程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

一个操作系统中可以有多个进程,而一个进程又可以开辟多个线程。

线程是 CPU调度和执行的单位
本质上是CPU轮询时间片

线程创建

线程的三种创建方式

  • Thread
    • 继承Thread类实现
  • Runnable
    • 实现Runnable接口
  • Callable
    • 实现Callable接口

继承Thread类实现

创建一个新的执行线程有两种方法。 一个是将一个类声明为Thread的子类。 这个子类应该重写run类的方法Thread 。

public class TestThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(1000 * 1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("我是子线程");
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Thread testThread = new TestThread();
        testThread.start();
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000 * 1);
            System.out.println("我是主线程");
        }
    }
}

小试牛刀:多线程下载图片

import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
/*
    @author luckyFang
    @date 2020 10 22
    @desc thread downloader demo .
 */


// 下载器类
class Downloader extends Thread{
    private String url;
    private String name;

    public Downloader(String url,String name) {
        this.url = url;
        this.name=name;
    }

    @Override
    public void run() {
        downloadFile();
    }

    private void downloadFile(){
        try {
            URL url = new URL(this.url);
            URLConnection urlConnection = url.openConnection();
            InputStream inputStream = urlConnection.getInputStream();
            FileOutputStream fileOutputStream = new FileOutputStream(this.name);

            int read =0;

            byte[] buffer =new byte[1024];
            while ((read = inputStream.read(buffer))!=-1){
                fileOutputStream.write(buffer,0,read);
            }

            fileOutputStream.close();
            inputStream.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


public class TestDownload  {
    public static void main(String[] args) {
        new Downloader("https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=2047170863,1302224659&fm=26&gp=0.jpg","a.png").start();
        new Downloader("https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=3380021095,725635716&fm=26&gp=0.jpg","b.png").start();
        new Downloader("https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=1912740209,3509854380&fm=26&gp=0.jpg","c.png").start();

    }
}

实现Runnable接口

Runnable接口应由任何类实现,其实例将由线程执行。 该类必须定义一个无参数的方法,称为run 。

/**
 * @author  luckyFang
 * @date 2020 10 22
 * @desc 实现Runnable 来开辟多线程
 */

public class TestRunnable {

    // 静态内部类
    public static class Inner implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("我是子线程");
                try {
                    Thread.sleep(1 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        Inner inner = new Inner();
        Thread thread = new Thread(inner);
        thread.start();
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000 * 1);
            System.out.println("我是主线程");
        }
    }
}

当然如果你的java基础够好的话我们可以将上面代码继续简化


    public static void main(String[] args) throws InterruptedException {
        new Thread(new Inner()).start();
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000 * 1);
            System.out.println("我是主线程");
        }
    }

继续深入

    public Thread(Runnable target) {
        this((ThreadGroup)null, target, "Thread-" + nextThreadNum(), 0L);
    }

package java.lang;

@FunctionalInterface
public interface Runnable {
    void run();
}





通过阅读Thread 类的 代码我们不难发现,他有一个构造函数是Runnable 类型。
那么 通过简单的揣摩,我们也发现Runnable是一个函数式接口,意味着我们可以使用匿名内部类
匿名内部类实现

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("我是子线程");
                    try {
                        Thread.sleep(1000 * 1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

既然可以使用匿名内部类,那么同理我们也可以使用lambda表达式来简化我们的代码

lambda表达式 jdk8 新特性之一,是一个匿名函数,Lambda表达式基于数学中的λ演算得名。
格式: ()->{}

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println("我是子线程");
            }
        }).start();
        
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000 * 1);
            System.out.println("我是主线程");
        }
    }

实现 callable

实现callable 接口和上面不同的是,可以拿到返回值。并且可以丢进线程池执行。

import java.util.concurrent.*;

/**
 * @author luckyFang
 * @date 2020 10 29
 * @desc 实现 callable接口 
 *  线程池的使用
 */

public class TestCallable implements Callable<Boolean> {
    @Override
    public Boolean call() throws Exception {
        for (int i = 0; i < 10; i++) {
            System.out.println("我在写BUG!");
        }
        return true;
    }

    public static void main(String[] args) {
        TestCallable testCallable1 = new TestCallable();
        TestCallable testCallable2 = new TestCallable();
        TestCallable testCallable3 = new TestCallable();
	// 创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(3);
	// 投递任务
        Future<Boolean> rs1 = executorService.submit(testCallable1);
        Future<Boolean> rs2 = executorService.submit(testCallable1);
        Future<Boolean> rs3 = executorService.submit(testCallable1);
	// 拿到返回值
        try {
            System.out.println(rs1.get());
            System.out.println(rs2.get());
            System.out.println(rs3.get());
	    // 关闭线程池
            executorService.shutdownNow();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

线程问题

虽然线程可以大幅度提升我们的程序处理能力,同时也会带来一些问题。
下面是一个模拟售票代码,我们创建了4个窗口,来实现售票。分别是A B C D

/**
 * @author luckyFang
 * @date 2020 10 29
 * @desc 多线程资源争抢问题
 */
public class TestTicket  implements Runnable {
    private int ticketNums=100;

    @Override
    public void run() {
        try {
            while (true){
                if (ticketNums<=0){
                    break;
                }
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        TestTicket testTicket = new TestTicket();
        new Thread(testTicket,"A").start();
        new Thread(testTicket,"B").start();
        new Thread(testTicket,"C").start();
        new Thread(testTicket,"D").start();

    }
}

运行结果

B-->98
C-->97
A-->100
D-->99
D-->96 ------
C-->96 ------
B-->96 ------
A-->96 ------
A-->94
C-->92
D-->93
B-->95
A-->91
B-->90
C-->88
D-->89
C-->85
B-->87
A-->86
D-->85

从上面可以看出,程序并没有按照我们想象的那样来执行。

多个线程同时操作一个数据出现了资源争抢问题。
理想代码
实际代码

线程状态

线程有五大状态

  • 新生 new Thread()
  • 就绪 new Thread().start()
  • 阻塞 sleep wait 同步锁
  • 运行 正在运行的线程
  • 死亡 线程中断或者结束

优雅de停止一个线程

线程的停止一直是一个备受争议的问题,假如当前线程正在读写非常重要的数据,结果被异常终止了,后果非常严重。因此不推荐被动结束线程。
而是在线程内部创建一个标志位让线程自己停下来。
示例代码

public class TestStopThread implements Runnable {
    private Boolean flag  = true;
    public static void main(String[] args) {
        TestStopThread testStopThread = new TestStopThread();
        new Thread(testStopThread).start();
        try {
            Thread.sleep(2*1000);
            testStopThread.stop();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    @Override
    public void run() {
        while (this.flag){
            System.out.println("我在写BUG!");
        }
    }

    private void stop(){
        this.flag=false;
    }
}

线程休眠

Sleep 不会释放锁
客户不给钱就多加Sleep

守护线程

高级线程低级线程

高级线程:JVM会在终止前等待其完成任务

低级线程:和上面恰恰相反(不会阻止JVM的退出)

守护线程就是一个低级线程

创建守护线程

new Thread().setDaemon(true);

示例代码

/**
 * @author luckyFang
 * @date 2020/11/19 22:35
 * @file TestThreadDaemon.java
 * @desc 守护线程
 */



class  God implements Runnable{

    @Override
    public void run() {
        while (true){
            System.out.println("I'm blessing with you!");
        }
    }
}


class You implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 30000; i++) {
            System.out.println("Happy!");
        }
    }
}

public class TestThreadDaemon {
    public static void main(String[] args) {
        // god
        Thread god = new Thread(new God());
        god.setDaemon(true);
        god.start();
        // you
        new Thread(new You()).start();
    }
}

按照我们正常的编程思维,上面我们写了个 死循环,程序会无限运行下去,但是实际情况去却不是这样,因为

守护线程是一个低级线程,不会影响JVM的退出

线程同步

本质上是线程的等待机制,排队访问+限制(锁)

要知道每个Java对都有一个锁,一旦一个线程拿到这个对象的锁后,将独占这个对象,其他线程访问必须要等待。

比如说一个厕所在正常情况下只能被一个人使用,其他人如果要使用必须要等待。

这好吗,这不好。

加入 A独占厕所很长时间,外面又有很多人要用,这样的话,效率堪忧!

多线程竞争下,加锁,释放锁,会引起较多的上下文切换,cpu调度频繁,从而导致性能问题。

同步代码块和同步关键字

synchronized

改造不安全代码为安全代码

/**
 * @author luckyFang
 * @date 2020 10 29
 * @desc 多线程资源争抢问题--解决  加锁
 */
public class TestTicket  implements Runnable {
    private int ticketNums=100;

    @Override
    public synchronized void run() {
        try {
            while (true){
                if (ticketNums<=0){
                    break;
                }
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        TestTicket testTicket = new TestTicket();
        new Thread(testTicket,"A").start();
        new Thread(testTicket,"B").start();
        new Thread(testTicket,"C").start();
        new Thread(testTicket,"D").start();

    }
}

A-->100
A-->99
A-->98
A-->97
A-->96
A-->95
A-->94
A-->93
A-->92
A-->91
A-->90
A-->89
A-->88

同样我们也可以使用 synchronized 代码块来锁对象

/**
 * @author luckyFang
 * @date 2020 10 29
 * @desc 多线程资源争抢问题
 */
public class TestTicket  implements Runnable {
    private int ticketNums=100;
    
    @Override
    public void run() {

        synchronized(this){
            try {
                while (true){
                    if (ticketNums<=0){
                        break;
                    }
                    Thread.sleep(100);
                    System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    public static void main(String[] args) {
        TestTicket testTicket = new TestTicket();
        new Thread(testTicket,"A").start();
        new Thread(testTicket,"B").start();
        new Thread(testTicket,"C").start();
        new Thread(testTicket,"D").start();

    }
}

死锁

多线程下,多个线程各自占有一些共享资源,并且互相等待。

面试官:告诉我什么是死锁我就给你offer

程序猿:你给我offer我就告诉你什么叫死锁

产生死锁的四个必要条件

1.互斥条件:一个资源每次只能被一个进程使用。

2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

3.不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。

4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

Lock锁

通过显式定义同步锁

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author luckyFang
 * @date 2020/11/19 23:44
 * @file TestLockTicket.java
 * @desc
 */

public class TestLockTicket implements Runnable {
    private static final ReentrantLock lock;

    static {
        lock = new ReentrantLock();
    }

    private int ticketNums = 100;

    @Override
    public void run() {
        try{
            while (true) {
                if (ticketNums <= 0) {
                    break;
                }
                Thread.sleep(100);
                lock.lock();
                System.out.println(Thread.currentThread().getName() + "-->" + ticketNums--);
                lock.unlock();
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {
        TestTicket testTicket = new TestTicket();
        new Thread(testTicket, "A").start();
        new Thread(testTicket, "B").start();
        new Thread(testTicket, "C").start();
        new Thread(testTicket, "D").start();

    }
}

线程通信

生产者消费者(管程法)

生产者:

- 生产产品前:通知消费者等待
- 生产产品后:通知消费者消费

消费者:

- 消费产品后:通知生产者供给

/**
 * @author luckyFang
 * @date 2020/11/20 0:15
 * @file TestConsumer.java
 * @desc 生产者消费者模型
 */


import java.util.*;

/**
 * 产品
 */
class  Product{

}


/**
 * 生产者
 */
class Productor implements Runnable{
    private Container<Product> container;

    public Productor(Container<Product> container) {
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            container.push(new Product());
        }
    }
}

/**
 * 消费者
 */
class  Consumer implements  Runnable{
    private Container<Product> container;


    public Consumer(Container<Product> container) {
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            container.pop();
        }
    }
}

/**
 * 缓冲区
 * @param <T>
 */
class Container<T> {
    private static final int BUFFER_SIZE = 10;
    private List<T> list =new ArrayList<>();
    private int count;

    public synchronized void push(T e){
        // over

        if(count == BUFFER_SIZE) {
            try {
                this.wait();
            } catch (InterruptedException interruptedException) {
                interruptedException.printStackTrace();
            }
        }
        list.add(e);
        ++count;
        this.notifyAll();
        System.out.println("生产产品"+count);
    }


    public  synchronized T pop(){
        if (count==0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        T product  = list.get(--count);
        this.notifyAll();
        System.out.println("消费产品"+count);
        return product;
    }
}

public class TestConsumer {
    public static void main(String[] args) {
        Container<Product> productContainer = new Container<>();

        new Thread(new Productor(productContainer)).start();
        new Thread(new Consumer(productContainer)).start();
    }

}