juc相关前置知识

线程

通识概念:

线程进程概念

进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。

线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。是程序执行的最小单位。

线程是cpu调度的最小单位,而进程是线程的集合,一个进程除了包含多个线程以外,还有其余的共享变量空间;

比如说我们的Java main方法启动之后就是一个进程,最起码也会有main线程和gc线程。这就是两个线程,包括堆和元空间实际上都是通过进程管理;

比如说我们的spring web项目启动之后就会有很多线程,除了上面提到的还有web线程,tomcat线程池用来处理网络io和用户请求。

cpu和线程的关系(类似于工厂的流水线)

例子:cpu类似于工厂。进程类似于车间。线程类似车间内的工人。

线程是 CPU 调度的最小单位,必须依赖于进程而存在 线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的、 能独立运行的基本单位。

一台机器的cpu数量在一开始就确定好了,例如4核8G说的就是4个cpu和8G的运行内存;

一台实例机器在运行的时候可以有很多线程,一般tomcat的默认参数配置就是200,至于为什么线程数量远多于cpu数量;

就是因为cpu在执行线程任务的时候,会出现阻塞的情况,此时cpu就没事干,就需要多线程并发执行来提升执行效率。根本原因还是cpu的执行太快,远超于磁盘和网络io,需要提高cpu的执行效率;

为何不让一个cpu执行一个线程(上面这个也在描述)

例子:如果一个工厂只让一个人干,效率低,工人休息的时候工厂也要停工;

主要是为了提高cpu的利用率,有时候会出现阻塞任务,此时cpu没事干就可以去执行其他任务;

单核cpu多线程,会出现并发问题吗?

不会,都是自己在修改数据

线程一定越多越好吗?

不是越多越好。

1、创建和销毁线程需要消耗系统资源,若线程数过多,开销可能会成为系统的负担。

2、当线程数过多时,线程之间的切换会变得频繁,这会增加系统的开销,降低程序的性能。

3、若多个线程同时访问同一资源,可能会发生资源的竞争,导致程序的性能下降。

线程上下文切换?

定义:指的是CPU从一个线程切换到另一个线程执行的过程。这个过程包括保存当前线程的状态(包括CPU寄存器的状态、程序计数器等),并恢复将要执行线程的状态。

寄存器:指CPU内部容量较小但速度很快的内存区域(与之相对应的是CPU外相对较慢的RAM主内存)。

程序计数器:是一个专用的寄存器,用于表明指令序列中CPU正在执行的位置,存储的值为正在执行的指令的位置或下一个将被执行的指令的位置,这依赖于特定的系统。

进程上下文切换?

进程上下文切换指的是CPU从一个进程切换到另一个进程执行的过程。这个过程同样包括保存当前进程的状态,并恢复将要执行进程的状态。

切换流程:

(1)挂起一个进程,将这个进程在CPU中的状态(上下文信息)存储于内存的PCB中。

(2)在PCB中检索下一个进程的上下文并将其在CPU的寄存器中恢复。

(3)跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码),并恢复该进程。

时间片轮转方式使多个任务在同一CPU上的执行有了可能。

引起线程上下文切换的原因

  • 当前正在执行的任务完成,系统CPU正常调度下一个任务。
  • 当前正在执行的任务遭遇I/O等阻塞操作,调度器挂起此任务,继续调度下一个任务
  • 多个任务并发抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续调度下一个任务。
  • 用户的代码挂起当前任务,比如线程执行sleep方法,而让出CPU。
  • 硬件中断。

用户态和内核态

用户态和内核态是操作系统的两种运行状态。

  • 内核态:处于内核态的 CPU 可以访问任意的数据,包括外围设备,比如网卡、硬盘等,处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且占用 CPU 不会发生抢占情况,一般处于特权级 0 的状态我们称之为内核态。
  • 用户态:处于用户态的 CPU 只能访问受限资源,不能直接访问内存等硬件设备,不能直接访问内存等硬件设备,必须通过「系统调用」陷入到内核中,才能访问这些特权资源。

如何创建线程:

Lambda表达式

我们可以把Lambda表达式理解为一段可以传递的代码(将代码像数据一样进行传递)。Lambda允许把函数作为一个方法的参数,使用Lambda表达式可以写出更简洁、更灵活的代码,而其作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。

 //Lambda方式
new Thread(() -> System.out.println("使用Lambda就对了")).start();

Lambda表达式在Java语言中引入了一个操作符**“->”**,该操作符被称为Lambda操作符或箭头操作符。它将Lambda分为两个部分:

  • 左侧:指定了Lambda表达式需要的所有参数
  • 右侧:制定了Lambda体,即Lambda表达式要执行的功能。

像这样:

(parameters) -> expression
或
(parameters) ->{ statements; }

函数式接口

==只包含一个抽象方法的接口,就称为函数式接口。==我们可以通过Lambda表达式来创建该接口的实现对象。

匿名内部类

  • 匿名内部类基本语法 :
new 父类构造器(参数) / 实现接口() {
    // 类的主体部分
};
  • 解释匿名内部类语法中所有的概念:
    • new 父类构造器(参数) 表示匿名内部类是某个类的子类实例。
    • 实现接口() 表示匿名内部类是某个接口的实现实例。
    • { ... } 内部是匿名内部类的主体部分,包含类的字段、方法等定义。

性质:

匿名内部类(Anonymous Inner Class)是一种在声明和创建对象的同时定义类的方式,它没有显式的类名。通过 匿名内部类 看这几个字的字面意思我们都知道这是个没有名字的类,即 非具名类 . 以下是匿名内部类的具备的一些性质 :

  1. 可以实现接口或继承类: 匿名内部类可以实现接口或继承某个类,从而提供具体的实现。
  2. 没有显式的类名: 匿名内部类没有显式的类名,因为它是一种临时的、一次性的实现。
  3. 一次性使用: 通常用于临时的、一次性的场景,不需要复用。因为匿名内部类没有类名,所以无法在其他地方重复使用。
  4. 可以访问外部类的成员: 匿名内部类可以访问外部类的成员,包括成员变量和方法。对于外部类的局部变量,有一些规则,比如必须是final或者事实上是final的。
  5. 可以包含字段和方法: 在匿名内部类的主体部分,可以包含字段(成员变量)和方法的定义。
  6. 不可以包含静态成员: 匿名内部类不能包含静态成员,包括静态方法和静态变量。

方法引用:

方法引用是一种直接引用已经存在的方法的方式,它允许我们在代码中通过方法的名称来引用方法。方法引用可以被看作是Lambda表达式的一种简化形式,它提供了一种更加简洁的方式来实现函数式接口。

在Java中,方法引用主要用于简化函数式接口的实现,特别是当我们需要将一个方法作为参数传递给另一个方法时,使用方法引用可以使代码更加清晰。

方法引用的语法由两部分组成:类名或对象名和方法名,中间使用双冒号(::)进行分隔。根据方法引用的情况,可以分为以下几种形式:

  • 静态方法引用:类名::静态方法名
  • 实例方法引用:对象名::实例方法名
  • 特定类的任意对象方法引用:类名::实例方法名
  • 构造方法引用:类名::new

注意事项

  • 方法引用只能用于函数式接口,即只包含一个抽象方法的接口。
  • 方法引用的方法签名必须与函数式接口的抽象方法的参数列表和返回类型一致。
  • 方法引用不能引用抽象方法,即不能引用接口中的默认方法。
  • 方法引用可以引用静态方法、实例方法和构造方法。

Stream 流

Stream将要处理的元素集合看作一种流,在流的过程中,借助Stream API对流中的元素进行操作,比如:筛选、排序、聚合等。

Stream可以由数组或集合创建,对流的操作分为两种:

  1. 中间操作,每次返回一个新的流,可以有多个。(筛选filter、映射map、排序sorted、去重组合skip—limit)
  2. 终端操作,每个流只能进行一次终端操作,终端操作结束后流无法再次使用。终端操作会产生一个新的集合或值。(遍历foreach、匹配find–match、规约reduce、聚合max–min–count、收集collect)
Stream的创建

1、通过 java.util.Collection.stream() 方法用集合创建流

// 创建一个顺序流Stream<String> stream = list.stream();

2、使用java.util.Arrays.stream(T[] array)方法用数组创建流

int[] array={1,3,5,6,8};
IntStream stream = Arrays.stream(array);

Java如何创建线程

java创建线程(Thread)的5种方式_java new thread-CSDN博客

方式一:继承于Thread类

步骤:
1.创建一个继承于Thread类的子类
2.重写Thread类的run() --> 将此线程执行的操作声明在run()中
3.创建Thread类的子类的对象
4.通过此对象调用start()执行线程

方式二:实现Runnable接口

步骤:
1.创建一个实现了Runnable接口的类
2.实现类去实现Runnable中的抽象方法:run()
3.创建实现类的对象
4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
5.通过Thread类的对象调用start()
① 启动线程
②调用当前线程的run()–>调用了Runnable类型的target的run()

方式三:实现Callable接口

步骤:
1.创建一个实现Callable的实现类
2.实现call方法,将此线程需要执行的操作声明在call()中
3.创建Callable接口实现类的对象
4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
6.获取Callable中call方法的返回值

实现Callable接口的方式创建线程的强大之处

call()可以有返回值的
call()可以抛出异常,被外面的操作捕获,获取异常的信息
Callable是支持泛型的
方式四:使用线程池

步骤:
1.以方式二或方式三创建好实现了Runnable接口的类或实现Callable的实现类
2.实现run或call方法
3.创建线程池
4.调用线程池的execute方法执行某个线程,参数是之前实现Runnable或Callable接口的对象

线程池好处:
1.提高响应速度(减少了创建新线程的时间)
2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3.便于线程管理
方式五:使用匿名类
Thread thread = new Thread(new Runnable() {
	@Override
	public void run() {
		// 线程需要执行的任务代码
		System.out.println("子线程开始启动....");
		for (int i = 0; i < 30; i++) {
			System.out.println("run i:" + i);
		}
	}
});
thread.start();

如何使用线程执行任务

异步线程的概念

在Java中,异步线程是指可以独立执行的线程,不会阻塞主线程的执行。通过使用异步线程,可以在主线程执行其他任务的同时,处理耗时的操作。异步线程可以通过Java中的Thread类或者ExecutorService接口来创建和管理。

线程池

java线程池使用最全详解_java 线程池-CSDN博客

在执行一个异步任务或并发任务时,往往是通过直接new Thread()方法来创建新的线程,这样做弊端较多,更好的解决方案是合理地利用线程池,线程池的优势很明显,如下:

  1. 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
  2. 提高系统响应速度,当有任务到达时,无需等待新线程的创建便能立即执行;
  3. 方便线程并发数的管控,线程若是无限制的创建,不仅会额外消耗大量系统资源,更是占用过多资源而阻塞系统或oom等状况,从而降低系统的稳定性。线程池能有效管控线程,统一分配、调优,提供资源使用率;
  4. 更强大的功能,线程池提供了定时、定期以及可控线程数等功能的线程池,使用方便简单。
java通过Executors提供四种线程池,分别为:
1.newCachedThreadPool:
创建一个可缓存的无界线程池,如果线程池长度超过处理需要,可灵活回收空线程,若无可回收,
则新建线程。
2.newFixedThreadPool:
创建一个指定大小的线程池,可控制线程的最大并发数,超出的线程会在LinkedBlockingQueue阻塞队列
中等待。
3.newScheduledThreadPool:
创建一个定长的线程池,可以指定线程池核心线程数,支持定时及周期性任务的执行。
4.newSingleThreadExecutor:
创建一个单线程化的线程池,它只有一个线程,用仅有的一个线程来执行任务,保证所有的任务按照指定
顺序(FIFO,LIFO,优先级)执行,所有的任务都保存在队列LinkedBlockingQueue中,
等待唯一的单线程来执行任务。

线程池原理

Executors类提供4个静态工厂方法:newCachedThreadPool()、newFixedThreadPool(int)、newSingleThreadExecutor和newScheduledThreadPool(int)。这些方法最终都是通过ThreadPoolExecutor类来完成的,这里强烈建议大家直接使用Executors类提供的便捷的工厂方法,能完成绝大多数的用户场景,当需要更细节地调整配置,需要先了解每一项参数的意义。

线程的生命周期

它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种状态。

  1. 新建 New
  2. 就绪 Runnable
  3. 运行 Running
  4. 阻塞 Blocked
  5. 死亡 Dead

详细分析线程生命周期:

新建状态(刚new出来一个对象,啥也没有的状态):

当程序使用new关键字创建了一个线程之后,该线程就处于一个新建状态(初始状态),此时它和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,并初始化了其成员变量值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。

就绪状态(当线程对象调用了Thread.start()方法之后,还未获取到cpu时间片):

调用了线程的start方法,线程进入Runnable状态,但是还未获取到时间片,所以处于还未执行的状态;

注意:就绪状态不会直接进入堵塞和死亡状态;

运行状态:

当调用start方法之后,并且获取到了cpu时间片(即被os调度选中了),此线程就进入运行状态;

阻塞状态:

阻塞状态是线程因为某种原因放弃cpu的使用权,暂时停止运行。直到线程进入就绪状态,才会有机会转入运行状态。

阻塞状态大概分为三种:

1.等待阻塞:线程执行wait方法,jvm会把当前线程放入等待池中;

2.同步阻塞:运行的线程获取到对象的同步锁时,发现同步锁被其他线程占用,则jvm会把线程放入锁池;

3.其他阻塞:线程执行sleep或者join方法,或者发出io请求就会进入阻塞状态;

死亡状态:

是线程的最终状态,在该状态不会切换到其他任何状态,线程进入该状态意味着线程的整个生命周期都结束了。

线程会以以下三种方式之一结束,结束后就处于死亡状态:

  • run()方法执行完成,线程正常结束。
  • 线程抛出一个未捕获的Exception或Error。
  • 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用。

Java线程与操作系统线程的关系?

首先,需要明确的是,在操作系统看来,每一个进程只有一个线程。然而,Java中的线程实现与操作系统的线程实现有所不同。当线程在用户空间下实现时,操作系统对线程的存在一无所知,它只能看到进程,而不能看到线程。所有的Java线程都是在用户空间实现的,这意味着在操作系统看来,Java的每一个进程(即一个Java虚拟机实例)内部只有一个线程。

然而,Java中的多线程编程模型允许在单个进程中创建多个线程,这些线程可以并发或并行地执行。这些线程在Java虚拟机内部实现,并通过Java线程调度器进行管理。Java线程调度器负责将Java线程映射到操作系统的线程(或进程)上,以便在CPU上执行。

线程执行任务的时候什么时候让出cpu?

1.第一种正常的结束,也就是时间片到了:AB线程争夺资源,A线程获得锁享受资源(CPU),B在等待,A时间片到了之后让出资源,B再进入!

2.第二种就是线程退出:

1)采用stop,但是不提倡使用!

2)采用interrupt:可以让sleep状态的线程或者running的线程退出

3.第三种就是暂定等待:

1)join()方法是宏观上让并行的线程串行,即等待某个thread完成之后才继续执行。

2)sleep()方法是让线程指定的时间进行休眠,让出CPU资源。但是不释放锁,指定时间到后又恢复运行。

3)awati()类似于sleep()方法,暂停运行,不同的是它可以释放锁,是Object的方法,不是Thread的,并且一般与同步代码块一起使用,调用notify()方法后才会被唤醒!

线程间的通信与配合

线程中断:

在合适的情况下,操作系统的内核会把 CPU 的使用权主动让给应用程序,也就是使 CPU 从内核态转换到用户态(内核态到用户态的切换是内核程序执行一个特权指令,将程序状态字PSW设置为用户态)。而 CPU 要想从用户态回到内核态,只能通过中断机制完成,如果没有中断机制,那么一旦应用程序上 CPU 运行(用户态),CPU 就会一直运行这个应用程序。也就是说,「中断是让操作系统内核夺回 CPU 使用权的唯一途径」。可以说,「操作系统是由中断驱动的」

当然,这里的中断机制非常广义,包含了三种手段,也就是说从用户态转换到内核态有三种手段:

  • 1)程序请求操作系统服务,执行系统调用(执行系统调用也是发生了一次内中断)
  • 2)程序运行时产生外中断事件(比如 I/O 操作完成),运行程序被中断,转向中断程序处理
  • 3)在程序运行时发生内中断(异常)事件,运行程序被打断,转向异常处理程序工作
  • 中断线程: interrupt() 是 Thread 类的一个实例方法,用于中断当前线程。当一个线程通过 interrupt() 方法被中断时,它会收到一个中断信号,但不会立即停止执行。具体的中断响应需要在线程的代码中进行处理。
  • 检查中断状态: isInterrupted() 是 Thread 类的另一个实例方法,用于检查线程的中断状态。通过调用该方法,可以判断线程是否被中断,并根据需要执行相应的逻辑。
  • 检查当前线程的中断状态: Thread 类还提供了一个静态方法 interrupted(),用于检查当前线程的中断状态,并清除中断状态。与 isInterrupted() 不同,interrupted() 是一个静态方法,检查的是调用该方法的线程的中断状态。

线程睡眠:

让线程休眠指定的毫秒数,此时会释放cpu资源但是不会释放锁;

因为sleep()是静态方法,所以最好的调用方法就是 Thread.sleep()。
线程的sleep方法应该写在线程的run()方法里,就能让对应的线程睡眠。


线程阻塞唤醒:

wait/notify

使用wait()方法来阻塞线程,使用notify()和notifyAll()方法来唤醒线程。
调用wait()方法后,线程将被阻塞,wait()方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll()方法后方能继续执行。
notify/notifyAll()方法只是解除了等待线程的阻塞,并不会马上释放监视器锁,而是在相应的被synchronized关键字修饰的同步方法或同步代码块执行结束后才自动释放锁。

park/unpark

park/unpark与 wait/notify功能类似,都是用来暂停和唤醒线程。park用来暂停线程,unpark用来将暂停的线程恢复。两个都是LockSupport类下的方法。

// 暂停当前线程
LockSupport.park(); 
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)

特点

  • wait、notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
  • park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify

java中一个线程等待另一个线程执行完后再执行

该问题大概有3种方法:

1.notify、wait方法,Java中的唤醒与等待方法,关键为synchronized代码块,也常用Object作为参数,示例如下。

package com.thread_lc;
 
class MyThread2 implements Runnable
{
    @Override
    public void run ()
    {
        Thread currThread = Thread.currentThread ();
        synchronized (currThread)
        {
            while ("t1".equals (currThread.getName ()))
            {
                try
                {
                    currThread.wait (0);
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace ();
                }
            }
            done ();
        }
    }
 
    public synchronized void done ()
    {
        System.out.println ("更改完毕");
    }
}

2.CountDownLatch类

一个同步辅助类,常用于某个条件发生后才能执行后续进程。给定计数初始化CountDownLatch,调用countDown()方法,在计数到达零之前,await方法一直受阻塞。

重要方法为countdown()与await();

3.join方法

将线程B加入到线程A的尾部,当A执行完后B才执行。示例如下。

package com.thread_lc;
 
public class Th extends Thread {
	private final String name;
 
	public Th(String name) {
		this.name = name;
	}
 
	@Override
	public void run() {
		super.run();
		for (int i = 0; i < 100; i++)
			System.err.println(name + "\t" + i);
	}
 
	public static void main(String[] args) throws Exception {
		Th t = new Th("t1");
		Th t2 = new Th("t2");
		t.start();
		t.join();
		t2.start();
	}
}

如何实现多线程任务编排(线程异步编排)

玩转CompletableFuture线程异步编排,看这一篇就够了_completablefuture异步编排-CSDN博客

介绍:

CompletableFuture可用于线程异步编排,使原本串行执行的代码,变为并行执行,提高代码执行速度。

学习异步编排先需要学习线程池和lambda表达式相关知识。

使用

说明:使用CompletableFuture异步编排大多方法都会有一个重载方法,会多出一个executor参数,用来传来自定义的线程池,如果不传就会使用默认的线程池。

贴出自定义线程池的代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        10,
        50,
        60,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(100000),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.AbortPolicy());

创建异步编排对象

public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier);
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor);

public static CompletableFuture<Void> runAsync(Runnable runnable);
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor);

有两种格式,一种是supply开头的方法,一种是run开头的方法

  • supply开头:这种方法,可以返回异步线程执行之后的结果
  • run开头:这种不会返回结果,就只是执行线程任务

举例代码:

// 异步起线程执行业务 无返回值
CompletableFuture<Void> voidCompletableFuture = CompletableFuture.runAsync(() -> {
    System.out.println("当前线程:" + Thread.currentThread().getId());
    int i = 10 / 2;
    System.out.println("运行结果:" + i);
}, executor);


//异步起线程执行业务 有返回值
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    System.out.println("当前线程:" + Thread.currentThread().getId());
    int i = 10 / 0;
    System.out.println("运行结果:" + i);
    return i;
}, executor).whenComplete((res,exc)->{
    // 可以接收到返回值和异常类型,但是无法处理异常
    System.out.println("异步任务成功完成了...结果是:" + res + ";异常是:" + exc);
}).exceptionally(throwable -> {
    // 处理异常,返回一个自定义的值,和上边返回值无关。
    return 10;
});

//方法执行完成后的处理
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    System.out.println("当前线程:" + Thread.currentThread().getId());
    int i = 10 / 0;
    System.out.println("运行结果:" + i);
    return i;
}, executor).handle((res,thr)->{
    // 无论线程是否正确执行,都会执行这里,可以对返回值进行操作。
    if(res != null){
        return res * 2;
    }
    if(thr != null){
        return 0;
    }
    return 0;
});

这里出现了三个新方法

CompletableFuture.whenComplete():用于接收带有返回值的CompletableFuture对象,无法修改返回值。

CompletableFuture.exceptionally():用于处理异常,只要异步线程中有抛出异常,则进入该方法,修改返回值。

CompletableFuture.handle():用于处理返回结果,可以接收返回值和异常,可以对返回值进行修改。

并发理论

什么是并发?

系统同时运行多个不同的任务,比如单核CPU,他同时只能干一件事,但是他可以通过时间分片来切换同时让多个线程运行,这就是并发。

并发编程三要素:

原子性:一个或者多个操作要么全部执行成功要么全部执行失败。

有序性:程序执行顺序按照代码顺序先后执行,但是CPU可能会对指令进行重排序。

可见性:当多个线程访问同一个变量时,如果一个线程修改了变量,其他线程立即获取最新的值。

三大特性举例:

原子性:在java当中,直接的读取操作和赋值(常量)属于原子性操作。对于原本不具有原子性的操作我们可以通过synchronized关键字或者Lock接口来保证同一时间只有一个线程执行同一串代码,从而也具有了原子性。

有序性:在java当中使用volatile关键字来保证一定的有序性
如下表所示是JMM针对编译器制定的volatile重排序规则表:

是否能重排序

操作二

操作二

操作二

操作一

普通读写

volatile读

volatile写

普通读写

volatile读

volatile写

另外也可以用synchronized关键字或Lock接口来保证有序性。

可见性:普通的、未加修饰的共享变量是不能保证可见性的。我们照样可以通过synchronized关键字和Lock接口来保证可见性,同样也能利用volatile实现。

当一个共享变量被volatile修饰时,就可以保证:
1.一个线程修改任意一个共享变量后,其他任何线程再次访问该变量都将获得最新值。
2.不仅是修改以后的值对其余线程可见,修改之前的值仍然具有可见性。

可见性细化分析:

4.什么是MESI缓存一致性协议?怎么解决并发的可见性问题?-腾讯云开发者社区-腾讯云

多核CPU的高速缓存数据一致性问题:

数据一致问题原因:

CPU0修改了数据之后,没有机制能够通知到CPU1,让CPU1它高速缓存上这个变量的数据失效掉,导致CPU1计算的时候还是使用旧的值。

解决方案(MESI一致性协议):

MESI实现的缓存一致性协议,正是CPU0修改了数据,通知到CPU1的那套通知机制的一种规范,计算机厂商根据这套规范实现了这种通知机制,但是不同的厂商之间实现方式可能稍微不同。

M(Modified): 修改过的,只有一个CPU能独占这个修改状态,独占的意思是当有一个CPU的高速缓存数据处于这个状态的时候,其它CPU的高速缓存对这个共享的数据均不能操作;高速缓存中的数据发生了更新,需要被刷入主内存中。

E(Exclusive): 独占状态,只有一个CPU能独占这个状态,同样当某个CPU的高速缓存的数据处于这个状态的时候,其它CPU的均不能操作这个共享数据

S(Share):共享的状态,当CPU的高速缓存中的数据这个状态的时候,各个CPU可以并发的对这个数据进行读取

I(Invalid):无效的,意思是当前高速缓存的这个数据已经是无效了或者过期了,不能使用。

状态之间的转换流程:

(1)首先像下面的图一样,CPU0CPU1将共享变量 i = 0 读取,进入自己高速缓存的时候;缓存的状态是S,也就是共享的;

(2)然后CPU0要对 i = 0 的变量进行修改操作,在MESI一致性里面大概会经过这些步骤:

  1. CPU0发送消息给总线,说我要修改数据了,帮我通知一下其它的CPU
  2. 其它的CPU收到总线通知消息,将自己高速缓存上i = 0 的数据状态变为Invalid过期
  3. 其它CPU返回给总线说我们都过期了
  4. 总线收到其它CPU返回过期OK了
  5. 总线返回给CPU0说,好了,其它CPU都通知到位了,它们高速缓存上的 i = 0的数据都是过期状态了
  6. CPU0收到了过期确认,都过期了,那我就可以独占这份数据了,嘿嘿,准备可以修改数据了

(3)CPU0修改数据,刷新回主内存,还会经过这些步骤:

7. CPU0执行 i++ 操作,将 i = 1 的最新结果刷入到高速缓存中,同时将高速缓存的数据状态设为M(修改过的)

  1. 然后将高速缓存中 i = 1 的最新结果又刷入主内存

(4)CPU1读取数据操作,发现高速缓存上数据过期了,回经过下面步骤:

9. CPU1发现自己高速缓存上 i = 0 的数据是 Invalid 过期状态,于是从主存重新读取

  1. 然后CPU1从主内存读取到 i = 1的最新的数据,将自己状态设置成S;

什么是总线风暴,先来看结论

在java中使用unsafe实现cas,而其底层由cpp调用汇编指令实现的,如果是多核cpu是使用lock cmpxchg指令,单核cpu 使用compxch指令。如果在短时间内产生大量的cas操作在加上 volatile的嗅探机制则会不断地占用总线带宽,导致总线流量激增,就会产生总线风暴。 总之,就是因为volatile 和CAS 的操作导致BUS总线缓存一致性流量激增所造成的影响。

JMM内存模型

jmm是什么

JMM就是Java内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。

JMM定义了什么

原子性,可见性,有序性。这三个特征可谓是整个Java并发的基础。

很多并发编程都使用了volatile关键字,主要的作用包括两点:

  1. 保证线程间变量的可见性。
  2. 禁止CPU进行指令重排序。

线程池

基本概念:

线程池是一种利用池化技术思想来实现的线程管理技术,主要是为了复用线程、便利地管理线程和任务、并将线程的创建和任务的执行解耦开来。我们可以创建线程池来复用已经创建的线程来降低频繁创建和销毁线程所带来的资源消耗。在JAVA中主要是使用ThreadPoolExecutor类来创建线程池,并且JDK中也提供了Executors工厂类来创建线程池(不推荐使用)。

线程池的优点:
降低资源消耗,复用已创建的线程来降低创建和销毁线程的消耗。
提高响应速度,任务到达时,可以不需要等待线程的创建立即执行。
提高线程的可管理性,使用线程池能够统一的分配、调优和监控。

ThreadPoolExecutor的构造组成

corePoolSize,核心线程数量,决定是否创建新的线程来处理到来的任务(20左右)
maximumPoolSize,最大线程数量,线程池中允许创建线程地最大数量(30-50)
keepAliveTime,线程空闲时存活的时间
unit,空闲存活时间单位
workQueue,任务队列,用于存放已提交的任务(几百到一两千)
threadFactory,线程工厂,用于创建线程执行任务
handler,拒绝策略,当线程池处于饱和时,使用某种策略来拒绝任务提交(一般时callerrunspolicy)

拒绝策略分类:

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务 ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务

原理

线程池是怎么区分核心线程和非核心线程的?

区别是非核心线程在配置的时间内没有执行任务会被销毁,而核心线程不会。

其实线程池是没有区分核心线程和非核心线程的!

线程池只有coresize和maximumsize,在数量上进行的逻辑处理,并没有在线程个体上做区分。

常见的几种线程池类型?

newCachedThreadPool——可缓存线程池:动态创建线程数;

newFixedThreadPool————指定线程数量:设置固定线程,实际使用较多;

newSingleThreadExecutor————单线程的Executor:可保证线程顺序执行;

newScheduleThreadPool——定时线程池:支持定时及周期性任务执行;

提交任务的流程?

1.当有新任务添加时,如果线程数小于核心线程数,直接通过线程工厂获取线程,封装成worker对象然后启动对应线程;

2.核心线程满了之后放入队列;

3.队列满了之后开始扩张直到线程到最大线程数量;

线程异常的处理方案?

1.手动try-catch,将任务放到补偿表,或者仅仅输出日志,后面通过日志监控进行补偿;

2.submit执行,Future.get()接收异常;

3.重写ThreadPoolExecutor的afterExecute方法,处理传递的异常引用;

4.为工作者线程设置uncaughtExceptionHandler,在uncaughtException方法中处理异常;

你们线上的线程池参数是如何配置的?

晚上流量与白天的流量不一致,如果在流量低的时候想要降低内存的使用?

可以考虑降低核心线程数,队列数调高,超时时间设置的高一些;

引入第三方依赖,实现动态线程池的参数配置;

线程核心数设置理论?

1.CPU密集型任务,需要尽量压缩线程数,参考值设置为N(CPU)+1;

注:CPU密集值得是这个请求进来之后一直在运算,计算量比较大,这时候为了避免CPU时间片切换线程执行,就根据该cpu核心数+1;

2.IO密集型任务,参考值可以设置为2*N(CPU);

注:IO密集型指的就是需要频繁的访问三方或者数据库,一定会引起CPU时间片切换,此时设置cpu核心数的两倍;

常用的JUC并发类?

等待多线程完成的CountDownLatch:CountDownLatch允许一个或多个线程等待其他线程完成操作。

同步屏障CyclicBarrier让一组线程到达一个屏障(也就是所谓的同步点)时被阻塞,知道最后一个线程到达屏障时,屏障才会开门,所有被该屏障所阻塞的线程才能够继续执行。

控制并发线程数的Semaphore(信号量):semaphore是用来控制同时访问特定资源的线程数量。

注:前两者使用较多,不过要注意区别,countdownlatch在等待时其他线程已经释放,可以进行其他工作,CyclicBarrier在等待其他线程时不会进行释放;


synchronized和reentrantlock区别?

1.从底层实现来看,synchronized是JVM层面的锁,是Java关键字,通过monitor对象来完成;而reentrantlock是从jdk1.5(juc下面的locks.Lock)以来提供的api层面的锁;

2.是否可手动释放:synchronized不需要用户手动释放,synchronized代码执行完毕之后系统会自动让线程释放对象锁的占用;Reentrantlock则需要用户手动去释放锁,如果没有手动释放锁,就可能会导致死锁,一般通过lock和unlock来实现;

3.是否可中断:synchronized是不可中断的,除非加锁的代码中出现异常或正常执行完成;而Reentrantlock是可以中断的,可以通过trylock设置超时方法放到代码块中,调用interrupt方法进行中断。

4.是否公平锁:synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁。

5.是否可绑定条件condition:synchronized是不可绑定的; ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒,而不是像synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值