Java并发知识点小结

《Java并发编程实战》小结

应用层面小结,活跃性、性能与测试书上是啥就是啥,了解就好,没做过多研究,书内学到的更多是思想

书内基本介绍的就是循序渐进引导读者思考问题,然后抛出各种并发包下的类,实现一些常见问题

一、问题引入

  1. 下面的DoubleCheckedLocking类的双重检查加锁(DCL)是否安全呢?

public class DoubleCheckedLocking{
    private static Resource resource;
    public static Resource getInstance(){
        if(resource == null){
            synchronized(DoubleCheckedLocking.class){
                if(resource == null){
                    resource = new Resource();
                }
            }
        }
        return resource;
    }
}

这种写法大多数书上都建议不可取,虽然看上去很聪明,降低了同步的影响,但是处于并发条件下,新分配了一个Resource,Resource的构造函数将把实例中的各个域由默认值修改为它们的初始值。由于两个线程没有使用同步,因此线程B看到的线程A中的操作顺序,可能与线程A执行这些操作时的顺序并不相同。因此即使线程A初始化Resource实例之后再将resource设置为指向它,线程B仍可能看到对resource的写入操作将在对Resource各个域的写入操作之前发生。因此线程B可能看到一个被部分构造的Resource实例,该实例可能处于无效状态,并在随后该实例状态可能出现无法预料的变化。

二、基础知识(需要掌握的,列出了几点)

  1. 一些基础的类

    • ThreadLocal、ConcurrentHashMap、ConcurrentSkipListMap、ArrayDeque、LinkedBlockingDeque、CopyOnWriteArrayList

      • 常见的由阻塞对列和消费者-生产者模式

    • 闭锁:CountDownLatch、FutureTask

      • CountDownLatch:首先闭锁就是一种同步工具,可以延迟线程的进度直到达到终止状态。这个类比较灵活,闭锁状态包括一个计数器,countDown方法递减计数器,await方法等待计数器到达零,如果非零,await方法将会一直阻塞知道为零。这个挺适合用来同时启动所有线程,或者获取多个网络请求的汇总等等

      • FutureTask也可以用做闭锁,它表示的计算是通过Callable来实现的,相当于一种可生成结果的Runnable,并且可处于一下三种状态:等待运行、正在运行、运行完成。Future.get的行为取决于任务的状态,任务完成会立即返回结果,否则将阻塞直到任务进入完成状态,然后返回结果或者抛出异常。可以用来提前加载稍后需要的数据

    • 信号量:Counting Sqmaphore

      • 这是用来控制同时访问某个特定资源的操作数量或者同时执行某个指定操作的数量,这个类管理着一组虚拟的许可,acquire将阻塞知道有许可,release将返回一个许可给信号量

    • 栅栏:Barrier eg:CyclicBarrier

      • 类似于闭锁,它能阻塞一组线程直到某个时间发生,也就是如果所有线程到达了栅栏处,那么栅栏打开,所有线程被释放,如果调用await超时或者await阻塞的线程被中断,那么可以认为栅栏被打开了,所有阻塞的await调用都将终止并抛出BrokenBarrierException,如果成功通过栅栏,await将为每个线程返回一个唯一的到达索引号

    • Executor框架

      • 众所周知,这个框架对应的四个创建线程池的方法

        • newFixedThreadPool

          • 创建固定长度的线程池,如果运行中的某个线程由于发生了未预期的Exception而结束,那么线程会补充一个新的线程

        • newCachedThreadPool

          • 创建一个可缓存的线程池,如果线程池的当前规模超过了处理需求时,那么将回收空闲的线程

        • newSingleThreadExecutor

          • 创建一个单线程的Executor,如果这个线程异常结束,会创建另一个线程来替代,能确保依照任务在队列中的顺序来串行执行

        • newScheduledThreadExecutor

          • 创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似Timer

      • Executor的生命周期

        • Executor的实现通常会创建线程来执行任务,但JVM只有在所有(非守护)线程全部终止后才会退出,所以如果无法正确关闭Executor,那么JVM将无法结束

        • shutdownNow()强行关闭,然后返回所有尚未启动的任务清单,shutdown()正常关闭

          //一般关闭Executor方法
           try {
                  pool.shutdown();
                  // (所有的任务都结束的时候,返回TRUE)
                  if(!pool.awaitTermination(awaitTime, TimeUnit.MILLISECONDS)){
                      // 超时的时候向线程池中所有的线程发出中断(interrupted)。
                      pool.shutdownNow();
                  }
              } catch (InterruptedException e) {
                  // awaitTermination方法被中断的时候也中止线程池中全部的线程的执行。
                  pool.shutdownNow();
              }
          ​
        • 任务取消大多会用到Future类,或者用一些volatile变量来控制。一般任务取消后,可能下一次需要从上次结果后继续处理,那么就需要把任务记录下来,用shutdownNow()方便。

  2. ReentrantLock显示锁和synchronized锁

    • 两者性能目前逐渐接近吧,吞吐量也还好,除非你用JDK5、JDK6之类的。

    • 个人觉得目前能用synchronized解决就不需要用ReentrantLock,现在的jvm已经将synchronized优化得足够好了

    • 使用Lock,需要注意lock和unlock方法需要成对出现,以防出现死锁。

    • lock锁,有公平性和非公平性而言,一般默认公平就好。在非公平锁中,只有当锁被某个线程持有时,新发出的请求得线程才会被放入队列中,也就是说,当一个线程请求非公平锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有等待线程并获得这个锁

    • synchronized和ReentrantLock尽量不要混用,容易发生错误,在一些内置锁无法满足需求下,才考虑ReentrantLock,例如可定时、可轮询的与可中断的锁的获取操作,公平队列,以及非块结果的锁。否则还是优先考虑synchronized

  3. AQS

    • java.util.concurrent类的许多阻塞类,例如ReentrantLock、Semaphore、ReentrantRead-WriteLock、CountDownLatch、SynchronousQueue和FultureTask等都是基于AQS构建的。AQS内部维护了一个等待线程队列,其中记录了某个线程请求的是独占访问还是共享访问

    • ReentrantLock:内部维护了一个变量来保存当前所有者线程的标识符,同步状态用于保存锁获取操作的次数,实现方式CAS

    • Semaphore与CountDownLatch:将AQS的同步状态用于保存当前许可的数量,利用tryAcquireShared通过compareAndSetState以原子方式降低许可的计数,如果成功,其它等待的线程同样会解除阻塞。CountDownLatch使用ASQ的方式和Semaphore类似,countDown方法调用release,从而导致计数值递减,await调用acquire,当计数器为零时,acquire将立即返回,否则将阻塞。

    • FutureTask使用AQS来保存任务的状态,并且还会维护一些额外的状态变量,用来保存计算结果或者抛出的异常,任务取消,线程就会中断

    • ReadWriteLock接口表示存在两个锁,一个读取锁,一个写入锁,但在基于AQS实现的ReentrantReadWriteLock中,单个AQS子类将同时管理读取加锁和写入加锁。ReentrantReadWriteLock使用了一个16位的状态来表示写入锁的计数,并且使用了另一个16位的状态来表示读取锁的计数。

  4. 独占锁是一种悲观技术,比较并交换(CAS)是乐观技术,大多数处理器架构使用的是CAS。CAS包括3个操作数----需要读写的内存位置V、进行比较的值A和拟写入的新值B,当且仅当V的值等于A时,CAS才会通过原子方式用新值B来更新V的值,否则不执行任何操作,无论位置V的值是否等于A,都将返回V的值。

  5. 乐观锁是一种乐观思想,即认为读多写少,遇到并发的可能性低。Java中的乐观锁基本是通过CAS操作实现的。AQS框架下的锁则是先尝试CAS乐观锁去获取锁,获取不到才会转为悲观锁,如ReentrantLock。

  6. synchronized或导致争用不到锁的线程进入阻塞状态,所以说它是java语言中一个重量级锁,为了缓解性能问题,JVM从1.5开始,引入了轻量锁与偏向锁,默认启用了自旋锁,这些都属于乐观锁。

  7. 对象内存结构,由三部分构成,分别是对象头、对象实例、对齐填充。其中对象头包括两部分,第一部分是markword,用于存储对象自身运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit。第二部分是类类型指针,即对象指向它的元数据指针,虚拟机通过这个指针来确定这个对象是哪个实例的。如果是数组对象的话,那么对象头中还必须有一块数据记录数组长度。对象实例这部分则是对象真正存储的有效信息,页时程序代码中所定义的各种类型的字段内容,无论是从父类继承下来的还是在子类中定义的,都需要记录下来。对齐填充不是必须的,仅仅起着占位符的作用,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

    • markword内容如下表所示

      状态标志位存储内容
      未锁定01对象哈希码、对象分代年龄
      轻量级锁定00指向锁记录的指针
      膨胀(重量级锁定)10执行重量级锁定的指针
      GC标记11空(不需要记录信息)
      可偏向01偏向线程ID、偏向时间戳、对象分代年龄
  8. 自旋锁原理很简单,如果持有锁的线程能在很短时间内释放资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免了用户线程和内核线程的切换消耗,注意:线程自旋是需要消耗CPU的,如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。适用于锁的竞争不激烈场景,减少线程阻塞。

  9. 偏向锁,顾名思义,它会偏向第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况下,就会给线程加一个偏向锁。适用场景为只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行,也就是在锁无竞争的情况下使用。一旦有了竞争就会升级为轻量级锁,升级为轻量级锁的时候就需要撤销偏向锁,撤销偏向锁的时候就会导致stop the word操作。

  10. 轻量级锁,由偏向锁升级而来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用时,就会升级为轻量锁。这些内容在JVM虚拟机一书内都有介绍,并对原理进行了阐述。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值