关于Java高并发编程你需要知道的“升段攻略”(1)

charlesc
发布于 2021-3-18 09:44
浏览
0收藏

基础


1、Thread对象调用start()方法包含的步骤:

 

  1. 通过jvm告诉操作系统创建Thread
  2. 操作系统开辟内存并使用Windows SDK中的createThread()函数创建Thread线程对象
  3. 操作系统对Thread对象进行调度,以确定执行时机
  4. Thread在操作系统中被成功执行


2、执行start的顺序不代表执行run的顺序


3、执行方法run和start有区别

xxx.run():立即执行run()方法,不启动新的线程

xxx.start():执行run()方法时机不确定,启动新的线程


4、线程停止:

  1. public static boolean interrupted():测试当前线程是否已经是中断状态,执行后具有清除状态标志值的功能
  2. public boolean this.isInterrupted():测试当前线程是否已经是中断状态,不清除状态标志
  3. 停止线程-异常
  4. 在sleep下停止线程,当sleep和interrupt同时出现时,会报错
  5. stop终止线程
  6. return停止线程
  7. 暂停线程

suspend 和 resume独占


5、synchronized原理:

 

方法:使用ACC_SYNCHRONIZED标记
代码块:使用monitorenter和monitorexit指令


6、静态同步synchronized方法与synchronized(class)代码块,同步synchronized方法与synchronized(this)代码块


7、volatile关键字特性:

可见性:一个线程修改共享变量的值,其他线程立马就知道
原子性:double和long数据类型具有原子性,针对volatile声明变量的自增不具有原子性
禁止代码重排序


8、利用volatile关键字解决多线程出现的死循环(可见性),本质是因为线程的私有堆栈和公有堆栈不同步

 

9、synchronized代码块具有增加可见性的作用


10、自增/自减操作的步骤

从内存中取值
计算值
将值写入内存

 

11、使用Atomic原子类进行i++操作实现原子性


12、sychronized代码块和volatile关键字都禁止代码重排序:位于代码块/关键字两侧的不能互相重排序,只能各自在前面或者后面重排序

 

13、wait/notify机制

  1. 使用前都必须获得锁,即必须位于synchronized修饰的方法内或者代码块内,且必须是同一把锁
  2. 使用wait后会释放锁,当调用notify后,线程再次获得锁并执行
  3. wait执行后会立即释放锁,而notify执行后不会立即让出锁,而是等到执行notify方法的线程将程序执行完以后才让出锁
  4. wait使线程暂停运行,notify使线程继续运行
  5. 处于wait状态下的线程会一直等待
  6. 在调用notify时若没有处于wait状态的线程,命令会被忽略
  7. 当调用了多个wait方法,同时需要多个notify方法时,所有等待的wait状态线程获取锁的顺序是执行wait方法的顺序,即先调用先获得锁
  8. notifyAll方法可以一次通知所有的处理wait状态的线程,但是这些线程获得锁的顺序是先调用的最后获得,后调用的先获得,通常还有其他的顺序,取决于jvm的具体实现


14、wait立即释放锁,notify不立即释放锁,sleep不会释放锁


15wait与interrupt方法同时出现会出现异常


16wait(long)表示等待一段时间后,再次获取锁,不需要notify方法通知,若没有获取锁则会一直等待,直到获取锁为止


17、if+wait建议替换成while+wait


18、线程间通过管道通信(字符流/字节流):PipInputStream/PipOutputStream、PipWriter/PipReader


19、join方法是所属的线程对象x正常执行run()方法中的任务,而使当前线程进行无期限的阻塞,等待线程x销毁后再继续执行线程z后面的代码,具有串联执行的效果


20、join方法具有使线程排队运行的效果,有类似同步的运行效果,但是join方法与synchronized的区别是join方法内部使用wait方法进行等待(会释放锁),而synchronized关键字使用锁作为同步


21、join与interrupt同时出现会出现异常


22、join(long)执行后会调用内部的wait(long)会释放锁,而Thread.sleep(long)则不会释放锁


23、ThreadLocal

  1. 将数据放入到当前线程对象中的Map中,这个Map是Thread类的实例变量。类ThreadLocal自己不管理、不存储任何数据,它只是数据和Map之间的桥梁,用于将数据放入Map中,执行流程如下:数据-->ThreadLocal-->currentThread() -->Map
  2. 执行后每个线程中的Map存有自己的数据,Map中的key存储的是ThreadLocal对象,value就是存储的值。每个Thread中的Map值只对当前线程可见,其他线程不可以访问。当前线程销毁,Map随之销毁,Map中的数据如果没有被引用、没有被使用,则随时GC回收
  3. 线程、Map、数据之间的关系可以做如下类比:人(Thread)随身带有兜子(Map),兜子(Map)里面有东西(Value),这样,Thread随身也有数据,随时可以访问自己的数据
  4. 可以通过继承类,并重写initialValue()来解决get()返回null的问题
  5. Thread本身不能实现子线程使用父线程的值,但是使用InheritableLocalThread则可以访问,不过,不能实现同步更新值,也就是说子线程更新了值,父线程中还是旧值,父线程更新了值,子线程中还是旧值
  6. 通过重写InheritableLocalThread的childValue可以实现子线程对父线程继承的值进行加工修改

 

24、Java多线程可以使用synchronized关键字来实现线程同步,不过jdk1.5新增加的ReentrantLock类可能达到同样的效果,并且在扩展功能上更加强大,如具有嗅探锁定、多路分支通知等功能


25、关键字synchronized与wait()、notify()/notifyAll()方法相结合可以实现wait/notify模型,ReentrantLock类也可以实现同样的功能,但需要借助于Condition对象。Condition类是jdk5的技术,具有更好的灵活性,例如,可以实现多路通知功能,也就是在一个Lock对象中可以创建多个Condition实例,线程对象注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活;在使用notify()/notifyAll()方法进行通知时,被通知的线程有jvm进行选择,而方法notifyAll()会通知所有waiting线程,没有选择权,会出现相当大的效率问题,但使用ReentrantLock结合Condition类可以实现“选择性通知”,这个功能是Condition默认提供的

26、Condition类

  1. Condition对象的创建是使用ReentrantLoack的newCondition方法创建的
  2. Condition对象的作用是控制并处理线程的状态,它可以使线程呈wait状态,也可以让线程继续运行
  3. 通过Condition中的await/signal可以实现wait/notify一样的效果Object中的wait方法相当于Condition类中的await方法
    Object中的wait(long timeout)方法相当于Condition类中的await(long time, TimeUnit unit)方法
    Object中的notify方法相当于Condition类中的signal1方法
    Object类中的notifyAll方法相当于Condition类中的signalAll方法
  4. await方法暂停线程的原理并发包内部执行了unsafe类中的public native void park(boolean isAbsolute, long time)方法,让当前线程呈暂停状态,方法参数isAbsolute代表是否为绝对时间
  5. 公平锁:采用先到先得的锁,每次获取之前都会检查队列里面有没有排队等待的线程,没有才尝试获取锁,如果有就将当前线程追加到队列中
  6. 非公平锁:采用“有机会插队”的策略,一个线程获取之前要先尝试获取锁而不是在队列中等待,如果获取成功,则说明线程虽然是启动的,但先获得了锁,如果没有获取成功,就将自身添加到队列中进行等待
  7. 线程执行lock方法后内部执行了unsafe.park(false, 0L)代码;线程执行unlock方法后内部执行unsafe.unpark(bThread)方法
  8. public int getHoldCount()查询“当前线程”保持锁定的个数,即调用lock方法的次数
  9. public final int getQueueLength()返回正等待获取此锁的线程估计数,例如,这里有5个线程,其中1个线程长时间占有锁,那么调用getQueueLength()方法后,其返回值是4,说明有4个线程同时在等待锁的释放
  10. public int getWaitQueueLength()返回等待与此锁有关的给定条件Condition的线程统计数。例如,有5个线程,每个线程都执行了同一个Condition对象的await()方法,则调用getWaitQueueLength()方法时,其返回的值是5
  11. public final boolean hasQueueThread(Thread thread)查询指定的线程是否正在等待获取此锁,也就是判断参数中的线程是否在等待队列中
  12. public final boolean hasQueueThreads()查询是否有线程正在等待获取此锁,也就是等待队列中是否有等待的线程
  13. public boolean hasWaiters(Condition condition)查询是否有线程正在等待与此锁有关的condition条件,也就是是否有线程执行了condition对象中的await方法而呈等待状态。而public int getWaitQueueLength(Condition condition)方法是返回有多少个线程执行了condition对象中的await方法而呈等待状态
  14. public final boolean isFair()判断是不是公平锁
  15. public boolean isHeldByCurrentThread()是查询当前线程是否保持此锁
  16. public boolean isLocked()查询此锁是否由任意线程保持,并没有释放
  17. public void lockInterruptibly()当某个线程尝试获取锁并且阻塞在lockInterruptibly()方法时,该线程可以被中断
  18. public boolean tryLock()嗅探拿锁,如果当前线程发现锁被其他线程持有,则返回false,程序继续执行后面的代码,而不是呈阻塞等待锁的状态
  19. public boolean tryLock(long timeout, TimeUnit unit)嗅探拿锁,如果当前线程发现锁被其他线程持有了,则返回false,程序继续执行后面的代码,而不是呈阻塞等待状态。如果当前线程在指定的timeout内持有了锁,则返回值是true,超过时间则返回false。参数timeout代表当前线程抢锁的时间
  20. public boolean await(long time, TimeUnit unit)和public final native void wait(long timeout)方法一样,都具有自动唤醒线程的功能
  21. public long awaitNanos(long nanoTimeout)和public final native void wait(long timeout)方法一样,都具有自动唤醒线程的功能,时间单位是纳秒(ns)。
  22. public boolean awaitUntil(Date deadline)在指定的Date结束等待
  23. public void awaitUninterruptibly()在实现线程在等待的过程中,不允许被中断


27、ReentrantReadWriteLock类

ReentrantLock类具有完全互斥排他的效果,同一时间只有一个线程在执行ReentrantLock.lock()方法后面的任务,这样做虽然保证了同时写实例变量的线程安全性,但效率非常低下,所以jdk提供了一种读写锁——ReentrantReadWriteLock类,使用它可以在进行读操作时不需要同步执行,提升运行速度,加快运行效率

 

读写锁有两个锁:一个是读操作相关的锁,也称共享锁;另一个是写操作相关的锁,也称排他锁

 

读写互斥,写读互斥,写写互斥,读读异步


28、Timer定时器

  1. 定时/计划任务功能在Java中主要使用Timer对象实现,它在内部使用多线程的方式进行处理,所以它和线程技术有很大的关联
  2. 在指定的日期执行任务:schedule(TimerTask task, Date firstTime, long period)
  3. 按指定周期执行任务:schedule(TimerTask task, long delay, long period)
  4. 注意:在创建Timer对象时会创建一个守护线程,需要手动关闭,有两种方法new Timer().concel():将任务队列中的任务全部清空
    new TimerTask().concel():仅将自身从任务队列中清空
  5. scheduleAtFixedRate()具有追赶性,也就是若任务的启动时间早于当前时间,那么它会将任务在这个时间差内按照任务执行的规律执行一次,相当于“弥补”流逝的时间
  6. 但timer队列中有多个任务时,这些任务执行顺序的算法是每次将最后一个任务放入队列头,再执行队列头TimerTask任务的run方法、


29、单例模式与多线程

饿汉式
懒汉式使用DCL(双重检查锁)来实现(volatile+synchronized)


进阶


并发编程的进阶问题


1、并发执行程序一定比串行执行程序快吗?

答:不是的。因为线程有创建和上下文切换的开销,所以说,在某种情况下线并发执行程序会比串行执行程序慢

 

2、上下文切换:cpu通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存是一个任务的状态,以便于下次切换回这个任务时,可以再加载这个任务的状态,故任务从保存到再加载的过程就是一次上下文切换。这就像我们同时读两本书,当我们在第一本英文的技术书时,发现某个单词不认识,于是便打开英文字典,但是放下英文技术书之前,大脑必须记住这本书读到了多少页的多少行,等查完单词后,能够继续读这本书。这样的切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度

3、减少上下文切换:

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


Java并发机制的底层实现原理


关于volatile


1、Java代码在编译后会变成Java字节码,字节码被类加载器加载到jvm里,jvm执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所有的并发机制依赖于jvm的实现和CPU的命令
2、volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度
3、volatile的定义:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保排他锁单独获得这个变量
4、相关CPU的术语定义内存屏障(memory barries):是一组处理器指令,用于实现对内存操作的顺序限制
缓冲行(cache line):缓存中可以分配的最小储存单元。处理器填写缓存线时会加载整个缓存线,需要使用多个主内存读周期
原子操作(atomic operations):不可中断的一个或一系列操作
缓存行填充(cache line fill):当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个缓存行到适当的缓存
缓存命中(cache hit):如果进行高速缓存填充操作的内存位置仍然是下次处理器访问的地址时,处理器从缓存中读取数据,而不是从内存读取
写命中(write hit):当处理器将操作数写回到一个内存缓存的区域时,它首先会检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是写回到内存
写缺失(write misses the cache):一个有效的缓存行被写入到不存在的内存区域
5、volatile底层通过使用lock前缀的指令来保证可见性,lock前缀的指令在多核处理器下会引发两件事情:

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


原子操作


原子操作(atomic operation):不可中断的一个或一系列操作

 

1、相关的CPU术语

缓存行(cache line):缓存的最小操作单位

 

比较并交换(compare and swap):CAS操作需要输入两个数值,一个旧值和新值,在操作期间先比较旧值有没有发生变化,若没有发生变化才交换成新值,发生了变化则不交换


CPU流水线(CPU pipeline):CPU流水线的工作方式就像工业生产上的装配流水线,在CPU中由56个不同功能的电路单元组成一条指令处理流水线,然后将x86指令分成56步后再由这些电路单元分别执行,这样就能实现在一个CPU时钟周期完成一条指令,因此提高CPU的运算速度


内存顺序冲突(memory order violation):内存顺序冲突一般是由假共享引起的,假共享是指多个CPU同时修改同一个缓存行的不同部分而引起其中一个CPU的操作无效,当出现这个内存顺序冲突时,CPU必须清空流水线


2、处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求被阻塞住,该处理器独占内存。例如:当多个线程执行i++操作时,多个处理器同时从各自的缓存中读取变量i,分别进行加1操作,然后分别写入系统内存中。那么,想要保证读改写共享变量的操作是原子的,就必须保证CPU1读改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存


频繁使用的内存会缓存在处理器的L1、L2和L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁。缓存锁定是指内存区域如果被缓存在处理器的缓存行中,并且在LOCK期间被锁定,那么但它执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许

它的缓存一致机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效。当CPU1修改缓存行中的i时使用了缓存锁定,那么CPU2不能同时缓存i的缓存行


两种情况不会使用缓存锁定

当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,则处理器会调用总线锁定
有些处理器不支持缓存锁定


3、Java实现原子操作的方式:锁和循环CASjvm中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操作直接成功为止
锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。jvm内部实现了很多种锁,有偏向锁、轻量级锁和互斥锁。除了偏向锁,jvm实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁

分类
已于2021-3-18 09:44:35修改
收藏
回复
举报
回复
    相关推荐