学习笔记之锁与多线程

  • 注释修改 ; serialVersionID ; serverlist.remove ; threadlocal.remove;


  • Memory Consistency Properties Chapter 17 of The Java™ Language Specification defines the happens-before relation on memory operations such as reads and writes of shared variables.

    synchronized and volatile constructs, as well as Thread.start() and Thread.join() methods, can form 【happens-before】 relationships.

    1. Each action in a thread happens-before every action in that thread that comes later in the program’s order.

    2. An unlock (synchronized block or method exit) of a monitor happens-before every subsequent lock (synchronized block or method entry) of that same monitor. And because the happens-before relation is transitive(重入), all actions of a thread prior to unlocking happen-before all actions subsequent to any thread locking that monitor.

    3. A write to a volatile field happens-before every subsequent read of that same field. Writes and reads of volatile fields have similar memory consistency effects as entering and exiting monitors, but do not entail mutual exclusion lockingMute.

    4. A call to start on a thread happens-before any action in the started thread.

    5. All actions in a thread happen-before any other thread(caller) successfully returns from a join on that thread.

  • ArrayList初始大小10;当发现容量不足时扩容,容量为原来的1.5倍

    LinkedList初始大小0;扩容机制:无。

    Vector初始大小10;当发现容量不足时,扩容到原来的2倍

    HashMap初始16;loader0.75,当k-v对(m.size())超过阈值(capa*loader)时扩容,扩容为2倍

    它的子链表达到8**时 ,转化成红黑树。小于6个时转化成链表。

    指定初始:7->8 11->16 3->4:expectedSize/0.75F + 1.0F -> closest 2^?

    why 调整:在使用2的幂的数字的时候,index的结果等同于HashCode后几位的值。只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。这是为了实现均匀分布

    链表头插法resize(rehash)过程中的会颠倒原来一个散列桶里面链表的顺序。在并发的时候原来的顺序被另外一个线程a颠倒了,而被挂起线程b恢复后拿扩容前的节点和顺序继续完成第一次循环后,又遵循a线程扩容后的链表顺序重新排列链表中的顺序,最终形成了

    HashSet初始大小16;扩容机制:同HashMap。

    HashTable初始大小11;扩容机制:加载因子0.75,当超过这个阈值时扩容,扩容为2倍+1

    StringBuffer 16 ; 盛不下 -> 2倍+2

    可以指定初始容量,则初始char[]的长度为:参数+16

  • 【ThreadLocalRandom】

    JDK 7之后提供并发产生随机数,能够解决多个线程发生的竞争争夺。

    ThreadLocalRandom不是直接用new实例化,而是第一次使用其静态方法current()

    Math.random()改变到ThreadLocalRandom有如下好处:

    我们不再有从多个线程访问同一个随机数生成器实例的争夺
    取代以前每个随机变量实例化一个随机数生成器实例,我们可以每个线程实例化一个

  • UUID.randomUUID().toString(); 128位;号称世界级的不重复;

    组成:当前日期和时间,时钟序列,全局唯一的IEEE机器识别号

    eg: 04f04c46-d2b2-4699-bc4d-91168cd9e5a4

    36个字符,抛去固定的4个"-",一共32个字符。可用str.replaceAll("-","");

  • 1.8中ConcurrentHashMap中的Node的 成员变量val, next 都用了 volatile 修饰;

  • 对于64位的long和double,如果没有被volatile修饰,32位OS下那么对其操作可以不是原子的。在操作的时候,可以分成两步,每次对32位操作。

    如果使用volatile修饰long和double,那么其读写都是原子操作;但volatile本身并不是来保证原子性的修饰关键字,而是可见性;

    在64bit的虚拟机下,处理是原子性的。

  • 【juc】

    如果按照用途与特性进行粗略的划分,JUC 包中包含的工具大体可以分为 6 类:

    1. 执行者(Executor)与线程池(ExecutorService,ThreadPoolExecutor)
    2. 并发队列
    3. 同步工具
    4. 并发集合
    5. 原子变量
  • 【线程的优雅停止】

    即便线程还在休眠,仍然能够响应中断通知,并抛出异常;对于线程的停止,最优雅的方式就是通过 interrupt 的方式来实现,如 InterruptedException 时,再次中断设置,让程序能后续继续进行终止操作。不过对于 interrupt 实现线程的终止在实际开发中发现使用的并不是很多,很多都可能喜欢另一种方式,通过标记位。

    设置一个volatile标记,让线程可见进而终止程序

    private static volatile boolean mark = false;

    interrupt 是中断信号传递,基于系统层次的,不受阻塞影响,而对于 volatile ,我们是利用其可见性而顶一个标记位标量,但是当出现阻塞等时无法进行及时的通知。

    public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }
    public Thread(Runnable target, String name) {
        init(null, target, name, 0);
    }
    
  • 【CountDownLatch】

    在多线程并发编程中充当一个计时器,维护一个int count的变量,并且其操作都是原子操作

    首先通过建立CountDownLatch对象,并且传入参数即为count初始值。如果一个线程调用了await()方法,那么这个线程便进入阻塞状态,并进入阻塞队列。如果一个线程调用了countDown()方法,则会使count-1;当count的值为0时,这时候阻塞队列中调用await()方法的线程便会逐个被唤醒,从而进入后续的操作。

  • 【CyclicBarrier】 (回环栅栏)

    让达到一定数目的所有线程都等待完成后,才会继续下一步行动。在某刻步调一致;

    CyclicBarrier初始化时规定一个数目,然后计算调用了CyclicBarrier.await()进入等待的线程数。当线程数达到了这个数目时,所有进入等待状态的线程被唤醒并继续

    CyclicBarrier初始时还可带一个Runnable的参数, 此Runnable任务在CyclicBarrier的数目达到后,所有其它线程被唤醒前被执行。

  • 【Semaphore】 (信号量)

    Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量(允许自定义多少数目线程同时访问)。就这一点而言,单纯的synchronized 关键字是实现不了的。

  • LinkedBlockingDeque 是一个由链表结构组成的双向阻塞队列;

    凡是后缀为 Deque 的都是双向队列意思;

    双向队列因为多了一个操作队列的入口,在多线程同时入队是,也就会减少一半的竞争;

    默认容量大小为Integer.MAX_VALUE,最好设置初始容量防止其过度膨胀;

  • 【LinkedTransferQueue】

    TransferQueue提供了一个场所,生产者线程使用 transfer 方法传入一些对象并阻塞,直至这些对象被消费者线程全部取出。SynchronousQueue 像个容量为 0 的 TransferQueue…

    BlockingQueue 是如果队列满了,线程才会阻塞;但是 TransferQueue 是如果没有消费元素,则会阻塞 (transfer 方法);

    LinkedTransferQueueConcurrentLinkedQueueSynchronousQueue (在公平模式下),无界的LinkedBlockingQueues等的超集;允许你混合使用阻塞队列的多种特性。

    所以,在合适的场景中,请尽量使用LinkedTransferQueue

    所有阻塞的方法都是一个套路:

    1. 阻塞方式 transfer(E e)

      如果当前有消费者正在等待消费元素,transfer 方法就可以直接将生产者传入的元素立刻 transfer (传输) 给消费者;如果没有消费者等待消费元素,那么 transfer 方法会把元素放到队列的 tail(尾部)节点,生产者一直阻塞,直到该元素被消费者消费才返回;

    2. 带有 try 的非阻塞方式 tryTransfer(E e)

      如果没有消费者等待消费元素,则马上返回 false ,程序不会阻塞

    3. 带有 try 和超时时间的非阻塞方式 tryTransfer(E e, long timeout, TimeUnit unit)

      尝试,如果超时时间到,还没有消费者消费元素,则返回 false;

  • 【大根堆小根堆】

    基于数组,容量无上界(在内存允许的情况下),OOM问题;

    a PriorityQueue() with the default initial capacity (11) ;默认小根堆;PriorityBlockingQueue() 支持优先级的无界阻塞队列,默认采用自然顺序升序排列, default initial capacity (11),可以给定初始容量,会按照一定的算法自动扩充;

  • 【SynchronousQueue】

    直译叫同步队列,如果在队列里面呆久了应该就算是“异步”了吧…

    假如需要在两个线程之间同步共享变量,如果不用 SynchronousQueue 你可能会选择用 CountDownLatch 来完成,这点小事就用计数器来实现,显然很不合适…

    aSynchronousQueue<E>是一个缓存值为1的阻塞队列,**内部没有使用AQS,而是直接使用CAS;**在某次添加元素(put)后必须等待(被阻塞)其他线程取走(take)后才能继续添加;

    AQS: 同步器forFIFO队列

    ​ for ReenLock:FairSync NonFairSync ->lock,tryAcquire不同 --> fair unfair LOCK

    Provides a framework for implementing blocking locks阻塞锁 and related synchronizers (semaphores, events, etc信号量、事件) that rely on first-in-first-out (FIFO) wait queues. This class is designed to be a useful basis for most kinds of synchronizers that rely on a single atomic int value to represent state.

    here SQ has two 数据结构:dual queue , dual stack --> fair unfair

    公平与否是指阻塞的线程能否按照阻塞的先后顺序访问资源,先阻塞先访问后阻塞后访问;

    默认:

    可公平(ts access in FIFO order&公平锁)->default内部类TransferQueueDual Queue;

    可非公平(非公平锁&unspecified order)->同包下可见内部类TransferStackdual stack;

    不能迭代,无需迭代; 不能peek,peek()永远返回null; isEmpty()永远返回true;

    take() happens-before put() && put() happens-before take()~

    take() & put(); //公平排序策略是指调用put的线程之间,或take的线程之间
    //取出并remove掉queue里的element(认为是在queue里的...),取不到东西会一直等。
    //往queue放进去一个element后,一直wait直到有其他thread进来把这个element取走。
    poll(2000, TimeUnit.SECONDS) & offer(2000, TimeUnit.SECONDS) ;
    poll() & offer();
    

    They are well suited for handoff designs, in which an object running in one thread must sync up with an object running in another thread in order to hand it some information, event, or task. -> into rpc 线程与线程间 一对一传递消息的模型;

    Executors.newCachedThreadPool()使用了SynchronousQueue<Runnable\>()缓存线程池中,线程数量几乎无限(上限为Integer.MAX_VALUE),因此提交的任务只需要在SynchronousQueue 队列中同步移交给空余线程即可, 所以有时也会说 SQ吞吐量要高于 LinkedBlockingQueueArrayBlockingQueue

    take put之间实际的操作由Transferer的实现类.transfer(null,x,x)/(e,x,x)来完成,内含自旋循环,CAS原子操作,和LockSupport.park(this)休眠阻塞

  • 【并发队列】

    并发场景下的 阻塞队列与非阻塞队列

    前者是基于实现的,后者则是基于CAS 非阻塞算法实现的;

    非阻塞(紫色是并发场景下的):

    【ConcurrentLinkedQueue】

    无界,FIFO,内部遵循 自旋直到成功+CAS(比较交换)方式来实现线程安全

    add()同offer(),不会false/抛出IllegalStateException,但may NullPointerException;

    不是每次出队时都更新head节点,当head节点里有元素时,直接弹出head节点里的元素,而不会更新head节点。只有当head节点里没有元素时,则弹出head的next结点并更新head结点为原来head的next结点的next结点。

    size() 内要遍历集合,很慢,尽量避免用size(),如果判断队列是否为空最好用isEmpty()而不是用size()来判断,此方法在并发时通常不用

    阻塞队列(wait queues)[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dpMf4jKn-1627207533716)(C:\Users\zky\AppData\Roaming\Typora\typora-user-images\image-20210628212626615.png)]

    img

  • public final class Integer extends Number implements Comparable<Integer> {
      /**
       * A constant holding the minimum value an {@code int} can
       * have, -2 <sup> 31</sup>.
       */
      @Native public static final int  MIN_VALUE = 0x80000000;
    
      /**
       * A constant holding the maximum value an {@code int} can
       * have, 2<sup>31</sup>-1.
       */
      @Native public static final int  MAX_VALUE = 0x7fffffff;
        
    abstract class Number implements java.io.Serializable
    finalclass Character implements java.io.Serializable, Comparable<Character> {
    
  • //Every Java application has a single instance of class Runtime that allows the application to interface with the environment in which the application is running. The current runtime can be obtained from the getRuntime method. 每个Java应用程序都有一个Runtime类实例,该类允许应用程序与应用程序运行的环境交互。当前运行时可以从getRuntime方法获得。
    
    System.out.println(Runtime.getRuntime().availableProcessors()); 
    //获得当前计算机的 CPU 核数
    
    // add a shutdown hook;
    // hook – An initialized but unstarted Thread object 初始化但未启动的Thread对象
    // When the virtual machine begins its shutdown sequence it will start all registered shutdown hooks in some unspecified order and let them run concurrently. 
    void addShutdownHook(Thread hook) {}
    
    //unlike the exit method, this method does not cause shutdown hooks to be started.  exit (equivalently, System.exit) 
    //0是正常关闭,其他非0为非正常关闭。
    void halt(int status){}
    void exit(int status){}  -->
    	//System:
        public static void exit(int status) {
            Runtime.getRuntime().exit(status);
        }
    //The name gc stands for "garbage collector". 
    //The VM performs this recycling process automatically as needed, in a separate thread, even if the gc method is not invoked explicitly.
    //只是提醒或告诉虚拟机,希望进行一次垃圾回收。至于什么时候进行回收还是取决于虚拟机,而且也不能保证一定进行回收
    native void gc(){}  -->
    	//System:
    	static void gc() {
            Runtime.getRuntime().gc();
        }
    
  • -1/2 --> 0 : 向0取整

    //Divides two integers and returns the floor of the quotient. For example, floorDivide(-1, 4) returns -1 while -1/4 is 0.   
    public static final int floorDivide(int n, int d) {
            return ((n >= 0) ? (n / d) : (((n + 1) / d) - 1));
        }  //向下取整  : 除数是2的幂次方的话  可以使用 “>>” 
    
  • //standard input, standard output, and error output streams; access to externally defined properties and environment variables; a means of loading files and libraries; and a utility method for quickly copying a portion of an array. 标准输入、标准输出和错误输出流;访问外部定义的属性和环境变量;一种加载文件和库的方法;以及用于快速复制数组的一部分的实用方法。
    java.xxx
    [and] 
    os.name   :Operating system name
    os.arch   :Operating system architecture
    os.version   :Operating system version
    file.separator   :File separator ("/" on UNIX)
    path.separator   :Path separator (":" on UNIX)
    line.separator   :Line separator ("\n" on UNIX)
    user.name   :User's account name
    user.home   :User's home directory
    user.dir   :User's current working directory
    
  • Java中,多线程并发控制有多种方式,一种是实用synchronized关键字/Lock类,互斥访问关键代码;另一种是实用CAS思想,进行乐观的并发。前者采用加锁的办法控制并发;CAS是先假设可以并发,当发现数据脏读的时候,回滚执行,使用native方法,依赖操作系统的底层;

    变量共享的时候,应该使用这两种(侧重于线程间的同步,保证并发执行下结果的一致性);而线程独立进行,互不依赖关联的时候(IO密集型场景下,不需要加锁,减少上下文切换,提高CPU的利用率),才能用ThreadLocal来保证线程安全。

    前者以时间换空间,而ThreadLocal以空间换时间。

  • 强引用:

    String str = “abc”;
    list.add(str);
    
            String str1 = new String("123"); 
            String x = str1;
            System.out.println(str1);  //123
            System.out.println(x); //123
            str1 = null;
            System.out.println((String) str1); //null   如果不知x的存在 内存泄漏
            System.out.println((String) x); //123
    

    弱引用:

    ThreadLocalMap的key会持有对ThreadLocal实例(一般为静态的)的弱引用(Weak Reference),value会引用TSO实例(Entry封装了real value(非引用类型比较好?)。

    TSO:thread specific object,即与线程相关的变量,如线程自身的状态。

  • 【ThreadLocal】

    //Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).
    //1 var -- 1 ThreadLocal --> differ threads differ var's transcripts      
    //1 thread -- 1 TLMap      底层是Entry[] -- 提供方法包装成map
    //				[key:TL<?>  value:Entry<TL<?>>]   【弱引用】
    //					int i = key.threadLocalHashCode & (table.length - 1);
    //	            	Entry e = table[i]; //entry.value:变量
    public class ThreadLocal<T> {
    	//import juc.atomic.AtomicInteger for generating threadlocal's hash
        private static AtomicInteger nextHashCode = new AtomicInteger(); //int 0
        //next :  update by Unsafe的原子操作来add
        
        //常常重写initValue();在其中才get set init for TSO
    ThreadLocal使得各线程能够保持各自独立地操作一个对象,实现原理其实是通过,每个线程都会重新创建这个对象(内容copy);线程是否安全取决于如何去填上所谓的“副本”。ThreadLocal中保存的不是什么对象的副本,里面没有需要保存的对象做任何复制,拷贝操作,这个对象完全取决于你的iniialtValue方法中如何去创建。
    

    当工作于多线程中的对象使用ThreadLocal维护变量时,ThreadLocal利用initValue为每个使用该变量的线程分配一个独立的变量副本。从线程的角度看,这个变量就像是线程的本地变量,这也是类名中“Local”所要表达的意思。

    ThreadLocal实例的弱引用,作为key存到thread的本地TLmap中,每一个线程都可以独立地改变自己的副本变量,故线程对变量的修改不会影响其他线程的执行

    一个ThreadLocal实例侧重于对每个线程保存一个TSO;常用来在一个线程中传递状态。在一个线程中,横跨若干方法调用,需要传递的对象,我们通常称之为上下文(Context),它是一种状态,可以是用户身份、任务信息等;

    Spring就是利用ThreadLocal来实现一个线程中的Connection是同一个,从而保证了事务。

    在管理request作用域的Bean、事务管理、任务调度、AOP等模块都出现了ThreadLocal。

  • 挂起和睡眠是主动的;挂起恢复就绪态需要主动完成,睡眠恢复则是自动完成。而阻塞是被动的,是在等待某种事件或者资源的表现,一旦获得所需资源或者事件信息就自动回到就绪态。

    睡眠和挂起是两种行为,阻塞则是一种状态。

  • Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。

    总共有4种锁状态,级别由低到高依次为:

    无锁状态偏向锁/间隙锁状态轻量级锁状态重量级锁状态

    偏向锁通过对比 Mark Word 解决加锁问题,避免执行CAS操作。

    轻量级锁是通过用CAS 操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。

    重量级锁是将除了拥有锁的线程以外的线程都阻塞

    这几个状态会随着竞争情况逐渐升级。

    在使用 synchronized 来同步代码块的时候,经编译后,会在代码块的起始位置插入 monitorenter指令,在结束或异常处插入 monitorexit指令。

    每一个 Java 对象有一把看不见的锁,称为内部锁或者 Monitor 锁。(对象 与 Monitor对象 与Monitor锁 一一对应)

    monitor 中有一个 Owner 字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

    Monitor 可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。 synchronized 用的monitor锁是存放在 Java对象头 中的。

    以 Hotspot 虚拟机为例,Hotspot 的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。

    Mark Word:默认存储对象的 HashCode,分代年龄和锁标志位信息。当一个线程访问同步代码块并获取锁时,会在锁对象的 Mark Word 里的threadid 字段存储其线程 ID。

    Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。


    偏向锁的状态是指:当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得monitor锁。只有遇到其他线程尝试竞争锁时(再次进入的时候会先判断threadid 字段与其线程 id不一致),持有锁(偏向锁状态)的线程会中止执行,JVM消除它身上的偏向锁,将锁恢复到标准的轻量级状态。线程是不会主动撤销偏向锁状态的;

    偏向锁可以恢复到无锁(结束访问了且无访问了,标志位为01)状态

    或轻量级锁(有**竞争的可能**了,标志位为00)的状态。


    轻量级锁的状态是指:当锁是偏向锁的时候,却被另外的线程所访问(有**竞争**了),此时偏向锁就会升级为轻量级锁,其他线程(同时等待的线程们)会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。此时锁对象的Mark Word 的锁标志位设置为“00”,表示处于轻量级锁定状态。而获得到轻量级状态下的锁的线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝,lock record的owner 指针指向锁对象的 Mark Word,过程中有CAS操作

    **自旋超过一定的次数**时,轻量级锁便会升级为重量级锁(锁膨胀)。当一个线程已持有锁,也有线程在自旋,而此时**又有线程新来访时**,轻量级锁也会升级为重量级锁(锁膨胀)。


    **(锁膨胀)**重量级锁的状态是指:当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。(对象 与 Monitor 与 Monitor锁 一一对应); Java 虚拟机中最为基础的锁实现。

    重量级锁直接通过对象内部的监视器(monitor)实现,而 monitor 本质依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本高。

    简言之,就是所有的控制权都交给了操作系统(前三种状态还是jvm(app)级别,用户模式),由操作系统来负责线程间的调度和线程的状态变更(内核模式)。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资源,导致性能低下。

    synchronized 关键字不能被继承。如果子类覆盖了父类的 被 synchronized 关键字修饰的方法,那么子类的该方法只要没有 synchronized 关键字,那么就默认没有同步,也就是说,不能继承父类的 synchronized。

  • 【自旋】

    自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。

    所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用 -XX:PreBlockSpin 来更改)没有成功获得锁,就应当挂起线程。

    自旋锁在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 来开启。

    JDK 6 中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。

  • volatile变量可以保证可见性、有序性(防止指令重排)但不能保证原子操作;只有同时满足这三个特性才能够保证对于一个变量的并发操作是合乎预期的。

    原子操作的实现:1.加锁的话需要对线程进行上下文切换,带来的开销是很大的。2.CAS的思想可以保证原子性:【以下是在java中的实现】

    使用原子性的FieldUpdaters,利用反射机制,开销也会增大;

    使用Atomic包下原子类进行间接管理,但增加了开销,也可能导致额外的问题如ABA问题;

    ​ (eg: *AtomicReference* 通过volatile和Unsafe提供的CAS函数实现的原子操作。还有AtomicIntegerfor int value atomic operations)

    使用sun.misc.Unsafe提供的JVM内置函数,但直接操作JVM可能会损害安全性和可移植性;


    针对以上的问题,VarHandle就是用来替代上述方式的一种方案,它提供了一系列标准的内存屏障操作,用于更细粒度的控制指令排序,在安全性、可用性、性能等方面都要优于现有的API,且基本上能够和任何类型的变量相关联。

  • 【VarHandle】

    VarHandle支持不同访问模型下对于变量的访问:

    简单的read/write访问,volatile read/write访问,以及CAS访问等;

    plainVar , volatileVar , casVar


    *VarHandle内部的访问模式会覆盖变量声明时的任何指令排序效果。*比如一个变量被声明为volatile类型,但是调用varHandle.get()方法时,其访问模式就是get模式,即简单读取方法;final native Object getVolatile() vs final native Object get()

    VarHandle相比于传统的对于变量的并发操作具有巨大的优势,在JDK9引入了VarHandle之后,JUCjava.util.concurrent包中对于变量的访问基本上都使用VarHandle,比如AQS中的CLH队列中使用到的变量等。

    CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,提供先来先服务的公平性。线程只需要在本地自旋,查询前驱节点的状态,如果前驱释放了锁,就结束自旋。

  • 【Unsafe】

    Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操作

    这个类尽管里面的方法都是public的,但是并没有办法使用它们,JDK API文档也没有提供任何关于这个类的方法的解释。对于Unsafe类的使用都是受限制的,只有授信的代码才能获得该类的实例,当然JDK库里面的类是可以随意使用的。

    **Unsafe提供了硬件级别的操作,比如说获取某个属性在内存中的位置,比如说修改对象的字段值,即使它是私有的。**不过Java本身就是为了屏蔽底层的差异,对于一般的开发而言也很少会有这样的需求。

    【CAS】---------java.util.concurrent包完全建立在CAS思想之上,没有CAS也就没有此包,可见CAS的重要性。当前的处理器基本都支持CAS,只不过不同的厂家的实现不一样罢了。CAS有三个操作数:内存值V、旧的预期值A、要修改的值B

    CAS的实现有Unsafe,看Unsafe下的三个native方法:

    public final native boolean compareAndSwapObject(Object paramObject1, long paramLong, Object paramObject2, Object paramObject3);
    
    public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);
    
    public final native boolean compareAndSwapLong(Object paramObject, long paramLong1, long paramLong2, long paramLong3);
    

    解决办法也很简单,给compareAndSwapInt方法加锁同步就行了,这样,compareAndSwapInt方法就变成了一个原子操作。CAS也是一样的道理,比较、交换也是一组原子操作,不会被外部打断,先根据paramLong/paramLong1获取到内存当中当前的内存值V,在将内存值V和原值A作比较,要是相等就修改为要修改的值B,由于CAS都是硬件级别的操作,因此效率会高一些。

  • 【ReentrantLock】

    ReentrantLock是基于AQS实现的,AQS的基础又是CAS。

    private final Sync sync; //synchronization control for lock: fair/unfair
    //inner class in ReentrantLock; AQS
    	abstract static class Sync extends AbstractQueuedSynchronizer {
    

    ReentrantLock根据传入构造方法的布尔型参数,可以实例化出Sync的实现类FairSyncNonfairSync,分别表示公平的Sync和非公平的Sync。用的比较多的是非公平(nonfairsync);

    (默认)false :nonfairsync

    当有其它线程A又进来想要获取锁时(state ->1), 恰好此前的某一线程恰好释放锁(state=0), 那么A会恰好在同步队列中所有等待获取锁的线程之前抢先获取锁。也就是说所有已经在同步队列中的尚未被取消获取锁 的线程是绝对保证串行获取锁,而其它新来的却可能抢先获取

           protected final boolean tryAcquire(int acquires) {
               if (c == 0) {
                   if (!hasQueuedPredecessors() &&
                       compareAndSetState(0, acquires)) {
                       setExclusiveOwnerThread(current);
                       return true;
                   }     //fair :可确保无饥饿;挂起唤醒频繁;
          //vs
                   if (compareAndSetState(0, acquires)) {
                       setExclusiveOwnerThread(current);
                       return true;
                   }    //unfair : 减少唤醒开销;饥饿问题;
    

    reentrantlock包装了sync(AQS),实际操作大多由sync;

    通过堵塞挂起的方式进行锁的竞争,等待队列中的线程以堵塞状态等待锁释放的通知信号unpark head.next,然后再次竞争锁

    // in reentlock    
    public void lock() {
       sync.acquire(1);
    }
    // in AQS
    public final void acquire(int arg) {
       if (!tryAcquire(arg) &&
           			acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
           selfInterrupt();
    }
    
     1 //unfair's tryAcquire():  sync父类的方法
         final boolean nonfairTryAcquire(int acquires) {
     2     final Thread current = Thread.currentThread();
     3     int c = getState(); 
     4     if (c == 0) {   //maybe last successful thread finish work
     5         if (compareAndSetState(0, acquires)) {
     6             setExclusiveOwnerThread(current);
     7             return true;  //get lock successfully
     8         }
     9     }
    10     else if (current == getExclusiveOwnerThread()) {
    11         int nextc = c + acquires;
    12         if (nextc < 0) // overflow
    13             throw new Error("Maximum lock count exceeded");
    14         setState(nextc);
    15         return true;
    16     }  //reget
    17     return false;  //no chance now  --> 
         			//will go into AQS的 addWaiter() :有CAS,也有自旋,但不是锁
         			//may need to init waiting queue  【CLH双向队列】
    18 }
    

    //sync的该方法里,可以让某个线程多次调用同一个ReentrantLock,每调用一次给sync的state+1,由于某个线程已经持有了锁,所以这里不会有竞争,因此不需要利用CAS设置state(相当于一个偏向锁);同一个锁最多能重入Integer.MAX_VALUE次;

    //init waiting queue :[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AwWjpXiV-1627207533720)(C:\Users\zky\AppData\Roaming\Typora\typora-user-images\image-20210626164639973.png)]

    这里用了CAS设置头Node,有可能线程2设置头Node的时候CPU切换了,线程3已经把头Node设置好了形成了上图所示的一个队列,这时线程2再循环一次获取tail,由于tail是volatile的,所以对线程2可见,线程2看见tail不为null,就走到了13行的else里面去往尾Node后面添加自身。

    【CLH中】线程没有成功竞争到, 那么就将此线程包装成Node加入同步队列尾部并阻塞;底层调用的是 Unsafe 的park 方法加锁,此时的ReenTrantLock是一把重量锁。

    当第一次执行lock方法时,并未调用unsafe的park()方法。故该情况下的ReenTrantLock是偏向锁;在t1执行时,t1再次进入lock(),未有新线程来,并未调用unsafe的方法。故ReenTrantLock依旧是一把轻量锁。而State的设置也表明了ReenTrantLock的可重入性。


    【AQS】---------AbstractQueuedSynchronizer,AQS,是java.util.concurrent的核心,CountDownLatch、FutureTask、ThreadPoolExecutor、ReentrantLock等都有一个内部类是这个抽象类的子类。

    ReentrantLock(a got t)中的内部类叫Sync

    ThreadPoolExecutor(a firsttask t)中的叫Worker

    state可以称为AQS的灵魂,基于AQS实现的好多JUC工具,都是通过操作state来实现的,

    state为0表示没有任何线程持有锁;state为1表示某一个线程拿到了一次锁,state为n(n > 1),表示这个线程获取了n次这把锁,用来表达所谓的“可重入锁”的概念。

    由于AQS是基于FIFO队列的实现,因此必然存在一个个节点,Node就是一个节点:

    More ActionsNode SHARED = new Node()表示Node处于共享模式; //for 读读

    Node EXCLUSIVE = null 表示Node处于独占模式; //for 读写

    Thread thread 这个Node持有的线程,表示等待锁的线程


    CLH队列节点都有一个状态位,该状态位与线程状态密切相关:

    CANCELLED = 1:因为超时或者中断,节点会被设置为取消状态,被取消的节点不会参与到竞争中,它会一直保持取消状态不会转变为其他状态;

    SIGNAL = -1:其后继节点已经被阻塞了,到时需要进行唤醒操作

    CONDITION = -2:表示这个结点在条件队列中,因为等待某个条件而被阻塞

    INITIAL = 0:新建节点一般都为0。

    整个AQS是典型的模板模式的应用,对于FIFO队列的各种操作在AQS中已经实现了,AQS的子类一般只需要重写tryAcquire(int arg)tryRelease(int arg)两个方法即可

    static final class Node {} ; //Node :FIFO 队列的基本单位 【inner class】
                             
    private transient Thread exclusiveOwnerThread;  //【transient】
      //AQS父类AbstractOwnableSynchronizer的属性,表示独占模式同步器的当前拥有者
    
    private volatile int state;	//同步状态,0表示未有thread占有,可获取锁【volatile】
    private transient volatile Node head; //【transient】【volatile】
    private transient volatile Node tail; //【transient】【volatile】
                             
    int getState();	//获取同步状态
    tryAcquire(int arg);//尝试获得锁(**由AQS的子类实现它**)
    tryRelease(int arg);//尝试释放锁(**由AQS的子类实现它**)
    isHeldExclusively();//是否独自持有锁
    acquire(int arg);//获取锁
    release(int arg);//释放锁
    
  • spinlock(自旋锁):忙等待循环 + CAS思想

    不休眠就不会引起上下文切换,但是会比较浪费CPU。

    对于自旋锁来说,自旋锁使线程处于用户态,而互斥锁需要重新分配,进入到内核态

    单核单线程CPU不适合自旋锁,现在的技术基本单核都是支持多线程的;

    循环与AtomicRefrence等原子类结合

    // use it as the way of ReentrantLock;
    public class SimpleSpinningLock {
        
    	private AtomicReference<Thread> obj = new AtomicReference();
        
    	private static ThreadLocal local = new ThreadLocal<Integer>(){
            @Override
            protected Integer initialValue(){
                return new Integer(10);
            }
        };
        
    	public void lock(){
    		Thread t = Thread.getCurrentThread();
        	while(!obj.compareAndSet(null,t)){
           		// busy
        	}
    	}
    
    	public void unLock(){
    		Thread t = Thread.getCurrentThread();
            if(obj.get()!= t){
            	throw new RuntimeException("error when release the lock"); 
        	}
        	obj.set(null);
    	}
    }
    
  • 读写锁的含义是准确的:是一种 读共享,写独占的锁;

    copyonwrite写时复制:这个机制比读写锁有改进的地方,就是读取的时候可以写入 ,这样省去了读写之间的竞争,同时写入的时候还是加锁only one could update。

    mysql中行锁: for update vs lock in share mode 事务中起作用;事务提交,行锁释放;

    表锁:LOCK TABLE xx READ (禁止自己和其他的写操作); lock table xx write;

    ​ lock tables [table1] read,[table2] write,[table3] read;

    UNLOCK TABLES;

    UNLOCK TABLES只有在LOCK TABLES已经获取到表锁时,会隐式提交任何活动事务。

    重新开事务(如START TRANSACTION)隐式地提交任何当前事务,会释放现有的表锁。

    LOCK TABLES和UNLOCK TABLES语句是在服务器层实现的,和存储引擎无关。他们有自己的用途,但不能代替事务处理,如果需要用到事务,还是应该选择事务型存储引擎。一般情况下,如果使用的InnoDB存储引擎,没有必要使用LOCK TABLES语句。用行锁。

  • 【Runnable Callable<T>】

    lang包 vs juc包;

    类 vs 泛型类

    public FutureTask(Callable<V> callable) {
        class FutureTask<V> implements RunnableFuture<V> {
        	interface RunnableFuture<V> extends Runnable, Future<V> {
    
  • 【pool】

    corePoolSize 核心线程数量
    maximumPoolSize 最大线程数量
    keepAliveTime 线程保持时间,N个时间单位
    unit 时间单位(比如秒,分)TimeUnit.seconds milliseconds minites
    workQueue 阻塞队列
    threadFactory 线程工厂
    handler 线程池拒绝策略

    任务提交给线程池之后的处理策略,主要有4点:

    • 如果当前池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个core线程(firstask != null 的worker created too)去执行(t.start()–>invoke run())这个任务;

    • 如果当前池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的临时线程,直接去执行这个任务(firstask != null 的worker created too)(t.start()—>invoke run());

    • 如果当前池中的线程数目达到maxPoolSize,则会采取任务拒绝策略进行处理;

    拒绝策略/饱和策略:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hXauN9ZQ-1627207533721)(C:\Users\zky\AppData\Roaming\Typora\typora-user-images\image-20210627203312549.png)]

    • 如果池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。

    public void execute(Runnable command) {
    if (command == null)
    throw new NullPointerException();

    int c = ctl.get();
    if (workerCountOf© < corePoolSize) {
    if (addWorker(command, true))
    return;
    c = ctl.get();
    }
    if (isRunning© && workQueue.offer(command)) {
    int recheck = ctl.get();
    if (! isRunning(recheck) && remove(command))
    reject(command);
    else if (workerCountOf(recheck) == 0)
    addWorker(null, false);
    }
    else if (!addWorker(command, false))
    reject(command);
    }


默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中

默认情况下,**只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,**直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;

private final BlockingQueue<Runnable> workQueue;  //tasks waiting
//【常用3种】:
	    public LinkedBlockingQueue() {
        	this(Integer.MAX_VALUE);
    	}
private final ReentrantLock mainLock = new ReentrantLock(); //control worker
private final HashSet<Worker> workers = new HashSet<>();

execute(runnable);
submit(callable);

// pool控制方法 都要加lock来进行,by worker来
private boolean addIfUnderCorePoolSize(Runnable firstTask) {
    Thread t = null;
    final ReentrantLock mainLock = this.mainLock;    //【lock】
    mainLock.lock();
    try {
        if (poolSize < corePoolSize && runState == RUNNING)  //again
            t = addThread(firstTask);  //创建线程去执行【firstTask】   
        } finally {
        mainLock.unlock();
    }
    if (t == null)   //no available, others took away the corethreads left
        return false;
    t.start();
    return true;
}

private Thread addThread(Runnable firstTask) {
    Worker w = new Worker(firstTask);  
    Thread t = threadFactory.newThread(w);  //创建一个线程,执行任务 //may null 
    if (t != null) {
        w.thread = t;            //将创建的线程的引用赋值为w的成员变量       
        workers.add(w);     //here
        int nt = ++poolSize;     //当前线程数加1       
        if (nt > largestPoolSize)
            largestPoolSize = nt;
    }
    return t;
}

beforeExecute(Thread t, Runnable r);
afterExecute(Runnable r, Throwable t);
//没有具体实现,用户可以根据自己需要重写这两个方法来进行一些统计信息,比如某个任务的执行时间等启动一个有序的关闭,在此关闭中执行先前提交的任务,但不接受新的任务。
public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(SHUTDOWN);
            interruptIdleWorkers();   //【idle】
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
}
public void shutdownNow(){
    ...
        interruptWorkers();  
    ...
}
//立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务

limit workerCount to (2^29)-1 (about 500 million) threads rather than (2^31)-1 (about 2 billion) otherwise representable;


【Worker】

worker的创建跟随to run thread的创建;

//Class Worker主要为运行任务的线程【维护中断控制状态】,以及其他次要记录。这个类适时扩展了AQS,以简化获取和释放围绕每个任务执行的锁。这样可以防止running中断,这些中断只是为了【唤醒】正在等待任务的工作线程,而不是中断正在运行的任务。我们实现了一个简单的【不可重入的互斥锁】,而不是使用ReentrantLock,因为我们不希望工作任务在调用setCorePoolSize之类的池控制方法时能够重新获得锁。此外,为了【在线程实际开始运行任务之前抑制中断】,我们将锁状态初始化为负值,并在启动时清除它(在runWorker中.unlock())。
private final class Worker extends AbstractQueuedSynchronizer
  													  implements Runnable{
    /** Thread this worker is running in.  Null if factory fails. */
    final Thread thread; //
    /** Initial task to run.  Possibly null. */
    Runnable firstTask;
    volatile long completedTasks;

    // TODO: switch to AbstractQueuedLongSynchronizer and move

    Worker(Runnable firstTask) {
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }

    /** Delegates main run loop to outer runWorker. */
    public void run() {
        runWorker(this);
    }

    // Lock methods
    //
    // The value 0 represents the unlocked state.
    // The value 1 represents the locked state.

    protected boolean isHeldExclusively() {
        return getState() != 0;
    }

    protected boolean tryAcquire(int unused) {
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }

    protected boolean tryRelease(int unused) {
        setExclusiveOwnerThread(null);
        setState(0);
        return true;
    }

    public void lock()        { acquire(1); }
    public boolean tryLock()  { return tryAcquire(1); }
    public void unlock()      { release(1); }
    public boolean isLocked() { return isHeldExclusively(); }

    void interruptIfStarted() {
        Thread t;
        if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
            try {
                t.interrupt();
            } catch (SecurityException ignore) {
            }
        }
  }
}

从内部类Worker的run()方法的实现可以看出,它托付给executor的runWorker()执行,包含beforeExecute()task.run()afterExecute():调用runTask()执行完firstTask之后,在while循环里面不断通过getTask()去取新的任务来执行:

while (task != null || (task = getTask()) != null) {
    w.lock(); // 有锁【lock】
    if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() &&
              	runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted())
     wt.interrupt();
    ...

getTask()中,先判断当前线程池状态(5种),

如果runState大于SHUTDOWN(即为STOPTIDYING、TERMINATED),直接返回null;

如果runState为SHUTDOWN或者RUNNING,则从任务缓存队列取任务。

if (runStateAtLeast(c, SHUTDOWN) && (runStateAtLeast(c, STOP) || 		    											workQueue.isEmpty())) {
    decrementWorkerCount();  //atomic int -1
    return null;
}
         } catch (SecurityException ignore) {
          }
      }
}

}


从内部类Worker的`run()`方法的实现可以看出,它托付给executor的`runWorker()`执行,包含`beforeExecute()`、`task.run()`、`afterExecute()`:调用`runTask()`执行完firstTask之后,在while循环里面不断通过`getTask()`去取新的任务来执行:

```java
while (task != null || (task = getTask()) != null) {
    w.lock(); // 有锁【lock】
    if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() &&
              	runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted())
     wt.interrupt();
    ...

getTask()中,先判断当前线程池状态(5种),

如果runState大于SHUTDOWN(即为STOPTIDYING、TERMINATED),直接返回null;

如果runState为SHUTDOWN或者RUNNING,则从任务缓存队列取任务。

if (runStateAtLeast(c, SHUTDOWN) && (runStateAtLeast(c, STOP) || 		    											workQueue.isEmpty())) {
    decrementWorkerCount();  //atomic int -1
    return null;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值