JUC知识概括
JUC简介
Runtime类
Runtime类的主要作用:
在每一个JVM进程里面都会存在有一个Runtime类的对象,这个类的主要功能是取得一些与运行时有关的环境属性,或者创建新的进程。
在Runtime类定义的时候,它的构造方法已经被私有化了(单例设计模式的应用),以此保证,在整个运行过程中,只有唯一一个Runtime类的对象。所以在Runtime类里面提供有一个static方法,取得Runtime类实例对象
public static Runtime getRuntime();
Runtime类的定义形式:
Runtime类中有以下方法:
public long totalMemory();//返回所有可用内存空间
public long maxMemory();//返回最大可用内存空间
public long freeMemory();//返回空余内存空间
线程和进程
- 进程:一个程序,QQ.exe Music.exe 程序的集合;
- 一个进程往往可以包含多个线程,至少包含一个!
- Java默认有几个线程? 2 个 mian、GC(垃圾回收线程)
- 线程:开了一个进程 Typora,写字,自动保存(线程负责的)
- 对于Java而言:Thread、Runnable、Callable
- Java 开不了线程,开线程由本地方法(底层的C++) 操作执行。
方法:
- java方法:是由java语言编写,编译成字节码,存储在class文件中的。java方法是与平台无关的。
- 本地方法:本地方法是由其他语言(如C、C++或其他汇编语言)编写,编译成和处理器相关的代码。本地方法保存在动态连接库中,格式是各个平台专用的,运行中的java程序调用本地方法时,虚拟机装载包含这个本地方法的动态库,并调用这个方法。
通过本地方法,java程序可以直接访问底层操作系统的资源,但是这么用的话,程序就变成了平台相关了,因为本地方法的动态库是与平台相关的,此外,使用本地方法还可能把程序变得和特定的java平台实现相关。
java的本地方法接口JNI,使得本地方法可以在特定主机系统上的任何一个java平台上实现运行。
如果希望使用特定主机上的资源,而他们又无法从JAVA API访问,那么可以写一个平台相关的java程序来调用本地资源。如果希望保证平台的无关性,那么只能通过JAVA API 来访问底层系统的资源。
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
*
* group threads created/set up by the VM. Any new functionality added
*
* to this method in the future may have to also be added to the VM.
*
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
*
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
// 本地方法,底层的C++ ,Java 无法直接操作硬件
private native void start0();
并发与并行:
- 并发(多线程操作同一个资源):
CPU 一核 ,模拟出来多条线程,天下武功,唯快不破,快速交替 - 并行(多个人一起行走):
CPU 多核 ,多个线程可以同时执行; 线程池 - 并发编程的本质:充分利用CPU的资源
线程有几个状态:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
- 1、新生
①A thread that has not yet started is in this state.
②一个被创建的线程,但是还没有调用start方法
- 2、运行
①A thread executing in the Java virtual machine is in this state.
②一个正在被执行的线程的状态
- 3、阻塞
①A thread that is blocked waiting for a monitor lock is in this state.
②一个线程因为等待临界区的锁被阻塞产生的状态,synchronize 关键字产生的状态
- 4、等待
①A thread that is waiting indefinitely for another thread to perform a particular action is in this state.
②一个线程进入了锁,但是需要等待其他线程执行某些操作,时间不确定
。当wait,join,park方法调用时,进入waiting状态,前提是这个线程已经拥有锁了
。 - 5、超时等待
①A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.
②一个线程进入了锁,但是需要等待其他线程执行某些操作,时间确定
。通过sleep、wait或join方法进入的限期等待的状态,,前提是这个线程已经拥有锁了
。 - 6、终止
①A thread that has exited is in this state.
②退出 - 注意:
①尝试获取锁也就是说使用锁来加锁。
②当尝试获取锁失败时进入阻塞状态的只有synchronize 关键字
。
③ReentrentLock.lock()底层调用的是LockSupport.park(),因此ReentrentLock.lock()进入的是等待状态。
@Override
public void lock() {
if (!tryLock()) { //先抢锁,所以是非公平锁
//没拿到锁,放到队列中去进行排队
waiters.add(Thread.currentThread());
//等待被唤醒
for (; ; ) {
if (tryLock()) { //非公平锁情况下,唤醒过来继续获取锁
waiters.poll(); //获取锁成功把自己从队列中取出来
return;
} else //获取锁失败
LockSupport.park(); //线程阻塞
}
}
}
概述:
线程阻塞和线程等待的区别:
- 两者都表示线程当前暂停执行的状态,而两者的区别,基本可以理解为:进入 waiting 状态是线程主动的,而进入 blocked状态是被动的。
- 更进一步的说,进入 blocked 状态是在同步(synchronized)代码之外。
- 而进入 waiting 状态是在同步代码之内(然后马上退出同步)。
wait/sleep 区别:
- 来自不同的类:
wait => Object
sleep => Thread - 关于锁的释放:
wait 会释放锁
sleep 睡觉了,抱着锁睡觉,不会释放! - 使用的范围是不同的:
wait必须在同步代码块或同步方法中
sleep 可以再任何地方睡 - 是否需要捕获异常:
wait 不需要捕获异常
sleep 必须要捕获异常
Lock锁
package com.kuang.demo01;
// 基本的卖票例子
import java.time.OffsetDateTime;
/**
* 真正的多线程开发,公司中的开发,降低耦合性
* 线程就是一个单独的资源类,没有任何附属的操作!
* 1、 属性、方法
*/
public class SaleTicketDemo01 {
public static void main(String[] args) {
// 并发:多线程操作同一个资源类, 把资源类丢入线程
Ticket ticket = new Ticket();
// @FunctionalInterface 函数式接口,jdk1.8 lambda表达式 (参数)->{ 代码 }
new Thread(()->{
for (int i = 1; i < 40 ; i++) {
ticket.sale();
}
},"A").start();
new Thread(()->{
for (int i = 1; i < 40 ; i++) {
ticket.sale();
}
},"B").start();
new Thread(()->{
for (int i = 1; i < 40 ; i++) {
ticket.sale();
}
},"C").start(); } }
// 资源类 OOP
class Ticket {
// 属性、方法
private int number = 30;
// 卖票的方式
// synchronized 本质: 队列,锁
public synchronized void sale(){
if (number>0){
System.out.println(Thread.currentThread().getName()+"卖出了"+(number- -)+"票,剩余:"+number);
}
}
}
Lock接口:
公平锁:十分公平:可以先来后到
非公平锁:十分不公平:可以插队 (默认)
Synchronized 和 Lock 区别:
- Synchronized 内置的Java关键字, Lock 是一个Java类
- Synchronized 无法判断获取锁的状态,Lock可以判断是否获取到了锁
- Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁
- Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去;
- Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以 判断锁,非公平(可以自己设置);
- Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码!
生产者和消费者问题
生产者和消费者问题 Synchronized 版:
package com.kuang.pc;
/**
* 线程之间的通信问题:生产者和消费者问题! 等待唤醒,通知唤醒
* 线程交替执行 A B 操作同一个变量 num = 0
* A num+1
* B num-1
*/
public class A {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
}
// 判断等待,业务,通知
class Data{ // 数字 资源类
private int number = 0;
//+1
public synchronized void increment() throws InterruptedException {
if (number!=0){ //0
// 等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
// 通知其他线程,我+1完毕了
this.notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
if (number==0){ // 1
// 等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
// 通知其他线程,我-1完毕了
this.notifyAll();
}
}
问题:存在虚假唤醒:
虚假唤醒示例:
虚假唤醒总结:
wait()方法之所以要用while而不是if是因为 :
当多个线程并发访问同一个资源的时候, 若消费者同时被唤醒,但是只有一个资源可用, 那么if会导致资源被用完后直接去获取资源(发生越界异常等),而while则会让每个消费者获取之前再去判断一下资源是否可用.可用则获取,不可用则继续wait住.
并发编程之 wait()为什么要处于while循环中?
JUC版的生产者和消费者问题:
- 任何一个新的技术,绝对不是仅仅只是覆盖了原来的技术,优势和补充!
- Condition 可以精准的通知和唤醒线程
8锁现象
如何判断锁的是谁!永远的知道什么锁,锁到底锁的是谁!
- synchronized方法 锁的对象是方法的调用者!
- static 静态方法,类一加载就有了!锁的是Class。
- synchronized代码块,锁的是括号里的对象,对给定的对象进行加锁,进入同步代码库前要获得给定对象的锁。
集合类不安全
Java ConcurrentModificationException异常原因和解决方法
List:
- List 不安全,并发访问时会引发java.util.ConcurrentModificationException 并发修改异常!
- 解决方案:
①List list = new Vector<>();
②List list = Collections.synchronizedList(new ArrayList<> ());
③List list = new CopyOnWriteArrayList<>(); - CopyOnWrite 写入时复制:
COW 计算机程序设计领域的一种优化策略; - 多个线程调用的时候,list,读取的时候,固定的,写入(覆盖)
- 在写入的时候避免覆盖,造成数据问题!
- 读写分离
COW策略
-
COW: CopyOnWrite 写入时复制
-
1.优点
对于一些读多写少的数据,写入时复制的做法就很不错,例如配置、黑名单、物流地址等变化非常少的数据,这是一种无锁的实现。可以帮我们实现程序更高的并发。
CopyOnWriteArrayList 并发安全且性能比 Vector 好。Vector 是增删改查方法都加了synchronized 来保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而 CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于 Vector。 -
2.缺点
数据一致性问题。这种实现只是保证数据的最终一致性,在添加到拷贝数据而还没进行替换的时候,读到的仍然是旧数据。
内存占用问题。如果对象比较大,频繁地进行替换会消耗内存,从而引发 Java 的 GC 问题,这个时候,我们应该考虑其他的容器,例如 ConcurrentHashMap。
Set:
- Set 不安全,并发访问时会引发java.util.ConcurrentModificationException 并发修改异常!
- 解决方案:
①Set set = Collections.synchronizedSet(new HashSet<>());
② CopyOnWriteArraySet<>();
Map:
-
Map不安全,并发访问时会引发java.util.ConcurrentModificationException 并发修改异常!
-
解决方案:
①synchronizedMap(Map<K,V> m) ;
② ConcurrentHashMapt<>(); -
ConcurrentHashMap 同步容器类是Java 5 增加的一个线程安全的哈希表。对 与多线程的操作,介于 HashMap 与Hashtable 之间。内部采用“锁分段” 机制替代 Hashtable 的独占锁。进而提高性能。
-
锁粒度
减小锁粒度是指缩小锁定对象的范围,从而减小锁冲突的可能性,从而提高系统的并发能力。减小锁粒度是一种削弱多线程锁竞争的有效手段,这种技术典型的应用是 ConcurrentHashMap(高性能的HashMap)类的实现。对于 HashMap 而言,最重要的两个方法是 get 与 set 方法,如果我们对整个 HashMap 加锁,可以得到线程安全的对象,但是加锁粒度太大。Segment 的大小也被称为 ConcurrentHashMap 的并发度。 -
锁分段
ConcurrentHashMap,它内部细分了若干个小的 HashMap,称之为段(Segment)。默认情况下 一个 ConcurrentHashMap 被进一步细分为 16 个段,既就是锁的并发度。
如果需要在 ConcurrentHashMap 中添加一个新的表项,并不是将整个 HashMap 加锁,而是首 先根据 hashcode 得到该表项应该存放在哪个段中,然后对该段加锁,并完成 put 操作。在多线程 环境中,如果多个线程同时进行put操作,只要被加入的表项不存放在同一个段中,则线程间可以做到真正的并行。
Callable
简介:
- 可以有返回值
- 可以抛出异常
- 方法不同,run()/ call()
关系:
代码示例:
package com.kuang.callable;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.locks.ReentrantLock;
/**
* 1、探究原理
* 2、觉自己会用
*/
public class CallableTest {
public static void main(String[] args) throws ExecutionException,InterruptedException {
// new Thread(new Runnable()).start();
// new Thread(new FutureTask<V>()).start();
// new Thread(new FutureTask<V>( Callable )).start();
new Thread().start(); // 怎么启动Callable
MyThread thread = new MyThread();
FutureTask futureTask = new FutureTask(thread); // 适配类
new Thread(futureTask,"A").start();
new Thread(futureTask,"B").start(); // 结果会被缓存,效率高
Integer o = (Integer) futureTask.get(); //这个get 方法可能会产生阻塞!把他放到 最后
// 或者使用异步通信来处理!
System.out.println(o);
}
}
class MyThread implements Callable<Integer> {
@Override
public Integer call() {
System.out.println("call()");
// 会打印几个call
// 耗时的操作
return 1024;
}
//细节:
//1、有缓存
//2、结果可能需要等待,会阻塞!
线程的工作内存
- 根据JLS(java语言规范)对线程工作内存的描述,线程的working memory只是cpu的寄存器和高速缓存的抽象描述。
线程的工作内存
常用的辅助类
CountDownLatch
简介:
- 允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。
- A CountDownLatch用给定的计数初始化。await方法阻塞,直到由于countDown()方法的调用而导致当前计数达到零,之后所有等待线程被释放,并且任何后续的await调用立即返回。 这是一个一次性的现象 - 计数无法重置。
代码示例
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 总数是6,必须要执行任务的时候,再使用!
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <=6 ; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" Go out");
countDownLatch.countDown(); // 数量-1
},String.valueOf(i)).start();
}
countDownLatch.await();
// 等待计数器归零,然后再向下执行
System.out.println("Close Door");
}
}
原理:
- countDownLatch.countDown(); // 数量-1
- countDownLatch.await(); // 等待计数器归零,然后再向下执行
- 每次有线程调用 countDown() 数量-1,假设计数器变为0,countDownLatch.await() 就会被唤醒,继续 执行!
CyclicBarrier
简介:
- 允许一组线程全部等待彼此达到共同屏障点的同步辅助。 循环阻塞在涉及固定大小的线程方的程序中很有用,这些线程必须偶尔等待彼此。 屏障被称为循环 ,因为它可以在等待的线程被释放之后重新使用。
- A CyclicBarrier支持一个可选的Runnable命令,每个屏障点运行一次,在派对中的最后一个线程到达之后,但在任何线程释放之前。 在任何一方继续进行之前,此屏障操作对更新共享状态很有用。
代码示例:
public class CyclicBarrierDemo {
public static void main(String[] args) {
/**
* 集齐7颗龙珠召唤神龙
*/
// 召唤龙珠的线程
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("召唤神龙成功!");
});
for (int i = 1; i <=7 ; i++) {
final int temp = i;
// lambda能操作到 i 吗
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"收 集"+temp+"个龙珠");
try {
cyclicBarrier.await(); // 等待
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
Semaphore
简介:
- 一个计数信号量。 在概念上,信号量维持一组许可证。 如果有必要,每个acquire()都会阻塞,直到许可证可用,然后才能使用它。每个release()添加许可证,潜在地释放阻塞获取方。 但是,没有使用实际的许可证对象;Semaphore只保留可用数量的计数,并相应地执行。
- 信号量通常用于限制线程数,而不是访问某些(物理或逻辑)资源。
代码示例:
public class SemaphoreDemo {
public static void main(String[] args) {
// 线程数量:停车位! 限流!
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <=6 ; i++) {
new Thread(()->{
// acquire() 得到
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"抢到车 位");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"离开车 位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
// release() 释放
}
},String.valueOf(i)).start();
}
}
}
原理:
- semaphore.acquire() 获得,假设如果已经满了,等待,等待被释放为止!
- semaphore.release(); 释放,会将当前的信号量释放 + 1,然后唤醒等待的线程!
- 作用: 多个共享资源互斥的使用!并发限流,控制最大的线程数!
读写锁
ReadWriteLock:
特点:
- 独占锁(写锁)---- 一次只能被一个线程占有
- 共享锁(读锁)----多个线程可以同时占有
- 读-读 可以共存!
- 读-写 不能共存!
- 写-写 不能共存!
阻塞队列
阻塞队列:
什么情况下我们会使用 阻塞队列:
- 多线程并发处理,
- 线程池!
四组API:
SynchronousQueue 同步队列:
- 没有容量,进去一个元素,必须等待取出来之后,才能再往里面放一个元素!
- 和其他的BlockingQueue 不一样, SynchronousQueue 不存储元素 put了一个元素,必须从里面先take取出来,否则不能在put进去值!
AQS
- AQS:AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等),JUC并发包的作者(DougLea)期望它能够成为实现大部分同步需求的基础。它是JUC并发包中的核心基础组件。
- AQS解决了子啊实现同步器时涉及当的大量细节问题,例如获取同步状态、FIFO同步队列。基于AQS来构建同步器可以带来很多好处。它不仅能够极大地减少实现工作,而且也不必处理在多个位置上发生的竞争问题。
- 在基于AQS构建的同步器中,只能在一个时刻发生阻塞,从而降低上下文切换的开销,提高了吞吐量。同时在设计AQS时充分考虑了可伸缩行,因此J.U.C中所有基于AQS构建的同步器均可以获得这个优势。
- AQS的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。
- AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取了锁,当state =0时表示释放了锁。它提供了三个方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))来对同步状态state进行操作,当然AQS可以确保对state的操作是安全的。
- AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。
线程池
线程池简介:
- 三大方法:
①Executors.newSingleThreadExecutor()//单个线程
②Executors.newFixedThreadPool(n); // 创建一 个固定的线程池的大小
③Executors.newCachedThreadPool(); // 可伸缩的,遇强则强,遇弱则弱
④Executors.newScheduledThreadPool(n);//可安排在给定延迟后运 行命令或者定期地执行
注意:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors返回的线程池对象的弊端如下:
<1>FixedThreadPool和 singleThreadPool和CachedThreadPool和 scheduledThreadPool:
允许的请求队列长度为Integer.MAX_VALUE(约为21亿),可能会堆积大量的请求,从而导致OOM。 - 7大参数:
int corePoolSize, // 核心线程池大小
int maximumPoolSize, // 最大核心线程池大小:当线程数>=corePoolSize,且任务队列已满时。
线程池会创建新线程来处理任务。
long keepAliveTime, // 线程空闲时间:当线程空闲时间达到keepAliveTime时,线程会退出,
直到线程数量=corePoolSize。
TimeUnit unit, // 超时单位
BlockingQueue<Runnable> workQueue, // 阻塞队列
ThreadFactory threadFactory, // 线程工厂:创建线程的,一般不用动
RejectedExecutionHandler handle // 拒绝策略
- 4种拒绝策略:
①new ThreadPoolExecutor.AbortPolicy()
<1>银行满了,还有人进来,不处理这个人的,抛出异常
<2>这种策略在拒绝任务时,会直接抛出一个类型为RejectedExecutionException 的 RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
②new ThreadPoolExecutor.CallerRunsPolicy()
<1>哪来的去哪里!
<2>这种策略算是最完善的相对于其他三个,当线程池无能力处理当前任务时,会将这个任务的执行权交予提交任务的线程来执行,也就是谁提交谁负责,这样的话提交的任务就不会被丢弃而造成业务损失,同时这种谁提交谁负责的策略必须让提交线程来负责执行,如果任务比较耗时,那么这段时间内提交任务的线程也会处于忙碌状态而无法继续提交任务,这样也就减缓了任务的提交速度,这相当于一个负反馈。也有利于线程池中的线程来消化任务。
③new ThreadPoolExecutor.DiscardPolicy()
<1>队列满了,丢掉任务,不会抛出异常!
<2>这种策略是当任务提交时直接将刚提交的任务丢弃,而且不会给与任何提示通知,所以这种策略使用要慎重,因为有一定的风险,对我们来说根本不知道提交的任务有没有被丢弃。
④new ThreadPoolExecutor.DiscardOldestPolicy()
<1>队列满了,尝试去和最早的竞争,也不会抛出异常!
<2>这种策略和上面相似。不过它丢弃的是队列中的头节点,也就是存活时间最久的。 - 流程图:
池化技术简介:
- 程序的运行,本质:占用系统的资源! 优化资源的使用!=>池化技术
- 池化技术:事先准备好一些资源,有人要用,就来我这里拿,用完之后还给我。
- 线程池的好处:
1、降低资源的消耗
2、提高响应的速度
3、方便管理。 - 线程复用、可以控制最大并发数、管理线程。
- 什么时候用多线程?
①要开发的项目是一个IT问答平台,其中用户发布问题后,需要给平台上所有选了该问题类别的邮箱中各发一份邮件。
线程池参数设置:
- 经验值(机器性能):配置线程数量首先要看任务的类型是 IO密集型还是CPU密集型
①IO密集型:
<1>频繁读取磁盘上的数据,或者需要通过网络远程调用接口。
<2>IO密集型配置线程数经验值是:2N,其中N代表CPU核数。
1、对于 IO 密集型任务最大线程数一般会大于 CPU 核心数很多倍,因为 IO 读写速度相比于 CPU 的速度而言是比较慢的,如果我们设置过少的线程数,就可能导致 CPU 资源的浪费
。而如果我们设置更多的线程数,那么当一部分线程正在等待 IO 的时候,它们此时并不需要 CPU 来计算,那么另外的线程便可以利用 CPU 去执行其他的任务,互不影响,这样的话在任务队列中等待的任务就会减少,可以更好地利用资源。
②CPU密集型:
<1>非常复杂的调用,循环次数很多,或者递归调用层次很深等。
<2>CPU密集型配置线程数经验值是:N + 1,其中N代表CPU核数。
1、如果设置过多的线程数,实际上并不会起到很好的效果。此时假设我们设置的线程数量是 CPU 核心数的 2 倍,因为计算任务非常重,会占用大量的 CPU 资源
,所以这时 CPU 的每个核心工作基本都是满负荷的,
2、而我们又设置了过多的线程,每个线程都想去利用 CPU 资源来执行自己的任务,这就会造成不必要的上下文切换
,此时线程数的增多并没有让性能提升,反而由于线程数量过多会导致性能下降。
③混合型如果IO密集型,和CPU密集型的执行时间相差不太大,可以拆分开,以便于更好配置。如果执行时间相差太大,优化的意义不大,比如IO密集型耗时60s,CPU密集型耗时1s。 - 《Java虚拟机并发编程》中提出的计算方式(机器性能):
①线程数 = CPU 核心数 / (1 - 阻塞系数)
②其中计算密集型阻塞系数为 0,IO 密集型阻塞系数接近 1,一般认为在 0.8 ~ 0.9 之间。比如 8 核 CPU,按照公式就是 2 / ( 1 - 0.9 ) = 20 个线程数
③阻塞系数可以通过公式:阻塞系数=阻塞时间/(阻塞时间+计算时间)。 - 《Java并发编程实战》中提出的计算方式(机器性能):
①线程数 = CPU 核心数 * (1 + IO 耗时/ CPU 耗时)
②通过这个公式,我们可以计算出一个合理的线程数量,如果任务的平均等待时间长,线程数就随之增加,而如果平均工作时间长,也就是对于我们上面的 CPU 密集型任务,线程数就随之减少。 - 根据生产机器数以及并发量(实际任务):
①生产服务器个数
②接口请求最高峰与最低峰个数
③每台机器处理请求个数(最大/核心) = (接口请求最高峰与最低峰个数)/生产服务器个数 - 根据性能要求配置(不实际):
①tasks :每秒的任务数,假设为500~1000
②taskcost:每个任务花费时间,假设为0.1s
③responsetime:系统允许容忍的最大响应时间,假设为1s
④threadcount = tasks/(1/taskcost) = tasks*taskcout = (500 ~ 1000)*0.1 = 50~100 个线程。 - 总结:
①虽说最佳线程数目算法更准确,但是线程等待时间和线程CPU时间不好测量,实际情况使用得比较少,一般用经验值就差不多了。再配合实际的系统压测,基本可以确定最适合的线程数。
②优先按照机器性能来设置核心线程数,然后再来看实际任务是多少,如果实在太大,可以考虑集群了。
③链接:
<1>线程池中各个参数如何合理设置
<2>动态调整线程池参数实践
<3>别再纠结线程池大小 + 线程数量了,没有固定公式的!
四大函数式接口和流式计算
新时代的程序员:
- lambda表达式、
- 链式编程、
- 函数式接口、
- Stream流式计算
函数式接口:
- 只有一个方法的接口
- Function 函数型接口, 有一个输入参数,有一个输出
- 断定型接口:有一个输入参数,返回值只能是 布尔值!
- Consumer 消费型接口: 只有输入,没有返回值
- Supplier 供给型接口 没有参数,只有返回值
Stream流式计算:
- 大数据:存储 + 计算
- 集合、MySQL 本质就是存储东西的;
- 计算都应该交给流来操作!
- 在传统的数据处理流程中,总是先收集数据,然后将数据放到DB中。当人们需要的时候通过DB对数据做query,得到答案或进行相关的处理。这样看起来虽然非常合理,但是结果却非常的紧凑,尤其是在一些实时搜索应用环境中的某些具体问题,类似于MapReduce方式的离线处理并不能很好地解决问题。这就引出了一种新的数据计算结构—流计算方式。它可以很好地对大规模流动数据在不断变化的运动过程中实时地进行分析,捕捉到可能有用的信息,并把结果发送到下一计算节点。
ForkJoin
什么是 ForkJoin:
- ForkJoin 在 JDK 1.7 , 并行执行任务!提高效率。大数据量!
- 大数据:Map Reduce (把大任务拆分为小任务)
- ForkJoin 特点:工作窃取
- 这个里面维护的都是双端队列
步骤:
- new一个类继承Class ForkJoinTask抽象基类的 RecursiveAction 或RecursiveTask实现类
- 然后new ForkJoinPool类,运行execute(ForkJoinTask<?> task) 方法。
异步回调
Future 设计的初衷:
- 对将来的某个事件的结果进行建模
- A Future计算的结果。 提供方法来检查计算是否完成,等待其完成,并检索计算结果。 结果只能在计算完成后使用方法get进行检索,如有必要,阻塞,直到准备就绪。 取消由cancel方法执行。 提供其他方法来确定任务是否正常完成或被取消。 计算完成后,不能取消计算。 如果您想使用Future ,以便不可撤销,但不提供可用的结果,则可以声明Future<?>表格的类型,并返回null作为基础任务的结果。
步骤:
- 使用链式编程。
System.out.println(completableFuture.whenComplete((t, u) -> {
System.out.println("t=>" + t); // 正常的返回结果
System.out.println("u=>" + u); // 错误信息:java.util.concurrent.CompletionException
}).exceptionally((e) -> {
System.out.println(e.getMessage());
return 23;// 可以获取到错误的返回结果
}).get());
JMM
JMM详解:
- 链接:全面理解Java内存模型(JMM)及volatile关键字
Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
- 由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,
- 首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,
- 其简要访问过程如下图:
JMM与Java内存区域的区别:
JMM与Java内存区域的划分是不同的概念层次,更恰当说JMM描述的是一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性,有序性、可见性展开的。
- JMM与Java内存区域唯一相似点,都存在共享数据区域和私有数据区域,在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。
- 或许在某些地方,我们可能会看见主内存被描述为堆内存,工作内存被称为线程栈,实际上他们表达的都是同一个含义。
JMM中的主内存和工作内存说明:
主内存:
①主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。
②由于是共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题。工作内存:
①主要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,
②就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关Native方法的信息。
③注意由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。
主内存与工作内存的数据存储类型以及操作方式:
- 根据虚拟机规范,对于一个实例对象中的成员方法而言,如果方法中包含本地变量是基本数据类型(boolean,byte,short,char,int,long,float,double),将直接存储在工作内存的帧栈结构中。
- 但倘若本地变量是引用类型,那么该变量的引用会存储在功能内存的帧栈中,而对象实例将存储在主内存(共享数据区域,堆)中。
- 但对于实例对象的成员变量,不管它是基本数据类型或者包装类型(Integer、Double等)还是引用类型,都会被存储到堆区。
- 至于static变量以及类本身相关信息将会存储在主内存中。
- 需要注意的是,在主内存中的实例对象可以被多线程共享,倘若两个线程同时调用了同一个对象的同一个方法,那么两条线程会将要操作的数据拷贝一份到自己的工作内存中,执行完成操作后才刷新到主内存,
- 简单示意图如下所示:
硬件内存架构:
- 经过简化CPU与内存操作的简易图,实际上没有这么简单,这里为了理解方便,我们省去了南北桥并将三级缓存统一为CPU缓存(有些CPU只有二级缓存,有些CPU有三级缓存)。
- 就目前计算机而言,一般拥有多个CPU并且每个CPU可能存在多个核心,多核是指在一枚处理器(CPU)中集成两个或多个完整的计算引擎(内核),这样就可以支持多任务并行执行,从多线程的调度来说,每个线程都会映射到各个CPU核心中并行运行。
- 在CPU内部有一组CPU寄存器,寄存器是cpu直接访问和处理的数据,是一个临时放数据的空间。一般CPU都会从内存取数据到寄存器,然后进行处理,但由于内存的处理速度远远低于CPU,导致CPU在处理指令时往往花费很多时间在等待内存做准备工作,
- 于是在寄存器和主内存间添加了CPU缓存,CPU缓存比较小,但访问速度比主内存快得多,如果CPU总是操作主内存中的同一址地的数据,很容易影响CPU执行速度,此时CPU缓存就可以把从内存提取的数据暂时保存起来,如果寄存器要取内存中同一位置的数据,直接从缓存中提取,无需直接从主内存取。需要注意的是,寄存器并不每次数据都可以从缓存中取得数据,万一不是同一个内存地址中的数据,那寄存器还必须直接绕过缓存从内存中取数据。
- 所以并不每次都得到缓存中取数据,这种现象有个专业的名称叫做缓存的命中率,从缓存中取就命中,不从缓存中取从内存中取,就没命中,可见缓存命中率的高低也会影响CPU执行性能,这就是CPU、缓存以及主内存间的简要交互过程,
- 总而言之当一个CPU需要访问主存时,会先读取一部分主存数据到CPU缓存(当然如果CPU缓存中存在需要的数据就会直接从缓存获取),进而在读取CPU缓存到寄存器,当CPU需要写数据到主存时,同样会先刷新寄存器中的数据到CPU缓存,然后再把数据刷新到主内存中。
Java线程与硬件处理器:
- 在Window系统和Linux系统上,Java线程的实现是基于一对一的线程模型,所谓的一对一模型,实际上就是通过语言级别层面程序去间接调用系统内核的线程模型,即我们在使用Java线程时,Java虚拟机内部是转而调用当前操作系统的内核线程来完成当前任务。
- 这里需要了解一个术语,内核线程(Kernel-Level Thread,KLT),它是由操作系统内核(Kernel)支持的线程,这种线程是由操作系统内核来完成线程切换,内核通过操作调度器进而对线程执行调度,并将线程的任务映射到各个处理器上。
- 每个内核线程可以视为内核的一个分身,这也就是操作系统可以同时处理多任务的原因。由于我们编写的多线程程序属于语言层面的,程序一般不会直接去调用内核线程,取而代之的是一种轻量级的进程(Light Weight Process),也是通常意义上的线程,由于每个轻量级进程都会映射到一个内核线程,
- 因此我们可以通过轻量级进程调用内核线程,进而由操作系统内核将任务映射到各个处理器,这种轻量级进程与内核线程间1对1的关系就称为一对一的线程模型。
- 如下图:
每个线程最终都会映射到CPU中进行处理,如果CPU存在多核,那么一个CPU将可以并行执行多个线程任务。
Java内存模型与硬件内存架构的关系:
- 多线程的执行最终都会映射到硬件处理器上进行执行,但Java内存模型和硬件内存架构并不完全一致。对于硬件内存来说只有寄存器、缓存内存、主内存的概念,并没有工作内存(线程私有数据区域)和主内存(堆内存)之分,也就是说Java内存模型对内存的划分对硬件内存并没有任何影响,
- 因为JMM只是一种抽象的概念,是一组规则,并不实际存在,不管是工作内存的数据还是主内存的数据,对于计算机硬件来说都会存储在计算机主内存中,当然也有可能存储到CPU缓存或者寄存器中,
- 因此总体上来说,Java内存模型和计算机硬件内存架构是一个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉。(注意对于Java内存区域划分也是同样的道理)
JMM存在的必要性:
- 由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,线程与主内存中的变量操作必须通过工作内存间接完成,主要过程是将变量从主内存拷贝的每个线程各自的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,如果存在两个线程同时对一个主内存中的实例对象的变量进行操作就有可能诱发线程安全问题。
- 如下图,主内存中存在一个共享变量x,现在有A和B两条线程分别对该变量x=1进行操作,A/B线程各自的工作内存中存在共享变量副本x。
①假设现在A线程想要修改x的值为2,而B线程却想要读取x的值,那么B线程读取到的值是A线程更新后的值2还是更新前的值1呢?
②答案是不确定,即B线程有可能读取到A线程更新前的值1,也有可能读取到A线程更新后的值2,这是因为工作内存是每个线程私有的数据区域,而线程A变量x时,
③首先是将变量从主内存拷贝到A线程的工作内存中,然后对变量进行操作,操作完成后再将变量x写回主内,而对于B线程的也是类似的,这样就有可能造成主内存与工作内存间数据存在一致性问题,
④假如A线程修改完后正在将数据写回主内存,而B线程此时正在读取主内存,即将x=1拷贝到自己的工作内存中,这样B线程读取到的值就是x=1,但如果A线程已将x=2写回主内存后,B线程才开始读取的话,那么此时B线程读取到的就是x=2,
⑤但到底是哪种情况先发生呢?这是不确定的,这也就是所谓的线程安全问题。
- 为了解决类似上述的问题,JVM定义了一组规则,通过这组规则来决定一个线程对共享变量的写入何时对另一个线程可见,
这组规则也称为Java内存模型(即JMM)
,JMM是围绕着程序执行的原子性、有序性、可见性展开的。
什么是JMM:
- JMM : Java内存模型,不存在的东西,概念!约定!
- 链接:线程的缓存何时刷新?
- 关于JMM的一些同步的约定:
①线程解锁前,必须把共享变量立刻刷回主存。
②线程加锁前,必须读取主存中的最新值到工作内存中!
③加锁和解锁是同一把锁
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外):
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM对这八种指令的使用,制定了如下规则:
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须 write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
Volatile
Volatile 是 Java 虚拟机提供轻量级的同步机制:
- 1、保证可见性
- 2、不保证原子性
- 3、禁止指令重排
如果不加 lock 和 synchronized ,怎么样保证原子性:
- 使用原子类,解决 原子性问题
- 这些类的底层都直接和操作系统挂钩!在内存中修改值!Unsafe类是一个很特殊的存在!
指令重排:
- 指令重排:你写的程序,计算机并不是按照你写的那样去执行的。
- 源代码–>编译器优化的重排–> 指令并行也可能会重排–> 内存系统也会重排—> 执行
- 处理器在进行指令重排的时候,考虑:数据之间的依赖性!
volatile可以避免指令重排:
- 内存屏障。CPU指令。作用:
1、大多数现代计算机为了提高性能而采取乱序执行,这使得内存屏障成为必须。语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障。
2、保证特定的操作的执行顺序!
3、可以保证某些变量的内存可见性 (利用这些特性volatile实现了可见性)
单例模式
- 饿汉式 :可能会浪费空间
- 懒汉式:不安全
- DCL懒汉式(双重检测锁模式的):安全,但仍可通过反射获取,因此需在构造方法处在处理一下。
- 静态内部类:不安全(可通过反射)
- 枚举:安全(通过反射抛异常)
CAS
CAS(compareAndSet : 比较并交换):
- public final boolean compareAndSet(int expect, int update)
- 比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环!CAS 是CPU的并发原语!
- 缺点:
1、 循环会耗时
2、一次性只能保证一个共享变量的原子性
3、ABA问题
Unsafe 类:
- Java无法操作内存
- Java可以调用Native本地方法(C++编写)
- C++可以操作内存
ABA问题:
- 解决ABA 问题, 引入原子引用! 对应的思想:乐观锁!
- 带版本号 的原子操作!
各种锁
公平锁、非公平锁
公平锁: 非常公平, 不能够插队,必须先来后到!
非公平锁:非常不公平,可以插队 (默认都是非公平)
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
可重入锁(递归锁)
可重入锁:
- Synchronized 和Lock等都是使用的可重入锁。
- 概念:可重入锁意味着:若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。
- 原理:重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。
自旋锁
自旋锁:
- 在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
代码案例:
/**
* 自旋锁
*/
public class SpinlockDemo {
// int 0
// Thread null
AtomicReference<Thread> atomicReference = new AtomicReference<>();
// 加锁
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "==> mylock");
// 自旋锁
while (!atomicReference.compareAndSet(null,thread)){
}
}
// 解锁
public void myUnLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "==> myUnlock");
atomicReference.compareAndSet(thread,null);
}
}
死锁
死锁:
解决问题:
- 日志
- 堆栈信息:
① 使用 jps -l 定位进程号
② 使用 jstack 进程号 找到死锁问题
锁粗化/锁消除
- 锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。
- 锁粗化:如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部
- 锁粗化和消除其实设计原理都差不多,都是为了减少没必要的加锁
偏向锁/轻量级锁/重量级锁
- 这三种锁是指锁的状态,并且是针对Synchronized。在Java5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
- 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
- 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
- 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。