多线程和并发

本文详细介绍了进程与线程的概念、区别以及它们在Java中的应用。讨论了并发与并行的区别,阐述了Java线程的创建、运行原理以及线程的同步与通信方法。此外,还探讨了线程池、死锁、线程安全和线程同步相关机制,如synchronized和volatile关键字的作用。
摘要由CSDN通过智能技术生成

进程与线程

进程与线程
  • 进程就是用来加载指令,管理内存,管理IO的,当一个程序运行,从磁盘加载这个程序到内存,就开启了一个进程
  • 一个进程能分为多个线程,一个线程就是一个指令流,将指令流中过的指令一条条以一定的顺序交给CPU执行
  • 线程是最小的调度单位,进程是资源分配的最小单位,windows中进程不是活动的,只是作为线程的容器
  • 对比
    • 进程基本都相互独立,而线程在进程内,是进程的子集
    • 进程拥有共享的资源,如内存空间,供其内部的线程共享
    • 进程间的通信较为哦复杂
      • 同一台计算机的进程通信称作IPC
      • 不同计算机之间的线程通信需要通过网络,遵守共同协议,如HTTP
    • 线程通信较为简单,共享进程内存的线程,多个内存可以访问一个共享变量
    • 线程更轻量,上下文切换成本比进程要低
  • CPU任务调度器:调度不同线程
并行与并发
  • 单核CPU下,串行执行,cpu把不同的时间片分给不同线程,轮流执行,叫做并发:concurrent
  • 多核CPU,不同的核心同时执行不同线程,叫做并行:parallel
  • 大部分情况下既有并发又有并行
异步调用
  • 同步:方法调用需要等待返回才能继续运行,叫做同步
  • 不用等待叫做异步
  • 多线程开发才能变成异步
  • 处理比较耗时的任务的时候用异步
  • tomcat的异步servlet,让用户线程处理耗时较长的操作,解放工作线程
  • 多线程可以利用多核CPU的优势,提高效率
  • linux是单核CPU
  • IO操作不占用CPU,但是一般拷贝文件使用的是阻塞IO,需要等待IO结束,没有充分利用线程
  • 需要非阻塞IO和异步IO提高线程利用率

java线程

  • 启动的时候默认有主线程在运行
TThread t = new Thread(){
     public void run(){
         //要执行的任务
     }
 };
 //起名
 t.setName("t1");
 //启动线程
 t.start();
Runnable task = new Runnable(){
            public void run(){
                //要执行的任务
            }
        };
        Thread t = new Thread(task);
        //起名
        t.setName("t1");
        //启动线程
        t.start();
  • Runnable是接口,Thread是对象
  • Runnable将任务和线程分离
Runnable task = () -> {
            //要执行的任务
        };
        Thread t = new Thread(task);
        //起名
        t.setName("t1");
        //启动线程
        t.start();
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return 100;
            }
        });
        Thread t1 = new Thread(task,"t1");
        t1.start();
        //返回值
        task.get();//主线程会一直在这里等待
查看线程进程的方法
  • windows
    • tasklist查看进程
    • taskkill杀死进程
  • linux
    • ps -fe:查看所有进程 ps -fe | grep java筛选含java的
    • ps -fT -p 查看某个进程的所有线程
    • kill 杀死进程
    • top按大写H切换是否显示线程
    • top -H -p ,查看某个进程的所有线程
  • Java:
    • jps命令查看所有java进程(有jdk就可以在linux上用)
    • jstack 查看某个java进程中的所有线程状态
    • jconsole 查看某个java进程中线程的运行情况
线程运行原理
  • 每个线程分配一块栈内存
  • 每个线程的每个方法有一个栈帧,只有一个正在执行的活动栈帧
  • 类的字节码进入方法区,完成类加载
  • 分配主线程的栈,每个栈里有程序计数器,分配main方法
  • 栈帧里面有局部变量表(args),返回地址,锁记录,操作数栈
  • 方法区内的代码放到程序计数器内,cpu调用下一行代码
  • 线程上下文切换:cpu不再执行当前线程,去执行另一个线程
    • 线程CPU时间片用完
    • 垃圾回收
      *有更高优先级的线程需要运行
    • 线程自己调用了sleep,yield,wait,join,park,synchronized,lock等方法
    • 操作系统保存当前线程的状态,并恢复另一个线程的状态,程序计数器具有这样的功能
    • 状态包括栈帧信息,局部变量,操作数栈,返回地址等
    • 频繁切换上下文会影响性能
start与run
  • start之后线程进入就绪状态(Runnable)
  • start方法不能调两次
sleep与yield
  • 进入睡眠后(timed waiting)
  • 可以其他线程调用该线程的interrupted强制唤醒,但是不一定唤醒后继续执行
  • 调用yield会让当前线程从running进入runnable,然后调度其他线程
线程和进程的区别
  • 进程是系统资源调度的最小单位,线程是CPU调度的最小单位
  • 一个线程属于一个进程,一个进程包含多个线程
  • 线程挂掉,对应的进程挂掉,进程挂掉,不会影响其他进程
  • 进程具有独立的内存单元,多个线程共享进程的内存
  • 进程的系统开销大于线程的开销,线程需要的系统资源较少
  • 线程和进程的通信方式不一样
进程间的通信方式
  • 匿名管道,有名管道,信号,消息队列,共享内存,信号量,Socket
  • 匿名管道:一种半双工的通信方式,数据只能单向流动,只能在具有亲缘关系的进程间使用
  • 有名管道:严格遵循先进先出,提供了一个路径名与之关联,以有名管道的文件名存在于文件系统中
    只要进程可以访问该路径,就能通过有名管道相互通信,可以实现任意两个进程的通信。有名管道以磁盘文件的形式存在,名字存放于文件系统中,内容在内存中
  • 信号:是进程间相互通信的一种方式,可以再任意时刻发给某一进程,无需知道状态。如果未执行,就由内核保存,直到恢复执行并传递给它
    如果信号被进程设置为阻塞,则该信号的传递被阻塞,信号的传递被延迟
  • 消息队列:是信息的链表,具有写权限的进程可以不断向消息队列内添加数据,有读权限的进程可以读取
  • 信号量:计数器,用于多线程对共享数据的访问,意图在于信号同步。主要用于解决与同步相关的问题并避免竞争条件(解决互斥和同步问题
    P值将信号量的数字减一,V值是将信号量的值加一,当引号量的值小于0,再进行p操作会使进程或县城被阻塞,直到另一个进程用V
  • 共享内存: 进程的内存是相互隔离的,而共享内存开辟了二者都能访问的空间,这两个进程就乐意共享部分数据,但是要靠互斥锁和信号量来控制对内存空间的访问
  • 套接字:在客户端与服务端之间通过网络进行通信,用于不同机器通信
  • 套接字是网络通信基本单元,不同主机双向通信的端点,用相关函数完成通信过程
线程之间的通信方式
  • wait(),notify(),notifyAll()
  • 这三个在使用synchronized来保证线程安全时使用
    • wait()让当前线程释放对象锁并进入阻塞状态
    • notify()用于唤醒正在等待相应对象锁的线程,进入就绪队列
    • notifyAll用于环形所有等待对象锁的线程,进入就绪队列
    • 每个锁都有就绪队列和阻塞队列,阻塞线程被唤醒后进入就绪队列,就绪线程被唤醒后进入阻塞队列
  • await(),signal(),signalAll()
  • 如果用Lock来保证线程安全,用这三个进行通信,对应上面三个
  • BlockingQueue
    • 主要用作线程通信的工作。生产者视图向其中加入元素而队列已满时,被阻塞。消费者视图拿出元素而队列已空时,被阻塞
    • 两个线程交替放入取出数据,从而实现线程通信
创建线程四种方式
  • 继承Thread类,不能再继承其他,但是直接用this就可以访问当前线程
  • 实现Runnable接口
    *多个线程可以共享同一个target,非常适合多线程处理同一个资源
    • 访问当前对象需要Thread.currentThread()方法
  • 实现callable接口,有返回值,可以抛出异常
  • 使用线程池创建
start()和run()的区别
  • start作用是启动新线程,进入就绪状态,一旦得到cpu式时间片就执行相应的run方法,run方法结束线程结束
  • start方法不能被重复调用
  • run方法可以重复调用,但是只是一个方法,直接调用不会产生新的线程
线程的生命周期
  • 新建new,还没有调用start()方法
  • 运行runnable,包含就绪和运行两种状态
  • 阻塞blocked:阻塞于锁
  • 无期限等待waiting,等待其他线程唤醒
  • 期限等待time_waiting,到时见自动唤醒
  • 终止terminated
  • sleep后进入timewaiting,wait后进入waiting
死锁
  • 两个或两个以上线程在执行过程中,争夺资源导致相互等待,无法释放
  • 必要条件
    • 互斥条件:该资源在任意时刻只由一个线程占用
    • 请求与保持条件:一个线程因为请求资源而阻塞,对已获得的资源保持不妨
    • 不剥夺条件:该线程获得的条件在未使用完之前不会被其他线程强行剥夺
    • 循环等待条件:若干资源间形成一种头尾香姐的循环等待资源关系
  • 避免出现死锁
    • 一次性申请所有的资源
    • 占有部分资源的线程如果申请不到其他资源,主动放弃已经申请的资源
    • 破坏循环等待条件,将所有资源设置标志位,排序,所有线程申请资源必须按照一定条件
sleep和wait的区别
  • 是Thread的方法,进入阻塞,但不会释放锁,可中断,到时候恢复运行
  • wait是Object中的方法,会释放锁,只能被其他线程唤醒,他们三个方法都必须在synchronized方法快中使用
  • wait需要通知对象内置的monitor对象释放锁,而因为OBJECT是所有的父类,所以是它的方法
线程池
  • 线程的集合称作线程池,线程池可以提高线程,线程池在启动时创建大量空闲线程,一个任务给线程池,就会分配一个线程去做,完成后,该线程并不会死亡,而是再次返回线程池中成为空闲状态
  • 一个线程只能执行一个任务,但是可以同时向线程池提交多个任务
  • 使用线程池
    • 降低资源消耗。重复利用线程减少创建和销毁
    • 提高响应速度,线程已经被创建好了
    • 提高线程的可管理性,使用线程池可以统一分配,调优和监控
    • 可以控制系统的最大并发数,以保证系统高效且安全的运行
  • 线程池的类型
    • newCachedThreadPool,线程池大小不固定,可灵活回收空闲线程,若无回收则创建新的
    • newFixedThreadPool,固定大小线程池,如果没有空闲线程,新任务会被缓存在空闲任务队列中
    • newScheduledThreadPool,定时线程池,支持定时及周期性任务
    • newSingleThreadExecutor,只创建一个线程,所有任务按顺序进行
  • 线程池核心参数
    • corePoolSize:核心线程数
    • maxmumPoolSize: 线程池中最大线程数
    • keepAliveTime:多余空闲线程数的存活时间,如果线程数大于corepoolsize,等待时间大于keepalivetime,多出来的线程被销毁
    • TimeUnit unit:上者的单位
    • workQueue:阻塞队列,提交但未被执行的任务
    • threadFactory:创建线程池中线程的线程工厂,一般用默认
    • handler:拒绝策略,当队列堵塞满了并且工作线程已经达到最大线程数
  • 线程池执行流程:
    • 线程数小于核心线程数时,使用核心线程数
    • 线程池大于核心线程数,任务放进阻塞队列
    • 阻塞队列满了,启动最大线程数
    • 最大线程数到了,启动拒绝策略
  • 线程池拒绝策略
    • AbortPolicy:默认拒绝策略,丢弃任务并抛弃异常
    • DiscartPolicy:丢弃任务,但是不抛出异常
    • DiscardOldest:丢弃队列最前面的任务,重新提交被拒绝的任务
    • CallerRunPolicy:不会抛弃任务,由调用线程处理
  • 线程池的参数确定
    • CPU密集时,少配置线程数,和CPU核数量相关,保证每个线程都在执行任务
    • IO密集型,大部分线程都阻塞,所以多配制线程数
  • 常见阻塞队列
    • ArrayBlockingQueue,常见有界队列,内部基于数组实现
    • LinkedBlockingQueue,链表实现的队列,几乎无穷大,先进先出
    • SynchronousQueue,不存储任何元素,每一个put都要take,支持公平锁和非公平锁
    • PriorityBlockingQueue,支持优先级排序的无界阻塞队列
    • DelayQueue,给上者添加了延迟获取队列
ThreadLocal
  • 每个线程私有的数据
  • 本地储存机制,可以利用该机制将数据存在线程内部,该线程可以在任意时刻任意方法获取其中过的数据
  • 底层通过ThreadLocalMap实现,每个Thread对象拥有一个 ThreadLocalMap,key为tThreadLocal,值为存进去的值
  • 应用于链接管理,可以一个线程持有一个链接,别人无法访问
  • 应用于线程池可能会造成线程泄露,一个线程被分配任务后它的ThreadLocal里面被填充了值
  • 线程通过强引用指向ThreadLocalmap,map的key为threadlocal的弱引用,而value为强引用,垃圾回收时不会清理value,造成内存泄露
线程安全
  • 多个线程访问同一个对象,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要额外同步,或者其他操作,调用这个对象都能产生正确的结果,称这个对象时线程安全的
  • 实现线程安全的方法
    • 互斥同步:保证共享数据在同一时刻只被一条(或一些)线程使用,使用synchronized和reentranlock等
    • 非阻塞同步,如CAS,乐观并发策略
    • 无同步策略,如threadLocal
CAS(比较交换)
  • 包含三个操作数:内存位置,预期原值和新值,如果内存位置的值与预期原值相匹配,就会将该位子更新为新值,否则不做任何操作
  • CAS的缺点
    • 循环时间长开销很大:配合无限循环使用,如果一直不成功会占用CPU
    • 只能保证一个变量的原子操作
    • ABA问题:曾经被改了又被改了回来,实际上还是发生了改变,要解决可以给状态加一个版本号或者时间戳
synchronized锁升级过程
  • 包括偏向锁,轻量级锁和重量级锁,主要针对对象头MarkWord
  • 偏向锁:大多数情况下不存在锁竞争,而是一个线程多次获得锁,没有必要每次都去竞争锁
  • 当线程一访问代码块并获得锁对象时,会在java对象头和栈帧中记录偏向锁的threadID,因为偏向锁不会主动释放锁,线程1
    再次获得锁,比较当前线程的threadID和java对象头中的ID是否一致,如果一致,就不用了CAS加锁,解锁
    如果不一致,则查看对象头中threadID的线程是否存活,如果不存活,锁对象被重置为无锁状态,其他线程竞争将它设为偏向锁
    如果存活,立刻查找线程1的栈帧对象,如果还是需要持有这个对象,则暂停线程1,撤销偏向锁,升级为轻量级锁,如果不在需要持有,则对象设置为无锁状态
  • 轻量级锁:竞争锁对象的线程不多,持有锁的时间也不长的情况。因为阻塞对CPU的消耗很大,这个时候干脆不阻塞CPU,让它自选等到锁释放
  • 线程1获得轻量级锁时会把对象头中的MarkWord复制一份到线程1的栈帧好中创建的用于储存锁记录的LockRecord,让LockRecord中的owner指向锁对象,
    然后用CAS把对象头中的内容换成LockRecord的地址
  • 如果线程2也复制了对象头的MarkWord,但是CAS的时候发现线程一已经把对象头换了,线程2CAS失败,线程2就自旋等待线程1释放锁
  • 如果自旋的时间太长了,或者自旋的过程中又有一个新的线程在来了,就会将轻量级锁膨胀为重量级锁(只修改标志位)
  • 其他线程就被阻塞了
Synchronized和ReentLock的区别是什么
  • 相同点
    • 都是可重入锁
    • 都保证可见性和互斥性
    • 都可以控制多线程对共享对象的访问
  • 不同点
    • reentlock等待可中断
    • s中的锁是非公平的,而r中的锁默认也是非公平的,但是可以修改参数实现公平锁
    • r绑定多个条件
    • s是java中的关键字,是jvm中的锁,r是接口下的实现类,是API层面的锁
    • s隐式获取锁和释放锁,r显示获取锁和释放锁,需要在finally中释放锁
syn和volatile的区别
  • volatile是线程同步的轻量级实现
  • volatile只能修饰变量,而syn修饰方法和代码块
  • volatile只能保证资源的可见性,不能保证资源的原子性,syn都可以
  • volatile实现可见性,syn实现同步性、
乐观锁和悲观锁
  • 悲观锁:总是认为别人会修改,所以每次拿数据都会上锁,阻塞别的线程,主要通过syn和lock实现
  • 乐观锁:总是认为别人不会修改,更新前判断一下,是一种非阻塞算法
  • 乐观锁适用于写比较少读比较多的场景,悲观锁适用于多写的场景
公平锁和非公平锁
  • 公平锁表示获取锁的顺序是按照请求锁的时间早晚来决定的
  • 公平锁通过队列实现,非公平锁采用抢夺策略
独占锁和共享锁
  • 独占锁只能被一个线程持有,而共享锁可能被很多线程持有
读写锁
  • 读可以共享,而写只能一个,读读不互斥,读写互斥,写写互斥
  • 并发不是很高的话没必要用
AQS抽象同步队列
  • 是一个抽象队列同步器,维护一个状态标志位和先进先出的(FIFO)的线程等待队列来实现多线程访问共享资源
  • 用来实现同步锁,常见的有ReentrantLock和CountDownLatch。是一个抽象类,通过继承的方法实现
  • 分类
    • 独占锁:每次只有一个线程能持有锁,如reentrantlock。
    • 共享锁:允许多个资源同时获得锁,并访问并发资源,如ReentrantReadWriteLock
  • FIFO双向队列,如果当前信息竞争锁失败,AQS会把当前线程和等待信息构成一个Node加入同步队列,同时阻塞该线程。当获取锁的线程释放锁之后,会从队列中唤醒一个阻塞的节点
  • state用于表示当前的同步状态,根据state状态判断处于锁定或其他状态
  • 队列中添加节点,在双向链表最后添加,同时CAS将tail指向它
  • 头结点释放状态,会唤醒后续节点,后续节点获得锁成功,则变为头节点,这里不需要CAS设置
  • 会先通过tryAcquire尝试独占锁,如果成功返回true,如果失败返回false
  • 失败会通过addWaiter将当前线程封装成Node添加到队尾
  • 如果添加到队列失败会一直自旋尝试,直到成功,用CAS保证原子性
  • 获取当前节点的pre节点,如果是head,则它就有资格争抢锁,因此只有队列第二个节点有资格争抢锁
  • head节点是当前持有锁的线程,当前线程释放会唤醒下一个节点的线程,此时state代表的是重入次数
  • 共享锁则是设置一个state,一个线程先尝试修改state,若返回值大于等于state,则获取成功,若小于0,则获取失败,线程进入同步队列等待
  • 总结:在获得同步锁时,同步维护一个同步队列,获取状态失败的线程都会被加入队列中并在队列中自旋
    移除节点的条件是前驱节点为头结点并且成功获取了同步状态,在释放同步状态时,唤醒头结点的后继节点
  • AQS常用组件:Semaphore,CountDownLatch,CyclicBarrier
Reentranlock
  • 是可重入的独占锁,同时只有一个线程可以获得该锁,其他获取该锁的线程会被阻塞放入该锁的AQS阻塞队列里面
  • 基于AQS实现,很久参数来决定内部是公平锁还是非公平锁,默认是非公平锁
  • state代表可重入次数,在没有被任何线程持有的时候默认为0
  • 获得该锁后CAS变为1,每次重入都加一
  • 释放锁会不断CAS减一,如果减到0则成功释放
volatile关键字
  • 保证可见性和防止指令重排
  • 修改volatile值的时候,JMM会把工作内存的共享变量刷新到主线程中
  • 读取volatile的时候,会把该线程的工作内存设置为无效,从主内存中读取共享变量值
  • 原理:内存屏障,保证指令重排不能排到之前的位置,会强制将缓存修改立刻存入主存,如果是写操作,会导致CPU中对应的缓存无效
  • 实际应用
    • 双重校验单例模式:防止指令重排
    • CAS中,使变量的变化对其他线程可见
    • AQS中,volatile修饰state变量
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值