一.线程的出现

1.进程与进程之间内存是不共享的,一个进程下可以有多个线程,线程和线程之间内存是共享的(线程一可以获得线程二的数据)
2.线程创建和销毁比进程消耗计算机资源低
3.线程是可以异步执行的,线程能够否执行取决于当前线程是否获取到对应的cpu执行权
4.cpu获取到之后,计算机底层cpu设置了一个时间片,寄存器:存储当前线程执行的位置,所以并不是执行完才会去释放cpu

1)线程出现是解决什么问题?

  1. 单位时间内处理复杂且庞大的数据或业务时(提升效率)

业务可以使用到多线程的呢?

  • 秒杀 下单处理
  • 获取支付结果(三方系统做了限流)
  • 批量的商品入库(ES索引库)
  • 商品的图片鉴黄鉴暴涉赌

    2)实现线程的四种方式

    ①继承Thread(实现了Runable接口)

    第一种继承Thread类 重写run方法
    run方法和start方法有什么区别?
    start()会开启一个子线程去执行此方法,run()还是主线程执行

    ②实现Runnable接口

    实现Runnable接口,重写run方法。

    ③实现Callable

    ④线程池

    线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
    @Async:异步线程执行,各个线程独立,线程即使失效,也不会影响主程序执行

    3)线程的状态

    image.png

  • NEW(新建)
    线程刚被创建,但是并未启动。

  • RUNNABLE(可运行)
    线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统CPU处理器。
  • BLOCKED(锁阻塞)
    当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
  • WAITING(无限等待)
    一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
  • TIMED_WAITING(计时等待)
    同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
  • TERMINATED(被终止)
    因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡

    二.多线程安全问题

    1.什么情况下会出现线程安全问题:

    • 1、在多线程环境下,是否存在【共享变量】— 位置:成员位置
      2、是否对共享变量进行 “写” 操作

      如何解决:
      1、加 synchronized 重量级锁(不推荐)
      2、加 ReentrantLock 非公平锁
      3、JUC Atomic 原子类解决 (推荐)

      2.判断当前程序中是否存在线程安全问题?

      1. 是否存在多线程环境
      2. 在多线程环境下是否存在共享变量
      3. 在多线程环境下是否存在对共享变量 “写” 操作

同步代码块

  1. Object lock = new Object(); //创建锁
  2. synchronized(lock){
  3. //可能会产生线程安全问题的代码
  4. }

同步方法

  1. //同步方法
  2. public synchronized void method(){
  3. //可能会产生线程安全问题的代码
  4. }

同步方法使用的是this锁
证明方式: 一个线程使用同步代码块(this明锁),另一个线程使用同步函数。如果两个线程抢票不能实现同步,那么会出现数据错误。

  1. //使用this锁的同步代码块
  2. synchronized(this){
  3. //需要同步操作的代码
  4. }

Lock锁

  1. Lock lock = new ReentrantLock();
  2. lock.lock();
  3. //需要同步操作的代码
  4. lock.unlock();

3.死锁

死锁需要满足的四大条件如下:

  1. 互斥条件:一个资源每次只能被一个线程使用。
  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源,在末使用完之前,不能强行剥夺。
  4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

    4.JMM内存模型

f10455522873080fbdc95bf80623330.png

三.Java并发编程三大特性

Java并发编程三大特性:原子性、 内存可见性、 有序性(指令重排序)

1. 原子性

一个线程在CPU中操作不可暂定,也不可中断,要不执行完成,要不不执行(无法保证)

不是原子操作,怎么保证原子操作呢? JMM对原子性的保证: 1.synchronized:同步加锁 2.JUC里面的lock:加锁

2 .内存可见性

多个线程访问同一个变量时,一个线程修改这个变量的值,其它线程能够看到修改之后的值,默认JMM对工作内存中的变量是不可见的。(无法保证)

3. 有序性

程序执行的顺序按照代码的先后顺序执行。(不保证,会优化,但是结果是一样的)

4.Volatile

volatile帮我们解决了什么问题?
● 内存可见性问题
● 指令重排序问题
不能保证 变量操作的原子性
解决方案:

  1. 使用synchronized
  2. 使用ReentrantLock(可重入锁)
  3. 使用AtomicInteger(原子操作)
    1. public static AtomicInteger count = new AtomicInteger(0);
    2. public void addCount() {
    3. for (int i = 0; i < 10000; i++) {
    4. //count++;
    5. count.incrementAndGet();
    6. }
    7. }

    5.Volatile 适用场景

    a)对变量的写入操作不依赖其当前值
    不满足:number++、count=count*5等
    满足:boolean变量、直接赋值的变量等
    b)该变量没有包含在具有其他变量的不变式中
    不满足:不变式 low总结:变量真正独立于其他变量和自己以前的值,在单独使用的时候,适合用volatile

    6.synchronized和volatile比较

    a)volatile不需要加锁,比synchronized更轻便,不会阻塞线程
    b)synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性
    与锁相比,Volatile 变量是一种非常简单但同时又非常脆弱的同步机制,它在某些情况下将提供优于锁的性能和伸缩性。如果严格遵循 volatile 的使用条件(变量真正独立于其他变量和自己以前的值 ) 在某些情况下可以使用 volatile 代替 synchronized 来优化代码提升效率。

    四.Java中各种锁

    锁唤醒机制:

    wait:表示持有对象锁的线程 释放对象锁权限,并且释 放 cpu 资源并进入阻塞状态。
    notify: 唤醒一个阻塞状态的线程
    notifyAll: 唤醒全部阻塞状态的线程

    1. synchronized(重量级锁)

    1、修饰在实例方法:作用当前实例加锁
    2、修饰在静态方法:作用当前类加锁
    3、修饰在代码块:进入同 步块加锁
    工作原理:JVM 是通过进入、退出对象监视器( Monitor )来实现对方法、同步块的同步的。
    image.png

    2.Lock锁

    可重入锁ReentrantLock
    重入锁: 表示支持重新进入的锁,调用 lock 方 法获取了锁之后,再次调用 lock,是不会再阻塞
  • API方法

    • lock()用来获取锁
    • unlock()释放锁,最好在finally块中释放

      3.Lock锁与Synchronized比较(面试题)

    1. 原始构成
      • Synchronized 是关键字,属于JVM层面,底层是通过 monitorenter 和 monitorexit 完成,依赖于 monitor 对象来完成。由于 wait/notify 方法也依赖于 monitor 对象,因此只有在同步块或方法中才能调用这些方法。
      • Lock 是 java.util.concurrent.locks.lock 包下的,是 api层面的锁。
    2. 使用方法
      • Synchronized 不需要用户手动释放锁,代码完成之后系统自动让线程释放锁
      • ReentrantLock 需要用户手动释放锁,没有手动释放可能导致死锁。
    3. 等待是否可以中断
      • Synchronized 不可中断,除非抛出异常或者正常运行完成
      • ReentrantLock 可以中断。一种是通过 tryLock(long timeout, TimeUnit unit),另一种是lockInterruptibly()放代码块中,调用interrupt()方法进行中断。
    4. 加锁是否公平
      • synchronized 是非公平锁
      • ReentrantLock 默认非公平锁,可以在构造方法传入 boolean 值,true 代表公平锁,false 代表非公平锁。
    5. 锁绑定多个 Condition
      • Synchronized 只有一个阻塞队列,只能随机唤醒一个线程或者唤醒全部线程。
      • ReentrantLock 用来实现分组唤醒,可以精确唤醒。

        五 J.U.C之CAS

        CAS,Compare And Swap,即比较并交换。Atomic原子类操作,ConcurrentHashMap在1.8的版本中也调整为了CAS+Synchronized
        CAS的思想很简单:三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当旧的预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功。
        9902933cf660c95ea988b3032441197.png
        AtomicInteger 不能解决 CAS ABA问题
        AtomicStampedReference 使用版本控制 解决 CAS ABA问题
        AtomicStampedReference可以看到每次修改都需要设置标识Stamp,相当于进行了1A-2B-3A的操作
        线程2进行操作的时候,虽然数值都一样,但是可以根据标识很容易的知道A是以前的1A,还是现在的3A

        六.J.U.C之并发工具类

        面试题:
        1、如何保证在多线程环境下 各个线程按照顺序来执行?
        CountDownLatch
        2、如何保证线程一起执行?
        CyclicBarrier

        七 J.U.C线程池

        线程池同样有五种状态:Running, SHUTDOWN, STOP, TIDYING, TERMINATED。

        工作中如何使用多线程?

  1. 线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。
    • 说明:使用线程池的好处是减少在创建和销毁线程上消耗的时间和系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程导致消耗完内存或者过度切换。
  2. 线程池不允许使用 Executors 去创建,也就是不能使用上述的三种线程池,而是要通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
    • FixedThreadPool 和 SingleThreadPool 都采用了 LinkedBlockingQueue,其允许的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,导致OOM。
    • CachedThreadPool 和 ScheduledThreadPool 允许创建的线程数量为 Integer.MAX_VALUE,可能创建大量的线程,导致OOM。