高并发线程/锁/内存处理模型

文章目录

锁与线程

文章目录
本文来自《Java高并发核心编程(2)》的学习笔记
JAVA入门中_说好不能打脸_CSDN博客-系统架构,javaer,系统间通信技术领域博主

一、进程/线程的基本介绍

进程

什么是进程?进程就是程序的一次启动执行

线程

什么是线程?线程是进程的代码片段,一个进程可以有一个或多个线程,各个线程之间共享进程的内存空间、系统资源

1 线程的调度与时间片

线程的调度模型目前主要分为两种:分时调度模型和抢占式调度模型。

(1)分时调度模型:系统平均分配CPU的时间片,所有线程轮流占用CPU,即在时间片调度的分配上所有线程“人人平等”

(2)抢占式调度模型:系统按照线程优先级分配CPU时间片。优先级高的线程优先分配CPU时间片,如果所有就绪线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些

由于目前大部分操作系统都是使用抢占式调度模型进行线程调度,Java的线程管理和调度是委托给操作系统完成的,与之相对应,Java的线程调度也是使用抢占式调度模型,因此Java的线程都有优先级。

2 优先级

线程运行的优先级,Java中使用抢占式调度模型进行线程调度。priority实例属性的优先级越高,线程获得CPU时间片的机会就越多

方法1:public final int getPriority(),获取线程优先级

方法2:public final void setPriority(int priority),设置线程优先级

3 生命周期

主要是Thread内部的枚举类public enum State

public enum State {
   
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }

新建、可执行、阻塞、等待、限时等待、终止

进程与线程的区别

  • 通俗的讲,进程是大于线程的,一个进程相当于多个线程的组成,一个进程最少有一个线程,那就是它本身
  • 线程是CPU调度的最小单位
  • 线程之间资源共享,进程之间相互独立
  • 上下文切换的速度来说,线程更轻量、更快

二、线程的使用

2.1 Thread类的介绍

在Thread类中,通常有这些操作

  • 线程ID

  • 名称

  • 优先级

    Java线程的最大优先级值为10,最小值为1,默认值为5

  • 守护线程

    什么是守护线程呢?

    守护线程是在进程运行时提供某种后台服务的线程,比如垃圾回收(GC)线程

  • 状态

    新建 | 就绪、运行 | 阻塞 | 等待 | 计时等待 | 结束

  • 启动和运行

  • 获取当前线程

2.2 创建线程的方法

Thread
public class ThreadDemo extends Thread {
   
    @Override
    public void run() {
   
        System.out.println("ThreadDemo 多线程测试");
        System.out.println("线程名称 " + currentThread().getName());
        System.out.println("ID " + currentThread().getId());
        System.out.println("状态 " + currentThread().getState());
        System.out.println("优先级 " + currentThread().getPriority());
    }
}

-----------------------------------
    
public class Demo01 {
   
    public static void main(String[] args) {
   
        System.out.println("main 主线程运行=======");
        new ThreadDemo().start();
    }
}
Runnable
  • 方法一:匿名类
new Thread(new Runnable() {
   
            @Override
            public void run() {
   
                System.out.println("Runnable 运行");
            }
        }).start();
  • 方法二:函数式编程
 new Thread(() -> {
   
            System.out.println("Runnable 运行");
        }).start();

使用实现Runnable接口这种方式是存在优缺点的

优点

(1)可以避免由于Java单继承带来的局限性。如果异步逻辑所在类已经继承了一个基类,就没有办法再继承Thread类

(2)逻辑和数据更好分离。通过实现Runnable接口实现多线程能更好地做到多个线程并发地完成同一个任务

缺点

(1)所创建的类并不是线程类,而是线程的target执行目标类,需要将其实例作为参数传入线程类的构造器,才能创建真正的线程

(2)如果访问当前线程的属性(甚至控制当前线程),不能直接访问Thread的实例方法,必须通过Thread.currentThread()获取当前线程实例,才能访问和控制当前线程

Callable

前面已经介绍了继承Thread类或者实现Runnable接口这两种方式来创建线程类,但是这两种方式有一个共同的缺陷:不能获取异步执行的结果。

这是一个比较大的问题,很多场景都需要获取异步执行的结果,通过Runnable无法实现,是因为它的run()方法不支持返回值。

为了解决异步执行的结果问题,Java语言在1.5版本之后提供了一种新的多线程创建方法:通过Callable接口和FutureTask类相结合创建线程

实现Callable接口的案例

class CallableDemo implements Callable{
   
    @Override
    public Object call() throws Exception {
   
        System.out.println("有返回值 并且受检异常");
        return null;
    }
}
Future

问题:Callable实例能否和Runnable实例一样,作为Thread线程实例的target来使用呢?答案是不行。Thread的target属性的类型为Runnable,而Callable接口与Runnable接口之间没有任何继承关系,并且二者唯一的方法在名字上也不同。显而易见,Callable接口实例没有办法作为Thread线程实例的target来使用。既然如此,那么该如何使用Callable接口创建线程呢?

答案就是Future

Thread中的target是什么?

Allocates a new Thread object. This constructor has the same effect as Thread (null, target, gname), where gname is a newly generated name. Automatically generated names are of the form “Thread-”+n, where n is an integer.
Params:
target – the object whose run method is invoked when this thread is started. If null, this classes run method does nothing.

分配一个新的Thread对象。这个构造函数的效果与Thread (null, target, gname)相同,其中gname是一个新生成的名称。自动生成的名称形式为“Thread-”+n,其中n是整数。

参数:
Target - 线程启动时调用其run方法的对象。如果为空,这个类运行方法什么也不做。

public Thread(Runnable target) {
    
this(null, target, "Thread-" + nextThreadNum(), 0);
}

Future是一个泛型接口,用于异步的计算提供了检查计算是否完成、等待计算完成以及检索计算结果的方法

public interface Future<V> {
   
boolean cancel(boolean mayInterruptIfRunning);

    // 取消任务的执行。参数指定是否立即中断任务执行,或者等等任务结束
    boolean cancel(boolean mayInterruptIfRunning);
    
    // 任务是否已经取消,任务正常完成前将其取消,则返回 true
    boolean isCancelled();

    // 任务是否已经完成。需要注意的是如果任务正常终止、异常或取消,都将返回true
    boolean isDone();

    // 等待任务执行结束,然后获得V类型的结果
    V get() throws InterruptedException, ExecutionException;

    // 参数timeout指定超时时间,uint指定时间的单位
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

FutureTask类是实现Runnable的Future实现

利用线程池

使用方式一:Executors(官方不推荐),原因后面

// 开辟线程池,放入10个线程数
ExecutorService Service = Executors.newFixedThreadPool(10);

// 适用于 Runnable,将Runnable实现类对象放入
Service.execute(new NumberThread());
Service.execute(new NumberThread2());
        
// 适用于 Callable
// Service.submit();

// 关闭线程池
Service.shutdown();

使用方式二:ThreadPoolExecutor(官方推荐

将在下章节对这两种方式进行说明

总结

2.3 为什么Executors被禁止使用

为什么阿里巴巴禁止使用 Executors 创建线程池?_singwhatiwanna-CSDN博客

阿里巴巴开发手册为什么禁止使用 Executors 去创建线程池


原因就是 newFixedThreadPool()newSingleThreadExecutor()两个方法允许请求的最大队列长度是 Integer.MAX_VALUE ,可能会出现任务堆积,出现OOM。

newCachedThreadPool()允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,导致发生OOM。


它建议使用ThreadPoolExecutor方式去创建线程池,通过上面的分析我们也知道了其实Executors 三种创建线程池的方式最终就是通过ThreadPoolExecutor来创建的,只不过有些参数我们无法控制,如果通过ThreadPoolExecutor的构造器去创建,我们就可以根据实际需求控制线程池需要的任何参数,避免发生OOM异常

2.4 线程的API操作

2.5 线程间的通信

线程是程序调度的最小单位,有自己的栈空间,线程之间是共享内存空间的(抛开ThrealLocal),一个进程下的各个线程相互协作通信

线程之间主要还是依靠**wait();notify();**实现相互之间的工作协助

知识点:

  1. wait() / notify() 方法原理
  2. 通过各类同步对象定义线程状态
  3. 需要在synchronized同步块的内部使用wait和notify

重点在于 wait() / notify() 方法原理 以及它们之间的通信要点

三、线程池的深入不出

3.1 ThreadPoolExecutor

构造参数说明

这个类 extendsAbstractExecutorService,下面针对它的有参构造参数进行说明

序号 名称 类型 含义
1 corePoolSize int 核心线程池大小
2 maximumPoolSize int 最大线程池大小
3 keepAliveTime long 线程最大空闲时间
4 unit TimeUnit 时间单位
5 workQueue BlockingQueue 线程等待队列
6 threadFactory ThreadFactory 线程创建工厂
7 handler RejectedExecutionHandler 拒绝策略
提交任务方式
  • 方式一:调用execute()方法

    Execute()方法只能接收Runnable类型的参数

  • 方式二:调用submit()方法

    submit()方法可以接收Callable、Runnable两种类型的参数

这个提交任务的方式就是说,你造了个线程池,然后用他对象的方法丢一个线程进去,这个方法就是我们说的提交任务的方式

阻塞队列

Java中的阻塞队列(BlockingQueue)与普通队列相比有一个重要的特点:在阻塞队列为空时会阻塞当前线程的元素获取操作。具体来说,在一个线程从一个空的阻塞队列中获取元素时线程会被阻塞,直到阻塞队列中有了元素;当队列中有元素后,被阻塞的线程会自动被唤醒(唤醒过程不需要用户程序干预)。

Java线程池使用BlockingQueue实例暂时接收到的异步任务,BlockingQueue是JUC包的一个超级接口,下面有很多比较常用的实现类

钩子方法
ThreadPoolExecutor threadPoolExecutor
                = new ThreadPoolExecutor(
                corePoolSize, maximumPoolSize, keepAliveTime,
                timeUnit, workQueue
        ){
   
            @Override
            protected void beforeExecute(Thread t, Runnable r) {
   
                System.out.println("任务执行前");
            }

            @Override
            protected void afterExecute(Runnable r, Throwable t) {
   
                System.out.println("任务执行后");
            }

            @Override
            protected void terminated() {
   
                System.out.println("线程池终止时");
            }
        };
拒绝策略

在线程池的任务缓存队列为有界队列(有容量限制的队列)的时候,如果队列满了,提交任务到线程池的时候就会被拒绝

总体来说,任务被拒绝有两种情况

(1)线程池已经被关闭

(2)工作队列已满且maximumPoolSize已满

无论以上哪种情况任务被拒绝,线程池都会调用RejectedExecutionHandler实例的rejectedExecution方法。RejectedExecutionHandler是拒绝策略的接口

JUC为该接口提供了以下几种实现

  • AbortPolicy:拒绝策略
  • DiscardPolicy:抛弃策略
  • DiscardOldestPolicy:抛弃最老任务策略
  • CallerRunsPolicy:调用者执行策略
  • 自定义策略

继承 RejectedExecutionHandler 接口实现 rejectedExecution 方法即可完成自定义策略

3.2 线程池的任务调度流程

3.3 线程池工厂

首先看看接口

里面只有一个方法,那就是造一个线程

线程池的工厂方法有四种实现,如下

我们还可以按需定制自己的线程池工厂

Java并发编程:Java的四种线程池的使用,以及自定义线程工厂 - 鄙人薛某 - 博客园 (cnblogs.com)

3.4 线程池生命周期/状态

// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static 
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值