Day117 并发编程基础

并发编程基础

在 Java Web 开发、Jdbc 开发、Web 服务器、分布式框架时会遇到线程安全问题,这一块比较难,涉及到很多抽吸概念的理解,需要用到JVM,操作系统,计算机组成原理等知识。

概述

  • 基本概念

    • 程序:程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。
    • 进程(Process):进程是程序的一次执行,当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。比如在Windows系统中,一个运行的xx.exe就是一个进程。进程是系统资源分配的基本单位、
    • 线程(Thread): 一个进程之内可以分为一到多个线程,一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行,Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。
    • 为什么要有线程:为了使程序能够更高效的并发执行,进程实现多处理是非常耗费CPU资源的,所以引入线程作为调度的基本单位。
    • Java有默认有两个线程,main线程和GC线程
    • Java无法直接创建线程,需要通过JVM协调系统与硬件创建,所以理解多线程涉及到JVM和操作系统还有组成原理。
    • 并发(concurrent)是同一时间应对(dealing with)多件事情的能力。多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。
    • 并行(parallel)是同一时间动手做(doing)多件事情的能力。单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。
    • 单核 cpu 下,线程实际还是 串行执行 的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是 同时运行的 。总结为一句话就是: 微观串行,宏观并行 。
    • 多线程:真正的多线程是指由多个CPU,即多核;但很多多线程都是模拟出来的,即在一个CPU的情况下,线程切换速度很快,好像同时执行一样。
    • 守护线程:守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程。(比如垃圾处理gc,记录日志,监控内存等)
    • 同步:多个线程井然有序的使用资源,通过队列和锁机制。
    • 保证线程同步的方式:synchronized关键字,ReentrantLock,ThreadLocal,BlockingQueue类,用原子变量,volatile关键字
      在这里插入图片描述
      并发编程的优缺点
  • 并发编程:如何充分利用CPU的资源。

  • 优点

    • 通过并发编程的形式可以将多核CPU的计算能力发挥到极致,
    • 方便进行业务拆分,提升系统并发能力和性能:现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础。
  • 缺点: 并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、线程安全、死锁等问题

  • 并发编程三要素

    • 原子性:原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
    • 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)
    • 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
  • 出现线程安全问题的原因

    • 线程切换带来的原子性问题
    • 缓存导致的可见性问题
    • 编译优化带来的有序性问题
  • 解决办法

    • JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
    • synchronized、volatile、LOCK,可以解决可见性问题
    • Happens-Before 规则可以解决有序性问题

创建线程

  • 继承 Thread 类:创建子类,重写run方法,创建对象,调用start()方法。
    • Thread类其实就是实现了Runnable接口,所以两者很像
  • 实现 Runnable 接口:要以Runnable实例作为target创建Thead对象;更具优势,避免了单继承的局限性,方便同一个对象被多个线程使用;
    • 这是用到了静态代理模式,让Thread类进行代理。
//一份资源
StartThread station = new StartThread();

//多个代理
new Thread(station, "1"),start();
new Thread(station, "2"),start();
new Thread(station, "3"),start();
  • 实现 Callable 接口:Callable的 call() 方法可以返回值和抛出异常,比Runable效率更高,企业级开发常用,参数为返回值类型。
    • 如何调用Callable接口,Thread中只能传入Runnable,所以需要使用适配类 FutureTask(Runnable的实现类) 来传入callable的实现类,
  • 创建线程池:为了减少创建和销毁线程的次数,让每个线程可以多次使用,可根据系统情况调整执行的线程数量,防止消耗过多内存,所以我们可以使用线程池.

public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // new Thread(new Runnable()).start();
        // new Thread(new FutureTask<V>()).start();
        // new Thread(new FutureTask<V>( Callable )).start();
        new Thread().start(); // 怎么启动Callable

        MyThread thread = new MyThread();
        FutureTask futureTask = new FutureTask(thread); // 适配类

        new Thread(futureTask,"A").start();
        new Thread(futureTask,"B").start(); // 结果会被缓存,效率高

        Integer o = (Integer) futureTask.get(); //这个get 方法可能会产生阻塞!把他放到最后
        // 或者使用异步通信来处理!
        System.out.println(o);

    }
}

class MyThread implements Callable<Integer> {

    @Override
    public Integer call() {
        System.out.println("call()"); // 会打印几个call
        // 耗时的操作
        return 1024;
    }

}
  • 企业中创建线程的方式:线程就是一个单独的资源类,把资源类丢入Thread类即可,可以用匿名内部类,一般用更简洁的lambda表达式
// 基本的卖票例子

import java.time.OffsetDateTime;

/**
 * 真正的多线程开发,公司中的开发,降低耦合性
 * 线程就是一个单独的资源类,没有任何附属的操作!
 * 1、 属性、方法
 */
public class SaleTicketDemo01 {
    public static void main(String[] args) {
        // 并发:多线程操作同一个资源类, 把资源类丢入线程
        Ticket ticket = new Ticket();

        // @FunctionalInterface 函数式接口,jdk1.8  lambda表达式 (参数)->{ 代码 }
        new Thread(()->{
            for (int i = 1; i < 40 ; i++) {
                ticket.sale();
            }
        },"A").start();

        new Thread(()->{
            for (int i = 1; i < 40 ; i++) {
                ticket.sale();
            }
        },"B").start();

        new Thread(()->{
            for (int i = 1; i < 40 ; i++) {
                ticket.sale();
            }
        },"C").start();


    }
}

// 资源类 OOP
class Ticket {
    // 属性、方法
    private int number = 30;

    // 卖票的方式
    // synchronized 本质: 队列,锁
    public synchronized void sale(){
        if (number>0){
            System.out.println(Thread.currentThread().getName()+"卖出了"+(number--)+"票,剩余:"+number);
        }
    }

}

线程状态

线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。

  • 创建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
  • 就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
  • 运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
  • 阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
  • 死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪   
    在这里插入图片描述
    Thread类中的源码
    public enum State {
        //新建
        NEW,
        //运行
        RUNNABLE,
        //阻塞
        BLOCKED,
        // 等待
        WAITING,
        // 超时等待,一直等
        TIMED_WAITING,
        // 终止
        TERMINATED;
    }

线程方法

  • sleep():方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。因为sleep() 是static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。
  • wait():wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程
  • sleep与wait的区别:
    • 来自不同的类:wait是Object类的;sleep来自Thread类
    • 关于锁的释放:wait会释放锁,sleep不释放锁
    • 使用的范围不同:wait必须在同步代码块汇总,sleep没有限制
  • 实际开发过程中用的更多的是JUC下的TImeUnit类

线程通信

生产者与消费者问题

synchronized+wait+notify 实现线程通讯
方法流程:判断等待,业务,通知
注意不用if判断,要用while判断,防止虚假唤醒,因为if只判断一次,而notifyAll会唤醒所有线程都执行+1,while则会持续进行判断


/**
 * 线程之间的通信问题:生产者和消费者问题!  等待唤醒,通知唤醒
 * 线程交替执行  A   B 操作同一个变量   num = 0
 * A num+1
 * B num-1
 */
public class A {
    public static void main(String[] args) {
        Data data = new Data();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();


        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"D").start();
    }
}

// 判断等待,业务,通知
class Data{ // 数字 资源类

    private int number = 0;

    //+1
    public synchronized void increment() throws InterruptedException {
        while (number!=0){  //0
            // 等待
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        // 通知其他线程,我+1完毕了
        this.notifyAll();
    }

    //-1
    public synchronized void decrement() throws InterruptedException {
        while (number==0){ // 1
            // 等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        // 通知其他线程,我-1完毕了
        this.notifyAll();
    }

}


也可以用JUC中的 Lock+await+signal来实现

死锁

  • 死锁:在并发环境下,进程(线程)因竞争资源而导致的一种互相等待对方手里的资源,导致各进程(线程)都阻塞,无法向前推进的现象,就是死锁,若无外力作用,它们都将无法推进下去。

  • 比如:当线程 A 持有独占锁 a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。

  • 再比如著名的哲学家进餐问题

  • 死锁产生的必要条件

    • 互斥条件:所谓互斥就是线程(进程)在某一时间内独占资源,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放
    • 请求与保持条件:一个线程(进程)因请求资源而阻塞时,对已获得的资源保持不放。
    • 不剥夺条件:线程(进程)已获得资源,在末使用完之前,不能强行剥夺,只有自己使用完毕后才释放资源。
    • 循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞
  • 如何预防死锁(静态策略):我们只要提前破坏产生死锁的四个条件中的其中一个就可以了。

    • 破坏互斥条件:这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的,没这条就达不到并发安全的效果了。
    • 破坏请求与保持条件:一次性申请所有的资源,一直占有到线程使用结束,但这样会造成极大的资源浪费,资源利用率低。
    • 破坏不剥夺条件
      • 一是让占用资源的线程申请其他资源时,如果申请不到,先主动释放自己占有的资源,但这可能会造成前面的工作失效,适用于易保存或者易恢复的资源;
      • 或者由操作系统协调,申请不到的话就对资源进行剥夺,但这一般要考虑线程的优先级,实现起来比较麻烦。
    • 破坏循环等待条件:顺序资源分配法,给系统中的资源编号,靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。
  • 如何避免死锁(动态策略)

    • 安全序列:保证每个进程都能顺利完成的分配资源的顺序(系统资源是有限的)。
    • 不安全状态:分配资源之后,系统找不出任何一个安全序列,系统就进入了不安全状态,如果没有进程提前归还资源,最坏情况可能所有进程都无法顺利执行下去。如何系统处于安全状态,就一定不会发生死锁,如果系统进入不安全状态,则可能会发生死锁。
    • 银行家算法
      • 由来:最初Dijkstra为银行系统设计,确保迎合在发放现金贷款时,不会发生不能满足所有客户需要的情况,后被用于操作系统中,用于避免死锁。
      • 作用:在资源分配之前预先判断这次分配是否会导致系统进入不安全状态, 以此决定是否答应资源分配请求。
      • 流程:
        1. 检查本次申请是否超过了之前声明的最大需求数
        2. 检查此时系统剩余的可用资源是否还能满足这次请求
        3. 试探着分配,更改各数据结构
        4. 用安全性算法检查此次分配是否会使系统进入不安全的状态
    • 安全性算法:检查当前剩余的资源都否满足某个进程的最大需求,如果可以,就把该进程加入安全序列,并把该线程持有的资源全部回收,不断重复上述过程,看最终能否让所有进程都加入安全序列。
    • 数据结构:
      在这里插入图片描述
  • 产生死锁后如何解决

    • 资源剥夺法。挂起(暂时放在外存上)某些死锁进程,并抢占它的资源,将这些资源分配给其他死锁进程,但是应防止被挂起的进程长时间得不到资源而饥饿。
    • 终止进程法。强制撤销死锁进程,并剥夺这些进程的资源,这种方法实现简单,但付出代价可能会很大,让有些进程功亏一篑,还得从头再来。
    • 进程回退法,让进程回到发生足以避免死锁的地步,这要求系统要记录进程的历史信息,设置还原点。
    • 一般我们会先解决优先级低的、执行时间还不长的、需要时间还多的、使用资源多的和没有和用户进行交互的进程。
  • 常用方法

    • 尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
    • 尽量使用 Java. util. concurrent 并发类代替自己手写锁。
    • 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
    • 尽量减少同步的代码块。

有多少种方法可以让线程阻塞?
  1、join:thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
  2、sleep:使当前线程睡眠一段时间,没有释放锁,睡眠时间到了会继续执行
  3、yield:暂停当前正在执行的线程对象,并执行其他线程。
  4、改变线程的优先级
  5、将线程设置成守护线程(jvm中的垃圾回收线程)

sleep与wait、notify与notifyAll的区别

  • sleep():使当前线程睡眠一段时间,没有释放锁,睡眠时间到了会继续执行,例如sleep(600),使线程睡眠6秒。不释放资源,可以使用在任何方法中,sleep必须捕获异常。
  • wait():释放锁。其他线程可以使用此同步块或方法。不必捕获异常。
  • notify():唤醒一个处于等待的线程,但是不确定唤醒哪一个。
  • notifyall():唤醒在此对象监视器上等待的所有线程。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值