并发编程基础和原理

2023-11-16

1.了解多线程的意义和使用

1.1.什么是进程?什么是线程?

进程:是一个正在执行中的程序 每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。我们打开电脑上的qq时,点击qq.exe,电脑就会运行一个qq的程序,这个程序就叫做进程。

线程:是进程中的一个控制单元

线程控制着进程的执行。当qq运行后, 我们可能会使用qq来打开多个聊天窗口进行聊天,那么每一个聊天窗口就算是一个线程。所以说,进程可以包括很多的线程。

两者关系: 一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。

线程和进程的区别?

进程是操作系统分配资源的最小单位,线程是CPU调度的最小单位。一个进程可以包括多个线程。

线程之间的执行是同时进行的,例如我们在qq聊天时,可以一边聊天,一边下载文件。此时下载文件和聊天这个两个操作就是两个线程。如果是一个线程的话,那么下载文件的过程中,我们就不能聊天了,只有等待文件下载完成之后我们才可以继续聊天,这就叫串形,而我们一边聊天一边下载就是并行。

说的再通俗一点:例如说我们现在想要打扫卫生,那么串形就是我自己一个人,就代表一个线程。我要先打扫卫生间,在打扫厨房,在打扫客厅,再打扫卧室.... 因为我一个人,所以只能按照顺序先后执行,这样的话,就会非常的耗时间。那么还有一种方式就是我找几个朋友或者找几个家政保洁一起打扫。这样的话每一个人就相当于一个线程,大家一块打扫,你打扫你的,我打扫我的,互相之间并没有关联。此时打扫卫生总耗时就是耗时最多的一个人的时间,比如客厅空间比较大,那么打扫完整个房间的总耗时就是打扫客厅的时间,这就是多线程与单线程的区别。单线程也叫串形,多线程也叫并行。

并发/高并发 简单来说,并发是指单位时间内能够同时处理的请求数。默认情况下Tomcat可以支持的最大请求数是150,也就是同时支持150个并发。当超过这个并发数的时候,就会开始导致响应延迟,连接丢失等问 题。

影响服务器吞吐量的因素

硬件

CPU、内存、磁盘、网络 软件层面

>最大化的利用硬件资源

线程数量、JVM内存分配大小、网络通信机制(BIO、NIO、AIO)、磁盘IO 线程数量如何提升服务端的并发数量 并发和并行 并行是指两个或者多个事件在同一时刻发生; 并发是指两个或多个事件在同一时间间隔内发生,这个词可以冲宏观和微观两个层面来讲,如果从微观 角度来看。以线程为例,假设当前电脑的cpu是单核,但是能不能支持多线程呢?当然也是能的,此时 如果是多线程运行的话,那么CPU是通过不断分配时间片的方式来实现线程切换,由于切换的速度足够 快,我们很难感知到卡顿的过程。

1.2 Java中的线程

Runnable 接口 Thread 类 Callable/Future 带返回值的 Thread这个工具在哪些场景可以应用 网络请求分发的场景 文件导入 短信发送场景

1.3 线程的生命周期

线程的启动 -> 结束 阻塞状态 WAITING ===> 调用wait方法时 TIME_WAITING===> 调用sleep方法是 BLOCKED===>阻塞状态,未获取到锁 IO阻塞

代码:

public class Demo {

    public static void main(String[] args) {
        new Thread(()->{
            while(true){
                try {
                    TimeUnit.SECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"STATUS_01").start();  //阻塞状态

        new Thread(()->{
            while(true){
                synchronized (Demo.class){
                    try {
                        Demo.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"STATUS_02").start(); //阻塞状态

        new Thread(new BlockedDemo(),"BLOCKED-DEMO-01").start();
        new Thread(new BlockedDemo(),"BLOCKED-DEMO-02").start();

    }
    static class BlockedDemo extends  Thread{
        @Override
        public void run() {
            synchronized (BlockedDemo.class){
                while(true){
                    try {
                        TimeUnit.SECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

 找到class类路径使用命令行执行jps命令查看进程号, 使用jstack查看对应pid的线程的堆栈信息

需要注意的是,操作系统中的线程除去 new 和 terminated 状态,一个线程真实存在的状态,只有: ready :表示线程已经被创建,正在等待系统调度分配CPU使用权。 running :表示线程获得了CPU使用权,正在进行运算 waiting :表示线程等待(或者说挂起),让出CPU资源给其他线程使用 在加上新建状态和死亡状态,一共5种

线程的启动

new Thread().start(); //启动一个线程
Thread t1=new Thread()
t1.run(); //调用实例方法

线程的终止

线程什么情况下会终止 ? run方法执行结束

interrupt()的作用 设置一个共享变量的值 true 唤醒处于阻塞状态下的线程。

2. 并发编程的挑战

一个问题引发的思考

    public static int count=0;
    public static void incr(){
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++;
        }
    public static void main( String[] args ) throws InterruptedException {
        for(int i=0;i<1000;i++){
            new Thread(()->App.incr()).start();
        }
        Thread.sleep(3000); //保证线程执行结束
        System.out.println("运行结果:"+count);
    }

结果是小于等于1000的随机数。

原因: 可见性、原子性 count++的指令, 使用命令 javap -v 类名.class 查看java指令

14: getstatic #5 // Field count:I
15: iconst_1
16: iadd
17: putstatic #5

2.1 锁(Synchronized)

互斥锁的本质是什么.

共享资源

锁的使用 可以修饰在方法层面和代码块层面

class Test {
    // 修饰非静态方法
    synchronized void demo() {
    // 临界区
    }
    // 修饰代码块
    Object obj = new Object();
    void demo01() {
        synchronized(obj) {
        // 临界区
        }
    }
}

锁的作用范围 synchronized有三种方式来加锁,不同的修饰类型,代表锁的控制粒度:

  1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁(对象锁)

  2. 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁(常量锁)

  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。(类锁)

public class SynchronizedDemo {

      Object lock=new Object();
      //只针对于当前对象实例有效.
      public SynchronizedDemo(Object lock){
              this.lock=lock;
      }
       void demo(){
           synchronized(lock){

           }
      }
      void demo03(){
          synchronized (this){
              //线程安全性问题.
          }
      }
      //-------------//类锁. 针对所有对象都互斥
      synchronized  static void demo04(){
      }
      void demo05(){
          synchronized (SynchronizedDemo.class){
          }
      }
      //锁的范围
      // 实例锁,对象实例
      // 静态方法、类对象、 类锁
      // 代码块
      public static void main(String[] args) {
          Class clazz=SynchronizedDemo.class;
          Object object=new Object();
          Object object1=new Object();
          SynchronizedDemo synchronizedDemo=new SynchronizedDemo(object);
          SynchronizedDemo synchronizedDemo2=new SynchronizedDemo(object);
          //锁的互斥性。
          new Thread(()->{
              synchronizedDemo.demo();
          },"t1").start();

          new Thread(()->{
              synchronizedDemo2.demo();
          },"t2").start();
      }

  }

2.2 锁的升级

偏向锁

        在大多数情况下,锁不仅仅不存在多线程的竞争,而且总是由同一个线程多次获得。在这个背景下就设 计了偏向锁。偏向锁,顾名思义,就是锁偏向于某个线程。 当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的ID,后续这个线程进入和退出这 段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线 程的偏向锁。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,引入偏向锁是为了 在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。(偏向锁的目的是消除数据在无竞争情 况下的同步原语,进一步提高程序的运行性能。)

轻量级锁

        如果偏向锁被关闭或者当前偏向锁已经已经被其他线程获取,那么这个时候如果有线程去抢占同步锁 时,锁会升级到轻量级锁。

重量级锁

        多个线程竞争同一个锁的时候,虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒 这些线程; Java 线程的阻塞以及唤醒,都是依靠操作系统来完成的:os pthread_mutex_lock() ; 升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指 针,此时等待锁的线程都会进入阻塞状态

总结 偏向锁只有在第一次请求时采用CAS在锁对象的标记中记录当前线程的地址,在之后该线程再次进 入同步代码块时,不需要抢占锁,直接判断线程ID即可,这种适用于锁会被同一个线程多次抢占 的情况。 轻量级锁才用CAS操作,把锁对象的标记字段替换为一个指针指向当前线程栈帧中的 LockRecord,该工件存储锁对象原本的标记字段,它针对的是多个线程在不同时间段内申请通一 把锁的情况。 重量级锁会阻塞、和唤醒加锁的线程,它适用于多个线程同时竞争同一把锁的情况。

2.3 线程的通信(wait/notify)

在Java中提供了wait/notify这个机制,用来实现条件等待和唤醒。这个机制我们平时工作中用的少,但 是在很多底层源码中有用到。比如以抢占锁为例,假设线程A持有锁,线程B再去抢占锁时,它需要等待 持有锁的线程释放之后才能抢占,那线程B怎么知道线程A什么时候释放呢?这个时候就可以采用通信机 制。

3. 小知识点

3.1.join方法

当A线程执行到B线程的join方法时,A就会等待,等待B线程都执行完后,A才会执行。 ​ join可以用来临时加入线程执行。

3.2.守护线程

setDaemon方法 public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。

3.3.优先级

优先级默认为5级,可以通过setPriority(int newPriority)方法来更改线程的优先级 ​ 最明显的值为 MAX_PRIORITY=10 NORM_PRIORITY=5 MIN_PRIORITY=1

3.4.yield()方法

暂停当前正在执行的线程对象,并执行其他线程。

3.5.死锁

public class DeadLock {

	public static void main(String[] args) {
		final Object locka = new Object();
		final Object lockb = new Object();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				while (true) {
					synchronized (locka) {
						System.out.println(Thread.currentThread().getName() + "获取locka...");
						synchronized (lockb) {
							System.out.println(Thread.currentThread().getName() + "获取lockb...");
						}
					}
				}
			}
		}, "t1").start();
		
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				while (true) {
					synchronized (lockb) {
						System.out.println(Thread.currentThread().getName() + "获取lockb...");
						synchronized (locka) {
							System.out.println(Thread.currentThread().getName() + "获取locka...");
						}
					}
				}
			}
		}, "t2").start();	
	}
}

4. 探索线程安全性背后的本质之volatile

一个问题引发的思考

public static void main(String[] args) throws InterruptedException {
    Thread thread=new Thread(()->{
        int i=0;
        while(!stop){
            i++;
            // System.out.println("rs:"+i);
            try {
            	Thread.sleep(0);
            } catch (InterruptedException e) {
            	e.printStackTrace();
            }
        }
    });
    thread.start();
    Thread.sleep(1000);
    stop=true;
}

print就可以导致循环结束

活性失败. JIT深度优化

public static void main(String[] args) throws InterruptedException {
    Thread thread=new Thread(()->{
        int i=0;
        while(!stop){
            i++;
            // System.out.println("rs:"+i);
            try {
            	Thread.sleep(0);
            } catch (InterruptedException e) {
            	e.printStackTrace();
            }
        }
    });
    thread.start();
    Thread.sleep(1000);
    stop=true;
}

这里分为两个层面来解答

  • println底层用到了synchronized这个同步关键字,这个同步会防止循环期间对于stop值的缓存。

  • 因为println有加锁的操作,而释放锁的操作,会强制性的把工作内存中涉及到的写操作同步到主

内存,可以通过如下代码去证明。

Thread thread=new Thread(()->{
    int i=0;
    while(!stop){
    	i++;
        synchronized (JITDemo.class){
        }
    }
});
  • 第三个角度,从IO角度来说,print本质上是一个IO的操作,我们知道磁盘IO的效率一定要比CPU的计算效率慢得多,所以IO可以使得CPU有时间去做内存刷新的事情,从而导致这个现象。比如 我们可以在里面定义一个new File()。同样会达到效果。

Thread.sleep(0)

官方文档上是说,Thread.sleep没有任何同步语义,编译器不需要在调用Thread.sleep之前把缓存在寄 存器中的写刷新到给共享内存、也不需要在Thread.sleep之后重新加载缓存在寄存器中的值。 编译器可以自由选择读取stop的值一次或者多次,这个是由编译器自己来决定的。 但是在Mic老师认为:Thread.sleep(0)导致线程切换,线程切换会导致缓存失效从而读取到了新的值。

4.1 volatile关键字(保证可见性)

什么是可见性

在单线程的环境下,如果向一个变量先写入一个值,然后在没有写干涉的情况下读取这个变量的值,那 这个时候读取到的这个变量的值应该是之前写入的那个值。这本来是一个很正常的事情。但是在多线程 环境下,读和写发生在不同的线程中的时候,可能会出现:读线程不能及时的读取到其他线程写入的最 新的值。这就是所谓的可见性

硬件层面         CPU/内存/IO设备

CPU层面的高速缓存

  • CPU层面增加了高速缓存

  • 操作系统,进程、线程、| CPU时间片来切换

  • 编译器的优化 ,更合理的利用CPU的高速缓存.

总线锁&缓存锁

总线锁,简单来说就是,在多cpu下,当其中一个处理器要对共享内存进行操作的时候,在总线上发出 一个LOCK#信号,这个信号使得其他处理器无法通过总线来访问到共享内存中的数据,总线锁定把CPU 和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定 的开销比较大,这种机制显然是不合适的 。 如何优化呢?最好的方法就是控制锁的保护粒度,我们只需要保证对于被多个CPU缓存的同一份数据是 一致的就行。在P6架构的CPU后,引入了缓存锁,如果当前数据已经被CPU缓存了,并且是要协会到主 内存中的,就可以采用缓存锁来解决问题。 所谓的缓存锁,就是指内存区域如果被缓存在处理器的缓存行中,并且在Lock期间被锁定,那么当它执 行锁操作回写到内存时,不再总线上加锁,而是修改内部的内存地址,基于缓存一致性协议来保证操作 的原子性。  

指令重排序

通过使用volatile关键字防止指令重排序和内存屏障机制导致的内存可见性问题

final可以保证内存的可见性

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

并发编程基础和原理 的相关文章

随机推荐

  • 面向对象设计基本原则(举例说明)

    单一职责原则 SRP 就一个类而言 应该仅有一个引起它变化的原因 如果一个类承担的职责过多 就等于把这些职责耦合在一起 一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力 这种耦合会导致脆弱的设计 当变化发生时 设计会遭受到意想不到的
  • 基于Qt的轻量级的Ribbon控件(Office样式UI)

    基于Qt的轻量级的Ribbon控件 Office样式UI 界面截图 它支持4种目前常见的ribbon样式在线切换 包括2种office模式 office模式是最常见的ribbon模式了 就是我们经常看到的word模式 office模式的ta
  • Rust vs Go:两者结合效果更好!

    最近看到一个程序员工资排行的图 435501份数据 调查显示 Rust 是最赚钱的 随着 Rust 的发展和它表现出的很多优点 越来越多 Gopher 开始关注 Rust 首先 Rust 没有历史包袱 集表达力 高性能 内存安全于一身 可以
  • 2022年11月7日--11月13日(ue4 tf1视频教程+cesium for ue源码CesiumUtility抄写,本周10小时,合计1737小时,剩余8263小时)

    目前 mysql 7 1 tf1 3 3 oss 12 1 蓝图反射 1 7 moba 1 5 webapp 2 4 mmoarpg 00A 04 socket 2 8 根据月计划 ue4 tf1视频教程 进度按照每天一小时时长视频 其余时
  • 博客点击率过万

    写博客有5个月了 今天一看点击率过万了 但是转载的一篇文章点击了2500多次 怎么大家喜欢看文字的东西 而不喜欢技术呢 还是我写的太烂了 2012 7 9
  • QT6+Halcon

    2020年12月8日 Qt公司正式发布了Qt 6 0 这一软件开发平台全新的主要版本 Qt 6 0 已被重新设计为面向未来 以生产力为重点的基础平台 QT迎来一个新时代 Qt Halcon这种组合在机器视觉方面应用非常广泛 一 Qt6全新理
  • C++面向对象+案例(附代码)

    C 核心编程 接上一篇 c 基础入门 文章目录 C 核心编程 1 内存分区模型 1 1 程序运行前 1 2程序运行后 1 3new操作符 2 引用 略 3 函数提高 4 类和对象 4 1封装 4 1 2struct与class区别 4 1
  • 现货交易技巧有哪些可以帮助大家

    想要在现代生活中实现资产升值 或许各种投资理财是最好的选择 但是 不管是选择哪一种 大家都需要掌握基本的一些投资技巧的 比如说现货交易 作为新兴的投资行业 大家必须要认真了解现货交易技巧 这样才可以更好地开展现货交易活动 只有现货交易基础打
  • three.js入门到实战

    学习之前 示例演示 参考资料 api查询 http www webgl3d cn threejs docs index html 代码地址 https github com mrdoob three js 学习方法讲解 对于没有基础的前端小
  • 电机与接触器小结

    目录 各类电机区别 交流 直流电机的区别 同步 异步两类电机区别 为什么会同步 为什么会不同步呢 永磁同步电机 永磁同步电机内部构造 永磁同步电机工作原理 普通 变频两类电机区别 电动机的绝缘强度问题 谐波电磁噪声与震动 低转速时的冷却问题
  • 全面认识Linux下打包解压压缩命令

    1 前言 最近通过sudo tar czf usr src tgz usr src 这个命令发现我对打包方面的命令一无所知 故正式学习记录下 这个命令动作为 将 usr src 目录下的文件打包压缩为当前路径下的usr src tgz文件
  • Explain详解与索引最佳实践

    文章目录 Explain 解释 示范表 使用语句 explain 每一列说明 id select type table type key len ref rows EXTRA 索引最佳实践 Explain 解释 示范表 DROP TABLE
  • VMware虚拟机下的CentOS7网络配置

    一 虚拟机设置 VMware界面最上面 选择虚拟机 gt 设置 将网络连接改为桥接模式 如下图所示 二 查看主机DNS地址 win R 输入cmd 启动命令行界面 输入ipconfig all 查看主机DNS服务器地址 如下图所示 三 修改
  • 基于ARM-contexA9蜂鸣器驱动开发

    上次 我们写了一个LED的驱动程序 这一节 我们只需稍微改动一下就可以实现蜂鸣器的驱动 让我们来看看吧 还是跟之前一样 先找电路图 找到电路板上对应的引脚和相关联的寄存器 1 看电路图 1 蜂鸣器接口位于电路板的底板 看电路图可知道是高电平
  • Servlet与Jsp之间有哪些数据传输的方式?

    前言 根据MVC架构大家都很清楚 servlet充当咱们mvc中的c 也就是controller 而jsp则是咱们的view 所以呀 根据它们各自的职责划分 servlet相当于是一个指挥官 将页面数据交给业务逻辑层去处理 处理后的数据也就
  • 斗破苍穹算法版—萧炎的成长之路(一)

    前言 作者主页 雪碧有白泡泡 个人网站 雪碧的个人网站 推荐专栏 java一站式服务 前端炫酷代码分享 uniapp 从构建到提升 从0到英雄 vue成神之路 解决算法 一个专栏就够了 架构咱们从0说 数据流通的精妙之道 文章目录 前言 主
  • 什么是CA数字证书,CA证书有什么作用?

    CA证书 也是根证书 是最顶级的证书 也是CA认证中心与用户建立信任关系的基础 用户的数字证书必须有一个受信任的根证书 用户的数字证书才是有效的 那么 CA数字证书是干嘛用的 有什么作用呢 通过下文来详细了解下 所谓CA认证中心 它是采用P
  • Latex模板elsevier爱思唯尔KBS投稿步骤

    1 注册账号 我投的是KBS Elsevier旗下的应该都差不多 2 选择文章类型 我选的 full length article 3 Attach Files 必填项可以去搜一下 都有模板 Elsevier作者指南都有超链接 可以直接看例
  • python手写光线追踪(不使用图形学API)——第二期

    本文未经允许禁止转载 B站 https space bilibili com 455965619 作者 Heskey0 赫斯基皇 二 specular材质和glass材质 在这个案例中 总共有4种材质 none specular glass
  • 并发编程基础和原理

    1 了解多线程的意义和使用 1 1 什么是进程 什么是线程 进程 是一个正在执行中的程序 每一个进程执行都有一个执行顺序 该顺序是一个执行路径 或者叫一个控制单元 我们打开电脑上的qq时 点击qq exe 电脑就会运行一个qq的程序 这个程