内存与多线程

Java内存模型:

运行时数据区域:

根据JVM规范,JVM内存共分为虚拟机栈方法区程序计数器本地方法栈五个部分。

程序计数器:

程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成

每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储是线程安全的。

此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

  • 每个线程都会有一个程序计数器
  • 各线程的程序计数器是线程私有的,互不影响,是线程安全的
  • 程序计数器记录线程正在执行的内存地址,以便被中断线程恢复执行时再次按照中断时的指令地址继续执行

栈(Java虚拟机栈)

与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的同时会创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

该区域在无法申请到足够内存时会抛出StackOverflowErrorOutOfMemoryError异常

  • 每个线程会对应一个java栈
  • 每个java栈由若干栈帧组成
  • 栈帧在方法运行时,创建并入栈;方法执行完,该栈帧弹出栈帧中的元素作为该方法的返回值,该栈帧被清除
  • 栈顶的栈帧叫活动栈,表示当前执行的方法,才可以被CPU执行
  • 线程请求的栈深度大于虚拟机所允许的深度,会抛出StackOverFlowError异常
  • 栈扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

本地方法栈:

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。

会抛出StackOverflowErrorOutOfMemoryError异常。

  • 本地方法栈和java栈所发挥的作用非常相似,区别不过是java栈为JVM执行java方法服务,而本地方法栈为JVM执行Native方法服务
  • 本地方法栈也会抛出StackOverflowErrorOutOfMemoryError异常

堆:

对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配.

但是随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。

Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Collected Heap)。如果从内存回收的角度看,如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。

会出OutOfMemoryError异常。

方法区:

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻。

会抛出OutOfMemoryError异常。

  • 方法区是java堆的永久区
  • 方法区存放了要加载的类信息(名称、修饰符)、类中的静态变量、类中定义为final类型的常量、类中的Filed信息、类中的方法信息
  • 方法区是被java线程共享的
  • 方法区要使用的内存超过允许的大小时,会抛出OutOfMemoryError:PremGen space的错误

运行时常量池:

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。具备动态性(如String类的intern()方法)。

会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

  • 常量池是方法区的一部分
  • 常量池中存储两类数据:字面量和引用量
    字面量:字符串、final变量等。
    引用量:类/接口、方法和字段的名称和描述符
  • 常量池在编译期间就被确定,并保存在已编译的.class文件中

对象访问:

在Java语言中,对象访问是如何进行的?

对象访问在Java语言中无处不在,是最普通的程序行为,但即使是最简单的访问,也会却涉及Java栈、Java堆、方法区这三个最重要内存区域。

如:

Object obj = new Object();

假设这句代码出现在方法体中

  1. 那“Object obj”这部分的语义将会反映到Java栈的本地变量表中,作为一个reference类型数据出现。
  2. 而“new Object()”这部分的语义将会反映到Java堆中,形成一块存储了Object类型所有实例数据值的结构化内存,这块内存的长度是不固定的。
  3. 在Java堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中。

常量池分类:

全局字符串池:

string pool也有叫做string literal pool

全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放。),只有一份, 被所有类,所有线程共享。

Class文件常量池:

.java文件被编译为.class文件时产生。存在于文件中。class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量和符号引用。

字面量就是我们所说的常量概念,如文本字符串、被声明为final的常量值等。 符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可

一般包括下面三种:

  1. 类和接口的全限定名
  2. 字段的名称和描述符
  3. 方法的名称和描述符

运行时常量池:

runtime constant pool
当java文件被编译成class文件之后,也就是会生成我上面所说的class常量池,那么运行时常量池又是什么时候产生的呢?

jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。

class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。

多线程通信

Lambda捕获异常:

使用自定义 Lambda 实现方法中抛异常的统一拦截处理

代码实现:

package com.multithreading.auto;

//该注解表示该类是一个函数式接口
@FunctionalInterface
public interface Runnable {
  void run() throws Exception;
}

@FunctionalInterface
public interface Function {
  T run() throws Exception;
}
package com.multithreading.auto;

public class AutoThrow {
  public static void exec(Runnable runnable) {
    try {
      runnable.run();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public static  T exec(Function function) {
    try {
      return function.run();
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }
}

具体使用:

//使用自己的函数式接口的方法来处理this.wait()方法要抛出异常的处理
AutoThrow.exec(this::wait);

休眠和唤醒方式:

示例一:

使用两个线程操作同一个变量,并根据不同的情况打印奇数还是偶数

代码实现:

import com.multithreading.auto.AutoThrow;

public class Run {
  //定义一个初始值
  private int i = 0;

  /**
   * 打印偶数的线程
   */
  private void odd() {
    while (i < 10) {
      synchronized (this) {
        if (i % 2 == 1) {
          System.out.println(Thread.currentThread().getName() + " : 类中的变量为奇数 " + i);
          i++;
          //唤醒其他等待的线程
          this.notify();
        } else {
          AutoThrow.exec(this::wait);
        }
      }

    }
  }

  /**
   * 打印偶数的线程
   */
  private void even() {
    while (i < 10) {
      synchronized (this) {
        if (i % 2 == 0) {
          System.out.println(Thread.currentThread().getName() + " : 类中的变量为偶数 " + i);
          i++;
          //唤醒其他等待的线程
          this.notify();
        } else {
          //让该线程等待
          AutoThrow.exec(this::wait);
        }
      }
    }
  }

  public static void main(String[] args) {
    Run run = new Run();
    //创建线程
    Thread thread1 = new Thread(run::odd);
    Thread thread2 = new Thread(run::even);
    //调用线程
    thread1.start();
    thread2.start();
  }
}

打印结果:

Thread-1 : 类中的变量为偶数 0
Thread-0 : 类中的变量为奇数 1
Thread-1 : 类中的变量为偶数 2
Thread-0 : 类中的变量为奇数 3
Thread-1 : 类中的变量为偶数 4
Thread-0 : 类中的变量为奇数 5
Thread-1 : 类中的变量为偶数 6
Thread-0 : 类中的变量为奇数 7
Thread-1 : 类中的变量为偶数 8
Thread-0 : 类中的变量为奇数 9

示例二:

使用 Lock 类来给线程加锁和解锁,使用 Conditionsignal()await()方法唤醒和等待线程

import com.multithreading.auto.AutoThrow;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Run {
  //定义一个初始值
  private int i = 0;
  //锁对象
  private final Lock lock = new ReentrantLock();
  private final Condition condition = lock.newCondition();

  //打印偶数的线程
  private void odd() {
    while (i < 10) {
      //加锁
      lock.lock();
      if (i % 2 == 1) {
        System.out.println(Thread.currentThread().getName() + " : 类中的变量为奇数 " + i);
        i++;
        //唤醒其他等待的线程
        condition.signal();
      } else {
        AutoThrow.exec(condition::await);
      }
      //解锁
      lock.unlock();
    }
  }

  //打印偶数的线程
  private void even() {
    while (i < 10) {
      //加锁
      lock.lock();
      if (i % 2 == 0) {
        System.out.println(Thread.currentThread().getName() + " : 类中的变量为偶数 " + i);
        i++;
        //唤醒其他等待的线程
        condition.signal();
      } else {
        //让该线程等待
        AutoThrow.exec(condition::await);
      }
      //解锁
      lock.unlock();
    }
  }

  public static void main(String[] args) {
    Run run = new Run();
    //创建线程
    Thread thread1 = new Thread(run::odd);
    Thread thread2 = new Thread(run::even);
    //调用线程
    thread1.start();
    thread2.start();
  }
}

打印结果:

Thread-1 : 类中的变量为偶数 0
Thread-0 : 类中的变量为奇数 1
Thread-1 : 类中的变量为偶数 2
Thread-0 : 类中的变量为奇数 3
Thread-1 : 类中的变量为偶数 4
Thread-0 : 类中的变量为奇数 5
Thread-1 : 类中的变量为偶数 6
Thread-0 : 类中的变量为奇数 7
Thread-1 : 类中的变量为偶数 8
Thread-0 : 类中的变量为奇数 9

ObjectCondition的等待和唤醒的区别:

  • Object.wait() 必须是在 synchronized(同步锁) 下使用
  • Object.wait() 唤醒必须通过notify() 方法进行唤醒
  • Condition.await() 必须使用Lock (互斥锁/共享锁) 配合使用
  • Condition.await() 必须使用signal()方法进行唤醒

CountDownLatch方式:

  • CountDownLatch是在java1.5被引入的,存在于java .util.concurrent包下。
  • CountDownLatch 能够使一个线程等待其他线程完成各自的工作后再执行
  • CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。

每当一个线程完成了自己的任务后,计数器就会减1。当计数器值达到0时,它表示所有的线程都以及执行完了任务,然后在闭锁上等待的线程可以恢复执行任务。

需求:教练训练运动员,教练需要等待所有运动员到齐才能开始训练任务。

代码实现:

import com.multithreading.auto.AutoThrow;
import java.util.concurrent.CountDownLatch;

public class Run {
  //定义CountDownLatch 并设置要等待几个线程执行完
  private final CountDownLatch countDownLatch = new CountDownLatch(3);

  //教练训练的方法
  private void coach() {
    //获取线程名称
    String name = Thread.currentThread().getName();
    //等待所有运动员准备完成
    System.out.println(name + " 等待运动员准备完成……");
    //调用CountDownLatch.await() 等待其他线程运行完
    AutoThrow.exec(countDownLatch::await);
    System.out.println(name + " 开始训练任务");
  }

  //运动员方法
  private void athletes() {
    //获取线程名称
    String name = Thread.currentThread().getName();
    System.out.println(name + " 开始准备……");
    //线程休眠一秒
    AutoThrow.exec(() -> Thread.sleep(1000));
    System.out.println(name + " 准备完成");
    //调用countDownLatch.countDown() 方法使等待计数中减1,否者一直都是设置的等待数
    countDownLatch.countDown();
  }

  public static void main(String[] args) {
    Run run = new Run();
    //创建线程,并设置线程名字
    Thread thread1 = new Thread(run::coach, "教练");
    Thread thread2 = new Thread(run::athletes, "运动员1");
    Thread thread3 = new Thread(run::athletes, "运动员2");
    Thread thread4 = new Thread(run::athletes, "运动员3");
    //调用线程
    thread1.start();
    thread2.start();
    thread3.start();
    thread4.start();
  }
}

打印结果:

教练 等待运动员准备完成……
运动员3 开始准备……
运动员1 开始准备……
运动员2 开始准备……
运动员1 准备完成
运动员2 准备完成
运动员3 准备完成
教练 开始训练任务

CyclicBarrier方式:

  • CyclicBarrier是在java1.5被引入的,存在于java.util.concurrent包下。
  • CyclicBarrier实现让一组线程等待至某个状态之后再全部同时执行
  • CyclicBarrier底层是基于ReentrantLockCondition实现。

需求:保证三个线程同时启动

代码实现:

import com.multithreading.auto.AutoThrow;
import java.util.concurrent.CyclicBarrier;

public class Run {
  //创建CyclicBarrier 对象,传递参数表示参与CyclicBarrier中的线程数
  private final CyclicBarrier cyclicBarrier = new CyclicBarrier(3);

  private void startThread() {
    String name = Thread.currentThread().getName();
    System.out.println(name + " 正在准备");
    //调用CyclicBarrier的awiat()方法等待线程全部准备完成
    AutoThrow.exec(cyclicBarrier::await);
    System.out.println(name + " 已经启动完毕 " + System.currentTimeMillis());


  }

  public static void main(String[] args) {
    Run run = new Run();
    //创建线程,并设置线程名字
    Thread thread1 = new Thread(run::startThread, "运动员1");
    Thread thread2 = new Thread(run::startThread, "运动员2");
    Thread thread3 = new Thread(run::startThread, "运动员3");
    //调用线程
    thread1.start();
    thread2.start();
    thread3.start();
  }
}

打印结果:

运动员1 正在准备
运动员3 正在准备
运动员2 正在准备
运动员2 已经启动完毕 1652436202457
运动员3 已经启动完毕 1652436202457
运动员1 已经启动完毕 1652436202457

semaphore方式:

  • semaphore是在java1.5被引入的,存在于java.util.concurrent包下。
  • semaphore用于控制对某组资源的访问权限。

需求:8个工人使用3台机器工作,机器为互斥资源(即每次只能一个人使用)

代码实现:

import com.multithreading.auto.AutoThrow;
import java.util.concurrent.Semaphore;

public class Run {
  //内部类实现Runnable接口
  static class work implements Runnable {
    //使用java.util.concurrent包下的类来表示机器数
    private final Semaphore semaphore;

    public work(Semaphore semaphore) {
      this.semaphore = semaphore;
    }

    @Override
    public void run() {
      AutoThrow.exec(() -> {
        //工人获取机器
        semaphore.acquire();
        String name = Thread.currentThread().getName();
        System.out.println(name + " 获取到机器,开始工作");
        //睡眠一秒钟,模拟工人使用机器的场景
        Thread.sleep(1000);
        //使用完毕是否资源
        semaphore.release();
        System.out.println(name + " 使用完毕,释放资源");
      });
    }
  }

  public static void main(String[] args) {
    //工人数
    int works = 8;
    //代表机器数
    Semaphore semaphore = new Semaphore(3);
    for (int i = 0; i < works; i++) {
      new Thread(new work(semaphore), "工人" + i).start();
    }
  }
}

打印结果:

工人2 获取到机器,开始工作
工人6 获取到机器,开始工作
工人4 获取到机器,开始工作
工人4 使用完毕,释放资源
工人3 获取到机器,开始工作
工人5 获取到机器,开始工作
工人2 使用完毕,释放资源
工人6 使用完毕,释放资源
工人7 获取到机器,开始工作
工人5 使用完毕,释放资源
工人0 获取到机器,开始工作
工人1 获取到机器,开始工作
工人7 使用完毕,释放资源
工人3 使用完毕,释放资源
工人0 使用完毕,释放资源
工人1 使用完毕,释放资源

总结:

sleepwait的区别:

wait sleep
同步 只能在同步上下文中调用wait方法,否则抛出
lliegalMonitorStateException异常
不需要在同步方法或同步代码块中调用
作用对象 wait方法定义在Object类中,作用于对象本身 sleep方法定义在java.lang.Thread中,作用于当前线程
释放资源
唤醒条件 其他线程调用对象的notify()或者notifyAll()方法 超时或者调用interrupt()方法体
方法属性 wait是实例方法 sleep是静态方法

waitnotify区别:

  • waitnotify都是Object中的方法
  • waitnotify执行前线程都必须获取锁
  • wait的作用是使当前线程进行等待
  • notify的作用是通知其他等待当前线程的对象锁的过程

多线程特性:

多线程编程要满足三个特性:原子性,可见性,一致性。

  • 原子性:原子性,即一个操作或多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
  • 可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。显然,对于单线程来说,可见性问题是不存在的。
  • 有序性:有序性即程序执行的顺序按照代码的先后顺序执行。

多线程控制类:

为了保证多线程的三个特性,java引入了很多线程控制机制,下面介绍其中常用的几种:

  • ThreadLocal:线程本地变量
  • 原子类:保证变量原子操作
  • Lock类:保证线程有序性
  • Volatile关键字:保证线程变量可见性

ThreadLocal

作用:

ThreadLocal提供线程局部变量,即为使用相同变量的每一个线程维护一个该变量的副本。当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal,比如数据库连接Connection,每个请求处理线程都需要,但又不相互影响,就是用ThreadLocal实现。

常用方法:

  • initialValue:副本创建方法
  • get:获取副本方法
  • set:设置副本方法

示例:

模拟两个线程转账:

public class Run {

  //创建银行对象:钱、取款、存款
  static class Bank {
    //匿名内部类
    private final ThreadLocal threadLocal = new ThreadLocal() {
      @Override
      protected Integer initialValue() {
        return 0;
      }
    };

    public Integer get() {
      return threadLocal.get();
    }

    public void set(Integer value) {
      threadLocal.set(threadLocal.get() + value);
    }
  }

  //创建转账对象:从银行中取钱,转账,保存到账户
  static class Transfer implements Runnable {
    private final Bank bank;

    public Transfer(Bank bank) {
      this.bank = bank;
    }

    @Override
    public void run() {
      for (int a = 0; a < 10; a++) {
        bank.set(10);
        Integer integer = bank.get();
        String name = Thread.currentThread().getName();
        System.out.println(name + " :账户余额 " + integer);
      }
    }
  }


  public static void main(String[] args) {
    Bank bank = new Bank();
    Transfer transfer = new Transfer(bank);
    Thread thread1 = new Thread(transfer, "客户1");
    Thread thread2 = new Thread(transfer, "客户2");
    thread1.start();
    thread2.start();
  }
}

打印结果:

客户1 :账户余额 10
客户2 :账户余额 10
客户1 :账户余额 20
客户2 :账户余额 20
客户1 :账户余额 30
客户2 :账户余额 30
客户1 :账户余额 40
客户2 :账户余额 40
客户1 :账户余额 50
客户1 :账户余额 60
客户2 :账户余额 50
客户1 :账户余额 70
客户2 :账户余额 60
客户1 :账户余额 80
客户2 :账户余额 70
客户1 :账户余额 90
客户2 :账户余额 80
客户1 :账户余额 100
客户2 :账户余额 90
客户2 :账户余额 100
  • ThreadLocal类中定义了一个 ThreadLocalMap
  • 每一个Thread都有一个ThreadLocalMap类型的变量 ThreadLocals
  • threadLocals内部有一个Entry,Entry的key是ThreadLocal,对象实例,value就是共享变量副本
  • ThreadLlocal的get方法就是根据ThreadLocal.对象实例获取共享变量副本
  • ThreadLlocal的set方法就是根据ThreadLocal对象实例保存共享变量副本

原子类:

Java的 java.util, concurrent.atomic包里面提供了很多可以进行原子操作的类,分为以下四类:

  • 原子更新基本类型:AtomicIntegerAtomicBooleanstomisLong
  • 原子更新数组类型:AtomicIntegerArrayAtomicLongArray
  • 原子更新引用类型:AtomicReferenceAtomicStampedReference
  • 原子更新属性类型: AtomicIntegerFieldUpdaterstomicLongFieldUpdater

提供这些原子类的目的就是为了解决基本类型操作的非原子性导致在多线程并发情况下引发的问题。

非原子操作问题演示:

非原子操作会引发什么问题?下面以 i++ 为例演示非原子操作问题。

i++ 并不是原子操作,而是由三个操作构成:

tp1=i;
tp2=tp1+1;
i=tp1;

所以单线程 i 的值不会有问题的,但多线程下就会出错,多线程代码示例:

import com.multithreading.auto.AutoThrow;

public class Run {
  //执行n++操作变量
  private static int n;


  public static void main(String[] args) {
    int j = 0;
    while (j < 100) {
      n = 0;
      Thread thread1 = new Thread(() -> {
        for (int i = 0; i < 1000; i++) {
          n++;
        }
      });

      Thread thread2 = new Thread(() -> {
        for (int i = 0; i < 1000; i++) {
          n++;
        }
      });
      thread1.start();
      thread2.start();
      AutoThrow.exec(() -> {
        thread1.join();
        thread2.join();
      });
      System.out.println("n的最终值为:" + n);
      j++;
    }
  }
}

打印结果:发现最后n的值可能达不到2000

n的最终值为:2000
n的最终值为:2000
n的最终值为:1559
n的最终值为:1645
n的最终值为:2000
n的最终值为:2000
n的最终值为:2000
n的最终值为:2000
n的最终值为:1781
n的最终值为:2000
n的最终值为:1480
n的最终值为:2000
n的最终值为:1945
n的最终值为:2000
……

原子类解决非原子类的操作问题:

AtomicInteger类可以保证++操作原子性:

//getAndIncrement() 对应:n++
//incrementAndGet() 对应:++n
//decrementAndGet() 对应:--n
//getAndDecrement() 对应:n--

以上代码修改如下:

import com.multithreading.auto.AutoThrow;
import java.util.concurrent.atomic.AtomicInteger;

public class Run {
  //执行n++操作变量
  // private static int n;
  //定义一个原子类的integer
  private static AtomicInteger atomicInteger;

  public static void main(String[] args) {
    int j = 0;
    while (j < 100) {
      //给原子类的integer设置初始值0
      atomicInteger = new AtomicInteger(0);
      Thread thread1 = new Thread(() -> {
        for (int i = 0; i < 1000; i++) {
          //用原子方法代替++方法
          atomicInteger.getAndIncrement();
        }
      });

      Thread thread2 = new Thread(() -> {
        for (int i = 0; i < 1000; i++) {
          //用原子方法代替++方法
          atomicInteger.getAndIncrement();
        }
      });
      thread1.start();
      thread2.start();
      AutoThrow.exec(() -> {
        thread1.join();
        thread2.join();
      });
      //调用atomicInteger.get() 获取值
      System.out.println("n的最终值为:" + atomicInteger.get());
      j++;
    }
  }
}

打印结果:

n的最终值为:2000
n的最终值为:2000
n的最终值为:2000
n的最终值为:2000
n的最终值为:2000
n的最终值为:2000
n的最终值为:2000
……全部都是2000

ABA问题解决:

tomicStampedRefexence解决ABA问题的方法

  • AtomicStampedReference(初始值,时间勤):构造函数设置初始值和初始时间戳
  • getStamp:获取时间戳
  • getReference:获取预期值
  • compareAndSet(预期值,更新值,预期时间戳,更新时间戳):实现CAS时间戳和预期值的比对
import com.multithreading.auto.AutoThrow;
import java.util.concurrent.atomic.AtomicStampedReference;

public class Run {
  //执行n++操作变量
  // private static int n;
  //定义一个原子类的integer
  private static AtomicStampedReference atomicInteger;

  public static void main(String[] args) {
    int j = 0;
    while (j < 100) {
      //给原子类的integer设置初始值0 时间戳
      atomicInteger = new AtomicStampedReference(0, 0);
      Thread thread1 = new Thread(() -> {
        for (int i = 0; i < 1000; i++) {
          //用原子方法代替++方法
          int stamp;
          Integer reference;
          do {
            //获取预期的时间戳
            stamp = atomicInteger.getStamp();
            //获取预期的值
            reference = atomicInteger.getReference();
          } while (!atomicInteger.compareAndSet(reference, reference + 1, stamp, stamp + 1));
          //CAS比较并替换 第一个参数是预期值,第二个参数是更新值,第三个参数预取时间戳,更新时间戳
        }
      });

      Thread thread2 = new Thread(() -> {
        for (int i = 0; i < 1000; i++) {
          int stamp;
          Integer reference;
          do {
            //获取预期的时间戳:版本号
            stamp = atomicInteger.getStamp();
            //获取预期的值
            reference = atomicInteger.getReference();
          } while (!atomicInteger.compareAndSet(reference, reference + 1, stamp, stamp + 1));
          //CAS比较并替换 第一个参数是预期值,第二个参数是更新值,第三个参数预取时间戳,更新时间戳
        }
      });
      thread1.start();
      thread2.start();
      AutoThrow.exec(() -> {
        thread1.join();
        thread2.join();
      });
      //调用atomicInteger.getReference() 获取值
      System.out.println("n的最终值为:" + atomicInteger.getReference());
      j++;
    }
  }
}

Lock类:

LockReadWriteLock是两大锁的根接口

Lock 接口支持重入、公平等的锁规则:实现类ReentrantLackReadLockWriteLockReadWriteLock 接口定义读取者共享而写入者独占的锁,实现类:ReentrantReadWriteLock

可重入锁:

不可重入锁,即线程请求它已经拥有的锁时会阻塞。

可重入锁,即线程可以进入它已经拥有的锁的同步代码块。

import java.util.concurrent.locks.ReentrantLock;

public class Run {
  public static void main(String[] args) {
    ReentrantLock reentrantLock = new ReentrantLock();
    for (int i = 0; i < 10; i++) {
      //加锁
      reentrantLock.lock();
      System.out.println("加锁次数:" + (i + 1));
    }
    for (int i = 0; i < 10; i++) {
      //解锁
      reentrantLock.unlock();
      System.out.println("解锁次数:" + (i + 1));
    }
  }
}

读写锁:

读写锁,即可以同时读,读的时候不能写;不能同时写,写的时候不能读。

import com.multithreading.auto.AutoThrow;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Run {
  //读写对象
  private final Map map = new HashMap<>();
  //定义锁
  private final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
  //读锁
  private final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
  //写锁
  private final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

  //读操作加锁
  public String get(String key) {
    String name = Thread.currentThread().getName();
    //加锁
    readLock.lock();
    Object exec = AutoThrow.exec(() -> {
      System.out.println(name + " 读操作已加锁,开始读操作……");
      Thread.sleep(3000);
      return map.get(key);
    });
    //解锁
    readLock.unlock();
    System.out.println(name + " 读操作已解锁");
    return String.valueOf(exec);
  }

  //写操作加锁
  public void put(String key, String value) {
    String name = Thread.currentThread().getName();
    //加锁
    writeLock.lock();
    AutoThrow.exec(() -> {
      System.out.println(name + " 写操作已加锁,开始写操作……");
      Thread.sleep(3000);
      map.put(key, value);
    });
    //解锁
    writeLock.unlock();
    System.out.println(name + " 写操作已解锁");
  }

  public static void main(String[] args) {
    Run run = new Run();
    //执行写操作
    run.put("te", "value");
    //开启线程多个读取操作
    new Thread(() -> System.out.println(run.get("te")), "线程1").start();
    new Thread(() -> System.out.println(run.get("te")), "线程2").start();
    new Thread(() -> System.out.println(run.get("te")), "线程3").start();
  }
}

打印结果:

main 写操作已加锁,开始写操作……
main 写操作已解锁
线程2 读操作已加锁,开始读操作……
线程3 读操作已加锁,开始读操作……
线程1 读操作已加锁,开始读操作……
线程1 读操作已解锁
value
线程3 读操作已解锁
value
线程2 读操作已解锁
value

Volatitle关键字:

一个共享变量(类的成员变量、类的静态成员变量〉被volatile修饰之后,那么就具备了两层语义:

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(注意:不保证原子性)
  • 禁止进行指令重排序。(保证变量所在行的有序性)

当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

应用场景:

基于Volatitle的作用,使用Volatitle必须满足两个条件

  • 对变量的写操作不依赖当前值
  • 该变量没有包含在具体的其他变量的不变式中,即单独使用
状态量的标记:
volatile boolean flag = false;
双重校验:
static class Singleton {
  private volatile static Singleton singleton = null;

  private Singleton() {
  }

  public static Singleton getInstance() {
    if (singleton == null) {
      synchronized (Singleton.class) {
        //防止多线程下实例多次创建的情况 两次if
        if (singleton == null) {
          singleton = new Singleton();
        }
      }
    }
    return singleton;
  }
}

线程池:

多线程的缺点:

  • 处理任务的线程创建和销毁都非常耗时并消耗资源。
  • 多线程之间的切换也会非常耗时并消耗资源。

解决方法:采用线程池

  • 使用时线程已存在,消除了线程创建的时耗
  • 通过设置线程数目,防止资源不足
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {}

参数介绍:

  • corePoolSize:线程池中核心线程数的最大值
  • maximumPoolSize:线程池中能拥有最多连接数
  • workQueue:用于缓存任务的阻塞队列,对于不同的应用场景我们可能采取不同的排队策略,这就需要不同类型的阻塞队列,在线程池中常用的阻塞队列有以下2种:
    • SynchronousQueue:此队列中不缓存任何一个任务。向线程池提交任务时,如果没有空闲线程来运行任务,则入列操作会阻塞。当有线程来获取任务时,出列操作会唤醒执行入列操作的线程。从这个特性来看,SynchronousQueue是一个无界入列,因此当使用SynchronousQueue作为线程池的阻塞队列时,参数maximumPoolSize没有任何作用。
    • LinkedBlockingQueue:顾名思义是用链表实现的队列,可以是有界的,也可以是无界的,但在Executors中默认使用无界的。

以上三个参数之间的关系如下:

  • 如果没有空闲的线程执行该任务且当前运行的线程数少于corePoolSize,则添加新的线程执行该任务。
  • 如果没有空闲的线程执行该任务且当前的线程数等于corePoolSize同时阻塞队列未满,则将任务入队列,而不添加新的线程。
  • 如果没有空闲的线程执行该任务且阻塞队列已满同时池中的线程数小于maximumPoolSize,则创建新的线程执行任务。
  • 如果没有空闲的线程执行该任务且阻塞队列已满同时池中的线程数等于maximumPoolSize,则根据构造函数中的 handler指定的策略来拒绝新的任务。
  • keepAliveTime:表示空闲线程的存活时间

  • unit:表示keepAliveTime的单位

  • handler:表示当workQueue已满,且池中的线程数达到maximumPoolSize时,线程池拒绝添加新任务时采取的策略。一般可以采取以下四种取值

    ThreadPoolExecutor.AbortPolicy 抛出RejectedExecutionHandler异常
    ThreadPoolExecutor.CallerRunsPolicy 由线程池提交任务的线程来执行
    ThreadPoolExecutor.DiscardOldestPolicy 抛弃最旧的任务(最先提交而没有得到执行的任务)
    ThreadPoolExecutor.DiscardPolicy 抛弃当前任务
  • threadFactory:指定创建线程的工厂

四种常用的线程池:

ThreadPoolExecutor构造函数的参数很多,使用起来很麻烦,为了方便的创建线程池,JavaSE中又定义了Executors类,Eexautors类提供了四个创建线程池的方法,分别如下:

  • newCachedThreadPool
  • newFixedThreadPool
  • newSingleThreadExecutor
  • newScheduledThreadPool
newCachedThreadPool

该方法可以创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

此类线程池特点是:

  • 工作线程的创建数基本没有限制(其实也有限制的数目为 Interger.MAX_VALUE
  • 空闲的工作线程会自动销毁,有新任务会重新创建
  • 在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
public static void main(String[] args) {
    ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
    for (int i = 0; i < 10; i++) {
      final int index = 1;
      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      newCachedThreadPool.execute(() -> System.out.println(index));
    }
  }
}
newFixedThreadPool

该方法创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。

优点:具有线程池提高程序效率和节省创建线程时所耗的开销。

缺点:在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。

public static void main(String[] args) {
  ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);
  for (int i = 0; i < 10; i++) {
    final int index = 1;
    try {
      Thread.sleep(100);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    newFixedThreadPool.execute(() -> System.out.println(index));
  }
}
newSingleThreadExecutor

该方法创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。

单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

public static void main(String[] args) {
  ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
  for (int i = 0; i < 10; i++) {
    final int index = 1;
    try {
      Thread.sleep(100);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    newSingleThreadExecutor.execute(() -> System.out.println(index));
  }
}
newScheduledThreadPool

该方法创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。

  public static void main(String[] args) {
    ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
    for (int i = 0; i < 10; i++) {
      final int index = 1;
      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      //第2个参数:周期执行时间
      //第3个参数:时间单位
      scheduledExecutorService.schedule(() -> System.out.println(index), 3, TimeUnit.MILLISECONDS);
    }
  }
}

你可能感兴趣的:(内存与多线程)