本章继续深入学习线程-线程池
线程池的基本概念
JDK5之前,都是程序员自己手动创建线程池,JDK5之后,内置线程池技术。
什么是线程池呢?
可以简单理解成容纳多个线程的容器,其中的线程是可以反复使用的,省去了频繁创建对象的操作。
为什么使用线程池呢?
在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。
线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。线程池的使用
一、线程池的创建
摘记API
static ExecutorService newFixedThreadPool(int nThreads) | 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。 |
static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) | 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程,在需要时使用提供的 ThreadFactory 创建新线程。 |
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。 |
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory) | 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。 |
二、线程安全
如果有多个线程在同时运行,而且这些线程可能会同时执行这段代码,程序每次运行的结果和单线程运行的结果是一样的,而且其他的变量值也是跟预期一样的,就是线程安全的。
只有多条线程在使用共享数据的时候会出现线程安全问题。
为什么会出现线程安全问题:
做一个经典模拟,电影院三个窗口卖100张电影票
public class TicketsDemo {
public static void main(String[] args) {
Runnable tick = new Tickets();
Thread t1 = new Thread(tick,"窗口1");
Thread t2 = new Thread(tick,"窗口2");
Thread t3 = new Thread(tick,"窗口3");
t1.start();
t2.start();
t3.start();
}
public static class Tickets implements Runnable {
// 总共有100张票
private Integer tickets = 100;
@Override
public void run() {
while (true) {
// 电影票售空之前 进入循环
if (tickets > 0) {
//------------------------------------流程 1
try {
// 模拟服务器卡顿,增加CPU切换执行概率.
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//------------------------------------流程 2
// 输出那个窗口买到第几张票
System.out.println(Thread.currentThread().getName() + " 购买到 " + tickets--);
}
}
}
}
}
console->
发现不仅买到了第0张,还买到了第-1张,这在程序当中肯定是不允许出现的。那么我们来分析一下,出现0和负数的原因,
当CPU执行线程一到流程①位置时候,已经进行了电影票判断,然后休眠的时候,线程二拿到了CUP执行权,同样线程二执行到流程①位置的时候,判断过电影票数,到休眠状态到达,这个时候CPU执行权被线程三抢到,等CPU执行线程三后,重新回来接着执行线程一的时候,CPU会接着上次的线程一休眠后流程②的位置开始往下执行,不会再去判断票数是否售空,此时票数再减减,接着回到线程二的休眠结束后流程②的位置往下执行,这样一来票数继续减减,那么出现买到票数0和负数的情况也就出现了。
如何解决线程安全问题:
1、使用关键字synchronized:
public class TicketsDemo {
public static void main(String[] args) {
Runnable tick = new Tickets();
Thread t1 = new Thread(tick,"窗口1");
Thread t2 = new Thread(tick,"窗口2");
Thread t3 = new Thread(tick,"窗口3");
t1.start();
t2.start();
t3.start();
}
public static class Tickets implements Runnable {
// 总共有100张票
private Integer tickets = 100;
// 创建Obj
private Object obj = new Object();
@Override
public void run() {
while (true) {
// 电影票售空之前 进入循环
synchronized (obj) {
if (tickets > 0) {
//------------------------------------流程 1
try {
// 模拟服务器卡顿,增加CPU切换执行概率.
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//------------------------------------流程 2
// 输出那个窗口买到第几张票
System.out.println(Thread.currentThread().getName() + " 购买到 " + tickets--);
}
}
}
}
}
}
console->
多次测试 并没有出现0或者负数的情况。那么成功解决线程安全问题,但是每一次都得使用这种语法,无疑会写很多代码,有没有办法简化这一过程呢,答案当然是有的,我们将需要同步的提出来,在方法前加上 synchronized 关键字变成同步方法。
public class TicketsDemo {
public static void main(String[] args) {
Runnable tick = new Tickets();
Thread t1 = new Thread(tick, "窗口1");
Thread t2 = new Thread(tick, "窗口2");
Thread t3 = new Thread(tick, "窗口3");
t1.start();
t2.start();
t3.start();
}
public static class Tickets implements Runnable {
// 总共有100张票
private Integer tickets = 100;
@Override
public void run() {
while (true) {
// 电影票售空之前 进入循环
pay();
}
}
private synchronized void pay() {
if (tickets > 0) {
try {
// 模拟服务器卡顿,增加CPU切换执行概率.
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出那个窗口买到第几张票
System.out.println(Thread.currentThread().getName() + " 购买到 " + tickets--);
}
}
}
}
console输出一样没有0或负数,成功解决安全问题。
仔细观察输出会发现,速度明显比不使用synchronized要慢的多得多,所以解决线程安全的同时,舍去的是效率。
值得注意的是synchronized不具有继承性,如果父类有一个带有synchronized的方法,子类继承并重写了这个方法,但是同步不能继承,所以你还需要在子类方法中添加synchronized关键字。
synchronized的缺陷:线程进入同步方法要获取锁,但是执行完成之后在哪里释放的锁我们并不知道。上述代码持有锁进入同步代码,如果sleep出现异常,不出同步方法,那么锁就永远不会被释放。那么JDK1.5之后为我们提供了LOCK接口,下面我们一起来看一下这个接口。
2、使用Lock接口:
摘自API
java.util.concurrent.locks 接口 Lock
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。下面是Lock接口为我们提供的方法。
void lock() | 获取锁。 |
void lockInterruptibly() | 如果当前线程未被中断,则获取锁。 |
Condition newCondition() | 返回绑定到此 Lock 实例的新 Condition 实例。 |
boolean tryLock() | 仅在调用时锁为空闲状态才获取该锁。 |
boolean tryLock(long time, TimeUnit unit) | 如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁。 |
void unlock() | 释放锁。 |
使用同样的售票代码,这次我们使用Lock接口进行改造。
public class TicketsDemo {
public static void main(String[] args) {
Runnable tick = new Tickets();
Thread t1 = new Thread(tick, "窗口1");
Thread t2 = new Thread(tick, "窗口2");
Thread t3 = new Thread(tick, "窗口3");
t1.start();
t2.start();
t3.start();
}
public static class Tickets implements Runnable {
// 总共有100张票
private Integer tickets = 100;
// 创建Lock实现类对象
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 获得锁
lock.lock();
if (tickets > 0) {
try {
// 模拟服务器卡顿,增加CPU切换执行概率.
Thread.sleep(100);
// 输出那个窗口买到第几张票
System.out.println(Thread.currentThread().getName() + " 购买到 " + tickets--);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 释放锁
lock.unlock();
}
}
}
}
}
}
查看console,重复多次也没有出现0或者负数,那么这个方法也是可行的,注意的是使用synchronized是自动释放锁,使用lock的时候,我们需要手动调用unlock()方法手动释放锁,为了保证锁一定会被释放,我们将unlock() 放入 finally,互斥代码放入try 。
JDK为我们提供了三个Lock接口的实现 。
ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock
根据功能分类可以分成:
- 排他锁:在同一时刻只允许一个线程进行访问,其他线程等待;
- 读写锁:在同一时刻允许多个读线程访问,但是当写线程访问,所有的写线程和读线程均被阻塞。读写锁维护了一个读锁加一个写锁,通过读写锁分离的模式来保证线程安全,性能高于一般的排他锁。
上面使用到的是Lock的实现类ReentrantLock,下面着重讲解一下读写锁。
读写锁的特性:
- 公平性选择:支持公平和非公平(默认)两种获取锁的方式,非公平锁的吞吐量优于公平锁。
- 可重入:允许读锁可写锁可重入。写锁可以获得读锁,读锁不能获得写锁。
- 锁降级:允许写锁降低为读锁。
- 中断锁的获取:在读锁和写锁的获取过程中支持中断 。
* @since 1.5
* @author Doug Lea
*/
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
private static final long serialVersionUID = -6992448646407690164L;
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
final Sync sync;
从源码中可以看到,ReentrantReadWriteLock类 实现了ReadWriteLock接口。并且定义了三个变量,一个读锁对象,一个写锁对象,一个实现同步。
而ReadWriteLock接口中只定义了两个抽象方法,一个读锁,一个写锁。
* @since 1.5
* @author Doug Lea
*/
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}
三、死锁