Java线程池

本文详细介绍了Java线程池的概念,包括进程与线程的区别、线程状态、线程安全问题以及synchronized和volatile的使用。重点讨论了线程池的工作原理、设计和监控,强调了线程池参数动态化和监控的重要性,旨在帮助开发者更好地理解和优化多线程应用。
摘要由CSDN通过智能技术生成

概念

进程和线程

  • 进程:有独立内存空间,每个进程中的数据空间都是独立的。(系统运行程序的基本单位
  • 线程:多线程之间堆空间与方法区是共享的,但每个线程的栈空间、程序计数器是独立的,线程消耗的资源比进程小的多。(进程中的一个执行单元
并发与并行
  • 并发(Concurrent):同一时间段,多个任务都在执行 ,单位时间内不⼀定同时执行。
  • 并行(Parallel):单位时间内,多个任务同时执行,单位时间内一定是同时执行。并行上限取决于CPU核数(CPU时间片内50ms)
线程上下文切换

一个CPU内核,同一时刻只能被一个线程使用。为了提升CPU利用率,CPU采用了时间片算法将CPU时间片轮流分配给多个线程,每个线程分配了一个时间片(几十毫秒/线程),线程在时间片内,使用CPU执行任务。当时间片用完后,线程会被挂起,然后把 CPU 让给其它线程。

CPU切换前会把当前任务状态保存下来,用于下次切换回任务时再次加载。任务状态的保存及再加载的过程就叫做上下文切换

程序计数器:用来存储CPU正在执行的指令的位置,和即将执行的下一条指令的位置。他们都是CPU在运行任何任务前,必须依赖的环境,被叫做CPU上下文。

上下文切换过程

  1. 挂起当前任务任务,将这个任务在 CPU 中的状态(上下文)存储于内存中的某处。
  2. 恢复一个任务,在内存中检索下一个任务的上下文并将在 CPU 的寄存器中恢复。
  3. 跳转到程序计数器所指定的位置(即跳转到任务被中断时的代码行)。
    在这里插入图片描述

过多的线程并行执行会导致CPU资源的争抢,产生频繁的上下文切换,常常表现为高并发执行时,RT延长。因此,合理控制上下文切换次数,可以提高多线程应用的运行效率。(也就是说线程并不是越多越好,要合理的控制线程的数量。)

  • 直接消耗:指的是CPU寄存器需要保存和加载,系统调度器的代码需要执行
  • 间接消耗:指的是多核的cache之间得共享数据,间接消耗对于程序的影响要看线程工作区操作数据的大小
线程状态(6种)
  • NEW(新建):线程刚被创建,但是并未启动。
  • RUNNABLE(可运行):线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。
  • BLOCKED(锁阻塞): 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
  • WAITING(无限等待): 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
  • TIMED_WAITING(计时等待): 同waiting状态,有几个方法有超时参数,调用他们将进入TimedWaiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
  • TERMINATED(被终止): 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。
    在这里插入图片描述
    常用方法:yield()线程让步;sleep()线程休眠;join()等待线程执行终止的方法;interrupt()线程中断;wait、notify等待与通知
wait与sleep区别
  • 主要区别:sleep()方法没有释放锁,而wait()方法释放了锁
  • 两者都可以暂停线程的执行
  • wait()通常用于线程间的交互/通信,sleep()通常用于暂停线程执行
  • wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象的notify或notifyAll。
  • sleep()方法执行完成后,线程会自动苏醒。或者可以使用wait(long)超时后,线程也会自动苏醒

线程安全问题

如果程序每次运行结果和单线程运行的结果一样,且其他的变量的值也和预期一样,就是线程安全的,反之则是线程不安全的。

引发线程安全问题: 线程安全问题都是由全局变量及静态变量【共享】引起的

  • 如果每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;
  • 如果有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全问题

解决问题:线程同步、volatile、CAS、AQS锁

线程同步synchronized
private final Object lock = new Object();//锁对象,可以是任意类型数据
//同步代码块
synchronized(lock){
   
	需要同步操作的代码
}

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

//Lock锁
Lock lock = new ReentrantLock();//可重入锁
lock.lock();
	需要同步操作的代码
lock.unlock();

多线程并发的3个特性 O(∩_∩)O

并发编程中,三个非常重要的特性:原子性,有序性和可见性

  • 原子性:即一个操作或多个操作,要么全部执行,要么就都不执。执行过程中,不能被打断
  • 有序性:程序代码按照先后顺序执行(因为指令重排
  • 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值(因为Java内存模型【JMM】)

要想多线程程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

volatile保证了操作的可见性,所修饰的变量不具备线程缓存,所以多个线程同时操作,也只会操作内存中的那个i,一个线程修改了i的值 其他线程可以立刻见到新值,这就是可见性.

指令重排序

重排序是编译器和处理器为了提高程序运行效率,会对输入代码进行优化的一种手段。它不保证程序中,各个语句执行先后顺序的一致。

按顺序执行不好么,为什么要重排序去执行

  • 不进行指令重排,就相当于没有编译优化,那么程序执行效率就有问题
  • 当前线程获取CPU时间片(几十毫秒),如果按照先后顺序执行,并不能把CPU性能发挥完全,上一个指令执行完执行下一个,CPU会出现空挡期。

什么是as-if-serial语义?

  • 不管编译器和处理器怎么优化字节码指令,怎样进行指令重排,单线程所执行的结果不能受影响.
  • 处理器在进行重排序时,会考虑指令之间的数据依赖性,如果一个 指令2 必须用到 指令1 的结果,那么处理器会保证指令1会在指令2之前执行
  • 虽然重排序不会影响单个线程内程序执行的结果,但是多线程会有影响。

java内存模式 JMM

JMM并不像JVM内存结构一样是真实存在。它只是一个抽象的概念。是和多线程相关的,描述了一组规则或规范,这个规范定义了一个线程对共享变量的写入时,对另一个线程是可见的。

JMM就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种
平台下对内存的访问都能保证效果一致的机制及规范。
在这里插入图片描述

  • 主内存:主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量)。
  • 本地内存:主要存储当前方法的所有变量,每个线程只能访问自己的本地内存。线程中的本地变量对其它线程是不可见的本地内存是抽象的,不真实存在,涵盖:缓存,写缓冲区,寄存器等
JMM线程操作内存的基本规则
  • 第一条,关于线程与主内存:线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主内存中读写
  • 第二条,关于线程间本地内存:不同线程之间无法直接访问其他线程本地内存中的变量,线程间变量值的传递需要经过主内存
内存可见性

可见性是一个线程对共享变量值的修改,能够及时的被其他线程看到。

  1. 首先,线程 A 把本地内存 A 中更新过的共享变量刷新到主内存中去。
  2. 然后,线程 B 到主内存中去读取线程 A 之前已更新过的共享变量。

JMM 通过控制主内存与每个线程的本地内存之间的交互,来为 Java 程序提供内存可见性的保证。

怎么解决?

  • 使用Synchronized同步代码块
  • 彻底禁止JMM?禁止重排序和读取本地内存副本
  • happens-before规则:按需使用重排序和本地内存副本,前提是需要满足happens-before规则
happens-before规则

happens-before规则是JMM中的一种,保障内存可见性的方案。
happens-before的实现:1.处理器重排序规则,2.编译器重排序规则。
在这里插入图片描述

synchronized

JMM关于synchronized的两条规定:

  • 线程解锁前:必须把自己本地内存中共享变量的最新值刷新到主内存中
  • 线程加锁时:将清空本地内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值

实现过程

  1. 获得互斥锁(同步获取锁)
  2. 清空本地内存
  3. 从主内存拷贝变量的最新副本到本地内存
  4. 执行代码
  5. 将更改后的共享变量的值刷新到主内存
  6. 释放互斥锁

同步原理

同步操作主要是monitorenter和monitorexit这两个jvm指令实现的。

public class Demo05Synchronized {
   
	public synchronized void increase(){
   
		System.out.println("synchronized 方法");
	}
	public void syncBlock(){
   
		synchronized (this){
   
			System.out.println("synchronized 块");
		}
	}
}

同步代码块同步方法的字节码是不同的

  • 对于synchronized同步块,对应的monitorentermonitorexit指令分别对应synchronized同步块的进入和退出。
    • 为什么会多一个monitorexit?编译器会为同步块添加一个隐式的try-finally,在finally中会调用monitorexit命令释放锁
  • 对于synchronized方法,对应ACC_SYNCHRONIZED关键字,JVM进行方法调用时,发现调用的方法被ACC_SYNCHRONIZED修饰,则会先尝试获得锁,方法调用结束了释放锁。在JVM底层,对于这两种synchronized的实现大致相同

monitorenter和monitorexit指令,主要是基于 标记字段MarkWord和Monitor(管程)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

rainbowcheng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值