Java教程

Java 多线程 万字最详解

本文主要是介绍Java 多线程 万字最详解,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

文章目录

  • Java多线程
    • 多线程概述
      • 线程与进程
      • 线程调度
      • 同步与异步
      • 并发与并行
      • 线程阻塞
    • Java 多线程的实现
      • 继承 Thread 类
      • 实现 Runnable 接口
      • 实现 `Callable`接口
    • Thread 类讲解
      • 线程状态`currentThread()`
      • 线程休眠`sleep()`
      • 线程中断`interrupt()`
      • 守护线程`setDaemon()`
      • 主线程之前完成`join()`
    • 线程安全问题
      • 线程不安全的演示
      • 解决方法 1:同步代码块
      • 解决方法 2:同步方法
      • 解决方法 3:显示锁 Lock
      • 更多 synchronized 用法
    • 显示锁的问题
      • 公平锁和非公平锁
      • 怎么实现公平锁
    • 多线程经典问题
      • 线程死锁
      • 多线程通信
        • 生产者和消费者
    • 线程的状态
    • 线程池
      • 概念
      • 好处
      • 线程池的分类

Java多线程

多线程概述

线程与进程

进程

  • 是内存中运行的应用程序,每个进程都有一个独立的内存空间

线程

  • 值进程的执行路径,共享一个内存空间,线程之间可以自由切换,并发执行,一个进程最少有一个线程
  • 线程实际上是在进程基础上的进一步划分,一个进程启动之后,里面若干的执行路径又可以划分成若干个线程

线程调度

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eoKunGnP-1620203162745)(/Users/faro_z/Library/Application Support/typora-user-images/image-20210504211610442.png)]

分时调度

  • 所有线程,轮流使用 CPU,平均分配每个线程占用 CPU 的时间

抢占式调度

  • 当 CPU 空闲的时候,CPU 会抛出一个时间片,谁抢到就是谁的。优先让优先级高的线程使用 CPU;如果线程优先级相同,那么,会随机选择一个(线程随机性),Java 使用的就是抢占式调度
  • CPU 使用抢占式调度模式在多个线程之间高速切换,对于 CPU 而言,某时刻,只能执行一个线程,而 CPU 在多个线程切换的速度很快,在我们看来,就像是多个线程在同时执行一样。其实多线程程序并不能提高程序的运行速度(甚至会因为切换的时候的时间消耗,使得总耗时还边长),但是能提高程序的运行效率,让 CPU 使用率更高。

同步与异步

**同步:**排队执行,效率低但是安全

**异步:**同时执行,效率高但是数据不安全

并发与并行

并发:指两个或多个事件,在==同一时间==内发生。(比如说一天内,一小时内)

**并行:**指两个或多个事件,在同一时间发生(同时发生)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UZDhhlwB-1620203162747)(/Users/faro_z/Library/Application Support/typora-user-images/image-20210504212449234.png)]

线程阻塞

一般,比较耗时的操作,可以看成是线程阻塞

比如说,我们要等待用户输入,那么,那一段线程,就是阻塞了

Java 多线程的实现

java 中实现多线程,一共有三种方法

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

继承 Thread 类

public class ThreadDemo {
    public static void main(String[] args) {
        new ThreadClass().start();
        for (int i = 0; i < 10; i++) {
            System.out.println("锄禾日当午"+i);
        }
    }
}

class ThreadClass extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("汗滴禾下土"+i);
        }
    }
}

image-20210504213505849

image-20210504213140920

每个线程都有自己的栈空间,但是共用一个堆空间

实现 Runnable 接口

public class RunnableDemo {
    public static void main(String[] args) {

        //子线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("汗滴禾下土"+i);
            }
        }, "子线程").start();

        //主线程(的一部分,整个 main 都是主线程)
        for (int i = 0; i < 10; i++) {
            System.out.println("锄禾日当午"+i);
        }
    }
}

实现 Callable<T>接口

Callable,是带返回值的线程

上面两个实现方法,可以看成主线程之外的子线程去完成任务,和主线程无关了

但是 Callable,相等于主线程让子线程去完成任务,等到子线程任务完成了,再给主线程返回一个结果。比如说,主线程可以派发 1000 个任务,给 1000 个线程,等着 1000 个任务执行完毕了,可以获得 1000 个结果。

Callable 的使用步骤如下:

  1. 编写实现 Callable 接口,实现 call 方法
class Xxx implements Callable<T> {
		@Override
		public <T> call() throws Exception {
			return T;
		}
	}
  1. 创建 FutureTask 对象,并传入编写的 Callable 对象
FutureTask<Integer> task = new FutureTask<>(callable);
  1. 通过 Thread,启动线程
new Thread(task).start

获取子线程返回结果的方法如下:

  • 使用 FutureTask 的get()方法

**注意:**使用get()会让主线程等待子线程执行完毕,然后去获得方法

public class CallableDemo {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        MyCallable cl = new MyCallable();
        FutureTask<Integer> task = new FutureTask<>(cl);
        //执行子线程
        new Thread(task).start();
        Integer res = task.get();
        System.out.println("子线程计算结果为:"+res);
        for (int i = 0; i < 10; i++) {
            Thread.sleep(100);
            System.out.println("main"+i);
        }

    }
}

class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000);
            System.out.println("子线程正在执行"+i);
        }
        return 100;
    }
}

image-20210505155751891

  • 使用使用 FutureTask 的isDone()方法

可以先判断子线程有没有执行完,执行完了,再去获取子线程的返回值,避免主线程等待

public class CallableDemo2 {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        MyCallable2 cl = new MyCallable2();
        FutureTask<Integer> task = new FutureTask<>(cl);
        new Thread(task).start();
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000);
            if (task.isDone()) {
                Integer res = task.get();
                System.out.println("子线程已经执行完了,结果是:"+res);
            }
            System.out.println("main"+i);
        }

    }
}

class MyCallable2 implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        for (int i = 0; i < 10; i++) {
            Thread.sleep(500);
            // System.out.println("子线程正在执行"+i);
        }
        return 100;
    }
}

image-20210505160205312

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r16OCaP5-1620203162753)(/Users/faro_z/Library/Application Support/typora-user-images/image-20210505154137082.png)]

Thread 类讲解

线程状态currentThread()

获取当前线程

public class ThreadDemo1 {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName());
        }, "子线程").start();
    }
}

image-20210505002516386

线程休眠sleep()

线程休眠

public class ThreadDemo1 {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000);
            System.out.println("hahaha");
        }
    }
}

线程中断interrupt()

在早期,有个 stop()方法,但是,使用 stop 强行让一个线程死亡,可能导致其没来得及释放资源,导致资源的浪费。

所以,我们要使用interrupt()来让线程察觉到外部标记,然后自己决定死亡

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

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName()+":"+i);
                    //这里的 InterruptedException ,就是检查有没有被中断的
                } catch (InterruptedException e) {
                    //e.printStackTrace();
                    System.out.println("检测到打扰标记,线程死亡!");

                    /**
                     * 在死亡前,可以在这里进行资源释放
                     * 类似于交代后事
                     */
                    System.out.println("爷要 si 了,资源都释放掉了!");


                    //如果要线程死亡,我们只要让 run 方法 return 就好了
                    return;
                }
            }
        }, "子线程");

        t1.start();

        for (int i = 0; i < 5; i++) {
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName()+":"+i);
        }

        /**
         * 给线程 t1 添加中断标记
         */
        t1.interrupt();
    }
}

image-20210505004435112

image-20210505004628491

守护线程setDaemon()

用户线程:当一个进程不包括任何一个存货的用户线程时,进行结束

**守护线程:**当最后一个用户线程结束后,守护线程自动死亡

==注意:==守护线程的设置,一定要在线程启动之前

public class ThreadDemo3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }, "守护线程");
        /**
         * 将 t1 设置为守护线程
         * 一定要在 start 之前,进行守护线程的设置
         */
        t1.setDaemon(true);
        t1.start();

        for (int i = 0; i < 5; i++) {
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

image-20210505005726522

主线程之前完成join()

join()的作用,是保证子线程,在主线程之前完成

如果没有设置超时,那么,主线程在子线程完成之前,不会与之抢夺时间片

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

        Thread t = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "子线程");
        t.start();
        t.join();

        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName());
        }
    }
}

image-20210505140653232

join(int time)还可以设置子线程排在主线程前面的时间:

其他代码不变,这里,我们设置子线程排在主线程前面的时间为

image-20210505141129533

并为主线程每次打印增加等待

image-20210505141538031

可以看到,在两秒后,主线程便开始与子线程抢夺时间片:

image-20210505141639111

线程安全问题

线程不安全的演示

public class ThreadDemo4 {
    public static void main(String[] args) {
        
        MyThread t = new MyThread();

        new Thread(t,"A").start();
        new Thread(t,"B").start();
        new Thread(t,"C").start();

    }
}

class MyThread implements Runnable {

    private int ticket = 10;

    @Override
    public void run() {
        while (ticket>0) {
            System.out.println(Thread.currentThread().getName()+"开始卖票");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticket--;
            System.out.println(Thread.currentThread().getName()+"余票:"+ticket);
        }
    }
}

image-20210505010857674

解决方法 1:同步代码块

public class ThreadDemo5 {    public static void main(String[] args) {        MyThread2 t = new MyThread2();        new Thread(t,"A").start();        new Thread(t,"B").start();        new Thread(t,"C").start();    }}class MyThread2 implements Runnable {		//临界资源区    private int ticket = 10;      /**     * 锁的对象,是要是唯一的,都可以     * 有时候为了不使用自定义的锁,甚至可以使用 Class 作为锁     * 比如使用 MyThread2.class     */    private static final Object lock=new Object();    @Override    public void run() {        while (true) {            synchronized (lock) {                if (ticket<=0) break;                else {                    System.out.println(Thread.currentThread().getName()+"开始卖票");                    try {                        Thread.sleep(1000);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                    ticket--;                    System.out.println(Thread.currentThread().getName()+"余票:"+ticket);                }            }        }    }}

image-20210505134803647

解决方法 2:同步方法

即在方法前,加上 synchronized 关键字

同步方法,其实使用的是隐式锁

如果是同步方法,锁的是当前对象

如果是静态方法,锁的是该类的 Class

解决方法 3:显示锁 Lock

使用 JUC 里的 lock

public class ThreadDemo6 {
    public static void main(String[] args) {

        MyThread3 t = new MyThread3();

        new Thread(t,"A").start();
        new Thread(t,"B").start();
        new Thread(t,"C").start();

    }
}

class MyThread3 implements Runnable {

    private int ticket = 10;

    private Lock lock = new ReentrantLock();

    @Override
    public void run() {

        while (true) {
            lock.lock();
            if (ticket<=0) break;
            else {
                System.out.println(Thread.currentThread().getName()+"开始卖票");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket--;
                System.out.println(Thread.currentThread().getName()+"余票:"+ticket);
            }
            lock.unlock();
        }
    }
}

image-20210505142828965

更多 synchronized 用法

之前我写过一个详细的 synchronized 用法的博客,点此跳转

或者看 java/JUC/多线程.md

显示锁的问题

公平锁和非公平锁

**公平锁:**先来,先得到锁

**非公平锁:**大家争抢这个锁

我们一般使用的,都是非公平锁(synchronized,ReentrantLock)

怎么实现公平锁

在构造 ReentrantLock 时,传入一个 true,表示这是一个公平锁

Lock lock = ReentrantLock(true);

多线程经典问题

线程死锁

什么是死锁?

线程 A,拿到 A锁,执行任务,中途又需要 B 锁

线程 B,拿到 B锁,执行任务,中途又需要 A 锁

如果这两个线程是同时执行的,那 A拿不到 B 锁,B拿不到 A 锁,就会相互僵持,出现死锁

多线程通信

比如说我们想要在下载完以后,自动播放电影

那么,当下载线程结束以后,要去通知播放线程

这,就是多线程通信

多线程通信的实现,是:

Object 里的 wait()notify()/notifyAll()方法

生产者和消费者

有了wait()notify()/notifyAll()方法,我们就可以解决生产者和消费者问题

所谓生产者和消费者问题,就是设置一个缓存,当缓存为空,消费者不能消费,生产者必须生产;当缓存已经满了,生产者必须停止,消费者必须消费;剩下的时候,生产者、消费者可以随意。

因为这个缓存是临界区,所以必须加锁

但是,光加锁,不能解决生产者、消费者问题。如果在任何时候,都让生产者和消费者自由争抢时间片,那如果缓存已满,可能生产者还会抢到时间片,这样,就会让缓存溢出。
这个时候,就需要用到线程通信,来解决生产者和消费者问题。

我们定义三个类:

**Cook:**生产者,每次做和上一次不一样的菜

**Waiter:**消费者,每次端出菜

**Food:**食物,缓存长度为 1,即没有菜的时候,cook必须做菜,waiter 不能端菜;有菜的时候,cook 不能做菜,waiter 必须端菜

public class ProducerAndCustomer {
    public static void main(String[] args) {
        Food food = new Food();
        new Thread(new Cook(food)).start();
        new Thread(new Waiter(food)).start();
    }
}


class Waiter implements Runnable {

    private Food food;

    public Waiter(Food food) {
        this.food = food;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            /**
             * 这里因为要访问临界资源 isEmpty,所以必须加锁
             */
            synchronized (food) {
                if (!food.isEmpty) {
                    food.get();

                    food.isEmpty=true;
                    food.notifyAll();
                    try {
                        food.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}


class Cook implements Runnable{

    private Food food;

    public Cook(Food food) {
        this.food = food;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            /**
             * 这里因为要访问临界资源 isEmpty,所以必须加锁
             */
            synchronized (food) {
                if (food.isEmpty) {
                    if (i%2==0) {
                        food.setName("煎饼果子");
                        food.setTest("咸味");
                    } else {
                        food.setName("豆腐脑");
                        food.setTest("超甜味");
                    }

                    System.out.println("厨师把菜做好了,菜品为:"+food.toString());
                    //厨师做好了菜,将盘子设置为不空
                    food.isEmpty=false;
                    //唤醒其他线程
                    food.notifyAll();
                    //当前线程休眠
                    try {
                        food.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}


class Food {
    private String name;
    private String test;
    public boolean isEmpty=true;

    public Food(String name, String test) {
        this.name = name;
        this.test = test;
    }

    public Food() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getTest() {
        return test;
    }

    public void setTest(String test) {
        this.test = test;
    }

    public void get() {
        System.out.println("服务员端走了菜:"+this.toString());
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("名称:").append(this.name).append("\n")
                .append("味道:").append(this.test).append("\n");
        return sb.toString();
    }
}

结果:

我们可以看到,是严格按照先做菜,再端菜的顺序执行的。

image-20210505152642468

线程的状态

线程一共有种状态

image-20210505153520871

线程池

概念

如果我们要使用大量线程,每个线程执行的时间很短,那每次创建、销毁线程,就会浪费大量时间。所以,我们要先提前创建部分线程,要用的时候,去里面取,不用的时候,再放回去

好处

  • 降低资源消耗
  • 提高响应速度
  • 提高线程的可管理性

虽然好处多多,但是,我们在日常开发的时候,使用自定义线程池的几率少之又少,因为 Java 本来就是按照多线程去设计的,不用我们再手动创建线程池。

线程池的分类

一共有四种线程池

  1. 缓存线程池
/**
缓存线程池. (长度无限制) 执行流程:
1. 判断线程池是否存在空闲线程
2. 存在则使用
3. 不存在,则创建线程 并放入线程池, 然后使用
* * * * * * */
ExecutorService service = Executors.newCachedThreadPool(); //向线程池中 加入 新的任务
service.execute(new Runnable() {
    @Override
    public void run() {
			System.out.println("线程的名称:"+Thread.currentThread().getName()); 
    }
});
service.execute(new Runnable() {
    @Override
    public void run() {
			System.out.println("线程的名称:"+Thread.currentThread().getName()); 
    }
});
service.execute(new Runnable() {
    @Override
    public void run() {
			System.out.println("线程的名称:"+Thread.currentThread().getName()); 
    }  
});
  1. 定长线程池
/**
* 定长线程池.
* (长度是指定的数值) * 执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
* 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程 
*/
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable() {
    @Override
    public void run() {
				System.out.println("线程的名称:"+Thread.currentThread().getName()); 
    }
});
service.execute(new Runnable() {
    @Override
    public void run() {
			System.out.println("线程的名称:"+Thread.currentThread().getName()); 
    }
});
  1. 单线程线程池

有时候,我们可能需要多个作业排队执行,这个时候,就需要用到这种线程池了

效果与定长线程池 创建时传入数值1 效果一致. 
  /**
   * 单线程线程池. 执行流程:
   * 1. 判断线程池 的那个线程 是否空闲
   * 2. 空闲则使用
   * 4. 不空闲,则等待 池中的单个线程空闲后 使用
	 */
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
    @Override
    public void run() {
			System.out.println("线程的名称:"+Thread.currentThread().getName()); 
    }
});
service.execute(new Runnable() {
    @Override
    public void run() {
			System.out.println("线程的名称:"+Thread.currentThread().getName()); 
    }
});
  1. 周期性任务定长线程池

image-20210505161849463

这篇关于Java 多线程 万字最详解的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!