简单了解多线程

是指从软件或者硬件上实现多个线程并发执行的技术
具有多线程的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能

并发和并行

并行:在同一时刻,有多个指令在多个CPU上同时执行
并发:在同一时刻,有多个指令在单个CPU上交替执行

进程和线程

进程:就是操作系统中正在运行的一个应用程序
线程:就是应用程序中做的事情,比如:360系统中的杀毒,扫描木马,清理垃圾

多线程的实现形式

1、实现Thread接口

  1. 创建一个类MyThread实现Thread接口
  2. 运行代码需要重写run方法
  3. 在主方法处新建MyThread对象
  4. start()方法来开启线程

问:为什么要重写run()方法?
答:因为run()方法是用来封装被线程执行的代码
问:run()方法和start()方法的区别?
答:run():封装线程执行的代码,直接调用,相当于普通方法的调用,并没有开启线程
start():启动线程;然后由JVM调用此线程的run()方法

2、实现Runnable接口

  1. 定义一个类MyRunnable实现Runnable接口
  2. 在MyRunnable类中重写run()方法
  3. 创建MyRunnable类的对象
  4. 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
  5. 启动线程

    3、Callable和Future

  6. 定义一个类MyCallable实现Callable接口

  7. 在MyCallable类中重写call()方法
  8. 创建MyCallable类的对象
  9. 创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数
  10. 创建Thread类的对象,把FutureTask对象作为构造方法的参数
  11. 启动线程
  12. 可以调用FutureTask的get方法,就可以获取线程结束之后的结果

    4、三种方式的对比

    | | 优点 | 缺点 | | —- | —- | —- | | 实现Runnable、Callable接口 | 扩展性强,实现该接口的同时还可以继承其他的类 | 编程相对复杂,不能直接使用Thread类中的方法 | | 继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 可以扩展性较差,不能再继承其他的类 |

5、获取和设置线程名称

获取线程名字
String getName():返回此线程的名称
Thread类中设置线程的名字

  • void setName(String name):将此线程的名称更改为等于参数name
  • 通过构造方法也可以设置线程名称

线程具有默认名称Thread-编号

6、获得当前线程的对象

public static Thread currentThread():返回对当前正在执行的线程对象的引用

7、线程休眠

public static void sleep(long time):让线程休眠指定的时间,单位为毫秒

8、线程调度

多线程的并发运行:计算机中的CPU,在任意时刻只能执行一条机器指令。每个线程只有获得CPU的使用权
才能执行代码。各个线程轮流获得CPU的使用权,分别执行各自的任务
线程有两种调度模型

  • 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
  • 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的获取的CPU的时间片相对多一些

线程的优先级
public final void setPriority(int newPriority):设置线程的优先级
public final int getPriority():获取线程的优先级
java线程优先级默认是5,最大优先级为10,最小优先级为1,不能小于1或者超过10

9、后台线程/守护线程

public final void setDaemon(boolean on):设置守护线程
守护线程:当普通线程执行完毕之后,守护线程也会直接结束,不会继续执行

线程安全

当多线程操作共享数据时会出现线程安全问题
如何解决?
基本思想:让程序没有安全问题的环境
怎么实现呢?

  • 把多条语句操作共享数据的代码给锁起来,让任意时刻只能由一个线程执行即可
  • java提供了同步代码块的方式来解决

    同步代码块

    锁多条语句操作共享数据,可以使用同步代码块来实现
    格式: ```java synchronized(锁对象){ 多条语句操作共享数据的代码 }
  1. - 默认情况是打开的,只要有一个线程进去执行代码了,锁就会关闭
  2. - 当线程执行完出来了,锁才会自动打开
  3. 同步的好处和弊端
  4. - 好处:解决了多线程的数据安全问题
  5. - 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
  6. 锁对象必须唯一
  7. <a name="a2o3T"></a>
  8. ### 同步方法
  9. 同步方法:就是把synchronized关键字加到方法上<br />格式:`修饰符 synchronized 返回值类型 方法名(方法参数){}`<br />同步代码块和同步方法的区别:
  10. - 同步代码块可以锁住指定代码,同步方法是锁住方法中所有代码
  11. - 同步代码块可以指定锁对象,同步方法不能指定锁对象
  12. 同步方法的锁对象是:`this`<br />同步静态方法:就是把synchronized关键字加到静态方法上<br />格式:`修饰符 static synchronized 返回值类型 方法名(方法参数){}`<br />同步静态方法的锁对象是:`类名.class`
  13. <a name="gEfxl"></a>
  14. ### Lock锁
  15. 虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock<br />Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作<br />Lock中提供了获得锁和释放锁的方法
  16. - `void lock()`:获得锁
  17. - `void unlock()`: 释放锁
  18. Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化<br />ReentrantLock的构造方法
  19. - `ReentrantLock()`:创建一个ReentrantLock的实例
  20. 应该将unlock方法放入finally中,保证锁的释放
  21. <a name="R7nE9"></a>
  22. ### 死锁
  23. 线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。<br />不要写锁的嵌套就不会出现死锁
  24. <a name="gljMt"></a>
  25. ## 生产者和消费者
  26. <a name="bS6Do"></a>
  27. ### 原理分析:
  28. 消费者步骤:
  29. 1. 判断桌子上是否有汉堡包
  30. 1. 如果没有就等待
  31. 1. 如果有就开吃
  32. 1. 吃完之后,桌子上的汉堡包就没有了,叫醒等待的生产者继续生产,汉堡包的总数量减一
  33. 生产者步骤
  34. 1. 判断桌子上是否有汉堡包,如果有就等待,如果没有才生产
  35. 1. 把汉堡包放在桌子上
  36. 1. 叫醒等待的消费者开吃
  37. <a name="EuHHF"></a>
  38. ### 等待和唤醒的方法
  39. 为了体现生产者和消费过程中的等待和唤醒,Java就提供了几个方法供我们使用,这几个方法在Object类中<br />Object类的等待和唤醒方法:
  40. | **方法名** | **说明** |
  41. | --- | --- |
  42. | `void wait()` | 导致当前线程等待,直到另一个线程调用该对象的notify()方法或者notifyAll()方法 |
  43. | `void notify()` | 唤醒正在等待对象监视器的单个线程 |
  44. | `void notifyAll()` | 唤醒正在等待对象监视器的所有线程 |
  45. <a name="Ysu9A"></a>
  46. ### 代码实现:
  47. ```java
  48. public class Foodie extends Thread{
  49. @Override
  50. public void run() {
  51. while(true){
  52. synchronized (Desk.lock){
  53. //食物全部吃完就结束循环
  54. if (Desk.foodCount == 0){
  55. break;
  56. }else {
  57. if (Desk.hasFood){
  58. System.out.println("正在吃饭");
  59. Desk.foodCount--;
  60. Desk.hasFood = false;
  61. Desk.lock.notifyAll();
  62. System.out.println("还剩" + Desk.foodCount + "份食物");
  63. }else {
  64. try {
  65. Desk.lock.wait();
  66. } catch (InterruptedException e) {
  67. throw new RuntimeException(e);
  68. }
  69. }
  70. }
  71. }
  72. }
  73. }
  74. }
  1. public class Cooker extends Thread{
  2. @Override
  3. public void run() {
  4. while (true){
  5. synchronized (Desk.lock){
  6. if (Desk.foodCount == 0){
  7. break;
  8. }else {
  9. if (!Desk.hasFood){
  10. System.out.println("厨师正在做饭");
  11. Desk.hasFood = true;
  12. Desk.lock.notifyAll();
  13. }else {
  14. try {
  15. Desk.lock.wait();
  16. } catch (InterruptedException e) {
  17. throw new RuntimeException(e);
  18. }
  19. }
  20. }
  21. }
  22. }
  23. }
  24. }
  1. public class Desk {
  2. public static boolean hasFood = false;
  3. public static int foodCount = 10;
  4. //锁对象
  5. public static final Object lock= new Object();
  6. }

注意点:锁对象要唯一,所以只能在桌子上创建
并且唤醒和等待都得是锁对象来调用,使某一个线程在锁对象上等待或者唤醒
修改后代码:

  1. public class Desk {
  2. //public static boolean hasFood = false;
  3. private boolean hasFood;
  4. //public static int foodCount = 10;
  5. private int foodCount;
  6. //锁对象
  7. //public static final Object lock= new Object();
  8. private final Object lock= new Object();
  9. public Desk(boolean hasFood, int foodCount) {
  10. this.hasFood = hasFood;
  11. this.foodCount = foodCount;
  12. }
  13. public Desk() {
  14. this(false,10);
  15. }
  16. public boolean isHasFood() {
  17. return hasFood;
  18. }
  19. public void setHasFood(boolean hasFood) {
  20. this.hasFood = hasFood;
  21. }
  22. public int getFoodCount() {
  23. return foodCount;
  24. }
  25. public void setFoodCount(int foodCount) {
  26. this.foodCount = foodCount;
  27. }
  28. public Object getLock() {
  29. return lock;
  30. }
  31. @Override
  32. public String toString() {
  33. return "Desk{" +
  34. "hasFood=" + hasFood +
  35. ", foodCount=" + foodCount +
  36. ", lock=" + lock +
  37. '}';
  38. }
  39. }
  1. public class Cooker extends Thread{
  2. private Desk desk;
  3. public Cooker(Desk desk) {
  4. this.desk = desk;
  5. }
  6. @Override
  7. public void run() {
  8. while (true){
  9. synchronized (desk.getLock()){
  10. if (desk.getFoodCount() == 0){
  11. break;
  12. }else {
  13. if (!desk.isHasFood()){
  14. System.out.println("厨师正在做饭");
  15. desk.setHasFood(true);
  16. desk.getLock().notifyAll();
  17. }else {
  18. try {
  19. desk.getLock().wait();
  20. } catch (InterruptedException e) {
  21. throw new RuntimeException(e);
  22. }
  23. }
  24. }
  25. }
  26. }
  27. }
  28. }
  1. public class Foodie extends Thread{
  2. private Desk desk;
  3. public Foodie(Desk desk) {
  4. this.desk = desk;
  5. }
  6. @Override
  7. public void run() {
  8. while(true){
  9. synchronized (desk.getLock()){
  10. //食物全部吃完就结束循环
  11. if (desk.getFoodCount() == 0){
  12. break;
  13. }else {
  14. if (desk.isHasFood()){
  15. System.out.println("正在吃饭");
  16. desk.setFoodCount(desk.getFoodCount() - 1);
  17. desk.setHasFood(false);
  18. desk.getLock().notifyAll();
  19. System.out.println("还剩" + desk.getFoodCount() + "份食物");
  20. }else {
  21. try {
  22. desk.getLock().wait();
  23. } catch (InterruptedException e) {
  24. throw new RuntimeException(e);
  25. }
  26. }
  27. }
  28. }
  29. }
  30. }
  31. }

阻塞队列实现等待唤醒机制

阻塞队列继承结构

阻塞队列继承结构.png
常见BlockingQueue:

  • ArrayBlockingQueue:底层是数组,有界
  • LinkedBlockingQueue:底层是链表,无界,但不是真正的无界,最大为int的最大值

代码实现

import java.util.concurrent.ArrayBlockingQueue;

public class Demo {
    public static void main(String[] args) {
        //创建阻塞队列,容量为1
        ArrayBlockingQueue<String> list = new ArrayBlockingQueue<>(1);

        Foodie f = new Foodie(list);
        Cooker c = new Cooker(list);

        f.start();
        c.start();
    }
}
import java.util.concurrent.ArrayBlockingQueue;

public class Foodie extends Thread{
    private ArrayBlockingQueue<String> list;
    public Foodie(ArrayBlockingQueue<String> list) {
        this.list = list;
    }

    @Override
    public void run() {
        while (true){
            try {
                String take = list.take();
                System.out.println("吃了一个" + take);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }
}
import java.util.concurrent.ArrayBlockingQueue;

public class Cooker extends Thread{
    private ArrayBlockingQueue<String> list;
    public Cooker(ArrayBlockingQueue<String> list) {
        this.list = list;
    }

    @Override
    public void run() {
        while (true){
            try {
                list.put("汉堡包");
                System.out.println("厨师放了一个汉堡包");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }
}

多线程高级

线程状态

线程状态.png

虚拟机中线程的六种状态:

  1. 新建状态(NEW):创建线程对象
  2. 就绪状态(RUNNABLE):start方法
  3. 阻塞状态(BLOCKED):无法获得锁对象
  4. 等待状态(WAITING):wait方法
  5. 计时等待(TIMED_WAITING):sleep方法
  6. 结束状态(TERMINATED):全部代码执行完毕

    线程池

    之前多线程弊端:每次都需要新建线程对象,用完就销毁
    改进:

  7. 创建一个池子,池子中是空的

  8. 有任务需要执行时,才会创建线程对象,当任务执行完毕,线程对象归还给池子

代码实现:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo {
    public static void main(String[] args) {
        //创建线程池,返回一个控制线程池的对象
        ExecutorService executorService = Executors.newCachedThreadPool();

        //提交任务到线程池
        executorService.submit(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + "执行" + i + "次");
            }
        });

        executorService.submit(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + "执行" + i + "次");
            }
        });
        //关闭线程池
        executorService.shutdown();
    }
}

1.创建线程池管理对象

  • static ExecutorService newCachedThreadPool():创建一个默认的线程池,最大值为int的最大值
  • static ExecutorService newFixedThreadPool(int nThreads):创建一个指定最多线程数量的线程池,最大值为设置的值,初始池子里还是0个线程,但是最多只能有设置的值的线程

2.任务执行:
ExecutorServicesubmit(Runnable task):提交任务,池子会自动的帮我们创建对象,任务执行完毕,也会自动把线程对象归还给池子
3.关闭线程池:
ExecutorServicevoid shutdown()

创建线程池对象

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(核心线程数,最大线程数,空闲线程最大存活时间(值),空闲线程最大存活时间(单位),任务队列,创建线程工厂,任务的拒绝策略);
  • 参数一:核心线程数量
    • 不能小于0
  • 参数二:最大线程数
    • 不能小于0,最大数量>=核心线程数量
  • 参数三:空闲线程最大存活时间
    • 不能小于0
  • 参数四:时间单位
    • 时间单位TimeUnit对象
  • 参数五:任务队列
    • 让任务在队列中等着,等有线程空闲了,再从这个队列中获取任务并执行
  • 参数六:创建线程工厂
    • 按照默认的方式创建线程对象
  • 参数七:任务的拒绝策略
    • 1.什么时候拒绝任务: 当提交的任务 > 池子中最大线程数量 + 队列容量的时候拒绝任务
      ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0, 10, 2, TimeUnit.SECONDS, new ArrayBlockingQueue<>(20), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
      
      任务的拒绝策略
      ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常,是默认的策略
      ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常,这是不推荐的做法
      ThreadPoolExecutor.DiscardOldestPolicy:抛弃队列中等待醉酒==最久的任务然后把当前任务加入队列中
      ThreadPoolExecutor.CallerRunsPolicy:调用任务的run()方法绕过线程池直接执行,就是让主函数执行

      Volatile关键字

  1. 堆内存是唯一的,每一个线程都有自己的线程栈
  2. 每一个线程在使用堆内存里面变量的时候,都会先拷贝一份到变量的副本中
  3. 在线程中,每一次使用都是从变量的副本中获取的

如果A线程修改了堆中共享变量的值,那么其他线程不一定能及时使用最新的值。
volatile关键字:强制线程在每次使用的时候,都会看一下共享区域最新的值
也可以使用Synchronized同步代码块

  1. 线程获得锁
  2. 清空变量副本
  3. 拷贝共享变量最新的值到变量副本中
  4. 执行代码
  5. 将修改后变量副本中的值赋值给共享数据
  6. 释放锁

    原子性

    所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行,并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体
    count++操作过程

  7. 从共享数据中读取数据到本线程栈中

  8. 修改本线程栈中变量副本的值
  9. 会把本线程栈中变量副本的值赋值给共享数据

count++不是一个原子性操作,也就是说他在执行的过程中,有可能被其他的线程打断操作
volatile关键字:只能保证线程每次在使用共享数据的时候是最新值,但是不能保证原子性
同步代码块可以保证原子性

原子类AtomicInteger

public AtomicInteger(): 初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integer

int get(): 获得值
int getAndIncrement(): 以原子方式将当前值加1,注意,这里返回的是自增前的值
int incrementAndGet(): 以原子方式将当前值加1,注意,这里返回的是自增后的值
int addAndGet(int data): 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果
int getAndSet(int value): 以原子方式设置为newValue的值,并返回旧值

AtomicInteger原理

自旋锁 + CAS算法
CAS算法:

  • 有三个操作数(内存值V, 旧的预期值A ,要修改的值B)
  • 当旧的预期值A == 内存值 此时修改成功,将V改为B
  • 当旧的预期值A != 内存值 此时修改失败,不做任何操作
  • 并重新获取现在的最新值(这个重新获取的动作就是自旋)

理解:
在修改共享数据的时候,把原来的旧值记录下来了
如果现在内存中的值跟原来的旧值一样,证明没有其他线程操作过内存值,则修改成功
如果现在内存中的值跟原来的旧值不一样了,证明已经有其他线程操作过内存值了。
则修改失败,需要获取现在最新的值,再次进行操作,这个重新获取就是自旋

synchronized和CAS的区别

相同点:在多线程情况下,都可以保证共享数据的安全性
不同点:
synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都有可能修改,所以在每次操作共享数据之前,都会上锁(悲观锁)
cas是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁,只不过在修改共享数据的时候,会检查一下,别人有没有修改过这个数据
如果别人修改说,那么我再次获取现在最新的值
如果别人没有修改过,那么我现在直接修改共享数据的值
(乐观锁)

并发工具类

Hashtable

HashMap是线程不安全的(多线程环境下可能会存在问题)
为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下

  1. Hashtable采取悲观锁synchronized的形式保证数据的安全性
  2. 只要有线程访问,会将整张表全部锁起来,所以Hashtable的效率低下

ConcurrentHashMap也是线程安全的,效率较高

ConcurrentHashMap1.7版本原理解析

创建过程:

  1. 创建一个默认长度为16,默认加载因子为0.75的数组,数组名Segment
    这个大数组一旦创建无法扩容
  2. 再创建一个长度为2的小数组,把地址值赋值给0索引,其他索引都是null