文章目录
Java 5 添加了一个新的包到 Java 平台,java.util.concurrent 包。这个包包含有一系列能够让 Java 的并发编程变得更加简单轻松的类。在这个包被添加以前,你需要自己去动手实现自己的相关工具类。下面带你认识下java.util.concurrent 包里的这些类,然后你可以尝试着如何在项目中使用它们。本文中将使用 Java 6 版本,我不确定这和 Java 5 版本里的是否有一些差异。我不会去解释关于 Java 并发的核心问题 – 其背后的原理,也就是说,如果你对那些东西感兴趣,参考《Java 并发指南》。
1 Java 的线程并发库介绍
Java5 的多线程并有两个大发库在 java.util.concurrent 包及子包中,子包主要的包有一下两个
1) java.util.concurrent 包 (多线程并发库)
- java.util.concurrent包含许多线程安全、测试良好、高性能的并发构建块。不客气地说,创建java.util.concurrent 的目的就是要实现Collection 框架对数据结构所执行的并发操作。通过提供一组可靠的、高性能并发构建块,开发人员可以提高并发类的线程安全、可伸缩性、性能、可读性和可靠性,后面、我们会做介绍
- 如果一些类名看起来相似,可能是因为 java.util.concurrent 中的许多概念源自 Doug Lea 的util.concurrent 库。
2) java.util.concurrent.atomic 包 (多线程的原子性操作提供的工具类)
-
查看 atomic 包文档页下面的介绍,它可以对多线程的基本数据、数组中的基本数据和对象中的基本数据进行多线程的操作(AtomicInteger、AtomicIntegerArray、AtomicIntegerFieldUpDater…)
-
通过如下两个方法快速理解 atomic 包的意义:
- 1) AtomicInteger 类的 boolean compareAndSet(expectedValue,
updateValue); - 2)AtomicIntegerArray 类的 int addAndGet(int i, int delta);
- 1) AtomicInteger 类的 boolean compareAndSet(expectedValue,
-
顺带解释 volatile 类型的作用,需要查看 java 语言规范。
-
1)volatile 修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。(具有可见性)
-
2)volatile 没有原子性。
3) java.util.concurrent.lock 包 (多线程的锁机制)
为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。该框架允许更灵活地使用锁和条件。本包下有三大接口,下面简单介绍下:
- Lock 接口:支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand over-hand
和锁重排算法)中使用这些规则。主要的实现是 ReentrantLock。 - ReadWriteLock 接口:以类似方式定义了一些读取者可以共享而写入者独占的锁。此包只提供了一个实现,即 ReentrantReadWriteLock,因为它适用于大部分的标准用法上下文。但程序员可以创建自己的、适用于非标准要求的实现。
- Condition 接口:描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。为了避免兼容性问题,Condition 方法的名称与对应的 Object 版本中的不同。
2 Java 的并发库入门
下面我们将分别介绍 java.util.concurrent 包下的常用类的使用。
1) java.util.concurrent 包
java.util.concurrent 包描述:
在并发编程中很常用的实用工具类。此包包括了几个小的、已标准化的可扩展框架,以及一些提供有用功能的类。此包下有一些组件,其中包括:
- 执行程序(线程池)
- 并发队列
- 同步器
- 并发 Collocation
下面我们将 java.util.concurrent 包下的组件逐一简单介绍:
A. 执行程序
Executors 线程池工厂类
线程池的作用
线程池作用就是限制系统中执行线程的数量。
根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程 排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程 池中有等待的工作线程,就可以开始运行了;否则进入等待队列
为什么要用线程池
- 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
- 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为因为消耗过多的内存,而把服务器累趴下(每个线程需要大约 1MB 内存,线程开的越多,消耗的内存也就越大,最后死机)
Executors 详解
Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 ExecutorService。ThreadPoolExecutor 是 Executors 类的底层实现。我们先介绍下 Executors。
线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源
Java5 中并发库中,线程池创建线程大致可以分为下面三种:
//创建固定大小的线程池
ExecutorService fPool = Executors.newFixedThreadPool(3);
//创建缓存大小的线程池
ExecutorService cPool = Executors.newCachedThreadPool();
//创建单一的线程池
ExecutorService sPool = Executors.newSingleThreadExecutor();
下面我们通过简单示例来分别说明:
固定大小连接池
package com.wjl.test.mythreadLocal;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 固定大小的线程池
*/
public class SizeThreadPool {
public static void main(String[] args) {
//创建一个可重用固定线程数的线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
//创建实现了 Runnable 接口对象,Thread 对象当然也实现了 Runnable 接口
Thread t1 = new MyThread();
Thread t2 = new MyThread();
Thread t3 = new MyThread();
Thread t4 = new MyThread();
Thread t5 = new MyThread();
//将线程放入池中进行执行
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
pool.execute(t4);
pool.execute(t5);
//关闭线程池
pool.shutdown();
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"正在执行。。。");
}
}
运行结果:
pool-1-thread-2正在执行。。。
pool-1-thread-1正在执行。。。
pool-1-thread-1正在执行。。。
pool-1-thread-2正在执行。。。
pool-1-thread-2正在执行。。。
从上面的运行来看,我们 Thread 类都是在线程池中运行的,线程池在执行 execute 方法来执行 Thread 类中的 run 方法。不管 execute 执行几次,线程池始终都会使用 2 个线程来处理。不会再去创建出其他线程来处理run 方法执行。这就是固定大小线程池。
单任务连接池
ExecutorService pool = Executors.newSingleThreadExecutor();
补充:在 java 的多线程中,一但线程关闭,就会成为死线程。关闭后死线程就没有办法在启动了。再次启动就会出现异常信息:Exception in thread “main” java.lang.IllegalThreadStateException。那么如何解决这个问题呢?我们这里就可以使用 Executors.newSingleThreadExecutor()来再次启动一个线程。(面试)
可变连接池
//创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。
ExecutorService pool = Executors.newCachedThreadPool();
pool-1-thread-1正在执行...
pool-1-thread-2正在执行...
pool-1-thread-3正在执行...
pool-1-thread-4正在执行...
pool-1-thread-5正在执行...
运行结果看出,可变任务线程池在执行 execute 方法来执行 Thread 类中的 run 方法。这里 execute 执行多次,线程池就会创建出多个线程来处理 Thread 类中 run 方法。所有我们看到连接池会根据执行的情况,在程序运行时创建多个线程来处理,这里就是可变连接池的特点。
那么在上面的三种创建方式,Executors 还可以在执行某个线程时,定时操作。那么下面我们通过代码简单演示
下。
延迟连接池
package com.wjl.test.mythreadLocal;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 延时连接池
*/
public class DelayThreadPool {
public static void main(String[] args) {
//创建一个线程池,它可以安排在给定延时后运行命令或者定期的执行
ScheduledExecutorService pool= Executors.newScheduledThreadPool(2);
//创建实现了 Runnable 接口对象,Thread 对象当然也实现了 Runnable 接口
Thread t1 = new MyThread();
Thread t2 = new MyThread();
Thread t3 = new MyThread();
Thread t4 = new MyThread();
Thread t5 = new MyThread();
//把线程放入池中进行执行
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
//使用定时执行风格的方法
pool.schedule(t4, 10000, TimeUnit.MILLISECONDS); //t4 和 t5 在 10 秒后执行
pool.schedule(t5, 10000, TimeUnit.MILLISECONDS);
//关闭线程池
pool.shutdown();
}
}
运行结果:
pool-1-thread-1正在执行...
pool-1-thread-2正在执行...
pool-1-thread-1正在执行...
pool-1-thread-2正在执行...
pool-1-thread-1正在执行...