Java内存模型定义了程序中各种变量的访问规则:
(1)所有变量都存储在主存,每个线程都有⾃⼰的⼯作内存;
(2)⼯作内存中保存了被该线程使⽤的变量的主存副本,线程对变量的所有操作都必须在⼯作空间进⾏,不能直接读写主内存数据;
(3)操作完成后,线程的⼯作内存通过缓存⼀致性协议将操作完的数据刷回主存。
编译器会对原始的程序进⾏指令重排序和优化。但不管怎么重排序,其结果都必须和⽤户原始程序输出的预定结果保持⼀致。
(1)程序次序规则:⼀个线程内,按照代码顺序,书写在前⾯的操作先⾏发⽣于书写在后⾯的操作;
(2)锁定规则:⼀个unLock操作先⾏发⽣于后⾯对同⼀个锁的lock操作;
(3)volatile变量规则:对⼀个变量的写操作先⾏发⽣于后⾯对这个变量的读操作;
(4)传递规则:如果操作A先⾏发⽣于操作B,⽽操作B⼜先⾏发⽣于操作C,则可以得出操作A先⾏发⽣于操作C;
(5)线程启动规则:Thread对象的start()⽅法先⾏发⽣于此线程的每⼀个动作;
(6)线程中断规则:对线程interrupt()⽅法的调⽤先⾏发⽣于被中断线程的代码检测到中断事件的发⽣;
(7)线程终结规则:线程中所有的操作都先⾏发⽣于线程的终⽌检测,我们可以通过Thread.join()⽅法结束、Thread.isAlive()的返回值⼿段检测到线程已经终⽌执⾏;
(8)对象终结规则:⼀个对象的初始化完成先⾏发⽣于他的finalize()⽅法的开始。
as-if-serial 保证单线程程序的执⾏结果不变,happens-before 保证正确同步的多线程程序的执⾏结果不变。
⼀个操作或者多个操作,要么全部执⾏并且执⾏的过程不会被任何因素打断,要么就都不执⾏,这就是原⼦性操作。
可⻅性指当⼀个线程修改了共享变量时,其他线程能够⽴即得知修改。volatile、synchronized、final 关键字都能保证可⻅性。
虽然多线程存在并发和指令优化等操作,但在本线程内观察该线程的所有执⾏操作是有序的。
(1)保证变量对所有线程的可⻅性。当⼀个线程修改了变量值,新值对于其他线程来说是⽴即可以得知的;
(2)禁⽌指令重排。使⽤ volatile 变量进⾏写操作,编译器在⽣成字节码时,会在指令序列中插⼊内存屏障来禁⽌特定类型的处理器进⾏重排序。
(1)实现Runnable接⼝;
(2)继承Thread类;
(3)实现Callable接⼝。
线程状态有 NEW、RUNNABLE、BLOCK、WAITING、TIMED_WAITING、THERMINATED。
(1)NEW:新建状态,线程被创建且未启动,此时还未调⽤ start ⽅法;
(2)RUNNABLE:运⾏状态。表示线程正在JVM中执⾏,但是这个执⾏,不⼀定真的在跑,也可能在排队等CPU;
(3)BLOCKED:阻塞状态。线程等待获取锁,锁还没获得;
(4)WAITING:等待状态。线程内run⽅法执⾏完Object.wait()/Thread.join()进⼊该状态;
(5)TIMED_WAITING:限期等待。在⼀定时间之后跳出状态。调⽤Thread.sleep(long) 、Object.wait(long)、Thread.join(long)进⼊状态。其中这些参数代表等待的时间;
(6)TERMINATED:结束状态。线程调⽤完run⽅法进⼊该状态。
(1)volatile 关键词修饰变量,保证所有线程对变量访问的可⻅性;
(2)synchronized关键词。确保多个线程在同⼀时刻只能有⼀个处于⽅法或同步块中;
(3)wait/notify⽅法;
(4)IO通信。
没有线程池的情况下,多次创建,销毁线程开销⽐较⼤。如果在开辟的线程执⾏完当前任务后复⽤已创建的线程,可以降低开销、控制最⼤并发数。
线程池创建线程时,会将线程封装成⼯作线程 Worker,Worker 在执⾏完任务后还会循环获取⼯作队列中的任务来执⾏。
将任务派发给线程池时,会出现以下⼏种情况:
(1)核⼼线程池未满,创建⼀个新的线程执⾏任务;
(2)如果核⼼线程池已满,⼯作队列未满,将线程存储在⼯作队列;
(3)如果⼯作队列已满,线程数⼩于最⼤线程数就创建⼀个新线程处理任务;
(4)如果超过⼤⼩线程数,按照拒绝策略来处理任务。
线程池参数:
(1)corePoolSize:常驻核⼼线程数。超过该值后如果线程空闲会被销毁;
(2)maximumPoolSize:线程池能够容纳同时执⾏的线程最⼤数;
(3)keepAliveTime:线程空闲时间,线程空闲时间达到该值后会被销毁,直到只剩下corePoolSize 个线程为⽌,避免浪费内存资源;
(4)workQueue:⼯作队列;
(5)threadFactory:线程⼯⼚,⽤来⽣产⼀组相同任务的线程;
(6)handler:拒绝策略。
拒绝策略有以下⼏种:
(1)AbortPolicy:丢弃任务并抛出异常;
(2)CallerRunsPolicy:重新尝试提交该任务;
(3)DiscardOldestPolicy 抛弃队列⾥等待最久的任务并把当前任务加⼊队列;
(4)DiscardPolicy 表示直接抛弃当前任务但不抛出异常。
Executor框架⽬的是将任务提交和任务如何运⾏分离开来的机制。
⽤户不再需要从代码层考虑设计任务的提交运⾏,只需要调⽤Executor框架实现类的Execute⽅法就可以提交任务。
(1)Executor:⼀个接⼝,其定义了⼀个接收Runnable对象的⽅法executor,该⽅法接收⼀个Runable实例
执⾏这个任务。
(2)ExecutorService:Executor的⼦类接⼝,其定义了⼀个接收Callable对象的⽅法,返回 Future 对象,
同时提供execute⽅法。
(3)ScheduledExecutorService:ExecutorService的⼦类接⼝,⽀持定期执⾏任务。
(4)AbstractExecutorService:抽象类,提供 ExecutorService 执⾏⽅法的默认实现。
(5)Executors:实现ExecutorService接⼝的静态⼯⼚类,提供了⼀系列⼯⼚⽅法⽤于创建线程池。
(6)ThreadPoolExecutor:继承AbstractExecutorService,⽤于创建线程池。
(7)ForkJoinPool: 继承AbstractExecutorService,Fork 将⼤任务分叉为多个⼩任务,然后让⼩任务执⾏,
Join 是获得⼩任务的结果,类似于map reduce。
(8)ThreadPoolExecutor:继承ThreadPoolExecutor,实现ScheduledExecutorService,⽤于创建带定时
任务的线程池。
(1)Running:能接受新提交的任务,也可以处理阻塞队列的任务。
(2)Shutdown:不再接受新提交的任务,但可以处理存量任务,线程池处于running时调⽤shutdown⽅法,会进⼊该状态。
(3)Stop:不接受新任务,不处理存量任务,调⽤shutdownnow进⼊该状态。
(4)Tidying:所有任务已经终⽌了,worker_count(有效线程数)为0。
(5)Terminated:线程池彻底终⽌。在tidying模式下调⽤terminated⽅法会进⼊该状态。
(1)newCachedThreadPool 可缓存线程池,可设置最⼩线程数和最⼤线程数,线程空闲1分钟后⾃动销毁。
(2)newFixedThreadPool 指定⼯作线程数量线程池。
(3)newSingleThreadExecutor 单线程Executor。
(4)newScheduleThreadPool ⽀持定时任务的指定⼯作线程数量线程池。
(5)newSingleThreadScheduledExecutor ⽀持定时任务的单线程Executor。
阻塞队列是⽣产者消费者的实现具体组件之⼀。当阻塞队列为空时,从队列中获取元素的操作将会被阻塞,当阻塞队列满了,往队列添加元素的操作将会被阻塞。具体实现有:
(1)ArrayBlockingQueue:底层是由数组组成的有界阻塞队列。
(2)LinkedBlockingQueue:底层是由链表组成的有界阻塞队列。
(3)PriorityBlockingQueue:阻塞优先队列。
(4)DelayQueue:创建元素时可以指定多久才能从队列中获取当前元素
(5)SynchronousQueue:不存储元素的阻塞队列,每⼀个存储必须等待⼀个取出操作
(6)LinkedTransferQueue:与LinkedBlockingQueue相⽐多⼀个transfer⽅法,即如果当前有消费者正等待接收元素,可以把⽣产者传⼊的元素⽴刻传输给消费者。
(7)LinkedBlockingDeque:双向阻塞队列。
ThreadLocal 是线程共享变量。ThreadLoacl 有⼀个静态内部类 ThreadLocalMap,其 Key 是 ThreadLocal对象,值是 Entry 对象,ThreadLocalMap是每个线程私有的。
(1)set 给ThreadLocalMap设置值。
(2)get 获取ThreadLocalMap。
(3)remove 删除ThreadLocalMap类型的对象。
存在的问题:对于线程池,由于线程池会重⽤ Thread 对象,因此与 Thread 绑定的 ThreadLocal 也会被重⽤,造成⼀系列问题。
⽐如说内存泄漏。由于 ThreadLocal 是弱引⽤,但 Entry 的 value 是强引⽤,因此当 ThreadLocal 被垃圾回收后,value 依旧不会被释放,产⽣内存泄漏。
对于 Java 语⾔,没有直接的指针组件,⼀般也不能使⽤偏移量对某块内存进⾏操作。这些操作相对来讲是安全(safe)的。
Java 有个类叫 Unsafe 类,这个类使 Java 拥有了像 C 语⾔的指针⼀样操作内存空间的能⼒,同时也带来了指针的问题。这个类可以说是 Java 并发开发的基础。
乐观锁认为数据发送时发⽣并发冲突的概率不⼤,所以读操作前不上锁。
到了写操作时才会进⾏判断,数据在此期间是否被其他线程修改。如果发⽣修改,那就返回写⼊失败;如果没有被修改,那就执⾏修改操作,返回修改成功。
乐观锁⼀般都采⽤ Compare And Swap(CAS)算法进⾏实现。顾名思义,该算法涉及到了两个操作,⽐较(Compare)和交换(Swap)。
CAS 算法的思路如下:
(1)该算法认为不同线程对变量的操作时产⽣竞争的情况⽐较少。
(2)该算法的核⼼是对当前读取变量值 E 和内存中的变量旧值 V 进⾏⽐较。
(3)如果相等,就代表其他线程没有对该变量进⾏修改,就将变量值更新为新值 N。
(4)如果不等,就认为在读取值 E 到⽐较阶段,有其他线程对变量进⾏过修改,不进⾏任何操作。