Java多线程整理
一、线程池
过于频繁的创建/销毁线程浪费性能,线程并发数量过多, JVM调度是抢占式的,线程上线文切换抢占系统资源导致阻塞。
1.线程池线程数:
一般CPU密集型:CPU+1
IO密集型:[(线程等待时间+线程CPU时间)/线程CPU时间]*CPU
2.线程池创建4种方式:
newCachedThreadPool 缓存线程池,没有线程可用就创建,空闲线程60秒未使用将被回收。
newFixedThreadPool 固定线程数,多余的任务进队列,(若需要)异常死后重新启动一个代替
newScheduledThreadPool 延迟或定期执行的线程池
newSingleThreadExecutor 单线程池,异常死后重新启动一个代替。
以上4个都是由Executors提供,底层都是由ThreadPoolExecutor实现的。
newFixedThreadPool(1)与newSingleThreadExecutor 主要区别,后者被DelegatedExecutorService装饰了,减少了很多暴露方法
3.ThreadPoolExecutor 构造参数有5/6/7个。
corePoolSize 核心线程数
maximumPoolSize 最大线程数
keepAliveTime 非核心线程数超时时长
TimeUnit 超时单位
BlockingQueue<Runnable> 线程池的任务对列
ThreadFactory 线程工厂 一般默认即可
RejectedExecutionHandler 拒绝策略
执行规则:
1. 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
2. 线程数量达到了corePools,则将任务移入队列等待
3. 队列已满,新建线程(非核心线程)执行任务
4. 队列已满,总线程数又达到了maximumPoolSize,就会抛出拒绝策略异常
5. 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止
常用队列:
ArrayBlockingQueue :由数组结构组成的阻塞队列,可支持公平与非公平
LinkedBlockingQueue :由链表结构组成的阻塞队列,两端独立锁提供并发
SynchronousQueue:无容量无缓冲的阻塞队列,对应 newCachedThreadPool
DelayedWorkQueue:延迟阻塞队列。对应 线程池 newScheduledThreadPool 。
拒绝策略:
AbortPolicy :默认的,丢弃任务并抛出RejectedExecutionException异常
DiscardPolicy:丢弃任务,不抛出异常
DiscardOldestPolicy:丢弃队列前面任务,重新尝试执行任务
CallerRunsPolicy:由调用线程处理该任务。
二、线程
1.线程状态:
新建:使用new对象,JVM仅分配内存与成员变量值
就绪:线程对象调用start()后,JVM创建 方法调用栈与程序计数器,等待CPU调度运行。
运行:线程获得CPU执行了run方法执行体。
阻塞:
死亡:
2.终止线程方式:
退出标志退出线程: 伺服线程使用volatile 类型标志设置值 判断while循环退出。
Interrupt()中断线程:线程处于阻塞时,抛出异常。非阻塞时 通过isInterrupted()判断线程的中断标志来退出循环
stop终止线程: 线程不安全,释放线程持有的所有锁,数据可能呈现不一致性
sleep()方法属于Thread类,线程不会释放对象锁,
wait()方法属于Object类,线程会放弃对象锁,进入等待此对象的等待锁定池。必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。用于多线程之间的通信。
notify() 方法,唤醒在此对象监视器上等待的单个线程,并不一定唤醒的是当前线程
yield()进入就绪状态
join() 方法,当前线程阻塞,等待其他线程终止后,在变成就绪状态
interrupt()方法并不会中断一个正在运行的线程。也就是说处于Running状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。在线程的run方法内部可以根据thread.isInterrupted()的值来优雅的终止线程。
![](https://img2018.cnblogs.com/blog/1694759/201906/1694759-20190604181354274-764144942.png)
ThreadLocal线程本地变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。每个线程创建了数据副本, 底层维护了一个map, key是ThreadLocal实例, Thread类中持有了threadLocalMap集合。map的entry继承弱引用, 构造函数将key传入父类构造函数, key为弱引用(第二次GC时回收), 当线程不结束后, key被回收, 但value会一直存在。但在每次调用get/set/remove方法时, 都会自动清理key为null的value, 但若不调用以上3个方法, value仍不会被回收, 一般这个问题会存在线程池,线程是一直在不断的重复运行的,从而也就造成了value可能造成累积的情况。解决办法, 手动调用remove方法。
多线程间可通过 共享数据变量对象、内部类共享成员变量 来共享数据
volatile变量可见性、禁止指令重排。保证了可见性和有序性,不能保证原子性,相比synchronized 不会阻塞线程
synchronized 通过操作系统Mutex Lock实现的,重量级锁。用在方法、代码块、对象、静态方法上(类锁),线程可见性、原子性、有序性都能保证. JDK1.6后进行了很多的优化有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高.
final 域,编译器和处理器要遵守两个重排序规则:
-
JMM保证final变量初始化时的有序性、禁止编译器和处理器重排序。
-
final作为不可变对象,正确初始化后(没有this逃逸),能够保障可见性。
3. synchronized和ReentrantLock的区别:
共同点:协调多线程对共享对象的访问;都是可重入锁,同一线程多次获取锁;都保持可见性与互斥性;
不同点:ReentrantLock显示的获得、释放锁,synchronized隐式获得释放锁;ReentrantLock是API级别的,synchronized是JVM级别的;ReentrantLock可以实现公平锁; synchronized是同步阻塞,使用的是悲观并发策略,lock是同步非阻 塞,采用的是乐观并发策略;synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生,Lock 必须在finally块主动通过unLock()释放锁;lock可响应中断,可轮回,可判断是否获取锁,更为灵活。
CAS(Compare And Swap/Set)比较并交换-乐观锁机制-锁自旋:当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入。通过版本号,解决ABA问题。
AbstractQueuedSynchronize 抽象的队列 同步器,AQS定义了一套多线程访问共享资源的同步器框架,具体资源的获取/释放方式交由自定义同步器去实现,许多同步类实现都依赖于它ReentrantLock/Semaphore/CountDownLatch。内部维护了一个共享资源和线程等待队列。定义了两种资源共享方式:独占资源(ReentrantLock)与共享资源(Semaphore/CountDownLatch)。ReentrantReadWriteLock两种方式都实现了
CountDownLatch 线程计数器:某个线程等待其他线程执行完后执行,不能重用
CyclicBarrier 回环栅栏: 一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行
Semaphore 信号量 :控制同时访问的线程个数,用于限流
自旋锁: 持有锁的线程能在很短时间内释放锁资源,那么等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
轻量级锁:是使用操作系统互斥量来实现的传统锁,为了在线程交替执行同步块时提高性能,若并发产生锁膨胀升级为重量锁。
偏向锁 :在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销 ,在只有一个线程执行同步块时进一步提高性能。
锁优化方案:减少锁持有时间、减小锁粒度(ConcurrentHashMap)、锁分离(ReadWriteLock)、锁粗化、锁消除(时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作,多数是因为程序员编码不规范引起)。
![](https://img2018.cnblogs.com/blog/1694759/201906/1694759-20190604181418512-299009246.png)
死锁的原因:
定义: 当多个线程相互等待对方释放资源时就会发生死锁。
死锁产生的条件: 互斥条件、不剥夺条件、请求和保持条件、循环等待条件。
避免死锁: 加锁顺序(线程按照一定的顺序加锁);加锁时限设置超时时间;
NIO与IO区别
NIO是采用多路复用IO模型,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件,单个线程可以监听多个数据通道。
IO是面向流Stream的,每次从流中读取一个或多个字节; NIO是面向缓冲区的,数据可以在缓冲区前后移动。
IO中线程调用read()、write()是阻塞的,NIO非阻塞。
IO模型不断轮询socket状态是用户线程进行的,NIO轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多。
Netty介绍
Netty 是一个高性能、异步事件驱动的NIO框架,基于JAVA NIO提供的API实现。它提供了对TCP、UDP和文件传输的支持,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。
Netty使用的堆外直接内存(零拷贝)不需要字节缓冲区的二次拷贝。传统的JVM将堆内存拷贝直接内存,然后才写入socket中,消息在发送过程中多了一次拷贝。Netty提供了组合Buffer操作、文件传输采用transferTo方法,都可以将缓冲区数据在堆外内存直接操作,避免内存拷贝问题。