多线程基础

本文详细介绍了Java中的多线程创建方式,包括继承Thread类、实现Runnable接口和Callable接口,以及使用线程池。接着,讨论了线程状态、线程API、线程同步和死锁的概念,以及线程通信中的等待唤醒机制。通过生产者消费者模型展示了线程同步的实际应用。此外,还对比了synchronized和Lock的区别。
摘要由CSDN通过智能技术生成

多线程基础

提示: 本材料只做个人学习参考,不作为系统的学习流程,请注意识别!!!



多线程基础

1. 多线程创建

1. 1 多线程创建方式

  1. 继承Thread类,重写run方法实现多线程
class Task1 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("Task1输出:" + i);
        }
    }
}

class Task2 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("                 Task2输出:" + i);
        }
    }
}

/**
 * @Description: 继承Thread,重写run方法,实现多线程
 * @Author noneplus
 * @Date 2020/8/3 17:34
 */
public class ExtendThread {
    public static void main(String[] args) {
        Task1 task1 = new Task1();
        task1.start();

        Task2 task2 = new Task2();
        task2.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("                                         mainTask输出:" + i);
        }
        //3个线程的执行顺序由CPU的线程调度决定
    }
}
  1. 实现Runnable接口
class Task3 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("Task1输出:" + i);
        }
    }
}

class Task4 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("                      Task2输出:" + i);
        }
    }
}

/**
 * @Description: 实现Runnable接口,实现多线程,弥补单继承的问题
 * @Author noneplus
 * @Date 2020/8/3 17:34
 */
public class ImplementRunnable {
    public static void main(String[] args) {
        Task3 runnable3 = new Task3();
        Task4 runnable4 = new Task4();

        Thread task3 = new Thread(runnable3);
        Thread task4 = new Thread(runnable4);

        task3.start();
        task4.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("mainTask输出:" + i);
        }
        //3个线程的执行顺序由CPU的线程调度决定
    }
}
  1. 实现Callable接口:Callable支持返回值(但用多线程加返回值有点奇怪)
class Task5 implements Callable {
    @Override
    public Integer call() throws Exception {
        Integer i = 0;
        for (; i < 10; i++) {
            System.out.println("Task5输出:" + i);
        }
        return i;
    }
}

class Task6 implements Callable {
    @Override
    public Integer call() throws Exception {
        Integer i = 0;
        for (; i < 10; i++) {
            System.out.println("             Task6输出:" + i);
        }
        return i;
    }
}


/**
 * @Description: 实现Callable接口,可以定义返回值
 * @Author noneplus
 * @Date 2020/8/3 17:53
 */
public class ImplementCallable {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        for (int i = 0; i < 10; i++) {
            System.out.println("mainTask输出:" + i);
        }

        FutureTask<Integer> futureTask = new FutureTask<Integer>(new Task5());
        FutureTask<Integer> futureTask1 = new FutureTask<Integer>(new Task6());

        Thread thread = new Thread(futureTask);
        Thread thread1 = new Thread(futureTask1);

        thread.start();
        thread1.start();

        System.out.println("任务返回结果:"+futureTask.get());
        System.out.println("任务返回结果:"+futureTask1.get());
    }
}
  1. 使用线程池ThreadPoolExecutor
class Task7 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("Task7输出:" + i + "当前线程" + Thread.currentThread().getName());
        }
    }
}

class Task8 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("Task8输出:" + i + "当前线程" + Thread.currentThread().getName());
        }
    }
}

/**
 * @Description: 使用线程池创建线程池,实现线程复用和管理
 * @Author noneplus
 * @Date 2020/8/5 15:38
 */
public class ThreadPool {

    public static void main(String[] args) {
        /**
         * 阿里推荐:ThreadPoolExecutor
         * ```
         * public ThreadPoolExecutor(int corePoolSize,
         *                               int maximumPoolSize,
         *                               long keepAliveTime,
         *                               TimeUnit unit,
         *                               BlockingQueue<Runnable> workQueue)
         * ```
         * 1、corePoolSize 核心线程数大小,当线程数 < corePoolSize ,会创建线程执行 runnable
         * 2、maximumPoolSize 最大线程数, 当线程数 >= corePoolSize的时候,会把 runnable 放入 workQueue中
         * 3、keepAliveTime 保持存活时间,当线程数大于corePoolSize的空闲线程能保持的最大时间。
         * 4、unit 时间单位
         * 5、workQueue 保存任务的阻塞队列
         * 6、threadFactory 创建线程的工厂
         * 7、handler 拒绝策略
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(3));
        for (int i=0;i<10;i++){
            threadPoolExecutor.execute(new Task7());
            threadPoolExecutor.execute(new Task8());
        }
    }
}

1. 1 多线程创建方式比较

实现Runnable接口比继承Thread类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同一个资源。
  2. 可以避免java中的单继承的局限性。
  3. 增加程序的健壮性,实现解耦(耦合:类与类之间关系)操作,代码可以被多个线程共享,代码和线程独立。
  4. 线程池只能放入实现Runable或Callable接口线程,不能直接放入继承Thread的类。

2. 线程状态

  1. NEW(新建)
    线程刚被创建即new出来的时候,但是并未启动。还没调用start方法。
  2. Runnable(可运行)
    线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器cpu。
  3. Blocked(锁阻塞)
    当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有时,该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable可运行状态。
  4. Waiting(无限等待)
    一个线程在等待另一个线程执行一个(唤醒)动作时,调用wait()方法,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify随机唤醒一个或者notifyAll方法才能够唤醒。
  5. Timed Waiting(计时等待)
    同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep (时间参数)、Object.wait(时间参数5000毫秒自动唤醒)。
  6. Teminated(被终止)
    执行完run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

在这里插入图片描述

3. 线程相关API

Object类相关api(相关的方法一定是当前线程在获取了对应的锁对象才能调用,否则会抛出异常)
o.wait() :锁对象调用该方法使当前线程进入等待状态,并立刻释放锁对象,直到被其他线程唤醒进入等锁池。
o.wait(long) :锁对象调用该方法使当前线程进入等待状态,同时释放锁对象。但是超过等待的时间后线程会自动唤醒,或者被其他线程唤醒,并进入等锁池中。
o.wait(long,int) :和o.wait(long)方法一样,如果int参数大于0则前面的long数字加1
o.notify():随机唤醒一个处于等待中的线程(同一个等待阻塞池中)
o.notifyAll():唤醒所有等待中的线程(同一个等待阻塞池中)

Thread类的相关api
Thread.currentThread():返回对当前线程对象的引用
Thread.interrupted():检测当前线程是否已经中断(调用该方法后后就将该线程的中断标志位设置位false,所以连续两次调用该方法第二次肯定时false)
Thread.sleep(long millis):使当前线程睡眠(不会释放锁对象,可以让其他线程有执行的机会)
Thread.yield():使当前线程放弃cpu的执行权(有可能立刻又被重新选中继续执行,只可能给优先级更高的线程机会)
t.getId():返回该线程的id
t.getName():返回该线程的名字
t.getPriority():返回该线程的优先级
t.setPriority(int i):设置线程的优先级
t.getState():返回该线程的状态
t.getThreadGroup():返回该线程所属的线程组
t.interrupt():将该线程中断(实际并不会中断,只是将中断标志设置为true),如果线程正处在sleep(),join(),wait()方法中时(也就是正在阻塞中)调用该方法,该方法会抛出异常。
t.interrupted():检测该线程是否已经中断(对中断标志位不作处理)
t.isAlive():检测该线程是否还活着
t.isDaemon():检测该线程是否为守护线程
t.isInterrupted():检测该线程是否已经中断
t.join():在a线程中调用b.join(),则a线程阻塞,直到b线程执行完
t.join(long millis):和上面的方法一样,不过a线程阻塞的时间根据long的大小有关,如果达到设定的阻塞时间,就算b线程没有执行完,a线程也会被唤醒。

4. 线程同步

线程安全问题:
多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题
同步概念:
同步就可以让cpu在某段时间内只让一个线程进来做事情,其他线程不能进来,等你做完了才能一个一个进来
同步机制:

  1. 同步代码块(锁对象设置为公共资源的对象)。
  2. 同步方法(独占锁,默认锁对象是this或当前类Class对象)。
  3. 锁机制(可重入锁:ReentrantLock,它拥有和synchronized相同的并发性和内存语义)。

同步缺点:

  1. 一个线程持有锁会导致其他线程线程挂起
  2. 多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
  3. 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题

lock与synchronized的区别:

  1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定。
    但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将 unLock()放到finally{} 中;
  2. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;
    而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
  3. Lock可以让等待锁的线程响应中断,线程可以中断去干别的事务,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断
  4. 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
  5. Lock可以提高多个线程进行读操作的效率。
    在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

举个例子:
当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是采用synchronized关键字来实现同步的话,就会导致一个问题:
如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的

死锁:
线程死锁是指两个或两个以上的线程互相持有对方所需要的资源,由于synchronized的特性,一个线程持有一个资源,或者说获得一个锁,在该线程释放这个锁之前,其它线程是获取不到这个锁的,而且会一直死等下去,因此这便造成了死锁。
死锁产生的条件:

  1. 互斥条件:一个资源,或者说一个锁只能被一个线程所占用,当一个线程首先获取到这个锁之后,在该线程释放这个锁之前,其它线程均是无法获取到这个锁的。
  2. 占有且等待:一个线程已经获取到一个锁,再获取另一个锁的过程中,即使获取不到也不会释放已经获得的锁。
  3. 不可剥夺条件:任何一个线程都无法强制获取别的线程已经占有的锁
  4. 循环等待条件:线程A拿着线程B的锁,线程B拿着线程A的锁。

如何避免死锁

  1. 加锁顺序:线程按照相同的顺序加锁。
  2. 加锁时限,线程获取锁的过程中限制一定的时间,如果给定时间内获取不到,就算了,别勉强自己。这需要用到Lock的一些API。
public class DeadLock {
    private  Object lock1 = new Object();
    private  Object lock2 = new Object();

    public  void method1() throws InterruptedException {
        synchronized(lock1){
            System.out.println(Thread.currentThread().getName() + "获取到lock1,请求获取lock2....");
            Thread.sleep(1000);
            synchronized (lock2){
                System.out.println("获取到lock2....");
            }
        }
    }
    public  void method2() throws InterruptedException {
        synchronized(lock2){
            System.out.println(Thread.currentThread().getName() + "获取到lock2,请求获取lock1....");
            Thread.sleep(1000);
            synchronized (lock1){
                System.out.println("获取到lock1....");
            }
        }
    }

    public static void main(String[] args) {
        DeadLock deadLock = new DeadLock();

        new Thread(()-> {
            try {
                deadLock.method1();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        
        new Thread(()-> {
            try {
                deadLock.method2();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

5. 线程通信(等待唤醒机制)

为什么要处理线程间的通信?
多个线程并发执行时, 在默认情况下,CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多个线程之间就需要一些协调和通信,帮我们达到多个线程间,共同操作同一份数据!!!

什么是等待唤醒机制?
在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后,再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用( notifyAll())来唤醒所有的等待线程。
wait/notify 就是线程间的一种协作机制。

等待唤醒中的方法

  1. wait:线程不再活动,不再参与调度,进入 wait set(等待集合) 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
  2. notify:则选取所通知对象的 wait set 中的一个线程释放;即随机唤醒其中一个等待的线程
  3. notifyAll:则释放所通知对象的 wait set 上的全部线程。

注意:
哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
总结如下:

  • 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
  • 否则,线程就从 WAITING 状态又变成 BLOCKED 状态

调用wait和notify方法需要注意的细节(把握一个原则,用锁等待,用同一个锁唤醒)

  1. wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
  2. wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
  3. wait方法与notify方法必须要在同步代码块或者是同步函数方法中使用。因为:必须要通过锁对象调用这2个方法。

5.1 生产消费者模型

  1. 生产消费者模型-管程法(利用缓存区解决)
package com.yuan.thread;

/**
 * @description: 测试线程通讯-生产者和消费者1-管程法(缓冲区)
 * @author: ybl
 * @create: 2021-02-22 11:22
 **/

//测试生产者和消费者
public class TestPC {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();

        new Productor(container).start();
        new Consumer(container).start();
    }
}

//生产者
class Productor extends Thread {
    SynContainer synContainer;

    public Productor(SynContainer synContainer) {
        this.synContainer = synContainer;
    }

    //生产
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            synContainer.push(new BaoZi(i));
            System.out.println("生产了第" + i + "个包子");
        }
    }
}

//消费者
class Consumer extends Thread {
    SynContainer synContainer;

    public Consumer(SynContainer synContainer) {
        this.synContainer = synContainer;
    }

    //消费
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println("消费了第" + synContainer.pop().id + "个包子");
        }
    }
}

//产品(包子)
class BaoZi {
    //产品编号
    int id;

    public BaoZi(int id) {
        this.id = id;
    }
}

//缓冲区
class SynContainer {
    //容器
    BaoZi[] baoZis = new BaoZi[10];
    //容器计数器
    int count = 0;

    //生产者生产产品
    public synchronized void push(BaoZi baoZi) {
        //如果容器满了,等待消费者消费,生产者等待
        if (count == baoZis.length) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //如果容器没有满,生产产品
        baoZis[count] = baoZi;
        count++;

        //通知消费者消费
        this.notifyAll();

    }

    //消费者消费产品
    public synchronized BaoZi pop() {
        //判断能否消费
        if (count == 0) {
            //等待生产者生产,消费者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //如果可以消费
        count--;
        BaoZi baoZi = baoZis[count];

        //吃完了通知生产者生产
        this.notifyAll();
        return baoZi;
    }

}

  1. 生产消费者模型-信号灯法(利用标志位解决)
package com.yuan.thread;

/**
 * @description: 测试线程通讯-生产者和消费者2-信号灯法(利用标志位解决)
 * @author: ybl
 * @create: 2021-02-22 11:22
 **/

//测试生产者和消费者
public class TestPC {
    public static void main(String[] args) {
        BaoZi bz = new BaoZi();//创建包子对象名,传入吃货线程和生产线程使用来判断状态,设置属性等

        ChiHuo ch = new ChiHuo("吃货",bz);
        BaoZiPu bzp = new BaoZiPu("包子铺",bz);

        ch.start();
        bzp.start();
    }
}

class BaoZi {
    String  pier ;//皮,包子属性,成员变量表示事物的属性
    String  xianer ;//馅
    boolean  flag = false ;//包子状态,false表示没有包子
}

class ChiHuo extends Thread{
    private BaoZi bz;

    public ChiHuo(String name,BaoZi bz){//构造传入线程名字和包子对象,才能判断包子的状态等
        super(name);
        this.bz = bz;
    }

    @Override
    public void run() {
        while(true){//死循环不断的吃包子显示包子属性,每次吃完包子,包子没有了标记为false,通知生产线程
            synchronized (bz){//等待唤醒方法要被锁对象调用,所以这里写在同步代码块里面
                if(bz.flag == false){//如果没包子,吃货线程等待
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                System.out.println("吃货正在吃"+bz.pier+bz.xianer+"包子");
                bz.flag = false;
                bz.notify();
            }
        }
    }
}

class BaoZiPu extends Thread {
    private BaoZi bz;

    public BaoZiPu(String name,BaoZi bz){
        super(name);
        this.bz = bz;
    }

    @Override
    public void run() {
        int count = 0;//标记控制

        while(true){//死循环不断生产包子,设置包子属性,每生产完一个包子,包子有了标记为true,通知吃货线程
            synchronized (bz){//等待唤醒方法要被锁对象调用,所以这里写在同步代码块里面
                if(bz.flag == true){//如果有包子,生产线程,等待,不生产
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                System.out.println("包子铺开始做包子");
                if(count%2 == 0){//奇偶标记控制,设置包子不同属性
                    bz.pier = "冰皮";
                    bz.xianer = "五仁";
                }else{
                    bz.pier = "薄皮";
                    bz.xianer = "牛肉大葱";
                }
                count++;

                bz.flag=true;
                System.out.println("包子造好了:"+bz.pier+bz.xianer);
                System.out.println("吃货来吃吧");
                //唤醒等待线程 (吃货)
                bz.notify();
            }
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值