多线程

基础知识讲解与核心知识准备
  几个概念

网站流量是指网站的访问量,用来描述访问网站的用户数量以及用户所浏览的网页数量等指标,常用的统计指标包括网站的独立用户数量、总用户数量(含重复访问者)、网页浏览数量、每个用户的页面浏览数量、用户在网站的平均停留时间等。

网站访问量的常用衡量标准:独立访客(UV) 和 综合浏览量(PV),一般以日为单位来衡量和计算。

独立访客(UV):指一定时间范围内相同访客多次访问网站,只计算为1个独立访客。

综合浏览量(PV):指一定时间范围内页面浏览量或点击量,用户每次刷新即被计算一次。

PV计算带宽

计算带宽大小需要关注两个指标:峰值流量和页面的平均大小。

举个例子:

    假设网站的平均日PV:10w 的访问量,页面平均大小0.4 M 。

网站带宽 = 10w / (24 60 * 60) 0.4M * 8 =3.7 Mbps

具体的计算公式是:网站带宽= PV / 统计时间(换算到S)平均页面大小(单位KB) 8

在实际的网站运行过程中,我们的网站必须要在峰值流量时保持正常的访问,假设,峰值流量是平均流量的5倍,按照这个计算,实际需要的带宽大约在 3.7 Mbps * 5=18.5 Mbps 。

PS:1. 字节的单位是Byte,而带宽的单位是bit,1Byte=8bit,所以转换为带宽的时候,要乘以 8。

2. 在实际运行中,由于缓存、CDN、白天夜里访问量不同等原因,这个是绝对情况下的算法。

PV与并发

具体的计算公式是:并发连接数 = PV / 统计时间 * 页面衍生连接次数 * http响应时间 * 因数 / web服务器数量;

解释:

页面衍生连接次数: 一个页面请求,会有好几次http连接,如外部的css, js,图片等,这个根据实际情况而定。

http响应时间: 平均一个http请求的响应时间,可以使用1秒或更少。

因数: 峰值流量 和平均流量的倍数,一般使用5 ,最好根据实际情况计算后得出。

例子:

10PV的并发连接数: (100000PV / 86400秒 * 50个派生连接数 * 1秒内响应 * 5倍峰值) / 1台Web服务器 = 289 并发连接数

所以,如果我们能够测试出单机的并发连接数,和 日pv 数,那么我们同样也能估算出需要web的服务器数量。

还有一套通过单机 QPS计算 pv 和 需要的web服务器数量的方法,目前一些公司采用这种计算方法,但是其实计算的原理都是差不多的。

QPS、PV和需要部署机器数量计算公式(转)
  术语说明:
    QPS = req/sec = 请求数/秒

【QPS计算PV和机器的方式】

QPS统计方式 [一般使用 http_load 进行统计]

QPS = 总请求数 / ( 进程总数 * 请求时间 )

QPS: 单个进程每秒请求服务器的成功次数

单台服务器每天PV计算

公式1:每天总PV = QPS * 3600 * 6

公式2:每天总PV = QPS * 3600 * 8

服务器计算

服务器数量 = ( 每天总PV / 单台服务器每天总PV )

【峰值QPS和机器计算公式】

原理:每天80%的访问集中在20%的时间里,这20%时间叫做峰值时间

公式:( 总PV数 * 80% ) / ( 每天秒数 * 20% ) = 峰值时间每秒请求数(QPS)

机器:峰值时间每秒QPS / 单台机器的QPS = 需要的机器

例子:每天300w PV 的在单台机器上,这台机器需要多少QPS?
       ( 3000000 * 0.8 ) / (86400 * 0.2 ) = 139 (QPS)

例子:如果一台机器的QPS是58,需要几台机器来支持?

139 / 58 = 3

在这里插入图片描述
并发及并发的线程安全处理
在这里插入图片描述
高并发处理的思路及手段
在这里插入图片描述

  1. 什么是线程?

  2. 线程和进程有什么区别?

  3. 如何在Java中实现线程?

  4. 用Runnable还是Thread?

  5. Thread 类中的start() 和 run() 方法有什么区别?

  6. Java中CyclicBarrier 和 CountDownLatch有什么不同?

  7. Java中的volatile 变量是什么?

  8. Java中的同步集合与并发集合有什么区别?

10) 如何避免死锁?

  1. Java中活锁和死锁有什么区别?

12) Java中synchronized 和 ReentrantLock 有什么不同?

13) Java中ConcurrentHashMap的并发度是什么?

  1. 如何在Java中创建Immutable对象?

15) 单例模式的双检锁是什么?

  1. 写出3条你遵循的多线程最佳实践

17) 如何避免死锁?

  1. 常用的线程池模式以及不同线程池的使用场景

Netty

1.BIO、NIO和AIO的区别?

2.NIO的组成?

3.Netty的特点?

4.Netty的线程模型?

5.TCP 粘包/拆包的原因及解决方法?

6.了解哪几种序列化协议?

7.如何选择序列化协议?

8.Netty的零拷贝实现?

9.Netty的高性能表现在哪些方面?

10.NIOEventLoopGroup源码?

说一下你了解的几种进程间的通信方式

  • 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

  • 高级管道popen:将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式。

  • 有名管道named pipe :有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

  • 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

  • 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。

  • 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

  • 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。

  • 信号sinal: 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

手写一段单例中的懒汉模式和饿汉模式,并且简单说一下他们的区别

Thread和Runnable的区别和联 系、多次start一个线程会怎么样、线程有哪些状态。

:java.lang.IllegalThreadStateException 线程状态非法异常

1)bio,nio,aio的区别;

2)nio框架:dubbo的实现原理;

3)京东内部的jsf是使用的什么协议通讯:可参见dubbo的协议;

1)说说阻塞队列的实现:可以参考ArrayBlockingQueue的底层实现(锁和同步都行);

2)进程通讯的方式:消息队列,共享内存,信号量,socket通讯等;

3)用过并发包的哪些类;

4)什么地方用了多线程;

5)Excutors可以产生哪些线程池;

6)为什么要用线程池;

为什么要用线程池

那先要明白什么是线程池

线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新建一个线程。

使用线程池的好处

1、线程池改进了一个应用程序的响应时间。由于线程池中的线程已经准备好且等待被分配任务,应用程序可以直接拿来使用而不用新建一个线程。

2、线程池节省了CLR 为每个短生存周期任务创建一个完整的线程的开销并可以在任务完成后回收资源。

3、线程池根据当前在系统中运行的进程来优化线程时间片。

4、线程池允许我们开启多个任务而不用为每个线程设置属性。

5、线程池允许我们为正在执行的任务的程序参数传递一个包含状态信息的对象引用。

6、线程池可以用来解决处理一个特定请求最大线程数量限制问题。

类似参考文章,请移驾:

JAVA线程池原理详解(1)

JAVA线程池原理详解(2)

Java多线程和线程池

7)volatile关键字的用法:使多线程中的变量可见;

熟练使用Linux系统的常用命令和开发环境,对网络、IO、多线程、高并发等有实践经验

为什么需要并行(多线程)?

业务要求,图像处理,服务端编程

多核性能

cpu主频4GHZ

同步synchronous 等待返回后,继续执行

异步 asynchronous 可以立即执行其他线程。并在后台执行自己的操作。

并发concurrency:一会做一件事

–阻塞Blocking: 只允许一个线程进入临界区。

–非阻塞non-Blocking: 允许多个线程进入临界区。

–无障碍Obstruction-Free

无障碍最弱的非阻塞

自由出入临界区

无竞争,有限步内完成操作

有竞争,回滚

–无锁

无障碍

保证有一个线程可以胜出。

–无等待

无锁

所有线程必须在有限步内完成。

无饥饿

并行Parallelism:同时执行。

Deadlock

Starvation(饥饿)

Livelock

amdahl(阿姆达尔定律) 只加CPU不提高并行化不能提高性能

加速比=优化前的系统耗时/优化后的系统耗时

Tn =T1(F+1/n(1-F))

Gustafson古斯塔夫森定律

只要有足够的并行化,那么加速比和CPU个数成正比

线程间的状态转换:

  1. 新建(new):新创建了一个线程对象。

  2. 可运行(runnable):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。

  3. 运行(running):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。

  4. 阻塞(block):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:

(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。

(二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。

(三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

  1. 死亡(dead):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

线程停止 stop不安全

线程中断

Thread.interrupt() 中断线程打标记

Thread.isInterrupted() 判断是否中断

Thread.interrupted() 判断是否中断,并清除标记

线程挂起suspend

线程继续执行resume

jstack +线程号

等待线程结束join()

谦让 yield();

wait notify notifyAll 必须先拿到锁monitor

守护线程 setDaemon(true);

JMM

原子性:

有序性: 在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

可见性:

Happen-Before规则:

程序顺序原则;

volatile规则

锁规则

传递性

线程的start()方法先于它的每一个动作

线程的所有操作先于线程的终结(Thread.join)

线程的中断(interrupt())先于被中断线程的代码

对象的构造函数执行结束先于finalize方法

线程安全的概念

各种同步控制工具的使用

ReentrantLock

可重入

可中断

可限时

公平锁

Condition

AQS的源代码 AbstractQueuedSynchronizer

它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。

state的访问方式有三种:

  • getState()
  • setState()
  • compareAndSetState()

AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

不同的自定义同步器争用共享资源的方式也不同。

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,成功则返回true,失败则返回false。

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

再来总结下它的流程吧:

  1. 调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
  2. 没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
  3. acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

由于此函数是重中之重,我再用流程图总结一下:

Semaphone

ReadWriteLock:ReentrantReadWriteLock

Java并发编程:CountDownLatch、CyclicBarrier和 Semaphore

CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。

CyclicBarrier字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。

Semaphore翻译成字面意思为 信号量,Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

CountDownLatch 一个线程执行完后,count会-1

CyclicBarrier 可重复使用,用来做其他的任务

LockSupport

LockSupport.park();

LockSupport.unpark(t1);

BlockingQueue

AtomicInteger的实现原理,主要能说清楚CAS机制并且AtomicInteger是如何利用CAS机制实现的

线程池的实现原理

newCachedThreadPool:创建带有缓存的线程池。

线程池为无限大(其实是Interger. MAX_VALUE),当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

newScheduledThreadPool:支持定时和周期性执行的线程池。

newSingleThreadExecutor:此线程池是一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序。如果这个线程异常结束,会有另一个取代它,保证顺序执行。

扩展和增强线程池

回调接口

beforeExecute

afterExecute

terminated

拒绝策略

自定义ThreadFactory

ConcurrentHashMap<K, V>

Map<String,String> map = Collections.synchronizedMap(new HashMap<String,String>());

notify()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。而notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。

一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。

ThreadLocal是Java里一种特殊的变量。每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。它是为创建代价高昂的对象获取线程安全的好方法,比如你可以用ThreadLocal让SimpleDateFormat变成线程安全的,因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。首先,通过复用减少了代价高昂的对象的创建个数。其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。

在Java并发程序中FutureTask表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞。一个FutureTask对象可以对调用了Callable和Runnable的对象进行包装,由于FutureTask也是调用了Runnable接口所以它可以提交给Executor来执行。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值