并发编程简单总结

在这里插入图片描述

单核CPU可以多线程么?

即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现
这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切
换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个
任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这
个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

为什么并发执行的速度会比串行慢

这是因为线程有创建和上下文切换的开销

如何减少上下文切换

减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。

  • 无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一
    些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
  • CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
  • 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这
    样会造成大量线程都处于等待状态。
  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

避免死锁

  • 避免一个线程同时获取多个锁。
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
  • 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
  • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

java字节码描述

Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节
码,最终需要转化为汇编指令在CPU上执行。

volatile关键字

被volatile关键字修饰的变量,再多线程中,一个线程修改他会立即被其他线程看到

有volatile变量修饰的共享变量进行写操作的时候会多出lock前缀指令:

  • 将当前处理器缓存行的数据写回到系统内存。
  • 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效

处理器为了提交效率,读取缓冲,当有写请求时,如果带上了volatile关键字,会发送一个前缀lock指令,将这个变量所在的缓冲行写到系统内存,但是有很多处理器,就会有多个缓冲行,他是怎么保证缓存一致的呢,每个处理器会嗅探总线传播的数据是不是过期了,如果缓存行的对应的内存地址被修改,那么就把缓存行设置为无效,当下次访问内存的时候,强制写到缓存行。

在每次volatile写之前会加上storestore屏障,写之后会加上storeload屏障。
在每次volatile读之前会加上loadload屏障,读之后加上loadstore屏障。
目的是为了指令重排序。

由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以
确保对整个临界区代码的执行具有原子性。

synchronized的实现原理与应用

具体表现为以下3种形式:

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的Class对象。
  • 对于同步方法块,锁是Synchonized括号里配置的对象。

synchronized内部实现

通过 javap -v XXX.class文件可以发现

代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是ACC_SYNCHRONIZED方法获取到的,无论哪种方式都是通过一个对象的monitor进行获取

任何线程对同步代码的访问,都要先获取监视器,如果获取失败,就会放到SyncchronizedQueue队列中,当持有线程释放了锁后,会释放阻塞在队列中的线程。

synchronized用的锁是存在Java对象头里的,HashCode、分代年龄和锁标记位。

偏向锁

当一个线程在访问同步代码块的时候,对象头mark_word会存储当前线程的Id,在重复获取锁的时候不需要cas进行获取,而是检查mark_word是否有当前线程的偏行锁,如果没有,检查mark_word是否是1,不是的话,cas竞争,将设置为1,如果为1,那么就将对象头的偏向锁指向当前线程。

偏向锁的撤销只有在竞争的时候才会释放,只有等到安全点的时候(这个时间没有执行的字节码的时候),首先会暂停拥有偏向锁的线程,检查拥有偏向锁的栈,拥有的将会被执行,最后会唤醒竞争的线程。

java保证原子性有哪些方式

简单分为两种:加锁和使用自旋cas

JVM锁除了偏向锁,都是使用cas实现的加锁

cas

比较预期值和更新之后的值是否相等,如果相等的话,就更形

cas 的缺点

  1. ABA问题,他是比较当前值和预期的值是否相等,如果一个值原来是A,变成了B,又变成了A,那么cas检测时,会发现这个值是没有变化的,但是实际上却变化了,ABA问题的解决的方法时加上版本号,每次更新的时候版本号会加一,JDK1.5出现的AtomicStampedReference来解决ABA问题,比较了当前标志是否等于预期的标志。
  2. 循环时间开销大。自旋时间太长会出现较大的CPU开销
  3. 只能保证一个共享变量的原子性。从JDK1.5出现了AtomicReference保证引用对象的原子性。可以把多个变量放到一个对象里面,进行cas操作。

java内存模型的基础

线程之间的通信有两种方式,分别为共享内存和消息传递。

在共享内存的并发模型中,通过共享程序的公共状态,隐式通信。

在消息传递的并发模型中,线程之间必须通知传递消息通信。

java内存模型的抽象结构

JMM定义了抽象关系,线程之间的共享变量存储在主内存中,每个线程都有自己的本地内存,本地内存存储了该线程的读写副本。

如果线程A与线程B之间要通信的话:

  1. 线程A把本地内存A中更新过的共享变量刷新到主内存中去。
  2. 线程B到主内存中去读取线程A之前已更新过的共享变量。

happens-before原则

从JDK 5开始,Java使用新的JSR-133内存模型(除非特别说明,本文针对的都是JSR-133内
存模型)。JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一
个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关
系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

与程序员密切相关的happens-before规则如下:

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的
    读。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个
操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见

一个happens-before规则对应于一个或多个编译器和处理器重排序规则

获取锁和释放锁的内存语义

释放锁的时候,会把共享变量刷到主内存里面。获取锁的时候,临界区的本地变量无效,从主存中刷到本地内存中。

aqs

Java队列同步器框架AbstractQueuedSynchronizer,AQS使用一个整型的volatile变量(命名为state)来维护同步状态。

主要包括:

  1. 同步队列
  2. 独占式同步状态获取与释放
  3. 共享式同步状态获取与释放
  4. 以及超时获取同步状态等同步器的核心数据结构与模板方法

同步队列

同步器依靠的是一个FIFO的双向队列来维持一个同步状态,当线程阻塞时,会把节点信息放到队尾(这个时候会调用compareAndSetTail,多个线程竞争,保证线程安全),当有线程释放的时候,会唤醒队头的线程(这个时候不用cas,只是头节点断开就行了,只会有一个成功)。

独占式同步状态获取与释放

在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋,移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。

ReentrantLock

ReentrantLock分为公平锁和非公平锁。

  • 公平锁和非公平锁释放时,最后都要写一个volatile变量state。
  • 公平锁获取时,首先会去读volatile变量。
  • 非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile
    写的内存语义

锁释放-获取的内存语义的实现至少有下面两种方式。

  1. 利用volatile变量的写-读所具有的内存语义。
  2. 利用CAS所附带的volatile读和volatile写的内存语义。

ReadWriteLock保证HashMap解决并发安全问题

每次修改操作之前加上写锁,读之前加上读锁。

concurrent包的实现

首先,声明共享变量为volatile。

然后,使用CAS的原子条件更新来实现线程之间的同步。

同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信

AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent
包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的

final域的内存语义

线程优先级

java将线程的优先级分为1-10,默认为5,线程的优先级越高,代表获取的时间片数量越高。

线程的状态

同一时刻,线程只能拥有一个状态

  • new 线程被构建,还没有调用start方法
  • Runnable 运行状态,java将就绪和运行笼统称作运行中
  • blocked 阻塞状态,表示线程阻塞于锁
  • waiting 等待状态,表示线程需要等待其他线程发出一些动作(通知或者中断)
  • time_waiting 超时等待状态,指定时间自行返回
  • terminated 终止状态

daemon线程

后台线程,只能在运行前设置为daemon

等待通知机制

注意

  1. 使用wait()、notify()和notifyAll()时需要先对调用对象加锁。
  2. 调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的
    等待队列
  3. notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或
    notifAll()的线程释放锁之后,等待线程才有机会从wait()返回
  4. notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()
    方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为
    BLOCKED。
  5. 从wait()方法返回的前提是获得了调用对象的锁。

WaitThread首先获取了对象的锁,然后调用对象的wait()方法,从而放弃了锁并进入了对象的等待队列WaitQueue中,进入等待状态。由于WaitThread释放了对象的锁,NotifyThread随后获取了对象的锁,并调用对象的notify()方法,将WaitThread从WaitQueue移到SynchronizedQueue中,此时WaitThread的状态变为阻塞状态。NotifyThread释放了锁之后,WaitThread再次获取到锁并从wait()方法返回继续执行。

Thread.join()的使用

如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才
从thread.join()返回,谁调用谁等待。

java并发框架

为什么要使用ConcurrentHashMap

  1. hashMap并发put导致死循环,因为会形成一个环形链表。
  2. hashTable使用synchronized,效率低
  3. 分段锁提升效率
  • get操作

get 操作不用加锁,经过两次hash运算,第一次找到对应的segment,第二次找到对应的entry,并且都将变量声明成volatile类型,保证segment的count字段和entry的value保证内容可见性,能够被多个线程同时读,即使两个线程同时修改和读取同一变量,由于happen before原则,对volatile的写优先于读。

  • put操作

需要对共享变量进行加锁,需要进行两个过程,第一,定位到segment,判断segment中的hashEntry是否需要扩容,第二,定位到对应的hashEntry

值得一提的是,Segment的扩容判断比HashMap更恰当,因为HashMap是在插入元素后判断元素是否已经到达容量的,如果到达了就进行扩容,但是很有可能扩容之后没有新元素插入,这时HashMap就进行了一次无效的扩容。

如何扩容,创建一个是原来两倍的数组,将原来数组的元素放到新的数组里面,为了高效,只是对segment进行扩容。

  • size 操作

先尝试通过不锁柱segment的方式统计两次各segment的大小,如果在统计的过程中,count发生了变化,则加锁统计所有segment的大小

如何判断容器是否发生变化了呢,有一个变量modCount,当有remove、clean、add操作时,modCont会加1,前后比较modCount的值即可判断

ConcurrentLinkedQueue

非阻塞的无界队列,cas实现。

阻塞队列

支持两个操作:

  1. 支持添加时队列如果满了,阻塞当前线程,直到队列不满。
  2. 支持从队列中移除元素,如果队列为空,获取元素的线程会等待到队列非空
抛出异常返回特殊值一直阻塞超时退出
addofferputoffer(e,time)
removepolltakepoll(e,time)

线程池

好处

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,
    还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用
    线程池,必须对其实现原理了如指掌。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值