一、线程池
-
无限制线程的缺陷
- 线程的创建和关闭需要花费时间。
- 线程本身也会占用内存空间,大量的线程回收加重GC的压力。
- 不能有效的控制和管理正在运行的线程。
因此在生产环境需要用线程池对线程进行控制和管理。
2.JDK线程池框架
- ThreadPoolExecutor表示一个线程池
- Executors表示一个线程池工厂
- ThreadPoolExecutor实现了Executor接口,因此任何Runnable对象都可以被它调度
2.1.Executors线程工厂
- newFixedThreadPool():返回一个固定线程数量的线程池,若有空闲线程,优先使用空闲线程,否则加入等待队列等待空闲线程。
- newSingleThreadExecutor():只有一个线程的线程池。
- newCachedThreadPool():根据实际情况调整线程数量的线程池,若有空闲线程,优先使用空闲线程,否则创建线程执行任务。
- newScheduledThreadPool():执行定时任务或周期任务的线程池。
2.2.ThreadPoolExecutor线程池
2.2.1线程池构造方法参数描述
- corePoolSize:池中常规线程数量
- maximumPoolSize:池中的最大线程数量
- keepAliveTime:池中线程数量大于corePoolSize时,多余的空闲线程存活时间
- unit:keepAliveTime的单位,比如秒
- workQueue:任务队列,存储被提交但未被执行的任务
- threadFactory:线程工厂,用于创建线程
- handler:拒绝策略,当任务太多来不及处理时,如何拒绝任务
2.2.2线程池的任务队列
任务队列,存储被提交但未被执行的任务,是一个BlockingQueue接口对象。执行流程如下:
如果池中的线程小于corePoolSize,则创建线程执行任务。
如果池中的线程不小于corePoolSize,则将任务加入队列。
如果队列满了,则在池中线程数量小于最大线程数的情况下创建新的线程执行任务。
如果池中线程等于最大线程数后,还有任务到达,则执行拒绝策略。
但不同类型的任务队列,表现出来的形式是不一样的,常见队列特性如下所示:
直接提交队列:SynchronousQueue,没有容量,不保存任务,总是将任
务提交给线程,如果没有空闲线程,则创建线程,如果线程数量达到最大值则执行拒绝策略。
有界任务队列:ArrayBlockingQueue,其构造方法参数表示队列最大容量。
如果池中线程数量小于corePoolSize则创建新线程,若等于corePoolSize则放入队列,当队列已满时,
则在总线程小于池的最大容量的前提下创建新线程;若总线程大于池的最大容量则执行拒绝策略。
无界任务队列:LinkedBlockingQueue,如果池中线程数量小于corePoolSize则创建新线程,
若大于corePoolSize则放入队列。因为这是一个链表,容量无限,因此不会创建大于corePoolSize个线程。
优先任务队列:PriorityBlockingQueue,根据任务的优先级顺序先后执行。
2.3.线程池的拒绝策略
线程池的拒绝策略,当任务数量超过系统实际承载能力时,改如何处理。jdk内置拒绝策略(RejectedExecutionHandler):
- AbortPolicy:直接抛出运行时异常,阻止程序运行。
- CallerRunsPolicy:只要线程池未关闭,让调用者线程执行当前被丢弃的任务,然后提交任务。这种方式不会有任务被丢弃。
- DiscardOldestPolicy:只要线程池未关闭,丢弃最老的一个任务,并提交当前任务。
- DiscardPolicy:丢弃无法处理的任务,不予任何处理,即丢弃当前需要提交的任务。
2.4ThreadPoolExecutor线程池的扩展
线程池提供了如下3个方法,通过扩展这3个方法,可以实现对线程池运行状态的跟踪。
- beforeExecute():线程池中某线程执行前执行
- afterExecute():线程池中某线程执行后执行
- terminated():线程池关闭时调用执行
二、并发数据结构
1、并发list
- Vector:相关方法都是同步方法
- Collections.synchronizedList(List list):相关方法都采用同步块方式
- CopyOnWriteArrayList:读没有加锁,写操作时,先加锁,再获取内部数组的副本,
在副本上添加元素,然后把副本写回,最后释放锁;但在构造方法中,没有使用锁,因此可以将一个
线程非安全的list构造进来。适用于读多写少的场景。
内部数组是由关键字volatile修饰,volatile关键字的作用见后面章节。
2、并发set
- Collections.synchronizedSet(Set set):相关方法都采用同步块方式
- CopyOnWriteArraySet:内部采用CopyOnWriteArrayList实现
3、并发map
- HashTable:相关方法都是同步方法
- Collections.synchronizedMap(Map map):相关方法都采用同步块方式
- ConcurrentHashMap:读不加锁;内部将整个集合分成N(默认16)个小集合(段),
每个段一把锁。当向集合写数据的时候,首先判断该key的hash值在那个段中,再获得该段的锁,
最后插入数据。多个put同时操作时,只要其key的hash值不在同一个段中,就可以并发写入数据。
如果是size方法,则要依次对所有段加锁。
4、并发queue
- ConcurrentLinkedQueue:内部采用无锁的方式实现高并发状态下的高性能。
- BlockingQueue:阻塞式队列,主要是简化多线程间的数据共享。如生产者消费者模式下的队列。
常用的BlockingQueue主要有ArrayBlockingQueue和LinkedBlockingQueue。
5、并发deque
LinkedBlockingDeque:内部使用链表实现,每个节点都有一个前驱和后驱节点,并且没有进行读写锁的分离,因此同时只有一个线程对其操作,性能低于LinkedBlockingQueue,更低于ConcurrentLinkedQueue。
三、并发控制方法
1、volatile
在Java中,每个线程有一块工作内存区,存储主内存中的变量的值的拷贝。在线程执行过程中,对变量的修改实际上是对工作内存区中变量的修改,修改完成后再写入主内存中。每个线程改变工作内存区的数据时,对其他线程来说是不可见的。可能有些朋友会问这样做多此一举,其实不是,这样做能提高系统性能。因为这里的工作内存区不是JVM中的堆栈,而可能是cpu的高速缓存。
volatile,使所有线程均读写主内存中的对应变量,其他线程对变量的修改,可以及时反映在当前线程中,当前线程对变量的修改,可以及时写回主内存中,并被其他线程所见,使用volatile的变量,jvm会保证其的有序性。
volatile解决了线程间共享变量的可见性问题,而不能解决线程同步问题,并且使用volatile会增加性能开销。
最佳实践:使用atomic包下的原子类型实现线程间变量的共享。
2、synchronized内部锁
synchronized方法:则调用对象必须获得当前对象(this)的锁。如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法。不同的对象实例的synchronized方法是不相干扰的。
synchronized(ob){}:调用对象必须获得ob对象的锁,原理和synchronized方法一致。
synchronized静态方法:则调用对象必须获得当前Class对象锁。对类的所有对象实例起作用。
synchronized关键字不能被继承。多线程间的交互:wait(),notify()。
打个比方:一个object就像一个大房子,大门永远打开。
房子里有 很多房间(也就是方法)。这些房间有上锁的(synchronized方法), 和不上锁之分(普通方法)。
房门口放着一把钥匙(key),这把钥匙可以打开所有上锁的房间。
另外我把所有想调用该对象方法的线程比喻成想进入这房子某个房间的人。
一个人想进入某间上了锁的房间,他来到房子门口,看见钥匙在那儿(说明暂时还没有其他人要使用上锁的房间)。
于是他走上去拿到了钥匙,并且按照自己的计划使用那些房间。注意一点,他每次使用完一次上锁的房间后会马上把钥匙还回去。
即使他要连续使用两间上锁的房间,中间他也要把钥匙还回去,再取回来。因此,钥匙的使用原则是:“随用随借,用完即还。”
这时其他人可以不受限制的使用那些不上锁的房间,一个人用一间可以,两个人用一间也可以,没限制。
但是如果当某个人想要进入上锁的房间,他就要跑到大门口去看看了。有钥匙当然拿了就走,没有的话,就只能等了。
要是很多人在等这把钥匙,等钥匙还回来以后,谁会优先得到钥匙?这个就不确定了
3、ReentrantLock重入锁
synchronized缺点:不能被中断、不能定时(万一方法内部是死循环,那么就没办法让其中断)。
ReentrantLock还提供了公平和非公平两种锁。公平锁可以保证在锁的等待队列里的线程是公平的,不会出现插队的情况,对锁的获取总是先进先出,而非公平锁不做这个保证,可能会存在插入的情况。公平锁的实现代价比非公平锁要高,因此从性能角度而言,非公平锁要好,因此在没有特殊要求的情况下,应该使用非公平锁。即在构造方法的参数里,用false来创建非公平锁。
在使用ReentrantLock锁时,一定要注意在最后释放锁,释放锁一般写在finally里。而synchronized,JVM在最后会自动释放锁。
ReentrantLock相关方法:
- lock():获得锁,如果锁被使用,则等待
- lockInterruptibly():获得锁,但优先响应中断。
- tryLock():尝试获得锁,如果获得锁返回true,否则返回false
- tryLock(long timeout,TimeUnit unit):在给定的时间范围内尝试获得锁。
- unlock():释放锁,一般写在finally中
4、ReadWriteLock读写锁
内部锁或者重入锁不管是读还是写,都需要获得锁,所有读写都是串行。而ReadWriteLock允许多个线程同时读,写写、读写直接需要加锁。ReadWriteLock在读多写少的情况下,比重入锁性能更高。读写锁示例如下:
private Object object;
private ReadWriteLock lock = new ReentrantReadWriteLock();
private Lock readlock= lock.readLock();
private Lock writelock=lock.writeLock();
public void get() {
readlock.lock();
System.out.println(Thread.currentThread().getName() + "准备读数据!!");
try {
Thread.sleep(new Random().nextInt(1000));
System.out.println(Thread.currentThread().getName() + "读数据为:" + this.object);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readlock.unlock();
}
}
public void put(Object object) {
writelock.lock();
System.out.println(Thread.currentThread().getName() + "准备写数据");
try {
Thread.sleep(new Random().nextInt(1000));
this.object = object;
System.out.println(Thread.currentThread().getName() + "写数据为" + this.object);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writelock.unlock();
}
}
5、Condition
Condition是与锁相关联的,通过Lock接口的newCondition方法生成一个与锁绑定的Condition示例。重入锁、读写锁都可以使用。Condition对象与锁的关系,就像wait、notify和synchronized的关系。Condition相关方法:
- await():当前线程等待,并释放锁。其他线程signal时,当前线程获得锁继续执行。若当前线程中断则跳出等待。
- awaitUninterruptibly():不会在等待过程中响应中断
- signal():唤醒一个在等待的线程,signalAll()唤醒所有等待的线程。
示例代码:
private final Lock lock=new ReentrantLock();
private final Condition notFull=lock.newCondition();
private final Condition notEmpty=lock.newCondition();
private int maxSize;
private List<Date> storage;
public void put() {
lock.lock();
try {
while (storage.size() ==maxSize ){//如果队列满了
System.out.print(Thread.currentThread().getName()+": wait \n");;
notFull.await();//阻塞生产线程
}
storage.add(new Date());
System.out.print(Thread.currentThread().getName()+": put:"+storage.size()+ "\n");
Thread.sleep(1000);
notEmpty.signalAll();//唤醒消费线程
} catch (InterruptedException e) {
}finally{
lock.unlock();
}
}
public void take() {
lock.lock();
try {
while (storage.size() ==0 ){//如果队列满了
System.out.print(Thread.currentThread().getName()+": wait \n");;
notEmpty.await();//阻塞消费线程
}
Date d=((LinkedList<Date>)storage).poll();
System.out.print(Thread.currentThread().getName()+": take:"+storage.size()+ "\n");
Thread.sleep(1000);
notFull.signalAll();//唤醒生产线程
} catch (InterruptedException e) {
}finally{
lock.unlock();
}
}
6、Semaphore信号量
java锁的缺点:一次都只允许一个线程访问一个资源。
Semaphore:是对锁的扩展,可以指定多个线程同时访问某个资源。一般用它和锁(内部锁或重入锁)来构建对象池。
Semaphore相关方法:
- Semaphore(int permits, boolean fair):同时能申请多少个许可,指定是否公平。
- acquire():申请一个许可,若无法获得,则线程等待,直到有线程释放许可或当前线程被中断。
- tryAcquire():尝试获得许可。
- release():是否一个许可。
示例代码:
private static final int SEM_MAX = 10;
public static void main(String[] args) {
Semaphore sem = new Semaphore(SEM_MAX);
//创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
//在线程池中执行任务
threadPool.execute(new MyThread(sem, 5));
threadPool.execute(new MyThread(sem, 4));
threadPool.execute(new MyThread(sem, 7));
//关闭池
threadPool.shutdown();
}
}
class MyThread extends Thread {
private volatile Semaphore sem; // 信号量
private int count; // 申请信号量的大小
MyThread(Semaphore sem, int count) {
this.sem = sem;
this.count = count;
}
public void run() {
try {
// 从信号量中获取count个许可
sem.acquire(count);
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " acquire count="+count);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放给定数目的许可,将其返回到信号量。
sem.release(count);
System.out.println(Thread.currentThread().getName() + " release " + count + "");
}
}
7、ThreadLocal线程局部变量
ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。一般在线程的某个阶段,将值放入ThreadLocal中,则在线程的整个生命周期中,都可以获得ThreadLocal对应的值,但需要注意的是,如果是线程池中的线程,则在线程run方法结束时,一般在finally中将其设置为空。
四、锁优化、无锁的使用
1、锁优化
- 减小锁持有的时间:锁代码块
- 减小锁的粒度:ConcurrentHashmap内部分为N个段,每个段再用锁进行控制
- 读写分离锁代替独占锁,如利用读写锁而不是内部锁或重入锁
- 锁分离:LinkedBlockingQueue中的读和取分别用了2个重入锁,一个控制读一个控制写,
这是因为对队列的操作,读和写都会改变队列里的数据。这样读和取就可以并发执行
2、无锁的使用
在jdk的java.util.concurrent.atomic包下有一组使用无锁算法实现的原子操作类,如:AtomicInteger。它采用CAS算法实现,是线程安全的类,可放心使用。
CAS算法:CAS(V,E,N),V表示要更新的值,E表示预期值,N表示新值。仅当V=E时,才将V的值设为E,最后返回当前V的真实值。