zoukankan      html  css  js  c++  java
  • Java多线程

    并发

    并发访问会产生 可见性有序性原子性 的问题,Java中的线程和操作系统的线程是一一对应的关系,可以实现Runnnable接口或者继承Thread类来实现多线程编程,不过一般建议实现Runnable接口来将业务和接口调用分离;PC服务器一般采用抢占式线程调度;手机等小型设备可能采用协作式线程调度

    没有可以强制中断线程方法,调用interrupt可以请求中断线程,线程的中断标志位被置位;通过isInterrupted检查线程是否被中断;当一个阻塞线程调用interrupt时会抛出Interrupted Exception

    Callable<V>中的V call()可以有返回值的异步方法,Funture实现了Runnable和Callable接口,可以传递给Thread线程类执行,主要用来监控任务执行的状态。

    Funture方法:

    V get() throws:调用阻塞直到方法执行完成 V get(long timeout,TimeUnit unit) throws:调用阻塞直到方法执行完成或者超时 void cancel(boolean isInterrupt):设为true则任务取消 boolean isCancelled():任务是否取消 boolean isDone():任务是否完成

    计算机以及虚拟机内存结构

    计算机中每个CPU都有专属的L1、L2缓存以及共享的L3缓存,一般情况下如果缓存命中则不会读取内存数据;通过缓存一致性协议(总线传输信号量)使其他核缓存失效,从而强制读取内存数据。

    Java内存模型将内存分为:主内存(线程共享)、工作内存(线程独享),执行代码变量必须从主内存复制到工作内存处理,经过一段时间后再同步到主内存,Java内存中的 主内存(线程共享)、工作内存(线程独享)都是逻辑上的概念,可能对应计算机的 寄存器内存缓存

    Java内存模型通过8个原子操作保证 主内存工作内存 一致性

    lock->read->load->user->assign->store->write->unlock

    多线程的问题

    创建对象多线程问题

    // 创建对象
    Instance instance = new Instance();
    /*
    创建对象在虚拟机层面分为三步
    1.分配对象内存空间
    2.初始化对象
    3.设置引用指向内存空间
    */
    // 2,3之间可能发生重排,也就是说返回了一个还未初始化的对象
    public class Single{
       private int num = 10;
       private Single s;
       private Single(){}
       public int getNum(){
           return num;
      }
       public final static Sigle getInstance(){
           if(null==s)
               s = new Single();
           return s;
      }
       public static void main(String[] args){
           // 多线程环境下可能返回false,因为指令重排,当前线程还未初始化完成就返回了对象
           // 对于常量,可以使用final修饰避免指令重排
           System.out.println(Single.getInstance().getNum==10)
      }
    }

    happens-before

    JMM对程序员保证一个操作happedns-before另外一个操作,第一个操作完成后一定对后一个操作可见,并且一个操作的执行顺序一定在第二个操作之前; JMM对编译器和处理器保证不改变程序正确执行时的语义,优化不做限制,即:存在指令重排序

    上下文切换

    根据时间片调度算法将cpu控制权交给不同的线程执行,再切换过程中需要保存前一个任务的状态,执行完后一个任务后,需要再次加载原来保存的信息,这个过程就叫做 上下文切换;多线程并发执行并不一定比单线程串行执行快,因为有 线程创建的成本上下文切换成本

    上下文切换成本包括保存栈帧数据(局部变量表、操作数栈、返回值、动态链接、PC指针)以及在占用总线资源传输的同步信息

    避免上线问切换方法
    1. 无锁并发:竞争锁会引起上下文切换,避免使用锁可以减少上下文切换,如果可以将数据的ID通过hash算法交给不同的线程分段处理

    2. cas算法,java的Atomic包

    3. 避免常见不必要线程

    4. 协程:单线程环境下由语言特性实现的并发编程

    死锁

    线程互相等待对方释放资源,而造成一种无法运行的状态

    如何避免死锁
    1. 避免一个线程获取多个锁

    2. 避免一个锁占用多个资源

    3. 使用锁的时候最好设置失效时间

    4. 数据库锁加锁解锁必须在一个连接中,否则会失效

    资源限制

    硬件资源限制有:带宽的上传/下载速度,硬盘读些速度(iops),cpu处理速度;软件资源限制有:数据库连接数,socket连接数;如果超过资源限制过度使用多线程则程序的执行速度可能会比单线程慢,因为资源受限的情况下花费创建线程以及上下文切换的开销,但是却没办法使用‘过度的线程’

    如何解决资源限制
    1. 硬件资源受限,可以考虑集群

    2. 软件按资源受限,可以考虑资源的复用,如:连接池、线程池技术

    3. 根据不同的资源限制,调整并发度

    volatile

    volatile修饰的共享遍历在写操作时,会加上一条lock 写入地址命令,这个命令做了两件事:1.将当前处理器缓存行写回到系统内存;2.写回内存操作会使在其他CPU缓存该地址数据无效;通过volatile+CAS+自旋可以实现无锁并发;atom相关类通过此原理实现原子操作;对volatile变量操作之后会插入一个store-load内存屏障,防止之后的读取操作重排序,保证下次读取变量时的可见性;写入volatile之前无论操作任何变量,都对另外的线程可见,且不可重排序;读取volatile变量无论后一个操作是什么都不能重排序

    volatile变量需要其他条件来保证对其操作的原子性,使用于赋值操作(底层原子),所以volatile变量适合用作触发器临界值,它可以保证之前的操作都不可重排

    每个处理器(CPU)都有自己的缓存机制,写入/读取数据到内存,会先经过操作系统缓存,这样在多核操作系统系统下就会产生 数据可见性 问题,即:一个线程修改了数据写入到当前处理器缓存,而另一个处理器线程读取到的数据是它自己的CPU缓存,导致数据不可见(缓存写入内存时机由操作系统决定);使用volatile的遍历可以将缓存中的遍历写入到内存,通过缓存一致性协议,每个处理器嗅探总线上传播的数据,检查自己的缓存值是否过期,volatile通过总线传播使每个缓存中的数据都过期。

    内存屏障是一种指令,防止指令重排;所有的计算机都支持存储-读取操作的重排序(不同CPU不同线程之间不会考虑依赖性),因此通过 store-load 内存屏障可以保证一个数据存储后对其他CPU都可见,该指令会之前所有的内存访问指令完成之后才会执行后续指令

    32位系统double、long不是原子操作,通过volatile可以变成原子性操作; 创建对象过程:分配空间、初始化对象、返回引用地址,这个过程可能产生重排序(非原子性),因此双重检查锁的单例类对象需要volatile修饰保证对象初始化的有序性

    // 双重检查锁懒汉式线程安全单例类
    public class Single{
       private static final volatile Single single;
       private Single(){}
       
       public static final getInstance(){
           // 双重检查锁
           if(null==single){
               // 多线程进入可能都在此阻塞
               synchronized(Single.class){
                   if(null==single){
                       single = new Single();
                  }
              }
          }
      }
    }
    // 类加载锁懒汉式线程安全单例类
    public class Single{
       private Single(){}
       private static final class SingleNode{
           // 因为是类变量,只会被初始化一次
           private static final Single SINGLE = new Single();
      }
      public static final getInstance(){
           // 内部类只有在需要的时候才会被加载并初始化,并且会加上初始化锁从,因此能够安全的实现延迟加载
           return SingleNode.SINGLE;
      }
    }

    synchronized

    synchronized是可以重入锁(防止死锁);同时也是非公平锁(解锁的同时多个线程竞争锁,并不是按抢占顺序加锁);synchronized不可中断,即:一个线程获取锁后,另一个线程会在同步代码块入口阻塞(不能去执行其他代码),中断失效(lock.tryLock()可以去尝试获取锁,获取不到会执行其他代码);线程释放锁后会将本地内存中的共享变量刷新到主内存,synchronized保证只有一个线程执行代码,因此不会因指令重排序影响程序的最终执行结果;并且某一个线程进入synchronized代码块前后,线程会获得锁,清空工作内存,从主内存拷贝共享变量最新的值到工作内存成为副本

    JDK1.6之前是重量级锁,竞争资源会阻塞唤醒,涉及到内核态用户态的上下文切换,性能下降很多,推荐用ReentrantLock;在JDK1.6之后,引入了偏向锁和轻量级锁性能和ReentrantLock持平;synchronized锁存储在Java对象头

    synchronized通过C++的ObjectMonitot对象实现,其中通过_object存储同步代码块监控对象;通过_owner存拥有该对象的线程;_recursions存储线程重入次数;_cxq存储多线程竞争的单向链表(第一次竞争锁);_EntryList存储阻塞线程(第二次没有竞争到锁,则会进入该单向链表);_waitSet存储等待状态的线程

    synchronized保证原子性

    进入同步代码块,monitorenter加锁;退出同步代码块,monitorexit解锁,保证在这一块代码中只能有一个线程执行

    synchronized保证可见性

    synchronized并不能解决指令重排问题,但是synchronized会使工作线程的变量失效,强制去主存中获取

    synchronized保证有序性

    synchronized并不会阻止指令重排(volatile修饰的变量可以),但是可以保证同步代码块中只能存在一个线程执行

    as-if-serial语义:不论怎么重排,单线程执行结果不会改变。即:没有依赖关系的语句,可能会进行重排;如果有依赖关系则不会进行重排

    计算机目标是不改变执行结果的情况下,尽可能提高并行度(C10K);happens-before并不是保证前后操作顺序的不可变性,而是保证前一个操作对后一个操作是可见的。

    当存在控制依赖时,也可能发生指令重排,这时候把控制块的中的变量缓存到硬件上的重排序缓冲区(ROB)判断通过后再处理计算值

    通过JIT的逃逸分析,如果在一个方法对一个对象多次重复加锁并且没有发生逃逸,则会将synchronized粗化;如果方法不被多个线程访问则会锁消除

    偏向锁

    当一个线程进入同步代码块,在 对象头栈帧 中通过CAS记录偏向的线程ID,直到出现其他线程竞争偏向锁,且到达全局安全点(没有正在运行的字节码),首先停用偏向锁线程,持有偏向锁的线程才释放锁

    在Java6以后默认开启偏向锁,并且会有几秒的延迟,在多线程环境下(线程池),使用偏向锁会加大开销,通过-XX:UseBiasedLocking=false关闭偏向锁

    偏向锁降低了单线程环境下的同步代码块的消耗,由于CAS会在总线上传播同步消息(使其他CPU缓存失效)因此偏向锁的出现就是为了消除CAS

    偏向锁是在无竞争下消除同步快

    轻量级锁

    当一个线程进入同步代码块,JVM先将对象头的MarkWord复制到当前线程栈帧锁记录中,然后通过CAS将对象头的MarkWord替换为指向锁记录的指针;如果成功则当前线程获得锁,如果失败,先判断是否重入(MarkWord是否是指向锁记录的指针)如果是重入直接进入执行,如果不是重入,则表示其他线程竞争锁,当前线程尝试使用 自旋锁 获取锁,超过10次(默认值)则会膨胀为重量级锁;轻量级锁释放时,会通过CAS将对象头的指针换回对象头;轻量级锁避免内核态与用户态的切换

    轻量级锁数据乐观锁机制,依据是:绝大部分锁,在同步周期内不存在竞争,如果竞争激烈则会在CAS后阻塞,比重量级阻塞更慢

    自旋锁会空转消耗CPU;默认10次,超过后就会变成重量级锁阻塞

    CAS会出现ABA问题;自旋空转消耗CPU问题;并且CAS只会保证一个变量操作的原子性,不能保证一组变量操作的原子性

    重量级锁

    synchronized的对象锁,每个对象都存在一个monitor对象(C++对象)与之关联,其他线程获取锁会被阻塞,唤醒线程会进行竞争抢占

    ThreadLocal

    每个线程对象(Thread)通过是通过静态内部类ThreadLocalMap来保存线程私有变量,ThreadLocal通过获取每个线程的该变量来进行操作;ThreadLocal作为一个独立的类存在,不依赖任何线程,但是其封装了对线程变量的操作;以及获取随机的哈希值(每次调用相同的ThreadLocal将产生不同的哈希值);其内部使用静态内部类 定义 了线程独立的变量Map(仅定义,具体实例对象在每个线程中保存)

    ThreadLocalMap一个初始容量为16,阈值(threshold)为容量2/3的Map,但是当元素个数达到阈值的3/4(即容量的1/2)时才会扩容,但是并没有实现Map接口

    实现Map接口的集合都是通过开散列实现,而ThreadLocalMap实现都是通过闭散列实现

    ThreadLocalMap实现原理

    ThreadLocalMap作为 ThreadLocal中独立的静态内部类 没有实现Map相关接口和抽象类,其底层通过数组开散列实现。在每个线程对象(Thread)保存一份实例,数组中每个元素是 ThreadLocal类中独立的静态内部类Entry Entry实现弱引用接口,将key作为弱引用,value为用户设置的值;

    • set:首次set会创建容量为16的ThreadLocalMap,ThreadLocal通过threadLocalHashCode决定其存储在每个线程对象中ThreadLocalMap集合的槽位置,threadLocalHashCode变量在创建ThreadLocal对象时只会被创建一次,即:一个ThreadLocal变量实例(作为key),在一个线程中是唯一的(唯一的槽);如果通过threadLocalHashCode找到的槽位置上有值,且key与当前设置的ThreadLocal不同,则循环从当前位置找到下一个不为null的位,设置value插入槽(循环过程中清除无效引用,即:将key失效的Entry设为null),每次插入元素后会从当前位置开始,向后清除过期元素(key为null)

    • get:通过当前ThreadLocal对象的threadLocalHashCode计算槽的位置,取出槽中key(ThreadLocal)与调用的this(ThreadLocal)进行hashCode比较,如果相同则说明是同一个线程局部变量,如果不同则说明该位置上的变量是之前为了 解决哈希冲突,闭散列寻找的下一个槽

    • rehash:ThreadLocalMap默认初始容量是16,在第一次set时候设置,threshold固定是容量2/3,当元素个数超过threshold先遍历map清除过期元素(key为null),清除完毕后,元素个数大于threshold的3/4(即容量的1/2)则会进行扩容,每次扩容是原容量的2倍

    public class ThreadLocal{
       private static AtomicInteger nextHashCode = new AtomicInteger();
       // 每次创建对象会计算该值,同一对象的值是相同的
       private final int threadLocalHashCode = nextHashCode();
       // 不断的累加该值,与2的整数次幂-1做与运算能得到
       private static final int HASH_INCREMENT = 0x61c88647;
       // 这种方式计算的哈希值能将冲突降到最低
       private static int nextHashCode() {
           return nextHashCode.getAndAdd(HASH_INCREMENT);
      }
    }

    public class Main{
       public static class MyRunnable implements Runnable{
           private ThreadLocal threadLocal = new ThreadLocal();
           @Override
           public void run(){
               threadLocal.set(Math.random());
          }
      }
       // threadLocal是不同的,线程不共享
       public static void main(String[] args){
           MyRunnable myRunnable = new MyRunnable();
           Thread thread1 = new Thread(myRunnable);
           Thread thread2 = new Thread(myRunnable);
      }
    }

    wait/notify 机制

    每个对象的对象头的MarkWord区域都包含了指向重量级锁(monitor)对象,因此每个对象都已作为锁对象,并且支持阻塞(wait)和唤醒(notify)方法

    • 调用wait的线程和notify的线程必须拥有相同的对象锁

    • wait方法和notify必须在Synchronized 方法或代码块中

    wait

    当前线程必须获得对象锁才可以调用wait,会在调用处暂停线程,并 立即释放对象锁

    notify

    调用notify/notifyAll方法不会立即释放对象锁,而是 执行完同步代码块才会释放锁

    sleep方法不会释放锁 notify随机唤醒一个wait的线程,notifyAll唤醒wait的所有线程

    suspend和resume被废弃,因为容易死锁,在同步块中挂起线程,另外的线程需要执行代码块才能才能唤醒

    队列同步器(AbstractQueuedSynchronizer)

    同步器是实现锁(也可以是任意同步组件)的关键,用同步器提供的3 个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。子类推荐被定义为自定义同步组件的静态内部;同步器面向的是锁的实现者,在锁的实现中聚合同步器,利用同步器实现锁

    构建锁或者其他同步组件的基础框架(ReentrantLock、ReentrantReadWriteLock和CountDownLatch、CyclicBarrier),它使用了一个int成员变量表示同步状态,通过内置的FIFO双向同步队列 来完成资源获取线程的排队工作,通过 单向等待队列 完成资源的通知等待,队列同步器基于 模板模式 实现,主要使用方式是继承,推荐使用静态内部子类通过继承同步器并实现它的抽象方法来管理同步状态

    AQS进入同步状态通过cas添加到同步队列尾部后,自旋(for死循环)检查前一个元素是否为头节点并且独占锁通过tryAcquire返回true表示成功(共享锁通过tryAcquireShared放回大于0的数表示成功);如果前一个节点不是头节点则通过CAS添加到同步队列并且自旋判断前节点是否是头节点(如果是前节点是头节点,则将自身设置为头节点(为了GC将前节点引用、线程引用释放)这样每个节点通过判断前节点是否是为头节点来决定是否从进入执行,可以防止中间的节点中断触发后节点的唤醒(中间节点不是头节点)

    在AQS子类中使用如下三个方法维护同步状态:

    • setState(int newState):设置初始化状态值

    • getState():获取状态值

    • compareAndSetState(int expect,int update):使用CAS更新当前状态值

    阻塞通过自旋,等待唤醒通过LockSupport.park(s.thread);LockSupport.unpark(s.thread)

    compareAndSetState底层调用unsafe的compareAndSwapInt方法,通过提供当前类的地址以及state值的偏移量、state旧值和state新值来原子性更新

    AbstractQueuedSynchronizer中有一个同步队列和多个等待队列;synchronized中只有一个等待队列和一个同步队列;同步队列控制线程获取能否获取到锁,等待队列控制获取到锁的线程通知与等待

    独占锁:调用同步器的acquire(int arg)方法获取独占锁同步状态,如果获取失败则会把当前线程添加到同步队列尾部,该方法对中断不敏感,即:后续线程进行中断时,线程不会从队列中移出,调用release方法后会唤醒head线程(其他线程在死循环内阻塞),但是只有前节点是头节点的线程才能进入进入同步状态得到执行,其他线程会再次阻塞(LockSupport.park())。也就是说独占锁每次只会唤醒一个独占锁线程

    共享锁:调用同步器的acquireShare(int arg)方法获取独占锁同步状态,如果获取成功(tryAcquireShare()>=0,如果=0则唤醒前节点是头节点的一个节点,如果>0则会继续唤醒后续的共享节点,直到独占节点),在tryAcquireShare()>=0时会直接放行,当tryAcquireShare()<0时候会在同步队列尾部添加共享节点并且调用locksupport.park阻塞

    原子类

    Java中原子类通过操作系统级别CAS保证了操作的原子性,并且无需加锁。原理细节:将操作变量设置为volatile保证操作对所有处理器的可见性,通过不断的自旋unsafe包下的CAS操作保证多线程下的操作有序性

    unsafe包的CAS操作是一个native方法,底层通过C++调用操作系统的cmpxchg指令,提供变量的地址以及旧值和新值。通过提供旧值和存储的旧值比较,如果相同说明没有被其他线程修改过,则更新旧值,返回true;如果被不同,则说明被其他线程修改过,返回false;Java中unsafe包CAS方法获取返回值如果是false,则不断自旋(循环)重试,直到成功。

    1. ReentrantLock默认是可重入非公平锁

    aqs中的state只保存当前状态,如果为0则表示当前空闲状态(轮转下一个节点),如果是1则表示已经被占用锁,如果大于1则表示当前锁被重入,公平锁:每当新线程尝试获取锁,先判断是否是同步队列头节点,如果不是则会加入,如果是则会cas设置状态0->1;非公平锁:当前状态为0,则表示需要可以上锁,直接cas判断0->1,不需要检测同步队列,而是和同步队列头节点竞争锁

    非公平锁可能出现 饥饿 问题,只要通过cas设置状态成功则表示当前线程获取同步状态;公平锁是等待时间最长的线程获取锁(锁是按照顺序获取),减少了 饥饿 但是降低了并发性,通过cas设置状态成功则表示当前线程获取同步状态,还要判断是否有前节点

    一个ReentrantLock可以和多个条件关联,调用newCondition即可,通过每个条件调用await和signal实现等待唤醒;而synchronized中只能通过wait和notify实现一个隐含的条件等待唤醒

    尝试获取非公平锁,通过cas设置值如果成功就设置成功;尝试获取公平锁,在cas同时多了一个判断头结点的下一个节点是否属于当前线程,如果是则cas设置成功就表示获取锁成功;释放锁流程都一样调用LockSupport.unpark,没有获取到锁的acquireQueued会判断阻塞

    1. ReentrantReadWriteLock实现读写锁,通过一个状态的高位和低位分别维护读锁(共享锁)和写锁(排他锁),均是可以重入锁

    锁降级是先获取写锁,再获取读锁,后释放写锁的过程

    1. LockSupport通过unsafe调用操作系统底层阻塞唤醒方法,AQS中的阻塞唤醒就是通过它实现;调用park()之前调用了unpark或者interrupt方法park直接返回,不会挂起

    2. Condition:每一个Object都在底层绑定了监视器对象,因此每个Object对象都有wait、notify、notifyAll方法来进行线程的等待与唤醒;java通过Condition实现了监控器对象,Condition作为AQS对象的内部类,维护多个单向链表,awit调用LockSupport.park阻塞线程并将线程从同步队列头节点加入到等待队列尾节点single调用LockSupport.park唤醒线程将等待队列头节点,加入到同步队列尾节点

    限流:通过Semaphore或者guava的RateLimiter

    1. CountDownLatch:只能在new时候给定初始值,之后不能再次修改,调用await方法使当前线程等待;调用countDownf方法将创建时指定次数减1(通过AQS的cas),当初始值减为0的时候,调用await的线程全部唤醒,通过AQS内部子类实现共享锁(唤醒所有线程抢占资源),当state减少为0唤醒所有线程,本质自旋

    2. CyclicBarrier:每个线程调用await表示已经到达线程,当调用await的线程数量达到创建时指定的数值时会自动释放,通过Condition的await和signal实现,通过lock操作对计数减一

    3. Semaphore做流量控制,创建时设置允许的并发数,通过acquire获取令牌,当令牌(线程)数量达到设置值时,则多余的线程会阻塞,通过release释放令牌,也可通过guava.RateLimiter来进行限流

    public class SemaphoreTest {    
       private static final int THREAD_COUNT = 30;    
       private static ExecutorServicethreadPool = Executors.newFixedThreadPool(THREAD_COUNT);
       // 设置令牌数,表示最大并发数是10
       private static Semaphore s = new Semaphore(10);
       public static void main(String[] args) {    
           for (inti = 0; i< THREAD_COUNT; i++) {    
               threadPool.execute(new Runnable() {                
                   @Override    public void run() {    
                       try {
                           // 获取令牌
                           s.acquire();
                           System.out.println("save data");
                           // 释放令牌
                           s.release();                    
                      } catch (InterruptedException e) {
                      }                
                  }            
              });        
          } threadPool.shutdown();    
      } }
    1. Exchanger提供一个同步点,在这个同步点,两个线程可以交换彼此的数据,,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也 执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。通过cas和LockSupport实现

    线程

    linux中进程分为三种状态:阻塞、可运行、正在运行;uptime统计的是1、5、15分钟内可运行和正在运行的进程数

    同一个线程start多次会抛出异常,一个线程不应该由于其它线程来强制中断,应该自行中断,所以Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了

    Thread.interrupt 的作用其实也不是中断线程,而是 通知线程应该中断了。具体到底中断还是继续运行,应该由被通知的线程自己处理。具体来说,当对一个线程,调用 interrupt() 时: ① 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。仅此而已。在调用阻塞方法时正确处理InterruptedException异常。 ② 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。

    Thread.interrupted():返回当前线程中断状态,并清除标志位是为了下次继续检测标志位

    JDK7提供了Fork/Join,继承RecursiveTask类重写compute方法分割子任务分别放在每个子任务的 双端队列,该类实现了 工作窃取 算法,即:一个任务执行完成后会从其他任务的 双端队列 的尾部取出子任务执行

    线程池

    thread.setDaemon(true);将当前线程设置为后台线程,当前程序中如果没有前台线程运行则后台线程自动中止并且finally中语句不一定执行

    线程调度分为:协同式线程调度和抢占式线程调度,Java是抢占式线程调度与内核线程是一对一关系

    阻塞状态:等待获取一个排他锁

    等待状态:等待一段时间或者唤醒动作

    线程池有两个数字corePoolSize(线程池基本大小)和maximumPoolSize(线程池最大线程数量),线程池具体执行流程:

    1. 如果当前线程数小于corePoolSize,则直接创建线程返回,核心线程数不会被销毁

    2. 如果线程数大于corePoolSize,工作队列没满,则放入工作队列,如果超过了keepAliveTime的时间,则超过核心线程数的线程会被销毁

    3. 如果工作队列已经满了,但是没有超过maximumPoolSize,则会在线程池创建新线程

    4. 如果线程数超过了maximumPoolSize,任务将被拒绝,并调用 RejectedExecutionHandler.rejectedExecution()

    UNSAFE类

    sun.misc.Unsafe类是单例,但是开发者不能使用getUnsafe方法获取实例对象,因为该方法会判断,只有使用系统类(根)加载器加载该对象时才会返回,否则会抛出安全异常;也不能通过class对象的newInstance方法创建实例,因为构造器私有;只能通过反射拿到实例对象

    Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
    theUnsafe.setAccessible(true);
    Unsafe unsafe = (Unsafe) theUnsafe.get(null);

    Unsafe调用本地方法实现(c/c++)可以直接操作内存或者进行系统级调用,所以是不安全的。Unsafe主要功能:

    1. 内存管理:可以分配内存,拷贝内存,释放内存等

    2. 非常规实例化对象:allocateInstance()可以直接生成对象实例,且无需调用构造方法和其它初始化方法

    3. 操作类、对象、变量、数组:通过偏移量直接操作对应的数据

    4. 多线程同步:cas相关操作

    5. 线程挂起与恢复:park、unpark

    6. 内存屏障:loadFence() 表示该方法之前的所有load操作在内存屏障之前完成。同理storeFence()表示该方法之前的所有store操作在内存屏障之前完成。fullFence()表示该方法之前的所有load、store操作在内存屏障之前完成。

  • 相关阅读:
    未来超市 轻松之旅
    超市淡季从竞争对手抓起
    如何监管超市收银漏洞
    一份好的方案需要注意哪些内容?
    超市负库存产生的原因及对策
    成功演示的关键步骤(三)
    成功演示的关键步骤(一)
    js iframe 地址
    js 弹出可拖动窗口
    js 关闭当前页面不提示
  • 原文地址:https://www.cnblogs.com/leon618/p/13783409.html
Copyright © 2011-2022 走看看