- 基于JUC的多线程与高并发
- 一、线程概述
- 二、多线程底层机制
- Java stack information for the threads listed above:
- 2.6、JVM对锁的优化
- 三、高并发及解决方法
基于JUC的多线程与高并发
一、线程概述
1.1、线程的概念
1.1.1、进程
- 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是操作系统进行资源分配与调度的基本单位。
- 可以把进程简单的理解为正在操作系统中运行的一个程序。
1.1.2、线程
- 线程(thread)是进程的一个执行单元。
- 一个线程就是进程中一个单一顺序的控制流, 进程的一个执行分支 。
- 进程是线程的容器,一个进程至少有一个线程,一个进程中也可以有多个线程。
- 在操作系统中是以进程为单位分配资源,如虚拟存储空间,文件描 述符等。
- 每个线程都有各自的线程栈,自己的寄存器环境,自己的线程本地存储。
1.1.3、主线程与子线程
- JVM 启动时会创建一个主线程,该主线程负责执行 main 方法。
- 主线程就是运行 main 方法的线程。
- Java 中的线程不孤立的,线程之间存在一些联系
- 如果在 A 线程中 创建了 B 线程, 称 B 线程为 A 线程的子线程, 相应的 A 线程就是 B 线 程的父线程
1.1.4、串行、并发与并行
- 串行
- 并发可以提高事务的处理效率,即一段时间内可以处理或者完成更多的事情。
- 并行是一种更为严格,理想的并发,从硬件角度来说,,如果单核 CPU,一个处理器一次只能执行一个线程的情况下,处理器可以使用时间片轮转技术 ,可以让 CPU 快速的在各个线程之间进行切换,,对于用户来说,感觉是三个线程在同时执行,如果是多核心 CPU,可以为不同的线程分配不同的 CPU 内核。
1.2、线程的创建与启动
- 在 Java 中,创建一个线程就是创建一个 Thread 类(子类)的对象(实 例)。
- Thread 类有两个常用 的构造方法:Thread()与 Thread(Runnable)
- 对应的创建线程的两种方式:
- 定义 Thread 类的子类
- 定义一个 Runnable 接口的实现类
- 这两种创建线程的方式没有本质的区别
- start()方法是同时执行,run()方法执行先进入run()方法体
- 线程创建流程
- 定义类继承 Thread
- 重写 Thread 父类中的 run
- 创建子线程对象
- 启动线程
- 注意事项
- 调用线程的 start()方法来启动线程, 启动线程的实质就是请求 JVM运行相应的线程,这个线程具体在什么时候运行由线程调度器(Scheduler)决定
- start()方法调用结束并不意味着子线程开始运行
- 新开启的线程会执行 run()方法
- 如果开启了多个线程,start()调用的顺序并不一定就是线程启动的顺序
- 多线程运行结果与代码执行顺序或调用顺序无关
- 运行无顺序,执行有顺序,静态方法>无参方法>普通方法>run()方法
1.2.1、创建线程
/**
* @author hguo
* @date2021/5/12 22:10
* @title 创建线程执行类
*/
//1、继承Thread父类,重写run()方法
public class Createthread extends Thread {
//run()就是子线程执行的任务
@Override
public void run() {
System.out.println("子线程打印内容");
}
@Test
public void createThread(){
System.out.println("JVMQ启动main线程,main线程启动main方法!");
//2、创建子线程对象
Createthread createthread = new Createthread();
//3、启动线程
createthread.start();
//4、打印线程启动结果
System.out.println("main线程已启动");
}
}
1.2.2、线程运行的随机性
/**
* @author hguo
* @date2021/5/12 22:44
* @title 多线程随机运行
*/
public class CreateThreadRandom extends Thread {
@Override
public void run() {
try {
for (int i = 1; i < 10; i++) {
System.out.println("thread:" + i);
int time = (int) (Math.random() * 1000);
//线程随眠随机
Thread.sleep(time);
}
}catch (InterruptedException e) {
e.printStackTrace();
}
}
@Test
public void CreateThreadRandom(){
//2、创建子线程对象
CreateThreadRandom createThreadRandom = new CreateThreadRandom();
//3、启动线程
createThreadRandom.start();
try {
for (int i = 1; i < 10; i++) {
System.out.println("--main:" + i);
int time = (int) (Math.random() * 1000);
//线程随眠随机
Thread.sleep(time);
}
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
1.2.3、以Runnable接口创建线程
- 线程的执行具有随机性
实现流程
- 定义实现类实现Runnable接口
- 重写Runnable接口的抽象方法run()
- 创建Runnable接口实现类对象
- 创建线程对象
- 启动线程 ```java /**
- @author hguo
- @date2021/5/12 23:14
@title Runnable接口创建线程 */ //1、定义实现类实现Runnable接口 public class RunnableCreateThread implements Runnable { //2、重写Runnable接口的抽象方法run() public void run() { //run()方法就是子线程执行的代码 for (int i = 1; i <10 ; i++) {
System.out.println("thread: "+i);
} }
@Test public void RunnableCreateThread(){ //3、创建Runnable接口实现类对象 RunnableCreateThread runnableCreateThread = new RunnableCreateThread(); //4.创建线程对象 Thread thread = new Thread(runnableCreateThread); //5、启动线程 thread.start(); //run()方法就是子线程执行的代码 for (int i = 1; i <10 ; i++) {
System.out.println("main: "+i);
} } } ```
1.2.4、以Callable接口创建线程
- Callable接口有返回值,重写的是call()方法,需要抛出异常
- 创建步骤:
- 创建目标对象
- 执行服务
ExecutorService threadPool = Executors.newSingleThreadExecutor();
- 提交执行
Future<String> future = threadPool.submit(new CreateCallable());
- 获取结果
future.get()
- 关闭服务
threadPool.shutdown();
/**
* @author hguo
* @date2021/5/18 22:35
* @title Callable接口实现
*/
public class CreateCallable implements Callable {
@Override
public Object call() throws Exception {
// 模拟耗时任务
Thread.sleep(1000);
System.out.println("CreateCallable 线程:" + Thread.currentThread().getName());
return "CreateCallable" ;
}
public static void main(String[] args) {
System.out.println("main start");
ExecutorService threadPool = Executors.newSingleThreadExecutor();
Future<String> future = threadPool.submit(new CreateCallable());
try {
// 这里会发生阻塞
System.out.println(future.get());
} catch (Exception e) {
} finally {
threadPool.shutdown();
}
System.out.println("main end");
}
}
1.2.5、龟兔赛跑解析线程竞争
模拟龟兔赛跑的游戏,定义两个线程竞争一个资源,在此期间,设定兔子有休息时间
/**
* @author hguo
* @date2021/5/18 21:59
* @title 模拟龟兔赛跑解析线程竞争问题
*/
public class TortoiseRabbitrRace extends Thread {
//定义胜利者
private static String winner;
@Override
public void run() {
//设置长度
for (int i = 0; i <=1000; i++) {
//让兔子线程休眠,每10ms休眠一次
if(Thread.currentThread().getName().equals("") && i%10==0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//判断比赛是否结束
boolean game = gameOver(i);
//如果比赛结束就停止程序
if(game){
break;
}
System.out.println(Thread.currentThread().getName() + "-->跑了 " + i + "米");
}
}
//判断比赛进度
public boolean gameOver(int steps){
//如何比赛结束,胜利者诞生
if(winner!=null){
return true;
}
//如果跑完长度等于100米,获取最先跑完的线程
if(steps==1000){
winner=Thread.currentThread().getName();
System.out.println("winner is:" + winner);
return true;
}
//没有跑完100米
return false;
}
public static void main(String[] args) {
//定义子线程
TortoiseRabbitrRace race = new TortoiseRabbitrRace();
new Thread(race,"乌龟").start();
new Thread(race,"兔子").start();
}
}
1.3、线程的常用方法
- Java 中的任何一段代码都是执行在某个线程当中的
- 执行当前代码的线程就是当前线程.
- 同一段代码可能被不同的线程执行, 因此当前线程是相对的
- Thread.currentThread()方法的返回值是在代码实际运行时候的线程对象
1、常用方法
/**
* @author hguo
* @date2021/5/13 18:54
* @title 线程常用方法
*/
//定义线程类
public class CurrentThreadMethod extends Thread{
@Override
public void run() {
System.out.println("run方法打印线程:"+Thread.currentThread().getName());
}
//无参构造
public CurrentThreadMethod(){
System.out.println("构造方法打印线程:"+Thread.currentThread().getName());
}
@Test
public void CurrentThreadMethod(){
System.out.println("main方法打印线程:" +Thread.currentThread().getName());
//创建子线程对象
CurrentThreadMethod currentThreadMethod = new CurrentThreadMethod();
//启动子线程
currentThreadMethod.start();
//run()方法不开启新的线程
//currentThreadMethod.run();
}
}
2、常用复杂方法
- setName()/getName()
- thread.serName(线程名称),设置线程名称
- thread.getName() 返回线程名称
- 通过设置线程名称,有助于程序调试,提高程序的可读性, 建议为每个线程都设置一个能够体现线程功能的名
/**
* @author hguo
* @date2021/5/13 20:21
* @title 复杂的线程方法
*/
public class CurrentThreadMethod2 extends Thread {
//重写run方法
@Override
public void run() {
System.out.println("run方法,Thread.currentThread().getName():"+Thread.currentThread().getName());
System.out.println("run方法,this.getName():"+this.getName());
}
//定义无参构造
public CurrentThreadMethod2() {
System.out.println("构造方法,Thread.currentThread().getName():"+Thread.currentThread().getName());
System.out.println("构造方法,this.getName():"+this.getName());
}
//执行测试
@Test
public void CurrentThreadMethod2() throws Exception {
//创建子线程
CurrentThreadMethod2 currentThreadMethod2 = new CurrentThreadMethod2();
//设置线程名称
currentThreadMethod2.setName("thread_2");
//启动线程2
currentThreadMethod2.start();
//线程休眠
Thread.sleep(500);
//调用Thread(Runnable)构造方法,传入实参currentThreadMethod2
Thread thread = new Thread(currentThreadMethod2);
//启动线程3
thread.start();
}
}
3、isAlive()活动状态
- thread.isAlive()判断当前线程是否处于活动状态
- 活动状态就是线程已启动并且尚未终止
/**
* @author hguo
* @date2021/5/13 20:52
* @title isAlive()活动状态的的使用
*/
//创建线程类
public class CurrentThreadMethod3 extends Thread {
@Override
public void run() {
//查看run()的活动状态
System.out.println("run方法,isAlive="+this.isAlive());
}
@Test
public void CurrentThreadMethod3(){
//创建子线程对象
CurrentThreadMethod3 currentThreadMethod3 = new CurrentThreadMethod3();
//查看子线程启动前活动状态
System.out.println("start="+currentThreadMethod3.isAlive());
//启动线程
currentThreadMethod3.start();
//查看子线程启动后活动状态
System.out.println("end="+currentThreadMethod3.isAlive());
}
}
4、sleep()线程睡眠
- Thread.sleep(millis); 让当前线程休眠指定的毫秒数
- 当前线程是指 Thread.currentThread()返回的线程
- 主线程启动,main方法就启动,main方法启动,子线程或run()方法才能启动(start只能启动线程,没有其他作用)
- 子线程或run()方法结束,main()方法才能结束,主线程才能结束
/**
* @author hguo
* @date2021/5/13 21:03
* @title sleep()线程睡眠方法
*/
public class CurrentThreadMethod4 extends Thread{
@Override
public void run() {
try {
System.out.println("run,threadname="+Thread.currentThread().getName()+",start-="+System.currentTimeMillis());
//当前线程睡眠2秒
Thread.sleep(2000);
System.out.println("run,threadname="+Thread.currentThread().getName()+",end-="+System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Test
public void CurrentThreadMethod4(){
//创建子线程
CurrentThreadMethod4 currentThreadMethod4 = new CurrentThreadMethod4();
//打印main方法启动
System.out.println("main_start="+System.currentTimeMillis());
//启动线程
//currentThreadMethod4.start();
//执行run()方法
currentThreadMethod4.run();
System.out.println("main_end="+System.currentTimeMillis());
}
}
5、简单的sleep计数器
- 使用Thread.sleep()方法控制线程睡眠时间,从而达到计时的效果
/**
* @author hguo
* @date2021/5/13 21:19
* @title sleep()实现计时器
*/
class SiomlpeTimer {
public static void main(String[] args){
//定义计时
int times = 60;
//读取main方法参数
if(args.length ==1){
times = Integer.parseInt(args[0]);
}
while (true){
System.out.println("Times:"+times);
times--;
if (times<0){
break;
}
try {
//执行线程睡眠
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("时间到!");
}
}
6、getId()线程唯一标识
- thread.getId()可以获得线程的唯一标识
- 注意的是
- 某个编号的线程运行结束后,该编号可能被后续创建的线程使
- 用重启JVM后同一个线程的编号可能不一样 ```java /**
- @author hguo
- @date2021/5/13 21:40
- @title 获取线程唯一表示
*/
public class GetIdThread extends Thread {
@Override
public void run() {
System.out.println(“thread_name=”+Thread.currentThread().getName()+”,id=”+this.getId());
}
@Test
public void GetIdThread() throws Exception {
System.out.println(“main_name=”+Thread.currentThread().getName()+”,id=”+Thread.currentThread().getId());
//获取子线程id
for (int i = 1; i <100 ; i++) {
} } } ```new GetIdThread().start();
Thread.sleep(100);
7、yied()放弃CPU资源
- Thread.yield()方法的作用是放弃当前的 CPU 资源,也称线程让步
/**
* @author hguo
* @date2021/5/13 21:49
* @title yield()线程让步,放弃CPU分配资源
*/
public class YieldThread extends Thread {
@Override
public void run() {
long begin = System.currentTimeMillis();
long sum =0;
for (int i = 1; i <1000000 ; i++) {
sum+=i;
//线程让步,放弃CPU执行权
Thread.yield();
}
long end = System.currentTimeMillis();
System.out.println("run方法,用时:"+(end-begin)+"毫秒");
}
public static void main(String[] arg){
//开启子线程,计算sum
YieldThread yieldThread = new YieldThread();
yieldThread.start();
//在main线程中计算sum
long begin = System.currentTimeMillis();
long sum = 0;
for (int i = 0; i <1000000 ; i++) {
sum+=1;
}
long end =System.currentTimeMillis();
System.out.println("main方法,用时:"+(end-begin)+"毫秒");
}
}
- 线程未让步时
- 线程让步后
8、setPriority()设置线程优先级
- Thread.setPriority( num )设置线程的优先级
- Java线程的优先级取值范围是 1 ~ 10 , 如果超出这个范围会抛出异常 IllegalArgumentException
- 在操作系统中,优先级较高的线程获得 CPU 的资源越多
- 线程优先级本质上是只是给线程调度器一个提示信息,以便于调度器决定先调度哪些线程, 注意不能保证优先级高的线程先运行
- Java 优先级设置不当或者滥用可能会导致某些线程永远无法得到运行,即产生了线程饥饿.
- 线程的优先级并不是设置的越高越好,一般情况下使用普通的优先级即可,即在开发时不必设置线程的优先级
- 程的优先级具有继承性, 在 A 线程中创建了 B 线程,则 B 线程的 优先级与 A 线程是一样的.
/**
* @author hguo
* @date2021/5/13 22:03
* @title 设置线程优先级
*/
public class ThreadsetPriority {
static class ThreadsetPriorityA extends Thread{
@Override
public void run() {
long begin = System.currentTimeMillis();
long sum = 0 ;
for(long i = 0 ; i<= 10000000; i++){
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("thread A : " + (end - begin));
}
}
static class ThreadsetPriorityB extends Thread{
@Override
public void run() {
long begin = System.currentTimeMillis();
long sum = 0 ;
for(long i = 0 ; i<= 10000000; i++){
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("thread B : " + (end - begin));
}
}
public static void main(String[] args){
//创建线程A对象
ThreadsetPriorityA threadsetPriorityA = new ThreadsetPriorityA();
//设置优先级
threadsetPriorityA.setPriority(1);
//启动线程A
threadsetPriorityA.start();
//创建线程B对象
ThreadsetPriorityB threadsetPriorityB = new ThreadsetPriorityB();
//设置优先级
threadsetPriorityB.setPriority(9);
//启动线程B
threadsetPriorityB.start();
}
}
- 不设置时
- 设置后
9、interrupt()线程中断
- 中断线程只是一个停止标志,真正的线程不会停止
- 注意调用interrupt()方法仅仅是在当前线程打一个停止标志,并不是真正的停止线程
/**
* @author hguo
* @date2021/5/13 22:22
* @title 控制线程中断,只是做一个停止标志而非真正停止线程
*/
public class InterruptThread extends Thread{
@Override
public void run() {
super.run();
for(int i = 1; i <= 10000; i++) {
//判断线程的中断标志,线程有 isInterrupted()方法,该方法返回线程的中断标志
if (this.isInterrupted()) {
System.out.println("当前线程的中断标志为 true, 中断线程");
//break; // 中断循环
//run()方法体执行完毕, 子线程运行完毕
//直接结束当前 run()方法的执行
return;
}
System.out.println(" run --> " + i);
}
}
public static void main(String[] args) {
InterruptThread interruptThread = new InterruptThread();
interruptThread.start(); ///开启子线程
//当前线程是 main 线程
for(int i = 1; i<=100; i++){
System.out.println("main --> " + i);
}
//中断子线程
interruptThread.interrupt(); ////仅仅是给子线程标记中断, }
}
}
10、setDaemon()线程守护
- Java 中的线程分为用户线程与守护线程
- 守护线程是为其他线程提供服务的线程,如垃圾回收器(GC)就是一 个典型的守护线程
- 守护线程不能单独运行, 当 JVM中没有其他用户线程,只有守护线程时,守护线程会自动销毁, JVM 会退出
/**
* @author hguo
* @date2021/5/13 22:31
* @title 守护线程
*/
public class ThreadsetDeamon extends Thread {
@Override
public void run() {
super.run();
while(true){
System.out.println(" thread.....");
}
}
public static void main(String[] args) {
ThreadsetDeamon thread = new ThreadsetDeamon();
//设置线程为守护线程且在线程启动前
thread.setDaemon(true);
thread.start();
//当前线程为 main 线程
for(int i = 1; i <= 10 ; i++){
System.out.println("main== " + i);
}
//当 main 线程结束, 守护线程 thread 也销毁了
}
}
1.4、线程的生命周期
1.4.1、线程周期介绍
- 线程的生命周期是线程对象的生老病死,即线程的状态
- 线程生命周期可以通过 getState()方法获得, 线程的状态是Thread.State实现的线程生命周期,分为5个部分:分别是新建状态、就绪状态、运行状态,阻塞状态、死亡状态。
- new:新建状态。
- 创建了线程对象,在调用start()启动之前的状态;
- Runnable:就绪状态。
- 当线程对象创建后,该线程对象自身或者其他对象调用了该对象的start()方法。该线程就位于了可运行池中,变的可运行,等待获取cpu的使用权。因为在同一时间里cpu只能执行某一个线程。
- Running:运行状态。
- 当就绪状态的线程获取了cpu的时间片或者说获取了cpu的执行时间,这时就会调用该线程对象的run()方法,然后就从就绪状态就入了运行状态。
- Blocked:阻塞状态。
- 阻塞状态就是线程因为某种原因暂时放弃了对cpu的使用权,暂时停止运行。直到线程再次进入就绪状态,才有机会转到运行状态。阻塞状态分为三种情况:
- 等待状态:运行状态的线程调用了wait()方法后,该线程会释放它所持有的锁,然后被jvm放入到等待池中,只有等其他线程调用Object类的notify()方法或者norifyAll()方法时,才能进入重新进入到就绪状态。
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,JVM就会把该线程设置为阻塞状态,一直到线程获取到同步锁,才能转入就绪状态。
- 其它阻塞:运行的线程在执行sleep()或者join()方法时,或者发出了I/O请求,JVM就会把该线程设置为阻塞状态,当sleep()状态超时、join()等待等待线程终止或者超时、或者I/O处理完毕时,线程重进转入到就绪状态。在这需要注意的是sleep()方法和wait()不同,sleep不会释放自身所持有的锁。
- 阻塞状态就是线程因为某种原因暂时放弃了对cpu的使用权,暂时停止运行。直到线程再次进入就绪状态,才有机会转到运行状态。阻塞状态分为三种情况:
- Dead:死亡状态。
- 当线程执行完了或者因异常退出了run()的执行,该线程的生命周期就结束了,死亡的线程不可再次复生。
- new:新建状态。
- 自我理解(这个例子味道极重,得先酝酿)
- 你去商城时突然想上厕所
- 准备去上厕所这个过程,就是新建状态(new)
- 人很多就要排队,排队的过程就是就绪状态(Runnable)
- 终于有位置了,放飞自我的过程就是运行状态(Running)
- 拉完后发现没有手纸,等待别人给送纸过来,这个状态就是阻塞(Blocked)
- 上完厕所出来,上厕所这件事情结束了,就是死亡状态,也就是线程销毁(Dead)
- 注意:便秘也是阻塞状态
- 第一种情况,便秘太久了,别人等不及了,把你赶走,就是挂起
- 另一种情况,便秘了,别人等不及了,跟你说你先出去酝酿一下,我先上,5分钟后再过来继续放飞自我,就是睡眠
- 你去商城时突然想上厕所
1.4.2、多线程的适用分析
- 优势
- 提高系统的吞吐率(Throughout)。多线程编程可以使一个进程有多个并发,同时进行的操作
- 提高响应性(Responsiveness)。Web服务器会加快了程序的响应速度,缩短了用户的等待时间
- 充分利用多核(Multicore)处理器资源。 通过多线程可以充分的 利用 CPU 资源
- 可以分别设置优先级以优化性能
- 缺点
- 线程安全(Thread safe)问题。多线程共享数据时,如果没有采取正确的并发访问控制措施,就可能会产生数据一致性问题。如数据脏读,丢失数据更新
- 线程活性(thread liveness)问题。由于程序自身的缺陷或者由资源稀缺性导致线程一直处于非 Runnable状态,这就是线程活性问题, 常见的活性故障有以下几种:
- 死锁(Deadlock)。
- 多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。类似于有结果的死循环原理
- 造成原因
- 恶性竞争资源
- 进程间推进顺序非法(插队)
- 死锁的必要条件
- 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 环路等待条件:在发生死锁时,必然存在一个进程—资源的环形链。
- 预防死锁
- 资源一次分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
- 剥夺资源:当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
- 资源有序分配:按照序号分配资源,每一个进程按编号递增的顺序请求资源。
- 活锁(Livelock)。
- 线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。这样你让我,我让你,最后两个线程都无法使用资源。类似无结果的死循环原理
- 饥饿(Starvation)。
- 是指如果线程T1占用了资源R,线程T2又请求封锁R,于是T2等待。T3也请求资源R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待。然后T4又请求封锁R,当T3释放了R上的封锁之后,系统又批准了T4的请求……,T2可能永远等待。类似于请求无响应原理
- 死锁(Deadlock)。
- 上下文切换(Context Switch)。
- 处理器从执行一个线程切换到执,行另外一个线程
- 可靠性。
- 可能会由一个线程导致 JVM 意外终止,其他的线程也无法执行
1.5、线程安全问题
1.5.1、线程的原子性
- 原子(Atomic)就是不可分割的意思。原子操作的不可分割有两层含义:
- 访问(读,写)某个共享变量的操作从其他线程来看,该操作要么已经执行完毕要么尚未发生,即其他线程看不到当前操作的中间结果
- 访问同一组共享变量的原子操作是不能够交叉的,如现实生活中从 ATM 机取款, 对于用户来说,要么操作成功,用户拿到钱, 余额减少了,增加了一条交易记录; 要么没拿到钱,相当于取款操作没有发生
- Java 有两种方式实现原子性:
- 一种是使用锁;
- 另一种利用处理器 的 CAS(Compare and Swap)指令。锁具有排它性,保证共享变量在某一时刻只能被一个线程访问,CAS 指令直接在硬件(处理器和内存)层次上实现,看作是硬件锁
1.5.2、线程的可见性
- 在多线程环境中, 一个线程对某个共享变量进行更新之后 , 后续其他的线程可能无法立即读到这个更新的结果, 这就是线程安全问 题的另外一种形式: 可见性(visibility)
- 如果一个线程对共享变量更新后, 后续访问该变量的其他线程可 以读到更新的结果, 称这个线程对共享变量的更新对其他线程可见, 否则称这个线程对共享变量的更新对其他线程不可见
- 多线程程序因为可见性问题可能会导致其他线程读取到了旧数据 (脏数据)
1.5.3、线程的有序性
- 有序性(Ordering)是指在什么情况下一个处理器上运行的一个线程所执行的内存访问操作在另外一个处理器运行的其他线程看来是乱序的(Out of Order)
- 乱序是指内存访问操作的顺序看起来发生了变化
1、重排序
- 在多核处理器的环境下,编写的顺序结构,这种操作执行的顺序可能是没有保障的:
- 编译器可能会改变两个操作的先后顺序;
- 处理器也可能不会按照目标代码的顺序执行;
- 这种一个处理器上执行的多个操作,在其他处理器来看它的顺序与目标代码指定的顺序可能不一样,这种现象称为重排序
- 重排序是对内存访问有序操作的一种优化,可以在不影响单线程程序正确的情况下提升程序的性能.但是,可能对多线程程序的正确性产生影响,即可能导致线程安全问题
- 重排序与可见性问题类似,不是必然出现的
- 与内存操作顺序有关的几个概念:
- 源代码顺序, 就是源码中指定的内存访问顺序.
- 程序顺序, 处理器上运行的目标代码所指定的内存访问顺序
- 执行顺序,内存访问操作在处理器上的实际执行顺序
- 感知顺序,给定处理器所感知到的该处理器及其他处理器的内存访问操作的顺序
- 可以把重排序分为指令重排序与存储子系统重排序两种
- 指令重排序主要是由 JIT 编译器,处理器引起的, 指程序顺序与执
- 行顺序不一样存储子系统重排序是由高速缓存,写缓冲器引起的, 感知顺序与执行顺序不一致
2、指令重排序
- 在源码顺序与程序顺序不一致,或者程序顺序与执行顺序不一致的情况下,我们就说发生了指令重排序(Instruction Reorder).
- 指令重排是一种动作,确实对指令的顺序做了调整, 重排序的对象指令
- javac 编译器一般不会执行指令重排序, 而 JIT 编译器可能执行指令重排序.
- 处理器也可能执行指令重排序, 使得执行顺序与程序顺序不一致
- 指令重排不会对单线程程序的结果正确性产生影响,可能导致多 线程程序出现非预期的结果.
3、存储子系统重排序
- 存储子系统是指写缓冲器与高速缓存.
- 高速缓存(Cache)是 CPU 中为了匹配与主内存处理速度不匹配而设计的一个高速缓存
- 写缓冲器(Store buffer, Write buffer)用来提高写高速缓存操作的效率
二、多线程底层机制
2.1、Java内存模式
- 每个线程都又独立的栈空间
- 每个线程都可以访问堆内存
- CPU不会直接从内存上读取数据,而是通过高速缓存来实现数据读取
- 计算机读取数据时并不是直接从CPU中读取,而是从内存中的缓存区读取,将数据读取到寄存器中
- 每个线程都能从寄存器中读取数据,但是在不同的CPU上不能相互读取,因为寄存器是独立的
- CPU、缓存区、寄存器都是相互独立的,不能跨域读取。但是达成缓存一致性协议时,可以读取别的缓存区的数据,这种情况称为缓存同步
- 线程共享
- 每个线程之间的共享数据都存储在主内存中
- 每个线程都有一个私有的本地内存
- 每个线程从主内存中把数据读取到本地内存中,保存副本线程并处理,该线程数据情况只对当前线程可见
- Java运行时存储
- Java运行时(Java runtime)空间可以分为栈区,堆区与方法区(非堆空 间)
- 栈空间(Stack Space)为线程的执行准备一段固定大小的存储空间, 每个线程都有独立的线程栈空间,创建线程时就为线程分配栈空间.
- 在线程栈中每调用一个方法就给方法分配一个栈帧,栈帧用于存储方法的局部变量,返回值等私有数据, 即局部变量存储在栈空间中, 基本类型变量也是存储在栈空间中, 引用类型变量值也是存储在栈空间中,引用的对象存储在堆中.
- 由于线程栈是相互独立的,一个线程不能访问另外一个线程的栈空间,因此线程对局部变量以及只能通过当前线程的局部变量才能访问的对象进行的操作具有固定的线程安全性
2.2、线程安全机制
2.2.1、线程安全的两种常见方法
- 线程加锁 ```java //获取锁对象 对象名.lock(); threadValue value =new threadValue(); value.lock();
//释放锁对象 value.unlock();
- 添加同步代码块或者同步方法
```java
//执行同步方法,在线程定义的方法中添加synchronized关键字
public synchronized void doLongTimeTest() {
......
}
//添加同步代码块
public void doLongTimeTest() {
synchronized (this){
......
}
}
2.2.2、线程同步
- 线程同步机制是一套用于协调线程之间的数据访问的机制.该机 制可以保障线程安全
- Java 平台提供的线程同步机制包括:
- 锁, volatile 关键字, final 关键 字,static 关键字
- 相关的 API,如 Object.wait()/Object.notify()等
2.2.3、锁机制
- 锁(Lock)可以理解为对共享数据进行保护的一个许可证,对于同 一个许可证保护的共享数据来说,任何线程想要访问这些共享数据必须先持有该许可证
- 一个锁只能被一个线程持有,但是一个线程可以持有多个锁
- 线程安全问题的产生前提是多个线程并发访问共享数据
- 将多个线程对共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问。锁就是复用这种思路来保障线程安全的
- 锁的持有线程在获得 锁之后和释放锁之前这段时间所执行的代码称为临界区
- 当线程出现异常时,会自动释放锁,届时线程安全无法保障
- 锁具有排他性(Exclusive), 即一个锁一次只能被一个线程持有。这种锁称为排它锁或互斥锁(Mutex)
- JVM中的锁
- 内部锁通过 synchronized 关键字实现
- 显示锁通过 java.concurrent.locks.Lock 接口的实现类实现的
2.2.4、锁的作用
- 锁可以实现对共享数据的安全访问
- 保障线程的原子性,可见性与有序性
- 锁是通过互斥保障原子性
- 可见性的保障是通过写线程冲刷处理器的缓存和读线程刷新处理器缓存这两个动作实现的
- 锁能够保障有序性.写线程在临界区所执行的在读线程所执行的 临界区看来像是完全按照源码顺序执行的
- 使用锁保障线程的安全性,必须满足以下条件:
- 这些线程在访问共享数据时必须使用同一个锁
- 即使是读取共享数据的线程也需要使用同步锁
2.2.5、锁的其他概念
- 可重入性
- 一个线程在持有一个锁的同时再次申请该锁或其他锁的行为
- 锁的争用与调度
- Java 平台中内部锁属于非公平锁,,显示 Lock 锁既支持公平锁又支 持非公平锁
- 锁的粒度
- 一个锁可以保护的共享数据的数量大小称为锁的粒度
- 锁保护共享数据量大,称该锁的粒度粗
- 锁的粒度过粗会导致线程在申请锁时会进行不必要的等待
- 锁的粒度过细会增加锁调度的开销
- 排他锁
- Java 中的每个对象都有一个与之关联的内部锁(Intrinsic lock).
- 这种锁也称为监视器(Monitor), 可以保障原子性,可见性与有序性
2.2.6、synchronized 同步代码块
- 同步可以理解未串行
- 创建两个线程对象,执行start()方法,线程是同时运行的,两个执行的线程子对象都能获取CPU的执行权
- synchronized 排他锁开启后,就只有一个线程能够获取CPU执行权,第二个线程只能进入等待区等待现场一执行完,才启动线程
- JVM执行的过程是自上到下,所以默认了最前面一个线程对象首先获取CPU执行权
- 实现线程同步,必须保证两个或多个线程子对象一致,确保锁对象的一致性,不然无法实现线程同步的效果
锁的同步
/**
* @author hguo
* @date2021/5/19 22:21
* @title synchronized同步代码块
*/
public class synchronized_01 {
//定义方法打印字符串
public void print(){
//经常使用this当前对象最为锁对象
synchronized (this){
for (int i = 0; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}
public static void main(String[] args) {
final synchronized_01 synchronized_01 = new synchronized_01();
//thread-0
new Thread(new Runnable() {
@Override
public void run() {
//使用锁的对象this就是synchronized_01对象
synchronized_01.print();
}
}).start();
//thread-1
new Thread(new Runnable() {
@Override
public void run() {
synchronized_01.print();
}
}).start();
}
}
同步代码块与同步方法的对比
/**
* @author hguo
* @date2021/5/19 22:21
* @title synchronized同步代码块与同步方法的效率对比
*/
public class Synchronized_02 {
public void doLongTimeTest1() throws InterruptedException {
System.out.println("Test start");
//模拟线程准备3秒时间
Thread.sleep(3000);
//执行同步代码块
synchronized (this){
System.out.println("开始同步");
for (int i = 0; i < 101; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
System.out.println("Test end");
}
public synchronized void doLongTimeTest() throws InterruptedException {
System.out.println("Test start");
//模拟线程准备3秒时间
Thread.sleep(3000);
//执行同步代码块
synchronized (this){
System.out.println("开始同步");
for (int i = 0; i < 101; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
System.out.println("Test end");
}
public static void main(String[] args) {
//创建线程子对象
final Synchronized_02 synchronized_02 = new Synchronized_02();
//创建线程一
new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized_02.doLongTimeTest();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
//创建线程二
new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized_02.doLongTimeTest();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
- 同步代码块,锁的粒度细,执行效率相对来说,较高
- 同步方法,需要进入方法体再运行,效率较低
2.2.7、脏读
- 出现读取属性值出现了一些意外, 读取的是中间值,而不是修改之后的值
- 出现脏读的原因
- 对共享数据的修改或读取不同步
解决方法
- 不仅对修改数据的代码块进行同步,还要对读取数据的代码块同步 ```java /**
- @author hguo
- @date2021/5/19 22:21
@title 脏读 */ public class Synchronized_03 { static class publicValue{ private String name = “hello”; private String pwd = “123456”;
public void getValue(){
System.out.println(Thread.currentThread().getName() + ",getter -- name :" + name + ", -- pwd:" + pwd);
} public void setValue(String name,String pwd){
this.name=name;
try {
//模拟线程准备1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.pwd=pwd;
System.out.println(Thread.currentThread().getName() + ",setter -- name:" + name + ", --pwd:" + pwd);
}
//定义线程,设置用户名和密码 static class SubThread extends Thread{ private publicValue publicValue; public SubThread(publicValue publicValue){ this.publicValue=publicValue; } @Override public void run() { publicValue.setValue(“hello11”,”111”); } }
public static void main(String[] args) throws InterruptedException { //开启子线程 publicValue publicValue = new publicValue(); SubThread subThread = new SubThread(publicValue); subThread.start(); //等待线程开启 Thread.sleep(1000); //读取信息 publicValue.getValue(); }
}
2.2.8、死锁
造成死锁的原因:
在多线程程序中,同步时可能需要使用多个锁,如果获得锁的顺序不一致,就可能造成死锁
如何避免:
当需要获得多个锁时,所有线程获得锁的顺序保持一致即可
2.3、轻量级同步机制(volatile)
2.3.1、volatile关键字的作用
- 使变量在多个线程之间可见,解决了线程之间的可见性和有序性
- 内存可见性(Memory Visibility):所有线程都能看到共享内存的最新状态。
- 防止指令重排序
2.3.2、volatile于Synchronized的区别
volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比 synchronized要好;
volatile只能修饰变量,而synchronized可以修饰方法以及代码块;
多线程访问volatile变量不会发生阻塞,而synchronized可能会阻塞;
volatile 能保证数据的可见性,但是不能保证原子性; 而 synchronized 可以保证原子性,也可以保证可见性;
关键字volatile解决的是变量在多个线程之间的可见性,synchronized 关键字解决多个线程之间访问公共资源的同步性
2.3.3、volatile的非原子性
- volatile 关键字增加了实例变量在多个 线程之间的可见性,但是不具备原子性
2.4、JUC线程安全(重点)
2.4.1、理解JUC
- JUC实质是一个包类名,是==Java.util.concurrent==的首字母简写
- 在JUC中包含了线程安全的实现方法及各类锁机制
- 在Java应用中,几乎涵盖了所有底层安全的内容
2.4.2、 线程安全的两种实现方法
1、Synchronized线程同步
- synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。
- 线程是一个单独的资源类,没有任何附属的操作,在发开中,尽量保障线程干净
- 一个好的线程中只能有属性和方法两样东西
- 线程同步就是线程有序执行,保证锁的正常获取和释放
/**
* @author hguo
* @date2021/5/23 14:30
* @title 定义售票线程
*/
public class SaleThread {
static class Sale{
//定义属性,总票数
private int num = 13;
//定义买票方法,设置同步方法
public synchronized void sales(){
if(num>0){
System.out.println(Thread.currentThread().getName() + "卖出了" + (num--) + "票,还剩" + num + "票");
}
}
}
public static void main(String[] args) {
//多线程操作同一个资源
//定义线程对象
final Sale sale = new Sale();
new Thread(() -> {
for (int i = 0; i < 4; i++) {
sale.sales();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
sale.sales();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 6; i++) {
sale.sales();
}
}, "C").start();
}
}
2、Lock机制实现线程同步
- lock锁包括了读锁、写锁、读写锁、可重入锁等
- 相较于Synchronized同步,lock更加轻便灵活
- 在lock锁中还有公平锁和非公平锁
- 公平锁:必须保证线程和锁的同步
- 非公平锁:可以插队,可按照执行实现分配线程和锁的顺序,默认是非公平锁
- 锁的使用
- 加锁、lock.lock();
- 业务代码
- 解锁、lock.unlock();
//加锁操作
lock.lock();
try {
if(num>0){
System.out.println(Thread.currentThread().getName() + "卖出了" + (num--) + "票,还剩" + num + "票");
}
} catch (Exception e) {
e.printStackTrace();
}finally {
//解锁
lock.unlock();
}
- 实例说明
/**
* @author hguo
* @date2021/5/23 14:30
* @title 定义售票线程
*/
public class SaleThreadLock {
static class Sale{
//定义属性,总票数
private int num = 13;
//定义锁
Lock lock=new ReentrantLock();
}
}
##### 3、Synchronized与Llock的区别
* 所在包结构不同
* Synchronized是**java内置的关键字**,Lock是一个Java类
* Synchronized**无法判断获取锁状态**,Lock可以判断是否获取到锁的状态
* Synchronized**会自动释放锁**,Lock要手动释放,如果不及时释放,将会造成死锁
* Synchronized在多线程运行时,可**能会造成多线程等待或阻塞**的情况,Lock不会
* Synchronized是**可重入的,不能中断,默认非公平锁**,Lock可重入,可中断,可选择是否公平锁或非公平锁
#### 2.4.3、生产者和消费者模式
##### 1、JVM级别的生产者与消费者
* 成对出现,一个生产对应一个消费,要让线程同步才能做到一一对应
* 开发使用
* 等待
* 业务
* 通知
```java
/**
* @author hguo
* @date2021/5/23 15:37
* @title 线程生产者与消费者
*/
public class ProducerConsumer {
static class Data{
private int num = 0;
//+1,生产者
public synchronized void increment() throws InterruptedException {
if(num!=0){
//等待操作
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName() + "--->" + num);
//通知线程,+1完毕
this.notifyAll();
}
//-1,消费者
public synchronized void decrement() throws InterruptedException {
if(num==0){
//等待操作
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName() + "--->" + num);
//通知线程,-1完毕
this.notifyAll();
}
}
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();
}
}
虚假唤醒,多线程中常见的问题之一(在现有A、B两个线程的基础上,再加线程就可能导致这个问题)
- if判断容易造成虚假唤醒
- 当线程中满足条件时,需求变更加线程时,if判断改成while判断即可
- 原因时if只能单次判断,while可以多次判断 ```java /**
- @author hguo
- @date2021/5/23 15:37
@title 线程生产者与消费者 */ public class ProducerConsumer {
static class Data{ private int num = 0; //+1,生产者 public synchronized void increment() throws InterruptedException {
while(num!=0){
//等待操作
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName() + "--->" + num);
//通知线程,+1完毕
this.notifyAll();
}
//-1,消费者 public synchronized void decrement() throws InterruptedException {
while (num==0){
//等待操作
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName() + "--->" + num);
//通知线程,-1完毕
this.notifyAll();
} }
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(); new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},”C”).start(); new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},”D”).start(); } } ```
2、JUC级别的生产者和消费者
- JUC中,lock锁的等待使用的是await()方法
- 传统方法中,synchronized等待使用的是wait()方法
conditon的优势
- JUC中lock锁的condition默认方法是随机执行的
- 实现condition精准唤醒通知,多定义多执行,开发中不建议使用 ```java **
- @author hguo
- @date2021/5/23 15:37
@title 线程生产者与消费者 */ public class ProducerConsumerJUC2 {
static class Data{ private int num = 1; //定义锁 Lock lock = new ReentrantLock(); Condition condition1 = lock.newCondition(); Condition condition2 = lock.newCondition(); Condition condition3 = lock.newCondition(); Condition condition4 = lock.newCondition(); ```
}
- JUC中lock锁的condition默认方法是随机执行的
2.4.4、八锁现象理解(重点)
1、多个线程使用同一把锁,顺序执行
- 多个线程持有一个对象,共用一把锁,执行顺序只与上下顺序有关,先调用先执行
示例:标志访问,先打电话还是先发短信?
/**
* @author hguo
* @date2021/5/23 21:18
* @title 多个线程使用同一把锁,顺序执行
*/
public class Lock_01 {
//定义一个功能类
static class Phone{
//功能类中实现功能的具体方法,synchronized修饰线程同步
public synchronized void sendinfo(){
System.out.println("---->发短信");
}
public synchronized void call(){
System.out.println("---->打电话");
}
}
public static void main(String[] args) {
Phone phone = new Phone();
//两个线程公用一个phone对象,先调用先执行
new Thread(()->{phone.sendinfo();},"A").start();
//干扰,使线程睡眠一秒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone.call();},"B").start();
}
}
2、多个线程使用同一把锁,其中某线程里存在阻塞,顺序执行
- 多个线程持有一个对象,共用一把锁,执行顺序只与上下顺序有关,先调用的先执行,即使在某方法中设置了阻塞。
示例:邮件方法暂停4秒钟,先打电话还是短信?
/**
* @author hguo
* @date2021/5/23 21:18
* @title 多个线程使用同一把锁,顺序执行
*/
public class Lock_02 {
//定义一个功能类
static class Phone{
//功能类中实现功能的具体方法,synchronized修饰线程同步
public synchronized void sendinfo(){
//干扰,使线程睡眠一秒
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("---->发短信");
}
public synchronized void call(){
System.out.println("---->打电话");
}
}
public static void main(String[] args) {
Phone phone = new Phone();
//两个线程公用一个phone对象,先调用先执行
new Thread(()->{phone.sendinfo();},"A").start();
//干扰,使线程睡眠一秒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone.call();},"B").start();
}
}
3、多个线程有锁无锁共存,无锁先行
- 有的线程有锁,有的线程无锁,线程之间不存在竞争同一把锁的情况,在不干涉无锁线程的前提下先后执行顺序是无锁先执行,因为它不用去竞争对象锁
示例:新增一个普通方法接收微信()没有同步,请问先打印邮件还是接收微信?
/**
* @author hguo
* @date2021/5/23 21:18
* @title 多个线程有锁无锁共存,无锁先行
*/
public class Lock_03 {
//定义一个功能类
static class Phone{
//功能类中实现功能的具体方法,synchronized修饰线程同步
public synchronized void sendinfo(){
//干扰,使线程睡眠一秒
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("---->发短信");
}
public synchronized void call(){
System.out.println("---->打电话");
}
//接收微信,没有锁
public void getWeChat() {
System.out.println("---->打开微信");
}
}
public static void main(String[] args) {
Phone phone = new Phone();
//两个线程公用一个phone对象,先调用先执行
new Thread(()->{phone.sendinfo();},"A").start();
//干扰,使线程睡眠一秒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone.call();},"B").start();
new Thread(()->{phone.getWeChat();},"C").start();
}
}
4、多个线程使用多把锁,随机执行
- 被 synchronized 修饰的方法,锁的对象是方法的调用者,调用者不同,它们之间用的不是同一个锁,相互之间没有竞争关系。
- 没有其他元素干扰的条件下,按照上下顺序执行,有干扰时,谁用时最短谁先执行
示例:两部手机、请问先打印邮件还是短信?
/**
* @author hguo
* @date2021/5/23 21:18
* @title 多个线程使用多把锁,随机执行
*/
public class Lock_04 {
//定义一个功能类
static class Phone{
//功能类中实现功能的具体方法,synchronized修饰线程同步
public synchronized void sendinfo(){
//干扰,使线程睡眠一秒
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("---->发短信");
}
public synchronized void call(){
System.out.println("---->打电话");
}
}
public static void main(String[] args) {
Phone phone_1 = new Phone();
Phone phone_2 = new Phone();
//两个线程公用一个phone对象,先调用先执行
new Thread(()->{phone_1.sendinfo();},"A").start();
//干扰,使线程睡眠一秒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone_2.call();},"B").start();
}
}
5、class锁:多个线程使用一个对象,顺序执行
- 被 synchronized 和 static 同时修饰的方法,锁的对象是类的 class 对象,是唯一的一把锁,线程之间是顺序执行。
锁Class和锁对象的区别:
1、Class 锁 ,类模版,只有一个,加载既有;
2、对象锁 , 通过类模板可以new 多个对象。
如果全部都锁了Class,那么这个类下的所有对象都具有同一把锁。
示例:两个静态同步方法,同一部手机,请问先打印邮件还是短信?/**
* @author hguo
* @date2021/5/23 21:18
* @title class锁:多个线程使用一个对象,顺序执行
*/
public class Lock_05 {
//定义一个功能类
static class Phone{
//功能类中实现功能的具体方法,synchronized修饰线程同步
public synchronized static void sendinfo(){
//干扰,使线程睡眠一秒
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("---->发短信");
}
public synchronized static void call(){
System.out.println("---->打电话");
}
}
public static void main(String[] args) {
Phone phone = new Phone();
//两个线程公用一个phone对象,先调用先执行
new Thread(()->{phone.sendinfo();},"A").start();
//干扰,使线程睡眠一秒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone.call();},"B").start();
}
}
6、class锁:多个线程使用多个对象,顺序执行
- 被 synchronized 修饰 和 static 修饰的方法,锁的对象是类的 class 对象,是唯一的一把锁。
- Class锁是唯一的,所以多个对象使用的也是同一个Class锁。
示例:两个静态同步方法,2部手机,请问先打印邮件还是短信?
/**
* @author hguo
* @date2021/5/23 21:18
* @title class锁:多个线程使用多个对象,顺序执行
*/
public class Lock_05 {
//定义一个功能类
static class Phone{
//功能类中实现功能的具体方法,synchronized修饰线程同步
public synchronized static void sendinfo(){
//干扰,使线程睡眠一秒
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("---->发短信");
}
public synchronized static void call(){
System.out.println("---->打电话");
}
}
public static void main(String[] args) {
Phone phone_1 = new Phone();
Phone phone_2 = new Phone();
//两个线程公用一个phone对象,先调用先执行
new Thread(()->{phone_1.sendinfo();},"A").start();
//干扰,使线程睡眠一秒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone_2.call();},"B").start();
}
}
7、class锁与对象锁:多个线程使用一个对象,顺序执行
- 被 synchronized和static修饰的方法,锁的对象是类的class对象!唯一的同一把锁;
- 只被synchronized修饰的方法,是普通锁,不是Class锁,线程之间不存在竞争同一把锁的情况,顺序执行。
实例:一个普通同步方法,一个静态同步方法,同一部手机,请问先打印邮件还是短信?
/**
* @author hguo
* @date2021/5/23 21:18
* @title class锁:多个线程使用多个对象,顺序执行
*/
public class Lock_06 {
//定义一个功能类
static class Phone{
//功能类中实现功能的具体方法,synchronized修饰线程同步
public synchronized static void sendinfo(){
//干扰,使线程睡眠一秒
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("---->发短信");
}
public synchronized void call(){
System.out.println("---->打电话");
}
}
public static void main(String[] args) {
Phone phone = new Phone();
//两个线程公用一个phone对象,先调用先执行
new Thread(()->{phone.sendinfo();},"A").start();
//干扰,使线程睡眠一秒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone.call();},"B").start();
}
}
8、class锁与对象锁:多个线程使用多个对象,顺序执行
- 不同锁级别和多个对象之间,不存在多线程之间锁的竞争,顺序执行
示例:一个普通同步方法,一个静态同步方法,2部手机,请问先打印邮件还是短信?
/**
* @author hguo
* @date2021/5/23 21:18
* @title class锁与对象锁:多个线程使用多个对象,顺序执行
*/
public class Lock_08 {
//定义一个功能类
static class Phone{
//功能类中实现功能的具体方法,synchronized修饰线程同步
public synchronized static void sendinfo(){
System.out.println("---->发短信");
}
public synchronized void call(){
//干扰,使线程睡眠一秒
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("---->打电话");
}
}
public static void main(String[] args) {
Phone phone_1 = new Phone();
Phone phone_2 = new Phone();
//两个线程公用一个phone对象,先调用先执行
new Thread(()->{phone_1.sendinfo();},"A").start();
//干扰,使线程睡眠一秒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone_2.call();},"B").start();
}
}
2.4.5、多线程集合处理
1、多线程下的集合安全处理一
集合在单线程下执行是安全的,无竞争锁资源的情况
/**
* @author hguo
* @date2021/5/24 10:29
* @title 多线程集合类的安全问题
*/
public class ArrayList_01 {
//传统集合类多线程不安全的实例
public static void main(String[] args) {
//创建单线程执行对象
ListTest list = new ListTest();
new Thread(()->{
//调用传统集合单线程
//list.arrayList();
}).start();
}
//传统集合的单线程运行是安全的
static class ListTest{
public void arrayList(){
List<String> objects = new ArrayList<>();
System.out.println(Thread.currentThread().getName() + "单线程集合");
for (int i = 1; i <= 10; i++) {
objects.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(objects);
}
}
}
}
在多线程下,集合类是不安全的,会造成内存溢出、并发修改异常等异常
/**
* @author hguo
* @date2021/5/24 10:29
* @title 多线程集合类的安全问题
*/
public class ArrayList_01 {
//传统集合类多线程不安全的实例
public static void main(String[] args) {
//创建多线程执行对象
ThreadListTest threadListTest = new ThreadListTest();
// java.util.ConcurrentModificationException,并发修改异常
new Thread(()->{
//调用传统集合多线程
threadListTest.arrayListThread();
}).start();
}
//传统集合的多线程运行是不安全的
static class ThreadListTest{
public void arrayListThread(){
List<String> objects = new ArrayList<>();
System.out.println(Thread.currentThread().getName() + "多线程集合-1");
for (int i = 1; i <= 10; i++) {
new Thread(()->{
objects.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(objects);
},String.valueOf(i)).start();
}
}
}
}
解决方法
- Vector()方法 ```java /**
- @author hguo
- @date2021/5/24 10:29
- @title 多线程集合类的安全问题 */ public class ArrayList_01 { //传统集合类多线程不安全的实例
public static void main(String[] args) { //Vector方法解决集合多线程执行的不安全问题
VectorListTest vectorListTest = new VectorListTest();
new Thread(()->{
vectorListTest.arrayListVector();
}).start();
}
//Vector处理多线程集合的执行安全 static class VectorListTest{
public void arrayListVector(){
List<Object> objects = new Vector<>();
System.out.println(Thread.currentThread().getName() + "多线程集合-2");
for (int i = 1; i <= 10; i++) {
new Thread(()->{
objects.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(objects);
},String.valueOf(i)).start();
}
}
} }
<br />![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210524112409179.png#crop=0&crop=0&crop=1&crop=1&id=p2hr3&originHeight=315&originWidth=692&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- **CopyOnWriteArrayList()方法**
```java
/**
* @author hguo
* @date2021/5/24 10:29
* @title 多线程集合类的安全问题
*/
public class ArrayList_01 {
//传统集合类多线程不安全的实例
public static void main(String[] args) {
//JUC解决多线程集合安全问题
CopyOnWriteList copyOnWriteList = new CopyOnWriteList();
new Thread(()->{
copyOnWriteList.copyOnWriteArrayList();
}).start();
}
//JUC解决多线程集合安全问题
static class CopyOnWriteList{
public void copyOnWriteArrayList(){
List<Object> objects = new CopyOnWriteArrayList<>();
System.out.println(Thread.currentThread().getName() + "多线程集合-3");
for (int i = 1; i <= 10; i++) {
new Thread(()->{
objects.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(objects);
},String.valueOf(i)).start();
}
}
}
}
CopyOnWriteArrayList()方法底层思想
- CopyOnWrite 写入时复制,简称COW,是计算机程序设计的一种优化策略
- 多线程调用时,list读数据时固定的,但写数据会被覆盖,因此不会使用Synchnrozed方法
2、多线程下的集合安全处理二
- Set方法下有HashSet()、CopyOnWriteSet()等方法
- HashSet()方法的底层原理实质还是HashMap的实现方法,利用Hash方法中Key的唯一性来创造了Set方法对多线程安全的处理
/**
* @author hguo
* @date2021/5/24 11:37
* @title 现世安稳,岁月静好,佛祖保佑,永无bug!
*/
public class ArraySet_01 {
//传统集合类多线程不安全的实例
public static void main(String[] args) {
//创建单线程执行对象
SetTest list = new SetTest() ;
new Thread(()->{
//调用传统集合单线程
//list.arrayList();
}).start();
//创建多线程执行对象
ThreadListSet threadListTest = new ThreadListSet();
// java.util.ConcurrentModificationException,并发修改异常
new Thread(()->{
//调用传统集合多线程
//threadListTest.arrayListThread();
}).start();
//Vector方法解决集合多线程执行的不安全问题
VectorListSet vectorListTest = new VectorListSet();
new Thread(()->{
//vectorListTest.arrayListVector();
}).start();
//JUC解决多线程集合安全问题
CopyOnWriteSet copyOnWriteList = new CopyOnWriteSet();
new Thread(()->{
copyOnWriteList.copyOnWriteArraySet();
}).start();
}
//传统集合的单线程运行是安全的
static class SetTest{
public void arrayList(){
List<String> objects = new ArrayList<>();
System.out.println(Thread.currentThread().getName() + "单线程集合");
for (int i = 1; i <= 20; i++) {
objects.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(objects);
}
}
}
//传统集合的多线程运行是不安全的
static class ThreadListSet{
public void arrayListThread(){
Set<String> objects = new HashSet<>();
System.out.println(Thread.currentThread().getName() + "多线程集合-1");
for (int i = 1; i <= 20; i++) {
new Thread(()->{
objects.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(objects);
},String.valueOf(i)).start();
}
}
}
//Vector处理多线程集合的执行安全
static class VectorListSet{
public void arrayListVector(){
List<Object> objects = new Vector<>();
System.out.println(Thread.currentThread().getName() + "多线程集合-2");
for (int i = 1; i <= 20; i++) {
new Thread(()->{
objects.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(objects);
},String.valueOf(i)).start();
}
}
}
//JUC解决多线程集合安全问题
static class CopyOnWriteSet{
public void copyOnWriteArraySet(){
Set<Object> objects = new CopyOnWriteArraySet<>();
System.out.println(Thread.currentThread().getName() + "多线程集合-3");
for (int i = 1; i <= 20; i++) {
new Thread(()->{
objects.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(objects);
},String.valueOf(i)).start();
}
}
}
}
3、HashMap对多线程集合执行的安全处理
- HashMap的底层主要是基于数组和链表来实现的,它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置。
- HashMap底层是通过链表来解决hash冲突的。也就是说,其链表结果主要是用来解决hash冲突的。
- 事实上,HashMap线程是不安全的。
- 在并发编程中使用HashMap可能导致程序死循环。使用线程安全的HashTable效率又非常低下,因此便有了ConcurrentHashMap的登场机会
- HashTable容器在竞争激烈的并发环境下表现出效率低下的原因
- 所有访问HashTable的线程都必须竞争同一把锁
- 假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而有效提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。
- 首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
/**
* @author hguo
* @date2021/5/24 11:37
* @title Hash解决多线程集合类的安全问题
*/
public class ArrayHash_01 {
//传统集合类多线程不安全的实例
public static void main(String[] args) {
//JUC解决多线程集合安全问题
ConcurrentHash concurrentHash = new ConcurrentHash();
new Thread(()->{
concurrentHash.concurrentHashMap();
}).start();
}
//JUC解决多线程集合安全问题
static class ConcurrentHash{
public void concurrentHashMap(){
Map<Object,Object> objects = new ConcurrentHashMap<>();
System.out.println(Thread.currentThread().getName() + "多线程集合-Hash");
for (int i = 1; i <= 20; i++) {
new Thread(()->{
objects.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,5));
System.out.println(objects);
},String.valueOf(i)).start();
}
}
}
}
2.4.6、Callable入门
- Callable接口类似于Runnable,都是为实例方法提供线程执行的类设计
- Callable接口不同于Runnable的是,有返回值,可以抛出异常,方法不同
- Thread对Callable的调用原理
- new Runnable()方法——>new FutrueTask<>()方法——>new FutrueTask<>(Callable)方法,Thread()调用Callable()方法,中间经过了三层调用才得以实现
- 实例分析 ```java /**
- @author hguo
- @date2021/5/24 16:59
- @title Callable原理学习
*/
public class CallableTest {
public static void main(String[] args) throws Exception {
//启动Thread的线程
new Thread().start();
//创建线程对象
TheCallable theCallable = new TheCallable();
//创建FutureTask线程对象
FutureTask
futureTask = new FutureTask<>(theCallable); //启动Thread中的futureTask子线程 new Thread(futureTask,”A”).start(); new Thread(futureTask,”B”).start(); //获得Callable的返回值并抛出异常 Object o = futureTask.get(); System.out.println(o); } }
//实现Callable接口并重写方法
class TheCallable implements Callable
- 实例中两个线程调用一个对象,但是打印一条内容,这是因为有缓存,让第二个线程直接使用了第一个线程的结果进行输出
<a name="e9f34ee2"></a>
##### 1、CountDownLatch
- CountDownLatch是使一个线程等待其他线程各自执行完毕后再执行的类,多用于自减方法
- CountDownLatch.countDown()表示自减基数
- countDownLatch.await()表示等待计数器归零,再执行后面的操作
- countDown()方法每执行一次,await()方法就会被唤醒一次,循环直至基数等于零,await()方法再执行后面的内容
```java
/**
* @author hguo
* @date2021/5/24 17:15
* @title CountDownLatch计数器的原理分析
*/
public class CountDownLatchTest {
public static void main(String[] args) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() + ":开始");
countDownLatch.countDown();
},String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println("关闭线程");
}
}
2、CyclicBarrier
- CyclicBarrier作用就是会让所有线程都等待完成后才会继续下一步行动。
- 线程调用 await() 表示自己已经到达栅栏,即将运行其他内容
- BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时
- 使用条件
- 一个线程组的线程需要等待所有线程完成任务后再继续执行下一次任务
/**
* @author hguo
* @date2021/5/24 17:37
* @title CyclicBarrier自增计数器的原理分析
*/
public class CyclicBarrierTest {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("神龙召唤成功!");
});
for (int i = 0; i < 7; i++) {
final int TEMP = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "已获得" + TEMP + "课龙珠");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
CountDownLatch于CyclicBarrier的区别
- CountDownLatch 是一次性的,CyclicBarrier 是可循环利用的
- CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。CyclicBarrier 参与的线程职责是一样的
3、Semaphore
- Semaphore也是一个线程同步的辅助类,可以维护当前访问自身的线程个数,并提供了同步机制。
- 使用Semaphore可以控制同时访问资源的线程个数,例如,实现一个文件允许的并发访问数。
- Semaphore作用多个共享资源互拆的作用,并发限流,控制最大线程数
- Semaphore的主要方法
- void acquire() 从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。
- void release() 释放一个许可,将其返回给信号量。
- int availablePermits() 返回此信号量中当前可用的许可数。
- boolean hasQueuedThreads() 查询是否有线程正在等待获取。 ```java /**
- @author hguo
- @date2021/5/24 17:52
- @title Semaphore原理分析
*/
public class SemaphoreTest {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
} } } ```new Thread(()->{
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();
}
},String.valueOf(i)).start();
4、ReadWriterLock
- ReadWriterLock管理一组锁,一个是只读的锁,一个是写锁
- 读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的,所有读写锁的实现必须确保写操作对读操作的内存影响
- 一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。
- 读写锁比互斥锁允许对于共享数据更大程度的并发。
- 每次只能有一个写线程,但是同时可以有多个线程并发地读数据。
- ReadWriteLock适用于读多写少的并发情况。
4.1、ReetrantReadWriteLock特性说明
- 顺序锁
- 非公平模式(默认)
- 当以非公平初始化时,读锁和写锁的获取的顺序是不确定的。非公平锁主张竞争获取,可能会延缓一个或多个读或写线程,但是会比公平锁有更高的吞吐量。
- 公平模式
- 当以公平模式初始化时,线程将会以队列的顺序获取锁。当当前线程释放锁后,等待时间最长的写锁线程就会被分配写锁;或者有一组读线程组等待时间比写线程长,那么这组读线程组将会被分配读锁。
- 非公平模式(默认)
- 可重入
- 什么是可重入锁,不可重入锁?
- 可重入锁,就是说一个线程在获取某个锁后,还可以继续获取该锁,即允许一个线程多次获取同一个锁。
- synchronized内置锁就是可重入的
- JDK提供的显示锁ReentrantLock也是可以重入的 ```java /**
- @author hguo
- @date2021/5/24 20:56
- @title 可重入锁的实现方法 */
//以JDK提供的ReentrantLock为例 public class ReentrantLockTest { public static void main(String[] args) throws InterruptedException { //获取ReentrantLock对象 ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); //创建线程 new Thread(()->{ //加写锁 lock.writeLock().lock(); //业务操作 System.out.println(“execute writelock”); //释放锁 lock.writeLock().unlock(); }).start(); //获取两次锁 lock.writeLock().lock(); lock.writeLock().lock(); TimeUnit.SECONDS.sleep(2); System.out.println(“realse one once”); //释放一次锁 lock.writeLock().unlock(); } }
![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210524210959488.png#crop=0&crop=0&crop=1&crop=1&id=VaUnL&originHeight=104&originWidth=628&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- 源码解析
- 从运行结果可以看出,程序并未执行run()方法,原因是主线程获取了两次锁,只释放了一次,导致子线程Thread永远获取不到锁,造成死锁现象
- **一个线程获取多少次锁,就必须释放多少次锁。这对于内置锁也是适用的,每一次进入和离开synchornized方法(代码块),就是一次完整的锁获取和释放。**
- **降级锁**
- 升降锁对于ReadWriteLock来说是比较重要的,实质上**ReentrantReadWriteLock**是不支持升级锁的
- **锁升级:从读锁变成写锁,先获取读锁后获取写锁的过程**
- **ReentrantReadWriteLock**不支持升级锁,在没有释放读锁的情况下,就去申请写锁就会造成死锁
```java
/**
* @author hguo
* @date2021/5/24 21:21
* @title 验证ReentrantReadWriteLock是不支持升级锁的
*/
public class UpDownLock {
public static void main(String[] args) {
//调用升级锁
upLock upLock = new upLock();
upLock.upLockTest();
}
}
//定义升级锁内部类,先获取读锁后获取写锁的过程称为升级锁
class upLock{
public void upLockTest(){
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
//先获取读锁
lock.readLock().lock();
System.out.println("第一次获取锁");
//后获取写锁
lock.writeLock().lock();
System.out.println("第二次获取锁");
lock.writeLock().unlock();
}
}
}
- 锁降级:从写锁变成读锁,先获取写锁后获取读锁的过程
- ReentrantReadWriteLock支持降级锁
- 从写锁降级成读锁,并不会自动释放当前线程获取的写锁,仍然需要手动的释放,否则别的线程永远也获取不到写锁
```java
/**
- @author hguo
- @date2021/5/24 21:21
- @title 验证ReentrantReadWriteLock是不支持升级锁的 */ public class UpDownLock { public static void main(String[] args) { //调用降级锁 downLock downLock = new downLock(); downLock.downLockTest(); } }
//定义降级锁内部类,先获取写锁后获取读锁的过程称为降级锁 class downLock{ public void downLockTest(){ ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); lock.writeLock().lock(); System.out.println(“第一次获取锁”); lock.readLock().lock(); System.out.println(“第二次获取锁”); lock.writeLock().unlock(); } }
<br />![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210524213531371.png#crop=0&crop=0&crop=1&crop=1&id=nl83T&originHeight=86&originWidth=568&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
<a name="5d15fc93"></a>
###### 4.2、ReetrantReadWriteLock对比使用
- **Synchnorized实现**
- 在使用ReetrantReadWriteLock实现锁机制前,我们先看一下,多线程同时读取文件时,用synchronized实现的效果
- 程序运行总时长 = 线程一运行时间 + 线程二运行的时间 = 239ms
```java
/**
* @author hguo
* @date2021/5/24 21:41
* @title 现世安稳,岁月静好,佛祖保佑,永无bug!
*/
public class SynchronizedTest {
public static void main(String[] args) {
new Thread(()->{synchronizedTest(Thread.currentThread());
}).start();
new Thread(()->{synchronizedTest(Thread.currentThread());
}).start();
}
public synchronized static void synchronizedTest(Thread thread){
Long start_time = (Long) System.currentTimeMillis();
System.out.println("启动线程:"+start_time);
for (int i = 1; i < 5; i++) {
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getName() + "线程正在执行....");
}
System.out.println(thread.getName() + "线程执行完毕!");
Long end_time = (Long) System.currentTimeMillis();
System.out.println("结束线程:" + end_time);
Long times = end_time - start_time;
System.out.println("耗时:" + times);
}
}
- ReetrantReadWriteLock实现
- ReetrantReadWriteLock()方法的实现比起Synchnorized()实现来说,效率有所提高
- 程序运行总时长 = 线程一启动到线程二结束的时间 = 144ms ```java /**
- @author hguo
- @date2021/5/24 22:16
- @title ReetrantReadWriteLock实现方法
*/
public class ReetrantReadWriteLockTest {
public static void main(String[] args) {
//启动
new Thread(()->{
}).start(); new Thread(()->{reetrantReadWriteLockTest(Thread.currentThread());
}).start(); } public static void reetrantReadWriteLockTest(Thread thread){ //获取ReentrantReadWriteLock对象 ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); //加锁 lock.readLock().lock(); //获取线程启动时间 Long start_time = System.currentTimeMillis(); System.out.println(“起始时间” + start_time); for (int i = 1; i <= 5; i++) {reetrantReadWriteLockTest(Thread.currentThread());
} System.out.println(thread.getName() + “线程执行完毕!”); //获取线程结束时间 Long end_time = System.currentTimeMillis(); System.out.println(“结束时间” + end_time); lock.readLock().unlock(); //获取执行时间差 Long times = end_time - start_time; System.out.println(“耗时:” + times +”毫秒”); } } ```try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getName() + "线程正在执行....");
4.3、ReetrantReadWriteLock读写锁互斥关系
- ReetrantReadWriteLock读锁使用共享模式,即:同时可以有多个线程并发地读数据。
- 写锁使用独占模式,换句话说,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的
ReetrantReadWriteLock读写锁关系
- 读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容,读写锁之间为互斥。
- 当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁 ```java /**
- @author hguo
- @date2021/5/24 20:49
@title 读写锁关系 */ public class ReadWriteLockTest { public static void main(String[] args) { //调用线程池,同时写入 ExecutorService executorService = Executors.newCachedThreadPool(); //启动线程 executorService.execute(()->{
readFile();
}); executorService.execute(()->{
writeFile();
}); } //获取ReentrantReadWriteLock对象 public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); //定义读方法 public static void readFile(){ //加锁 lock.readLock().lock(); //判断锁是否开启 boolean readLock = lock.isWriteLocked(); if(!readLock){
System.out.println("当前持有的锁是读锁!");
} try {
for (int i = 1; i < 6; i++) {
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":正在进行读操作……");
}
System.out.println(Thread.currentThread().getName() + ":读操作完毕!");
}finally {
System.out.println("释放读锁!");
//释放锁
lock.readLock().unlock();
} }
//定义写方法 public static void writeFile(){ //加锁 lock.writeLock().lock(); //判断锁是否开启 boolean writeLock = lock.isWriteLocked(); if (writeLock) {
System.out.println("当前持有的锁是写锁!");
} try {
for (int i = 1; i < 6; i++) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":正在进行写操作……");
}
System.out.println(Thread.currentThread().getName() + ":写操作完毕!");
} finally {
System.out.println("释放写锁!");
lock.writeLock().unlock();
} } } ```
5、阻塞队列BlockingQueue
1、阻塞队列BlockingQueue介绍
- 阻塞队列
- 阻塞队列首先是一个队列,当队列是空的时候,从队列获取元素的操作将会被阻塞(等待生产),当队列是满的时候,从队列插入元素的操作将会被阻塞(等待消费 )。
- 阻塞队列首先是一个队列,当队列是空的时候,从队列获取元素的操作将会被阻塞(等待生产),当队列是满的时候,从队列插入元素的操作将会被阻塞(等待消费 )。
- 阻塞队列的好处
- 在多线程领域,所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒。
- 为什么需要使用BlockingQueue?
- 好处是我们不需要关心什么时候去阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQue都能一手包办了。
- 阻塞队列BlockingQueue接口架构图
- 阻塞队列BlockingQueue种类:
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue:由链表结果组成的有界阻塞队列(默认大小Integer.MAX_VALUE)阻塞队列。(接近无界)
- SychronousQueue:不存储元素的阻塞队列,也即单个元素队列。
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
- DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
- LinkedTransferQueue:由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque:由链表结构组成的双端阻塞队列。
阻塞队列BlockingQueue核心方法
- 共同点:插入成功或者删除成功都返回true(除了put和take),区别在于处理异常情况
- Queue 中 element() 和 peek()都是用来返回队列的头元素,不删除。
- 在队列元素为空的情况下,element() 方法会抛出NoSuchElementException异常,peek() 方法只会返回 null。
- 抛出异常的方法
- 当阻塞队列满时,再往队列里add元素会抛出IllegalStateException:Queue full;
- 当阻塞队列空时,再从队列里remove移除元素会抛NoSuchElementException。 ```java /**
- @author hguo
- @date2021/5/25 8:45
@title 阻塞队列四种API */ public class BlockingQueueTest { public static void main(String[] args) { BackingStoreException bse = new BackingStoreException(); bse.blockingQueueException();
} //异常抛出 static class BackingStoreException{ public void blockingQueueException(){//add()/remove()基数超过new ArrayBlockingQueue<>()的定义数量就会抛出异常
ArrayBlockingQueue<Object> objects = new ArrayBlockingQueue<>(3);
System.out.println(objects.add("A"));
System.out.println(objects.add("B"));
System.out.println(objects.add("C"));
System.out.println(objects.add("D"));
System.out.println(objects.remove());
System.out.println(objects.remove());
System.out.println(objects.remove());
System.out.println(objects.remove());
} } } ```
返回特殊值(boolean值)的方法
- 插入成功,返回true;插入失败,返回false;
- 删除成功返回出队列元素;删除失败返回null; ```java /**
- @author hguo
- @date2021/5/25 8:45
@title 阻塞队列四种API */ public class BlockingQueueTest { public static void main(String[] args) {
BackingStoreBlooean bsb = new BackingStoreBlooean();
bsb.blockingQueueException();
} //返回特殊值类型 static class BackingStoreBlooean{
public void blockingQueueException(){
//add()/remove()基数超过new ArrayBlockingQueue<>()的定义数量再往队列里offer元素会返回false.
ArrayBlockingQueue<Object> objects = new ArrayBlockingQueue<>(4);
System.out.println(objects.add("A"));
System.out.println(objects.add("B"));
System.out.println(objects.add("C"));
System.out.println(objects.add("D"));
System.out.println(objects.offer("D"));
System.out.println(objects.poll());
System.out.println(objects.poll());
System.out.println(objects.poll());
System.out.println(objects.poll());
System.out.println(objects.poll());
}
} } ```
阻塞的方法(添加无返回值)
- 当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产线程直到put数据or响应中断退出。
- 当阻塞队列空时,消费者线程试图take队列里的元素,队列会一直阻塞消费者线程直到队列有可用元素。 ```java /**
- @author hguo
- @date2021/5/25 8:45
@title 阻塞队列四种API */ public class BlockingQueueTest { public static void main(String[] args) throws InterruptedException { BackingStorePutTake bst = new BackingStorePutTake(); bst.backingStorePutTake(); }
//阻塞队列的方法 static class BackingStorePutTake{ public void backingStorePutTake() throws InterruptedException {
//当阻塞队列满时,再往队列里put元素会阻塞队列.
//当阻塞队列空时,再往队列里take元素会阻塞队列
ArrayBlockingQueue<Object> objects = new ArrayBlockingQueue<>(4);
// 当阻塞队列满时,put方法会阻塞队列演示
objects.put("A");
objects.put("B");
objects.put("C");
objects.put("D");
// 当阻塞队列空时,take方法会阻塞队列演示
System.out.println(objects.take());
System.out.println(objects.take());
System.out.println(objects.take());
System.out.println(objects.take());
System.out.println(objects.take());
} }
}
<br />![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210525095720334.png#crop=0&crop=0&crop=1&crop=1&id=YQ7P0&originHeight=140&originWidth=548&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- **超时的方法**
- 当向阻塞队列offer元素时候,时间超过了设定的值,就会出现超时中断;
- 当向阻塞队列poll元素时候,时间超过了设定的值,就会出现超时中断。
```java
/**
* @author hguo
* @date2021/5/25 8:45
* @title 阻塞队列四种API
*/
public class BlockingQueueTest {
public static void main(String[] args) throws InterruptedException {
BackingStoreOvertime bsot = new BackingStoreOvertime();
bsot.backingStoreOvertime();
}
//超时
static class BackingStoreOvertime{
public void backingStoreOvertime() throws InterruptedException {
//add()/remove()基数超过new ArrayBlockingQueue<>()的定义数量再往队列里offer元素会返回false.
ArrayBlockingQueue<Object> objects = new ArrayBlockingQueue<>(4);
// 当阻塞队列满时,offer方法执行超过3秒,阻塞队列会超时中断演示
System.out.println(objects.offer("A",3, TimeUnit.SECONDS));
System.out.println(objects.offer("B",3,TimeUnit.SECONDS));
System.out.println(objects.offer("C",3,TimeUnit.SECONDS));
System.out.println(objects.offer("D",3,TimeUnit.SECONDS));
System.out.println(objects.offer("D",3,TimeUnit.SECONDS));
System.out.println("延时3秒");
// 当阻塞队列空时,poll方法执行超过3秒,阻塞队列会超时中断演示
System.out.println(objects.poll(3,TimeUnit.SECONDS));
System.out.println(objects.poll(3,TimeUnit.SECONDS));
System.out.println(objects.poll(3,TimeUnit.SECONDS));
System.out.println(objects.poll(3,TimeUnit.SECONDS));
System.out.println(objects.poll(3,TimeUnit.SECONDS));
System.out.println("延时3秒");
}
}
}
2、同步队列SynchnrousQueue
因为消费者线程每隔2秒钟取一个元素,所以生产者线程也是每隔5秒钟往Synchronous阻塞队列中添加一个元素
/**
* @author hguo
* @date2021/5/25 10:00
* @title SychronousQueue类阻塞队列代码验证
*/
public class SychronousQueueTest {
public static void main(String[] args) {
//创建SynchronousQueue对象
BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
// 生产者线程进行put操作
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "\t put 第一次");
blockingQueue.put("一");
System.out.println(Thread.currentThread().getName() + "\t put 第二次");
blockingQueue.put("二");
System.out.println(Thread.currentThread().getName() + "\t put 第三次");
blockingQueue.put("三");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "T1").start();
// 消费者线程进行take操作
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "\t take第一次");
blockingQueue.take();
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "\t take第二次");
blockingQueue.take();
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "\t take第三次");
blockingQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T2").start();
}
}
2.4.7、线程池(重要)
1、揭秘线程池
- 线程池(thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。
- 线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。
- 线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。
- 如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。
- 如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。
- 超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
- 线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。
- 可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。 例如,线程数一般取cpu数量+2比较合适,线程数过多会导致额外的线程切换开销。
- 线程池模式一般分为两种:HS/HA半同步/半异步模式、L/F领导者与跟随者模式。
- 半同步/半异步模式又称为生产者消费者模式,是比较常见的实现方式,分为
- 同步层
- 队列层
- 异步层
- 领导者跟随者模式,任何时刻线程池只有一个领导者,线程在线程池中的线程可处在3种状态之一:
- 领导者leader
- 追随者follower
- 工作者processor
- 半同步/半异步模式又称为生产者消费者模式,是比较常见的实现方式,分为
- 线程池的伸缩性对性能有较大的影响
- 创建太多线程,将会浪费一定的资源,有些线程未被充分使用。
- 销毁太多线程,将导致之后浪费时间再次创建它们。
- 创建线程太慢,将会导致长时间的等待,性能变差。
- 销毁线程太慢,导致其它线程资源饥饿。
- 线程池的组成部分
- 服务器程序利用线程技术响应客户请求已经司空见惯,这样做效率确实已经很高,但有没有想过优化一下使用线程的方法。
- 线程池管理器(ThreadPoolManager):用于创建并管理线程池
- 工作线程(WorkThread): 线程池中线程
- 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行。
- 任务队列:用于存放没有处理的任务。提供一种缓冲机制。
- 线程池的优势
- 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
- 提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
- 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM(Out Of Memory),并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
- 提供更强大的功能,延时定时线程池。
2、线程池重点
三大方法
- ExecutorService threadPool = Executors.newSingleThreadExecutor(); — 单个线程 ```java /**
- @author hguo
- @date2021/5/25 11:59
- @title 线程池三大方法
*/
public class ThreadPoolMethod {
public static void main(String[] args) {
//调用单线程方法
SingleThreadExecutor ste = new SingleThreadExecutor();
ste.singleThreadExecutor();
}
//创建单线程方法
static class SingleThreadExecutor{
public void singleThreadExecutor(){
} } } ```ExecutorService threadpool = Executors.newSingleThreadExecutor();
try{
for (int i = 0; i < 11; i++) {
threadpool.execute(()->{
System.out.println(Thread.currentThread().getName() + "执行!");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadpool.shutdown();
}
ExecutorService threadPool = Executors.newFixedThreadPool(5); — 创建大小固定的线程 ```java /**
- @author hguo
- @date2021/5/25 11:59
@title 线程池三大方法 */ public class ThreadPoolMethod { public static void main(String[] args) { //创建大小固定的线程 FixedThreadPool ftp = new FixedThreadPool(); ftp.fixedThreadPool(); }
//创建大小固定的线程 static class FixedThreadPool{ public void fixedThreadPool(){
ExecutorService threadpool = Executors.newFixedThreadPool(5);
try{
for (int i = 0; i < 11; i++) {
threadpool.execute(()->{
System.out.println(Thread.currentThread().getName() + "-执行!");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadpool.shutdown();
}
} }
}
<br />![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210525121633163.png#crop=0&crop=0&crop=1&crop=1&id=YqyKU&originHeight=309&originWidth=621&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- **ExecutorService threadPool = Executors.newCachedThreadPool(); — 可延展的线程,遇强则强**
```java
/**
* @author hguo
* @date2021/5/25 11:59
* @title 线程池三大方法
*/
public class ThreadPoolMethod {
public static void main(String[] args) {
//可延展的线程,遇强则强
CachedThreadPool ctp = new CachedThreadPool();
ctp.cachedThreadPool();
}
//可延展的线程,遇强则强
static class CachedThreadPool{
public void cachedThreadPool(){
ExecutorService threadpool = Executors.newCachedThreadPool();;
try{
for (int i = 0; i < 11; i++) {
threadpool.execute(()->{
System.out.println(Thread.currentThread().getName() + "-执行!");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadpool.shutdown();
}
}
}
}
- 源码分析
```java
//newCachedThreadPool()方法
public static ExecutorService newSingleThreadExecutor() {
}return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
//newFixedThreadPool()方法
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue
//newCachedThreadPool()方法
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue
//共同点:都返回了ThreadPoolExecutor()方法对象
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue
//三大方法最底层使用的还是七大参数
- **七大参数**
- **int corePoolSize** — 线程池基本大小
- 当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时才会停止
- 除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。
- **int maximumPoolSize** — 线程池最大大小
- 最大线程数是根据计算机硬件来定的,不同配置的电脑最大线程数不同
- 获取本机最大线程数:`Runtime.getRuntime().availableProcessors()`
```java
System.out.println(Runtime.getRuntime().availableProcessors());
- 线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务
- long keepAliveTime — 线程存活保持时间
- 当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数
- TimeUnit unit — 超时单位
- 用于定义休眠时间,Thread.TimeUnit.时间格式.sleep()
- BlockingQueue workQueue — 任务队列
- 用于传输和保存等待执行任务的阻塞队列。
- ThreadFactory threadFactory — 线程工厂
- 用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
- RejectedExecutionHandler handler — 拒接策略
- 当线程池和队列都满了,再加入线程会执行此策略。
- 使用须知
- 最大线程并发数=队列+最大线程数
- 超过最大并发数,程序就会抛出异常
- 线程并发数达到最大值时就要开启拒绝策略,共有四种
- 四种策略
- new ThreadPoolExecutor.AbortPolicy() — 队列满了,还有人进来,不处理这个人的,抛出异常
- new ThreadPoolExecutor.CallerRunsPolicy() — 哪来的去哪里!,一般由main线程接管
- new ThreadPoolExecutor.DiscardPolicy() — 队列满了,就丢掉多余任务,不会抛出异常!
- new ThreadPoolExecutor.DiscardOldestPolicy() — 队列满了,尝试去和早的竞争,可能会覆盖执行较早的线程,也不会抛出异常!
/**
* @author hguo
* @date2021/5/25 16:47
* @title 七大参数的使用
*/
public class ThreadPoolParameter {
public static void main(String[] args) {
//定义线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2, //线程大小
5, //线程最大允许量
3, //存活时间
TimeUnit.SECONDS, //休眠时间
new LinkedBlockingDeque<>(3), //队列任务
Executors.defaultThreadFactory(), //线程工厂
new ThreadPoolExecutor.DiscardOldestPolicy() //异常处理
);
try{
//最大线程并发数=队列+最大线程数,超过最大并发数,程序就会抛出异常
for (int i = 0; i < 10; i++) {
threadPoolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName() + ":正在执行...");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
//关闭线程,用完即关
threadPoolExecutor.shutdown();
}
}
}
3、线程池补充
- 线程创建的说明
- Executors工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
- Executor 接口对象能执行我们的线程任务。
- ExecutorService接口继承了Executor接口并进行扩展,提供了更多方法获取任务执行的状态并可以获取任务返回值。
- 使用ThreadPoolExecutor可以创建自定义线程池。
- Future表示异步计算的结果,他提供了检查计算是否完成的方法,可以使用get()方法获取计算的结果。
- 工作流程
- 当一个任务通过submit或者execute方法提交到线程池的时候,如果当前池中线程数(包括闲置线程)小于coolPoolSize,则创建一个线程执行该任务。
- 如果当前线程池中线程数已经达到coolPoolSize,则将任务放入等待队列。
- 如果任务不能入队,说明等待队列已满,若当前池中线程数小于maximumPoolSize,则创建一个临时线程(非核心线程)执行该任务。
- 如果当前池中线程数已经等于maximumPoolSize,此时无法执行该任务,根据拒绝执行策略处理。
4、配置线程池
- 重点问题:如何定义最大线程数
- 回答:CPU密集型和IO密集型
- CPU密集型
- 大部分时间用来做计算,逻辑判断等CPU动作的程序称之CPU密集型
- 获取CPU核数:
Runtime.getRuntime().availableProcessors()
- 多用于调优
- IO密集型
- 系统运行,大部分的状况是CPU在等 I/O(硬盘/内存)的读/写
- 判断IO线程的消耗情况
- 一般定义IO为需求的两倍
2.4.8、四种函数型接口
1、函数式接口
- 函数式接口:只有一个方法体的接口
- 优点:简化编程模型,提高程序效率
- lambda表达式
- 语法形式: () -> {},其中 () 用来描述参数列表,{} 用来描述方法体,-> 为 lambda运算符
Lock lock = (参数)->{
//方法体
};
//简化
Lock lock =(参数)-> 方法体;
- 语法形式: () -> {},其中 () 用来描述参数列表,{} 用来描述方法体,-> 为 lambda运算符
lambda表达式只能用于函数式接口
函数型接口function()
public static void main(String[] args) {
//方式一
Function<String,String> function = new Function<String,String>(){
@Override
public String apply(String str) {
return str;
}
};
System.out.println(function.apply("abc"));
//方式二,使用lambda表达式简化的
Function<String,String> function1 = (str)->{return str;};
System.out.println(function1.apply("def"));
}
2、断定型接口
- 判断或比较真假问题的问题,返回boolean类型
- 断定型接口:有一个输入参数,返回值只能是 布尔值!
断定型接口Predicate()
public static void main(String[] args) {
// 方式一
// 判断字符串是否为空
Predicate<String> stringPredicate = new Predicate<String>() {
@Override
public boolean test(String str) {
return str.isEmpty();
}
};
System.out.println(stringPredicate.test("abc"));
//方式二 使用lambda表达式简化
Predicate<String> predicate = (str)->{return str.isEmpty();};
System.out.println(predicate.test("bcd"));
}
3、消费型接口
- 只有输入,没有返回值
- 消费型接口Consumer()
public static void main(String[] args) {
Consumer<String> consumer = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println("消费了" + s);
}
};
Consumer<String> consumer = (str) -> {
System.out.println("消费了" + str);
};
consumer.accept("美食");
}
4、供给型接口
- 没有参数,只有返回值
- 供给型接口supplier ()
public static void main(String[] args) {
Supplier<Integer> supplier = new Supplier<Integer>() {
@Override
public Integer get() {
System.out.println("调用了get方法");
return 1024;
}
};
Supplier<Integer> supplier = () -> {return 1024;};
System.out.println(supplier.get());
}
2.4.9、Stream流式计算与ForkJoin详情
1、Stream流式计算
- Stream流式计算就是把计算交给流来操作,而集合只做存储。
- 集合、MySQL等本质是存储数据
- 大数据=存储+计算
- 变量转换为流的方法
- list.stream().方法(); ```java /**
- @author hguo
- @date2021/5/25 22:02
@title Stream流式计算、链式编程 / public class StreamTest { / 题目要求:一分钟内完成此题,只能用一行代码实现! 现在有5个用户!筛选: 1、ID 必须是偶数 2、年龄必须大于23岁 3、用户名转为大写字母 4、用户名字母倒着排序 5、只输出一个用户! */ public static void main(String[] args) {
Stream stream = new Stream();
stream.streams();
}
static class Stream{
public void streams(){
User u1 = new User(1,"a",21);
User u2 = new User(2,"b",26);
User u3 = new User(3,"c",23);
User u4 = new User(4,"d",24);
User u5 = new User(6,"e",25);
// 集合就是存储
List<User> list = Arrays.asList(u1, u2, u3, u4, u5);
// 计算交给Stream流
// lambda表达式、链式编程、函数式接口、Stream流式计算
list.stream()
//过滤id和年龄
.filter(u->{return u.getId()%2==0;})
.filter(u->{return u.getAge()>23;})
//获取用户名并转换大写
.map(u->{return u.getName().toUpperCase();})
//倒叙输出
.sorted((uu1,uu2)->{return uu2.compareTo(uu1);})
//限制输出个数
.limit(3)
.forEach(System.out::println);
}
} } ```
2、Fork/join详情
2.1、理解ForkJoin原理
- Fork主要作用是将一个大的任务拆分成多个子任务进行并行处理
- Join主要作用是将Fork拆分的子任务结果合并成最后的计算结果,并进行输出
- ForkJoinPool继承AbstractExecutorService,实现了Executor,ExecutorService。
- ForkJoin框架的核心是ForkJoinPool类
- ForkJoinPool中维护了一个队列数组WorkQueue[],每个WorkQueue维护一个ForkJoinTask数组和当前工作线程。
- ForkJoinPool实现了工作窃取(work-stealing)算法并执行ForkJoinTask。
- ForkJoinPool中执行的默认线程是ForkJoinWorkerThread,由默认工厂产生,可以自己重写要实现的工作线程。同时会将ForkJoinPool引用放在每个工作线程中,供工作窃取时使用。
2.2、工作窃取算法
- 工作窃取(work-stealing)算法是指某个线程执行结束后从其他未执行结束的队列里窃取任务来执行。
- 理解
- 一个大任务分割为若干个互不依赖的子任务,为了减少线程间的竞争,把这些子任务分别放到不同的队列里,并未每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应
- CPU为两个线程调度的任务数量级不同,先执行完的线程就会把未执行完排队等待执行的任务窃取过来执行
- 这个过程存在一个问题,两个线程可能会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务线程永远从双端队列的尾部拿任务执行。
- 优点:充分利用线程进行并行计算,减少线程间的竞争
- 缺点:在某些情况下还是会存在竞争,比如双端队列里只有一个任务时。并且该算法会消耗更多的系统资源, 比如创建多个线程和多个双端队列
- 在Java中的运用
- 可以使用LinkedBlockingDeque来实现工作窃取算法
- JDK1.7引入的Fork/Join框架就是基于工作窃取算法
2.3、FortJoinPool原理
- ForkJoinPool 不是为了替代 ExecutorService,而是它的补充,在某些应用场景下性能比 ExecutorService 更好。
- ForkJoinPool 主要用于实现“分而治之”的算法,特别是分治之后递归调用的函数,例如 quick sort 等。
- ForkJoinPool 最适合的是计算密集型的任务,如果存在 I/O,线程间同步,sleep() 等会造成线程长时间阻塞的情况时,最好配合使用 ManagedBlocker。
- Fork()和Join()方法分开处理
- Fork():开启一个新线程(或是重用线程池内的空闲线程),将任务交给该线程处理。
- Join():等待该任务的处理线程处理完毕,获得返回值。
- ForkJoinPool 的每个工作线程都维护着一个工作队列(WorkQueue),这是一个双端队列(Deque),里面存放的对象是任务(ForkJoinTask)
2.4、FortJoinPool实现方法
- 以计算[1,10000000]的和为例子进行分析
- 实现方法有四种
- 接口内容
- for循环和ForkJoinPool解决方法的接口 ```java /**
- @author hguo
- @date2021/5/26 0:03
- @title 定义计算接口
/
public interface Calculator {
/*
- 把传进来的所有numbers 做求和处理 *
- @param numbers
- @return 总和 */ long sumUp(long[] numbers); } ```
- 接口内容
方案一:最为普通的for循环解决
- 最简单的,显然是不使用任何并行编程的手段,只用最直白的 for-loop 来实现 ```java /**
- @author hguo
- @date2021/5/26 0:02
@title 方案一:最为普通的for循环解决 */ public class ForLoopCalculator implements Calculator{ @Override public long sumUp(long[] numbers) { long total = 0; for (long i : numbers) {
total += i;
} return total; }
public static void main(String[] args) { //采用流方法进行计算,提高执行效率 long[] numbers = LongStream.rangeClosed(1, 10000000).toArray();
Instant start = Instant.now(); Calculator calculator = new ForLoopCalculator(); long result = calculator.sumUp(numbers); Instant end = Instant.now(); System.out.println(“耗时:” + Duration.between(start, end).toMillis() + “ms”);
System.out.println(“结果为:” + result); } } ```
方案二:ExecutorService多线程方式实现
- 统一使用 ExecutorService,从接口的易用程度上来说 ExecutorService 就远胜于原始的 Thread,更不用提 java.util.concurrent 提供的数种线程池,Future 类,Lock 类等各种便利工具。 ```java /**
- @author hguo
- @date2021/5/26 0:09
@title 方案二:ExecutorService多线程方式实现 */ public class ExecutorServiceCalculator { private int parallism; private ExecutorService pool;
public ExecutorServiceCalculator() { parallism = Runtime.getRuntime().availableProcessors(); // CPU的核心数 默认就用cpu核心数了 pool = Executors.newFixedThreadPool(parallism); }
//处理计算任务的线程 private static class SumTask implements Callable
{ private long[] numbers; private int from; private int to; public SumTask(long[] numbers, int from, int to) {
this.numbers = numbers;
this.from = from;
this.to = to;
}
@Override public Long call() {
long total = 0;
for (int i = from; i <= to; i++) {
total += numbers[i];
}
return total;
} } ```
}
方案三:采用ForkJoinPool(Fork/Join)
- 借助工作窃取算法,将具体的任务到线程的映射交给了 ForkJoinPool 来完成,减少线程之间的竞争,从而达到提高线程执行效率的效果 ```java /**
- @author hguo
- @date2021/5/25 23:46
@title 方案三:采用ForkJoinPool(Fork/Join) */ public class ForkJoinCalculator implements Calculator{
private ForkJoinPool pool;
//执行任务RecursiveTask:有返回值 RecursiveAction:无返回值 private static class SumTask extends RecursiveTask
{ private long[] numbers; private int from; private int to; public SumTask(long[] numbers, int from, int to) {
this.numbers = numbers;
this.from = from;
this.to = to;
}
//此方法为ForkJoin的核心方法:对任务进行拆分 拆分的好坏决定了效率的高低 @Override protected Long compute() {
// 当需要计算的数字个数小于6时,直接采用for loop方式计算结果
if (to - from < 6) {
long total = 0;
for (int i = from; i <= to; i++) {
total += numbers[i];
}
return total;
} else { // 否则,把任务一分为二,递归拆分(注意此处有递归)到底拆分成多少分 需要根据具体情况而定
int middle = (from + to) / 2;
SumTask taskLeft = new SumTask(numbers, from, middle);
SumTask taskRight = new SumTask(numbers, middle + 1, to);
taskLeft.fork();
taskRight.fork();
return taskLeft.join() + taskRight.join();
}
} }
public ForkJoinCalculator() { // 也可以使用公用的线程池 ForkJoinPool.commonPool(): // pool = ForkJoinPool.commonPool() pool = new ForkJoinPool(); }
@Override public long sumUp(long[] numbers) { Long result = pool.invoke(new SumTask(numbers, 0, numbers.length - 1)); pool.shutdown(); return result; }
public static void main(String[] args) {
Instant start = Instant.now(); long result = LongStream.rangeClosed(0, 10000000L).parallel().reduce(0, Long::sum); Instant end = Instant.now(); System.out.println(“耗时:” + Duration.between(start, end).toMillis() + “ms”);
System.out.println(“结果为:” + result); // 打印结果500500 } } ```
方案四:采用并行流(JDK8以后的推荐做法)
- 并行流底层还是Fork/Join框架,任务拆分优化得很好,极大程度的发挥了线程执行的性能 ```java /**
- @author hguo
- @date2021/5/26 0:18
@title 方案四:采用并行流(JDK8以后的推荐做法) */ public class ForkJoinPool {
public static void main(String[] args) { Instant start = Instant.now(); long result = LongStream.rangeClosed(0, 10000000L).parallel().reduce(0, Long::sum); Instant end = Instant.now(); System.out.println(“耗时:” + Duration.between(start, end).toMillis() + “ms”); System.out.println(“结果为:” + result); // 打印结果500500 } } ```
- 四种方法的执行效率
- 方案三 —> 方案四 —> 方案二 —> 方案一
- Fork/Join 并行流等当计算的数字非常大的时候,优势才能体现出来。
- 如果你的计算比较小,或者不是CPU密集型的任务,不太建议使用并行处理
2.4.10、回调方法
1、理解回调函数
- 回调函数一般是在封装接口的时候,回调显得特别重要
- 假设有两个程序,A程序写了底层驱动接口,B程序写了上层应用程序,然而此时底层驱动接口A有一个数据C需要传输给B,此时有两种方式:
- A将数据C存储好放在接口函数中,B自己想什么时候去读就什么时候去读,这就是我们经常使用的函数调用,此时主动权是B。
- A实现回调机制,当数据变化的时候才将通知B,你可以来读取数据了,然后B在用户层的回调函数中读取速度,此时主动权是A。
- 解读这两种方法
- 显然第一种执行效率要低一些,B不知道什么时候去调用函数读取数据C
- 第二种,B的执行就依赖于A来完成,进而实现了中断读取
- 假设有两个程序,A程序写了底层驱动接口,B程序写了上层应用程序,然而此时底层驱动接口A有一个数据C需要传输给B,此时有两种方式:
2、回调方法
- 回调方法可以分为同步回调和异步回调
- 同步回调:把函数B传递给函数A。执行A的时候,回调了B,A要一直等到B执行完才能继续执行;
- 异步回调:把函数B传递给函数A。执行A的时候,回调了B,然后A就继续往后执行,B独自执行。
- 注意:
- 同步可以是单线程也可以是多线程
- 异步必须是多线程或多进程(每个进程可以是单线程) ==> 换句话说,异步必须依靠多线程或多进程才能完成
2.4.11、浅谈JMM
- JMM:Java并发采用的内存模型,只是一个概念,内存模型的约定
- JMM对线程的常见约定
- 线程解锁前,必须把共享变量立刻刷回主存
- 线程加锁前,必须读取主存中最新的值存入主内存区中
- 加锁和释放锁必须是同一把锁
- 当代计算机的多核并发缓存架构:
- L1缓存:256KB
- L2缓存:1.0MB
- L3缓存:8.0MB
- JMM内存模型
- Java线程内存模型跟cpu的缓存模型类似,是基于cpu缓存模型来建立的,Java线程内存模型是标准化的,屏蔽掉了底层不同计算机的区别
- 例子:线程A和线程B同时访问主内存中的共享变量,实际上是先拷贝一份副本,这时线程A中无法看到线程B中的共享数据,这时就要将共享数据标记为volatile
- volatile:保证了多个线程副本的可见性
- JMM的原子操作
- read(读取):从主内存读取数据
- load(载入):将主内存读取到的数据写入工作内存
- use(使用):从工作内存读取数据来计算
- assign(赋值):将计算好的值重新赋值到工作内存中
- store(存储):将工作内存数据写入主内存
- write(写入):将store过去的变量值赋值给主内存中的变量
- lock(锁定):将主内存变量加锁,标识为线程独占状态
- unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
- 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作用在线程中的化,main线程无法同步修改的内容
/**
* @author hguo
* @date2021/5/29 20:44
* @title JMM
*/
public class JmmThread {
private static int num = 0;
public static void main(String[] args) {
new Thread(()->{
while(num==0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num=1;
System.out.println(num);
}
}
- num的值改变了,但是程序不会终止,这是一个很大的问题
2.4.12、volatile
1、保证线程间的可见性
可以识别得到线程间的方法或内容更新
/**
* @author hguo
* @date2021/5/29 20:44
* @title JMM
*/
public class JmmThread {
private volatile static int num = 0;
public static void main(String[] args) {
new Thread(()->{
while(num==0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num=1;
System.out.println(num);
}
}
2、原子性
- 线程执行时,要么全都成功,要么全都失败
- 使用AtomicInteger之后,最终的输出结果同样可以保证是200000。并且在某些情况下,代码的性能会比Synchronized更好。
/**
* @author hguo
* @date2021/5/29 21:16
* @title 现世安稳,岁月静好,佛祖保佑,永无bug!
*/
public class VolatileThread {
private static AtomicInteger num = new AtomicInteger();
public static void add(){
//AtomicInteger中getAndIncrement的加一操作
num.getAndIncrement();
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while(Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" "+num);
}
}
3、禁止指令重排
- 禁止指令重排也是保证线程安全的一种重要方法
- 指令重排:计算机不按照程序本该执行的设想执行程序
volatile
关键字主要是为了防止指令重排- 如果不用 ,
singleton = new Singleton();
,进程其实是分为三步:- 分配内存空间。
- 初始化对象。
- 将
singleton
对象指向分配的内存地址。
- 加上
**volatile**
是为了让以上的三步操作顺序执行,作用在内存屏障之间,单例模式使用最为广泛 不加的话有可能第二步在第三步之前被执行就有可能某个线程拿到的单例对象是还没有初始化的,以致于报错
![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210529220816675.png#crop=0&crop=0&crop=1&crop=1&id=fH6BC&originHeight=550&originWidth=352&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
2.4.13、UnSafe
- AtomicInteger可以直接更改内存中的值,而AtomicInteger是Unsafe类中的一种方法,全限定名是
sun.misc.Unsafe
,从名字中我们可以看出来这个类对普通程序员来说是“危险”的,一般应用开发者不会用到这个类。 - UnSafe类中,包含了普通读写操作,volatile读写操作,有序写操作,直接内存操作,CAS父类操作,线程调度,类加载和内存屏障等方法
- 在CAS中,主要相关的UnSafe方法是CAS父类操作、直接内存操作、线程调度和内存屏障
2.4.14、单例模式
- 单例模式属于创建型模式,它提供了一种创建对象的最佳方式
- 单例模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。
- 这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
- 单例模式缺点:无法防止反射来重复构建对象
- 注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
1、单例模式的分类
- private static Singleton instance = null; 还未构建,则构建单例对象并返回。说明比较懒,这个写法属于单例模式当中的懒汉模式。
/**
* @ClassName: Singleton_Simple
* @Description: 单例模式——饿汉模式
* @author Ran
* @date 2021/05/2 22:22
*
*/
public class Singleton_Simple {
//创建一个null对象
private static final Singleton_Simple simple = null;
//让构造函数为 private,这样该类就不会被实例化
private Singleton_Simple(){}
//获取唯一可用的对象
public static Singleton_Simple getInstance(){
return simple;
}
}
private static Singleton instance = new Singleton();单例对象一开始就被new Singleton()主动构建,则不再需要判空操作,只有饿了才会主动觅食,这种写法属于饿汉模式。
/**
* @ClassName: Singleton_Simple
* @Description: 单例模式——饿汉模式
* @author Ran
* @date 2011-2-4 上午12:46:15
*
*/
public class Singleton_Simple {
private static final Singleton_Simple simple = new Singleton_Simple();
private Singleton_Simple(){}
public static Singleton_Simple getInstance(){
return simple;
}
public void showMessage(){
System.out.println("这是单例模式的饿汉式");
}
public static void main(String[] args) {
//获取唯一可用的对象
Singleton_Simple object = Singleton_Simple.getInstance();
//显示消息
object.showMessage();
}
}
- 两种创建方式的区别
- 饿汉式
- 好处:相对线程是比较安全的,在类创建的同时就已经创建好一个静态的对象供系统使用,以后不在改变
- 坏处:对象加载时间过长,浪费内存
- 懒汉式
- 好处:延迟对象的创建
- 坏处:创建实例对象时不加上synchronized会导致对象访问的线程不安全
- 两种方式实现过程大概个4步骤:
- 创建类、
- 构造器私有化、
- 声明这个类的对象并私有化、
- 声明调用这个对象的方法并public、static修饰
- 回头把这个类的对象也static(因为static方法只能访问类中的静态成员变量)
- 饿汉式
- 上述两种创建方式,线程都不是绝对安全的
2、双锁机制
- 做两次判空的机制叫做双重检测机制
- 为了防止new Singleton被执行多次,因此在new操作之前加上Synchronized 同步锁,锁住整个类(注意,这里不能使用对象锁)
- 进入Synchronized 临界区以后,还要再做一次判空。因为当两个线程同时访问的时候,线程A构建完对象,线程B也已经通过了最初的判空验证,不做第二次判空的话,线程B还是会再次构建instance对象。
public class Singleton {
private Singleton() {} //私有构造函数
private static Singleton instance = null; //单例对象
//静态工厂方法
public static Singleton getInstance() {
if (instance == null) { //双重检测机制
synchronized (Singleton.class){ //同步锁
if (instance == null) { //双重检测机制
instance = new Singleton();
}
}
}
return instance;
}
}
- 双锁机制同样并不能保证线程绝对安全,因为JVM编译器可能导致指令重排
3、volatile禁止指令重排
- 指令重排:计算机不按照设想的思路执行程序
- 以 instance = new Singleton 为例:
- 设想编译顺序为:
- memory =allocate(); //1:分配对象的内存空间
- ctorInstance(memory); //2:初始化对象
- instance =memory; //3:设置instance指向刚分配的内存地址
- 会被编译器编译成如下JVM指令:
- memory =allocate(); //1:分配对象的内存空间
- instance =memory; //3:设置instance指向刚分配的内存地址
- ctorInstance(memory); //2:初始化对象
- 设想编译顺序为:
- 当线程A执行完1,3,时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。
- volatile方法
public class Singleton {
private Singleton() {} //私有构造函数
private volatile static Singleton instance = null; //单例对象
//静态工厂方法
public static Singleton getInstance() {
if (instance == null) { //双重检测机制
synchronized (Singleton.class){ //同步锁
if (instance == null) { //双重检测机制
instance = new Singleton();
}
}
}
return instance;
}
}
- volatile方法能够禁止指令重排,让执行顺序固定,instance对象的引用要么指向null,要么指向一个初始化完毕的Instance,而不会出现某个中间态,保证了安全。
4、静态内部类实现单例模式
- 从外部无法访问静态内部类LazyHolder,只有当调用Singleton.getInstance方法的时候,才能得到单例对象INSTANCE
INSTANCE对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类LazyHolder被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){
}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
5、防止反射构建
- 利用反射打破单例:
简单归纳为三个步骤:
- 第一步,获得单例类的构造器。
- 第二步,把构造器设置为可访问。
- 第三步,使用newInstance方法构造对象。 ```java public class Test {
//创建单例模式 ….
public static void main(String[] args) {
//获得构造器
Constructor con = Singleton.class.getDeclaredConstructor(); //设置为可访问 con.setAccessible(true); //构造两个不同的对象 Singleton singleton1 = (Singleton)con.newInstance(); Singleton singleton2 = (Singleton)con.newInstance(); //验证是否是不同对象 System.out.println(singleton1.equals(singleton2)); } } ```
枚举发防止反射打破单例:
- 枚举自带单例模式,可以有效防止反射对单例模式的破坏
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
- 枚举自带单例模式,可以有效防止反射对单例模式的破坏
2.4.15、理解CAS
- CAS的连环坑
- CAS -> Unsafe -> CAS底层思想 -> ABA -> 原子引用更新 -> 如何规避ABA问题
1、什么是CAS
- CAS(Compare And Swap)比较和替换是设计并发算法时用到的一种技术。
- CAS 由三个步骤组成,分别是“读取->比较->写回”
- CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
- 是一种用于在多线程环境下实现同步功能的机制。CAS 操作包含三个操作数 — 内存位置、预期数值和新值。CAS 的实现逻辑是将内存位置处的数值与预期数值想比较,若相等,则将内存位置处的值替换为新值。若不相等,则不做任何操作。
在 Java 中,Java 并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的。Java 代码需通过 JNI 才能调用。
/**
* @author hguo
* @date2021/5/30 14:46
* @title 现世安稳,岁月静好,佛祖保佑,永无bug!
*/
public class AtomicBooleanTest implements Runnable {
private static AtomicBoolean flag = new AtomicBoolean(true);
public static void main(String[] args) {
AtomicBooleanTest ast = new AtomicBooleanTest();
new Thread(ast).start();
new Thread(ast).start();
}
public void run() {
System.out.println("thread:"+Thread.currentThread().getName()+";flag:"+flag.get());
if (flag.compareAndSet(true,false)){
System.out.println(Thread.currentThread().getName()+""+flag.get());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag.set(true);
}else{
System.out.println("重试机制thread:"+Thread.currentThread().getName()+";flag:"+flag.get());
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
run();
}
}
}
Thread-1、Thread-0都会执行if=true条件,而且还不会产生线程脏读脏写,这是如何做到的了,这就用到了compareAndSet(boolean expect,boolean update)方法
- CAS的缺点:
- CPU开销较大
- 在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
- 不能保证代码块的原子性
- CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了
- 会造成ABA问题
- 狸猫换太子,在更新过程中有线程插队,导致更新结果和预期值不相同,但是依然能执行
- CPU开销较大
2、ABA问题
- CAS中最常见的问题之一
- CAS逻辑中,两个线程的执行情况:
- 时刻1:线程1执行读取操作,获取原值 A,然后线程被切换走(插队线程干了这个事情,但线程一并不知道值被修改)
- 时刻2:线程2执行完成 CAS 操作将原值由 A 修改为 B
- 时刻3:线程2再次执行 CAS 操作,并将原值由 B 修改为 A
- 时刻4:线程1恢复运行,将比较值(compareValue)与原值(oldValue)进行比较,发现两个值相等。
- 时刻5:最后用新值(newValue)写入内存中,完成 CAS 操作
- 原子引用解决ABA问题
- 带版本号的原子操作,实质上就是一个乐观锁
- 原子引用,携带时间戳,可以有效解决ABA问题 ```java /**
- @author hguo
- @date2021/5/30 15:07
- @title 现世安稳,岁月静好,佛祖保佑,永无bug!
*/
public class ThreadABA {
@Test
public void AtomicIntegerTest(){
atomicInteger atomicInteger = new atomicInteger();
atomicInteger.atomicIntegers();
}
```
static class atomicStampedReference{
}
}
3、CAS中的锁
3.1、可重入锁
- 某线程已经获得了某个锁,可以再次获取锁而不会出现死锁的过程。
- 可重入锁
- synchronized
- ReentrantLock
- 锁的实现
- Synchronized是依赖于JVM实现的
- ReenTrantLock是JDK实现的
- 性能的区别
- Synchronized引入了偏向锁,轻量级锁(自旋锁),对线程的优化提高不少
- ReenTrantLock用到了CAS技术,CAS又会发生ABA问题,但性能上还是优于上者的
- 功能区别
- Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放
- ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁
- 锁的细粒度和灵活度
- 很明显ReenTrantLock优于Synchronized
- ReenTrantLock独有的能力:
- ReenTrantLock可以指定是公平锁还是非公平锁,synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁
- ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,synchronized要么随机唤醒一个线程要么唤醒全部线程。
- ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
3.2、自旋锁
- 自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。
public class SpinLock implements Lock {
/**
* use thread itself as synchronization state
* 使用Owner Thread作为同步状态,比使用一个简单的boolean flag可以携带更多信息
*/
private AtomicReference<Thread> owner = new AtomicReference<>();
/**
* reentrant count of a thread, no need to be volatile
*/
private int count = 0;
@Override
public void lock() {
Thread t = Thread.currentThread();
// if re-enter, increment the count.
if (t == owner.get()) {
++count;
return;
}
//spin
while (owner.compareAndSet(null, t)) {
}
}
@Override
public void unlock() {
Thread t = Thread.currentThread();
//only the owner could do unlock;
if (t == owner.get()) {
if (count > 0) {
// reentrant count not zero, just decrease the counter.
--count;
} else {
// compareAndSet is not need here, already checked
owner.set(null);
}
}
}
}
- 分析
- SimpleSpinLock里有一个owner属性持有锁当前拥有者的线程的引用,如果该引用为null,则表示锁未被占用,不为null则被占用。
- AtomicReference是为了使用它的原子性的compareAndSet方法(CAS操作),解决了多线程并发操作导致数据不一致的问题,确保其他线程可以看到锁的真实状态。
- 缺点
- CAS操作需要硬件的配合;
- 保证各个CPU的缓存(L1、L2、L3、跨CPU Socket、主存)的数据一致性,通讯开销很大,在多处理器系统上更严重;
- 没法保证公平性,不保证等待进程/线程按照FIFO顺序获得锁。
自旋锁的种类
TicketLock 是为了解决上面的公平性问题,凡事必须公平,必须排队等待前面的锁释放才能持有
public class TicketLock implements Lock {
private AtomicInteger serviceNum = new AtomicInteger(0);
private AtomicInteger ticketNum = new AtomicInteger(0);
private final ThreadLocal<Integer> myNum = new ThreadLocal<>();
@Override
public void lock() {
myNum.set(ticketNum.getAndIncrement());
while (serviceNum.get() != myNum.get()) {
}
}
@Override
public void unlock() {
serviceNum.compareAndSet(myNum.get(), myNum.get() + 1);
myNum.remove();
}
}
缺点
- Ticket Lock 虽然解决了公平性的问题,但是多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量serviceNum ,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。
- CLHLck是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。 ```java public class CLHLock implements Lock {
/**
- 锁等待队列的尾部
*/
private AtomicReference
tail; private ThreadLocal preNode; private ThreadLocal myNode;
public CLHLock() { tail = new AtomicReference<>(null); myNode = ThreadLocal.withInitial(QNode::new); preNode = ThreadLocal.withInitial(() -> null); }
@Override public void lock() { QNode qnode = myNode.get(); //设置自己的状态为locked=true表示需要获取锁 qnode.locked = true; //链表的尾部设置为本线程的qNode,并将之前的尾部设置为当前线程的preNode QNode pre = tail.getAndSet(qnode); preNode.set(pre); if(pre != null) {
//当前线程在前驱节点的locked字段上旋转,直到前驱节点释放锁资源
while (pre.locked) {
}
} }
@Override public void unlock() { QNode qnode = myNode.get(); //释放锁操作时将自己的locked设置为false,可以使得自己的后继节点可以结束自旋 qnode.locked = false; //回收自己这个节点,从虚拟队列中删除 //将当前节点引用置为自己的preNode,那么下一个节点的preNode就变为了当前节点的preNode,这样就将当前节点移出了队列 myNode.set(preNode.get()); }
private class QNode { /**
- true表示该线程需要获取锁,且不释放锁,为false表示线程释放了锁,且不需要锁 */ private volatile boolean locked = false; } } ```
- 缺点
- 每个线程有自己的内存,如果前趋结点的内存位置比较远,自旋判断前趋结点的locked域,性能将大打折扣,但是在SMP系统结构下该法还是非常有效的。一种解决NUMA系统结构的思路是MCS队列锁。
MCSLock是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。
public class MCSLock implements Lock {
private AtomicReference<QNode> tail;
private ThreadLocal<QNode> myNode;
public MCSLock() {
tail = new AtomicReference<>(null);
myNode = ThreadLocal.withInitial(QNode::new);
}
@Override
public void lock() {
QNode qnode = myNode.get();
QNode preNode = tail.getAndSet(qnode);
if (preNode != null) {
qnode.locked = false;
preNode.next = qnode;
//wait until predecessor gives up the lock
while (!qnode.locked) {
}
}
qnode.locked = true;
}
@Override
public void unlock() {
QNode qnode = myNode.get();
if (qnode.next == null) {
//后面没有等待线程的情况
if (tail.compareAndSet(qnode, null)) {
//真的没有等待线程,则直接返回,不需要通知
return;
}
//wait until predecessor fills in its next field
// 突然有人排在自己后面了,可能还不知道是谁,下面是等待后续者
while (qnode.next == null) {
}
}
//后面有等待线程,则通知后面的线程
qnode.next.locked = true;
qnode.next = null;
}
private class QNode {
/**
* 是否被qNode所属线程锁定
*/
private volatile boolean locked = false;
/**
* 与CLHLock相比,多了这个真正的next
*/
private volatile QNode next = null;
}
}
- 三种锁的区别
- 从代码实现来看,CLH比MCS要简单得多。
- 从自旋的条件来看,CLH是在前驱节点的属性上自旋,而MCS是在本地属性变量上自旋。
- 从链表队列来看,CLHNode不直接持有前驱节点,CLH锁释放时只需要改变自己的属性;MCSNode直接持有后继节点,MCS锁释放需要改变后继节点的属性。
- CLH锁释放时只需要改变自己的属性,MCS锁释放则需要改变后继节点的属性。
3.3、死锁排查
- Java死锁如何排查?又如何解决呢?何为死锁呢?
- 死锁
- 概念:多个并发进程因争夺系统资源而产生相互等待的现象。
- 原理:当一组进程中的每个进程都在等待某个事件发生,而只有这组进程中的其他进程才能触发该事件,这就称这组进程发生了死锁。
- 本质原因:
- 系统资源有限
- 进程推进顺序不合理
- 造成死锁的四个必要因素:
- 只要满足其中一条,必然会造成死锁,发生死锁的进程无法进行下去,它们所持有的资源也无法释放。这样会导致CPU的吞吐量下降。所以死锁情况是会浪费系统资源和影响计算机的使用性能的。
1、互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
2、占有且等待: 一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
3、不可抢占: 别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
4、循环等待: 存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。
- 只要满足其中一条,必然会造成死锁,发生死锁的进程无法进行下去,它们所持有的资源也无法释放。这样会导致CPU的吞吐量下降。所以死锁情况是会浪费系统资源和影响计算机的使用性能的。
- 避免死锁的方法
- 1、死锁预防 ——- 确保系统永远不会进入死锁状态
- 破坏造成死锁的必要条件
- 破坏“占有且等待”条件
- 破坏“不可抢占”条件
- 破坏“循环等待”条件
- 破坏造成死锁的必要条件
- 2、避免死锁 ——- 在使用前进行判断,只允许不会产生死锁的进程申请资源
- 如果一个进程的请求会导致死锁,则不启动该进程
- 如果一个进程的增加资源请求会导致死锁 ,则拒绝该申请。
- 银行家算法
- 3、死锁检测与解除 ——- 在检测到运行系统进入死锁,进行恢复。
- 允许系统进入到死锁状态
- 死锁检测、死锁解除
- 抢占资源:从一个或多个进程中抢占足够数量的资源分配给死锁进程,以解除死锁状态。
- 终止(或撤销)进程:终止或撤销系统中的一个或多个死锁进程,直至打破死锁状态。
- 1、死锁预防 ——- 确保系统永远不会进入死锁状态
死锁排查的方法
- 死锁实例,不会报错,不会终止进程 ```java /**
- @author hguo
- @date2021/5/31 15:24
@title 死锁排查 */ public class DeadLock { public static void main(String[] args) { String lockA = “lockA”; String lockB = “lockB”; new Thread(new Thred01(lockA,lockB),”T1”).start(); new Thread(new Thred01(lockB,lockA),”T2”).start();
}
} class Thred01 implements Runnable{ private String lockA; private String lockB;
public Thred01(String lockA,String lockB) {
this.lockA=lockA;
this.lockB=lockB;
}
@Override
public void run() {
//lockA/B都加同步锁,A线程抢占B线程资源,B线程抢占A线程资源
synchronized (lockA){
System.out.println(Thread.currentThread().getName()+"lock:"+lockA+"-->get:"+lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println(Thread.currentThread().getName()+"lock:"+lockB+"-->get:"+lockA);
}
}
}
}
![](https://gitee.com/hg14150/blogiamges/raw/master/img/image-20210531154141494.png#crop=0&crop=0&crop=1&crop=1&id=WNZoE&originHeight=153&originWidth=553&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- **排查方法**
- 使用jps定位进程号:`jps -l`
```java
E:\J2EE_Project\Thread_project>jps -l
20544 sun.tools.jps.Jps
8992 org.jetbrains.kotlin.daemon.KotlinCompileDaemon
18388 com.iflytek.lock.DeadLock
19092
7128 org.jetbrains.jps.cmdline.Launcher
- 使用jstack查看进程信息:
jstack 进程号
```java E:\J2EE_Project\Thread_project>jstack 18388Java stack information for the threads listed above:
“T2”: at com.iflytek.lock.Thred01.run(DeadLock.java:41)- waiting to lock <0x00000000d60d2678> (a java.lang.String)
- locked <0x00000000d60d26b0> (a java.lang.String) at java.lang.Thread.run(Thread.java:748) “T1”: at com.iflytek.lock.Thred01.run(DeadLock.java:41)
- waiting to lock <0x00000000d60d26b0> (a java.lang.String)
- locked <0x00000000d60d2678> (a java.lang.String) at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.//发现一个死锁
- **面试回答死锁排查**
- 查看日志内容
- 查看堆栈信息
<a name="2971b330"></a>
### 2.5、TreadLocal
<a name="ee8f65c0"></a>
#### 2.5.1、什么是TreadLocal
- ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于**当前**线程,该变量对其他线程而言是隔离的。
- **ThreadLocal为变量在每个线程中都创建了一个副本**,那么每个线程可以访问自己内部的副本变量。
- 多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。
- **ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。**
<a name="0d2476a5"></a>
#### 2.5.2、TreadLocal的作用
- **在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束**
- **线程间数据隔离**
- **进行事务操作,用于存储线程事务信息**
- **数据库连接,Session会话管理**
<a name="c79cbd1b"></a>
#### 2.5.3、ThreadLocal如何使用
- ThreadLocal的作用是每一个线程创建一个副本,让每个线程可以访问自己内部的副本变量
- 每一个线程都有各自的local值,设置了个休眠时间就是为了另外一个线程也能够及时的读取当前的local值
- 用于何处
- 数据库管理类的复制连接
- ThreadLocal在每个线程中对连接会创建一个副本,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。
```java
/**
* @author hguo
* @date2021/5/31 13:40
* @title 现世安稳,岁月静好,佛祖保佑,永无bug!
*/
public class ThreadLocalTest01 {
public static void main(String[] args) {
ThreadLocal local = new ThreadLocal();
local.threadlocal();
}
//创建内部类
static class ThreadLocal{
public void threadlocal(){
//创建一个ThreadLocal对象
java.lang.ThreadLocal<Object> local = new java.lang.ThreadLocal<>();
//新建随机数
Random random = new Random();
//使用Stream创建5个线程
IntStream.range(0,5).forEach(a->new Thread(()->{
//设置每一个线程对应的local值
local.set(a+" "+random.nextInt(10));
System.out.println("线程——local:" + local.get());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start());
//释放资源
local.remove();
}
}
}
2.5.4、ThreadLocal方法分析
- 在ThreadLocal中有几种常用的方法
- set()
- 首先获取到了当前线程t,然后调用getMap获取ThreadLocalMap,如果map存在,则将当前线程对象t作为key,要存储的对象作为value存到map里面去。如果该Map不存在,则初始化一个
- get()
- 首先获取当前线程,然后调用getMap方法获取一个ThreadLocalMap,如果map不为null,那就使用当前线程作为ThreadLocalMap的Entry的键,然后值就作为相应的的值,如果没有那就设置一个初始值。
- set()
- remove()
- 判断该当前线程对应的threadLocals变量是否为null,不为null就直接删除当前线程中指定的threadLocals变量
- ThreadLocalMap()
- 每个线程内部有一个名为threadLocals的成员变量,该变量的类型为ThreadLocal.ThreadLocalMap类型(类似于一个HashMap),其中的key为当前定义的ThreadLocal变量的this引用,value为我们使用set方法设置的值。每个线程的本地变量存放在自己的本地内存变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量就会一直存在(所以可能会导致内存溢出),因此使用完毕需要将其remove掉。
- 总结
- 每个Thread维护着一个ThreadLocalMap的引用
- ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
- ThreadLocal创建的副本是存储在自己的threadLocals中的,也就是自己的ThreadLocalMap。
- ThreadLocalMap的键值为ThreadLocal对象,而且可以有多个threadLocal变量,因此保存在map中
- 在进行get之前,必须先set,否则会报空指针异常,当然也可以初始化一个,但是必须重写initialValue()方法。
- ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
2.5.5、ThreadLocal解决内存泄漏问题
- 从ThreadLocalMap上对ThreadLocal使用不当会造成内存泄漏问题
- ThreadLocal的内存泄漏问题:线程执行完ThreadLocal为null,ThreadLocalMap的key没了但是value还在
- ThreadLocal解决内存泄漏问题:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况
- ThreadLocalMap的使用涉及到四个也能用类型
- 强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。
- 软引用:简言之,如果一个对象具有弱引用,在JVM发生OOM之前(即内存充足够使用),是不会GC这个对象的;
- 只有到JVM内存不足的时候才会GC掉这个对象。
- 软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中
- 弱引用(ThreadLocalMap中的Entry类的重点):如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉
- 被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉。
- 弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象呗回收掉之后,再调用get方法就会返回null
- 虚引用:虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。(不能通过get方法获得其指向的对象)
- Thread中有一个map,就是ThreadLocalMap
- ThreadLocalMap的key是ThreadLocal,值是我们自己设定的
- ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收
- 当ThreadLocal为null,也就是要被垃圾回收器回收,但是此时ThreadLocalMap生命周期和Thread的一样,它是不会回收的,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。
- 解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。
2.6、JVM对锁的优化
1、ReentrantLock 和 synchronized 的锁优化
- 主要是在公平和非公平上、性能上的性能提高,ReentrantLock是显式锁,实现于 Lock 接口,synchronized 是隐式锁,更加的原生;JDK1.6对synchronized 进行了性能优化,锁的级别得到了提高。
2、自旋锁的优化
- 锁膨胀以后,虚拟机为了避免线程真实地在操作系统层面挂起,虚拟机还会在做最后的努力—自旋锁。由于线程暂时无法获得锁,但是什么时候可以获得锁还是个未知数。系统会假设,在不久的将来,该线程会获得锁,因此,虚拟机会让线程进行空循环,在经过若干次的循环后,如果可以得到锁,那么就顺利进入临界区。如果还不能获得锁,才会真实地将线程在操作系统层面挂起。
3、偏向锁优化
- 锁偏向的核心思想就是:如果一个线程获得了锁,那么锁就进入偏向模式,当该线程再次请求锁时,无需做同步操作,这样就节省了大量有关锁申请的操作,从而提高了系统的性能。因此,在线程数不多的情况下,这种锁偏向有比较好的优化效果,但是在大量高并发的情况下,这种就不适合、因为每次都有可能是不同的线程来请求锁,偏向模式就会失效,因此,还不如不启用。使用-XX:+UseBiasedLocking可以开启偏向锁
4、轻量级锁
- 如果偏向锁失败,虚拟机并不会立即挂起线程,它还会使用一种称为轻量级锁的优化手段。轻量级锁的操作也很轻便,它只是简单的将对象的头部作为指针,指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁。如果线程获得轻量级锁成功,则可以顺利进入临界区。如果轻量级加锁失败,则表示其他线程抢先争夺到了锁,那么当前线程的锁请求就会自我膨胀为重量级锁
5、锁消除
- 锁消除,顾名思义,就是在没有竞争的情况下将锁消除掉,不加锁。比如,很可能在一个不存在并发的竞争场合使用Vector。Vector内部使用到了synchronized请求锁。
三、高并发及解决方法
3.1、认识高并发
- 高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一
- 它通常是指,通过设计保证系统能够同时并行处理很多请求。
- 通俗来讲,高并发是指在同一个时间点,有很多用户同时的访问同一 API 接口或者 Url 地址。它经常会发生在有大活跃用户量,用户高聚集的业务场景中。
- 高并发相关常用的一些指标
- 响应时间(Response Time),系统对请求做出响应的时间
- 吞吐量(Throughput),单位时间内处理的请求数量
- 每秒查询率QPS(Query Per Second),每秒响应请求数
- 并发用户数,同时承载正常使用系统功能的用户数量
…….
3.2、提升系统的并发能力
1、理解系统并发
- 互联网分布式架构设计,提高系统并发能力的方式,方法论上主要有两种:
- 垂直扩展(Scale Up)
- 水平扩展(Scale Out)
- 垂直扩展:提升单机处理能力。
- 垂直扩展的方式又有两种:
- 增强单机硬件性能,增加CPU核数如32核,升级更好的网卡如万兆,升级更好的硬盘如SSD,扩充硬盘容量如2T,扩充系统内存如128G;
- 提升单机架构性能,使用Cache来减少IO次数,使用异步来增加单服务吞吐量,使用无锁数据结构来减少响应时间;
- 垂直扩展的方式又有两种:
- 不管是提升单机硬件性能,还是提升单机架构性能,都有一个致命的不足:
- 单机性能总是有极限的。所以互联网分布式架构设计高并发终极解决方案还是水平扩展。
- 水平扩展:只要增加服务器数量,就能线性扩充系统性能,但是水平扩展对架构有要求。
2、常见的互联网分层架构
- 架构分析:
- 客户端层:典型调用方是浏览器browser或者手机应用APP
- 反向代理层:系统入口,反向代理
- 站点应用层:实现核心应用逻辑,返回html或者json
- 服务层:如果实现了服务化,就有这一层
- 数据-缓存层:缓存加速访问存储
- 数据-数据库层:数据库固化数据存储
3、分层水平扩展架构实践
- nginx反向代理的水平扩展
- 反向代理层的水平扩展,是通过“DNS轮询”实现的:
- dns-server对于一个域名配置了多个解析ip,每次DNS解析请求来访问dns-server,会轮询返回这些ip。
- 当nginx成为瓶颈的时候,只要增加服务器数量,新增nginx服务的部署,增加一个外网ip,就能扩展反向代理层的性能,做到理论上的无限高并发。
- 站点层的水平扩展
- 站点层的水平扩展,是通过“nginx”实现的。通过修改nginx.conf,可以设置多个web后端。
- 当web后端成为瓶颈的时候,只要增加服务器数量,新增web服务的部署,在nginx配置中配置上新的web后端,就能扩展站点层的性能,做到理论上的无限高并发。
- 服务层的水平扩展
- 服务层的水平扩展,是通过“服务连接池”实现的。
- 站点层通过RPC-client调用下游的服务层RPC-server时,RPC-client中的连接池会建立与下游服务多个连接,当服务成为瓶颈的时候,只要增加服务器数量,新增服务部署,在RPC-client处建立新的下游服务连接,就能扩展服务层性能,做到理论上的无限高并发。如果需要优雅的进行服务层自动扩容,这里可能需要配置中心里服务自动发现功能的支持。
4、数据层的水平扩展
- 在数据量很大的情况下,数据层(缓存,数据库)涉及数据的水平扩展,将原本存储在一台服务器上的数据(缓存,数据库)水平拆分到不同服务器上去,以达到扩充系统性能的目的。
- 按照范围水平拆分
- 每一个数据服务,存储一定范围的数据
- user0库,存储uid范围1-1kw
- user1库,存储uid范围1kw-2kw
- 拆分数据层的好处
- 规则简单,service只需判断一下uid范围就能路由到对应的存储服务;
- 数据均衡性较好;
- 比较容易扩展,可以随时加一个uid[2kw,3kw]的数据服务;
- 拆分数据层的不足
- 请求的负载不一定均衡,一般来说,新注册的用户会比老用户更活跃,大range的服务请求压力会更大;
- 按照哈希水平拆分
- 每一个数据库,存储某个key值hash后的部分数据
- user0库,存储偶数uid数据
- user1库,存储奇数uid数据
- 拆分哈希的好处
- 规则简单,service只需对uid进行hash能路由到对应的存储服务;
- 数据均衡性较好;
- 请求均匀性较好;
- 拆分哈希的不足
- 不容易扩展,扩展一个数据服务,hash方法改变时候,可能需要进行数据迁移;
- 总结
- 通过水平拆分来扩充系统性能,与主从同步读写分离来扩充数据库性能的方式有本质的不同
- 通过水平拆分扩展数据库性能:
- 每个服务器上存储的数据量是总量的1/n,所以单机的性能也会有提升;
- n个服务器上的数据没有交集,那个服务器上数据的并集是数据的全集;
- 数据水平拆分到了n个服务器上,理论上读性能扩充了n倍,写性能也扩充了n倍(其实远不止n倍,因为单机的数据量变为了原来的1/n);
- 通过主从同步读写分离扩展数据库性能:
- 每个服务器上存储的数据量是和总量相同;
- n个服务器上的数据都一样,都是全集;
- 理论上读性能扩充了n倍,写仍然是单点,写性能不变;
3.3、高并发的解决方法
- 高并发的解决方法一直都是企业追寻至今的千古难题,当一个项目访问达到一定数量级之后,高并发的解决方法就成为了首要问题
1、HTML静态化
- 效率最高、消耗最小的就是纯静态化的html页面,部署项目时能够实现HTML静态资源化能有效提高并发效率
2、资源动静分离
- 将资源分开部署,动态资源直接通过tomcat服务器部署访问,静态资源单独部署访问,好处是动态资源可以更高效的加载
3、数据库集群和库表散列
- 对于大型项目来说,数据库的集群部署时非常重要的,说白了,项目最终要解决的都是数据问题,分库分表进行读写操作只是基础,在应用程序中安装业务和应用或者功能模块将数据库进行分离,不同的模块对应不同的数据库或者表,再按照一定的策略对某个页面或者功能进行更小的数据库散列能够有效提高分文效率,可以解决一定程度的并发
4、缓存
- 缓存可以减轻访问对数据库带来的压力 MyBatis.pdf ,同时能高效的创造一个性能良好的请求—响应环境
5、镜像
- 镜像是大型网站常采用的提高性能和数据安全性的方式,镜像的技术可以解决不同网络接入商和地域带来的用户访问速度差异,比如ChinaNet和EduNet之间的差异就促使了很多网站在教育网内搭建镜像站点,数据进行定时更新或者实时更新。在镜像的细节技术方面,这里不阐述太深,有很多专业的现成的解决架构和产品可选。也有廉价的通过软件实现的思路,比如Linux上的rsync等工具。
6、负载均衡
- 均衡负载是解决高并发最常用方法之一,优点就是能解决高负荷访问和大量并发请求采用的终极解决办法
- 最常用的负载均衡方法就是Nginx部署负载均衡,实现多服务器资源分配的问题,从而减轻访问对服务器负载的压力