1.进程是系统分配资源的最小单位,线程是系统调度的最小单位,线程依赖于进程,进程不在了,线程也就没了。
2.进程的状态
-
一个类实现Callable接口然后重写call方法可以返回线程执行结果,创建一个FutureTask实例,传入Callable对象,最终可以通过FutureTask对象传入线程实例通过FutureTask对象的get方法得到返回值
-
start方法是启动线程的方法,执行线程时就是并发执行。如果使用run方法,只是调用了线程的普通方法,还是串行执行,run可以调用多次,而start只能一次。
-
线程中断:
**Interrupt()方法可以在线程阻塞时立即改变静态方法Interrupted()状态和普通方法isInterrupted()状态,线程阻塞时使用Interrupt会直接抛出异常,在异常里可以break或其他操作。
**对于静态方法Interrupted()状态被修改后会立即恢复,因为还要供其他线程使用。
**普通方法isInterrupted()被打断后则状态不会改变。
**默认的状态都是false -
yield:不会释放锁,会出让CPU执行权,但是只会让相同优先级及以上来竞争执行权,自己本身也参与竞争,即便使用了yield也可能继续执行该线程。而使用sleep也不释放锁,也会让出CPU执行权,但是任何优先级线程都可以参与竞争,而自己本身不参与竞争。
-
线程不安全的原因:
**CPU抢占式执行,提升性能后遗留问题,无法改变
**非原子性:一个整体的操作没有全部执行成功
**内存可见性问题:由于JMM规定了Java内存模型中对各个内存,寄存器数据的访问顺序,在某些情况下,编译器会对代码执行顺序进行优化,对于有些变量的修改,修改后的值并不会放入主内存,而是放入自己的工作内存,其他线程读取的时候读到的值就是主内存中的旧值,明显就不符合预期。
**多个线程修改同一个变量。 -
线程安全问题解决:
**synchronized:在操作系统层面是一个mutex互斥锁,而jvm层面是一把监视器锁,当一个线程获取到该锁后,会在锁对象头中修改锁指向的线程,并且有一个计数器记录重入次数,第一次对线程加锁重入次数为1,每次重入都会加一,如果该计数器为0说明没有线程拥有这把锁。
–wait()方法会阻塞线程并释放锁,notify()和notifyAll()方法会唤醒线程,这些都是Object的方法。wait和notify必须在同步块内进行调用,也就是必须在加锁代码中去操作对象锁的wait和notify阻塞/唤醒对应线程,notify随机唤醒一个持有该对象锁的线程,notifyAll唤醒所有持有该锁的线程,具体谁能抢到锁无法确定。
–synchronized在JDK1.6优化后,不会直接加synchronized重量级锁,会先经历偏向锁,轻量级锁,最终才是重量级锁。同时还增加了CAS自旋锁,锁消除,锁粗化策略。
–偏向锁:当一个锁被某一个线程获取,就进入偏向模式,当再次请求该锁,不需要进行相关获取操作。如果在此之前有其他线程进行了锁请求就会退出偏向模式。当锁竞争激烈时,锁状态很难保持在偏向模式,反复的切换可能还会降低系统性能
–轻量级锁:偏向锁获取失败,就会让线程申请轻量级锁。升级轻量级锁时会把对象头中的Mark Word复制到线程栈的所记录中,因为轻量级锁加锁过程需要以CAS的比较条件作为判断是否加锁成功的依据。
–重量级锁:轻量级锁加锁失败会进行锁膨胀,变为重量级锁,此时很可能线程会被操作系统挂起,这样的话线程调度成本就高了,所以在此之前还会进行自旋操作获取锁,所谓自旋就是不断尝试获取锁,如果能在限制次数内获取锁,就不会膨胀为重量级锁,但是最坏情况下,即便自旋一定次数后仍然避免不了被挂起,反而增加了开销,但是这毕竟是一种补救措施,同时为了防止自旋次数过多,工程师们也对自旋的次数做出了规定。自旋的次数由虚拟机自行调整。
**Lock:JUC下的同步锁,主要由ReentrantLock实现,加锁和释放锁都需要显式的执行,一般释放锁操作需要放在finally中执行,也是可重入锁,还可以指定参数实现公平锁与非公平锁,默认为false,即非公平锁。如果是非公平锁,就由线程抢占式执行,而公平锁则是按照加锁顺序去执行。
**CAS自旋锁:虽然叫做自旋锁,但是其实是以一种无锁方式,具体操作是使用三个参数,分别包含旧内存值V,新内存值E,新值N。只有当旧的内存值等于新读取的内存值,才会将旧值替换为新值。如果不相等就会一直自旋等待,这就是CAS自旋锁的操作。java.util.concurrent.atomic包下就有一系列使用使用无锁算法的原子操作类。但是会存在ABA问题,当线程A尝试修改一个内存值,如果中间一个线程将其修改,而另一个线程又把它改了回来,那么当线程A再去修改时会发现旧的内存值和新的内存值相等,那么就会将旧值更新,但是此时并不是线程安全的,所以针对这个问题要引入版本号,就可以解决ABA问题。
**非公平锁由线程竞争获取,运行效率高,但是不可控,公平锁运行效率低,但是可控。 -
线程池:
**线程池:使用池化技术管理和使用线程。
**线程创建需要开辟内存资源,本地方法栈,虚拟机栈等线程私有变量,而销毁也需要资源的调度,所以为了节省频繁创建线程带来的消耗,使用线程池可以降低创建和销毁带来的性能损耗。
**必须使用原始的ThreadPoolExecutor创建线程池
–七大参数,
–(1)corePoolSize:线程池常驻核心线程数
–(2)maximumPoolSize:线程池能够容纳同时执行线程数
–(3)keepAliveTime:多余的空闲线程的存活时间,当时间达到keepAliveTime,多余线程会被销毁,直到剩下线程数目为核心线程数
–(4)workQueue:任务队列,被提交但尚未被执行的任务
–(6)threadFactory:表示生成线程池中工作线程的线程工厂,用户创建新线程,一般采用默认
–(7)handler:拒绝策略,表示当前线程队列满了并且工作线程大于等于线程池的最大数目,此时如何拒绝请求执行的runnable策略。
**因为线程池创建线程是懒加载的方式,刚开始没有任务线程数目是0。线程池中数目小于corePoolSize,拿到一个新任务将会创建新线程执行任务,当任务数量大于核心线程数,就会把新任务放入任务队列,等待线程池中任务调度执行,当任务队列已满,且最大线程数大于核心线程数,此时就要创建新线程处理任务,如果任务队列满,线程数目也达到了最大线程数,此时再拿到新任务就会执行拒绝策略,对于核心线程以外的空闲线程,只要其空闲时间达到keepAliveTime,就会进行回收。如果设置了allowCoreThreadTimeOut(true),核心线程的空闲时间超过规定空闲时间,也会进行回收。
**线程池的拒绝策略:
–(1)CallerRunsPolicy:当触发拒绝策略,只要线程池没关闭,则使用调用线程直接运行任务。会使用main线程处理任务。
(2)AbortPolicy:丢弃任务,并抛出拒绝执行RejectedExecutionException异常,这也是线程池默认的拒绝策略。
(3)DiscardPolicy:直接丢弃,其他什么操作都没有
(4)DiscardOldestPolicy:当触发拒绝策略,只要线程池没有关闭,丢弃阻塞队列中最老的任务,将新任务加入。
**线程池相比于线程来说是长生命周期的,即使没有任务,也会运行等待任务。线程池关闭使用shutdown()方法,此时拒绝新任务加入,等待任务全部执行结束在销毁线程池。还可以使用shutdownNow()方法,拒绝新任务加入,不会等待任务队列中人物执行结束,执行完核心线程任务后就销毁线程池。 -
ThreadLocal:线程级别私有变量,每个线程拥有一个,可以理解为线程本地变量,每个线程往这个ThreadLocal读写数据是线程隔离的,它提供了一种将可变数据通过每个线程有自己独立副本从而实现线程封闭机制。
**ThreadLocal中存入当前线程值使用set方法,其内部实现原理也类似于HashMap,KEY值就是当前线程,value值是set方法放入的值。
**ThreadLocal用完一定要调用remove方法释放资源,因为如果当前线程不回收,那么本地变量就一直在。
**ThreadLocal的key使用弱引用,而线程一直存在且没有调用remove方法,那么GC时key就会回收弱引用key,而强引用value还是存在,就可能造成内存泄漏。