(一)多线程基础知识--传统线程机制的回顾
(1)传统使用类Thread和接口Runnable实现
1.在Thread子类覆盖的run方法中编写运行代码
new Thread(){
@Override
public void run(){
while(true){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
2. 在传递给Thread对象的Runnable对象的run方法中编写代码
new Thread(new Runnable(){
public void run(){
while(true){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
}).start();
3. 总结
查看Thread类的run()方法的源代码,可以看到其实这两种方式都是在调用Thread对象的run方法,如果Thread类的run方法没有被覆盖,并且为该Thread对象设置了一个Runnable对象,该run方法会调用Runnable对象的run方法。
@Override
public void run() {
if (target != null) {
target.run();
}
}
(2)实现定时器Timer和TimerTask
请模拟写出双重定时器(面试题)
要求:使用定时器,间隔4秒执行一次,再间隔2秒执行一次,以此类推执行。
class TimerTastCus extends TimerTask{
private static volatile int count = 0;
@Override
public void run() {
count = (count +1)%2;
System.err.println("Boob boom ");
new Timer().schedule(new TimerTastCus(), 2000+2000*count);
}
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTastCus(), 2000+2000*count);
while (true) {
System.out.println(new Date().getSeconds());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
(3)线程互斥与同步
在引入多线程后,由于线程执行的异步性,会给系统造成混乱,特别是在急用临界资源时,如多个线程急用同一台打印机,会使打印结果交织在一起,难于区分。当多个线程急用共享变量,表格,链表时,可能会导致数据处理出错,因此线程同步的主要任务是使并发执行的各线程之间能够有效的共享资源和相互合作,从而使程序的执行具有可再现性。
当线程并发执行时,由于资源共享和线程协作,使用线程之间会存在以下两种制约关系。
1. 间接相互制约。一个系统中的多个线程必然要共享某种系统资源,如共享CPU,共享I/O设备,所谓间接相互制约即源于这种资源共享,打印机就是最好的例子,线程A在使用打印机时,其它线程都要等待。
2. 直接相互制约。这种制约主要是因为线程之间的合作,如有线程A将计算结果提供给线程B作进一步处理,那么线程B在线程A将数据送达之前都将处于阻塞状态。
间接相互制约可以称为互斥,直接相互制约可以称为同步,对于互斥可以这样理解,线程A和线程B互斥访问某个资源则它们之间就会产个顺序问题——要么线程A等待线程B操作完毕,要么线程B等待线程操作完毕,这其实就是线程的同步了。因此同步包括互斥,互斥其实是一种特殊的同步。
下面我们通过一道面试题来体会线程的交互。
要求:子线程运行执行10次后,主线程再运行5次。这样交替执行三遍
public class Demo {
public static void main(String[] args) {
final Bussiness bussiness = new Bussiness();
//子线程
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
bussiness.subMethod();
}
}
}).start();
//主线程
for (int i = 0; i < 3; i++) {
bussiness.mainMethod();
}
}
}
class Bussiness {
private boolean subFlag = true;
public synchronized void mainMethod() {
while (subFlag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+ " : main thread running loop count -- " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
subFlag = true;
notify();
}
public synchronized void subMethod() {
while (!subFlag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 0; i < 10; i++) {
System.err.println(Thread.currentThread().getName()+ " : sub thread running loop count -- " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
subFlag = false;
notify();
}
}
(4)线程局部变量ThreadLocal
ThreadLocal的作用和目的:用于实现线程内的数据共享,即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
ThreadLocal的应用场景:
订单处理包含一系列操作:减少库存量、增加一条流水台账、修改总账,这几个操作要在同一个事务中完成,通常也即同一个线程中进行处理,如果累加公司应收款的操作失败了,则应该把前面的操作回滚,否则,提交所有操作,这要求这些操作使用相同的数据库连接对象,而这些操作的代码分别位于不同的模块类中。
银行转账包含一系列操作:把转出帐户的余额减少,把转入帐户的余额增加,这两个操作要在同一个事务中完成,它们必须使用相同的数据库连接对象,转入和转出操作的代码分别是两个不同的帐户对象的方法。
例如Strut2的ActionContext,同一段代码被不同的线程调用运行时,该代码操作的数据是每个线程各自的状态和数据,对于不同的线程来说,getContext方法拿到的对象都不相同,对同一个线程来说,不管调用getContext方法多少次和在哪个模块中getContext方法,拿到的都是同一个。
1. ThreadLocal的使用方式
(1) 在关联数据类中创建private static ThreadLocal
在下面的类中,私有静态 ThreadLocal 实例(serialNum)为调用该类的静态SerialNum.get() 方法的每个线程维护了一个“序列号”,该方法将返回当前线程的序列号。(线程的序列号是在第一次调用SerialNum.get() 时分配的,并在后续调用中不会更改。)
public class SerialNum {
private static int nextSerialNum = 0;
private static ThreadLocal serialNum = new ThreadLocal() {
protected synchronized Object initialValue() {
return new Integer(nextSerialNum++);
}
};
public static int get() {
return ((Integer) (serialNum.get())).intValue();
}
}
另一个例子,也是私有静态 ThreadLocal 实例:
public class ThreadContext {
private String userId;
private Long transactionId;
private static ThreadLocal threadLocal = new ThreadLocal() {
@Override
protected ThreadContext initialValue() {
return new ThreadContext();
}
};
public static ThreadContext get() {
return (ThreadContext) threadLocal.get();
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public Long getTransactionId() {
return transactionId;
}
public void setTransactionId(Long transactionId) {
this.transactionId = transactionId;
}
}
2. 在Util类中创建ThreadLocal
这是上面用法的扩展,即把ThreadLocal的创建放到工具类中。
public class HibernateUtil {
private static Log log = LogFactory.getLog(HibernateUtil.class);
private static final SessionFactory sessionFactory;//定义SessionFactory
static {
try {
//通过默认配置文件hibernate.cfg.xml创建SessionFactory
sessionFactory = new Configuration().configure().buildSessionFactory();
} catch (Throwable ex) {
log.error("初始化SessionFactory失败!", ex);
throw new ExceptionInInitializerError(ex);
}
}
//创建线程局部变量session,用来保存Hibernate的Session
public static final ThreadLocal session = new ThreadLocal();
//获取当前线程中的Session
public static Session currentSession() throws HibernateException {
Session s = (Session) session.get();//如果Session还没有打开,则新开一个Session
if (s == null) {
s = sessionFactory.openSession();session.set(s); //将新开的Session保存到线程局部变量中
}
return s;
}
public static void closeSession() throws HibernateException {
//获取线程局部变量,并强制转换为Session类型
Session s = (Session) session.get();
session.set(null);
if (s != null)
s.close();
}
3. 在Runnable中创建ThreadLocal
在线程类内部创建ThreadLocal,基本步骤如下:
①、在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。
②、[endif]在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型.
③、[endif]在ThreadDemo类的run()方法中,通过调用getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。
public class ThreadLocalTest implements Runnable {
ThreadLocal studenThreadLocal = new ThreadLocal();
@Override
public void run() {
String currentThreadName = Thread.currentThread().getName();
System.out.println(currentThreadName + " is running...");
Random random = new Random();
int age = random.nextInt(100);
System.out.println(currentThreadName + " is set age: " + age);
Studen studen = getStudent(); // 通过这个方法,为每个线程都独立的new一个student对象,每个线程的的student对象都可以设置不同的值
studen.setAge(age);
System.out.println(currentThreadName + " is first get age: " + studen.getAge());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(currentThreadName + " is second get age: " + studen.getAge());
}
private Studen getStudent() {
Studen studen = (Studen) studenThreadLocal.get();
if (null == studen) {
studen = new Studen();
studenThreadLocal.set(studen);
}
return studen;
}
public static void main(String[] args) {
ThreadLocalTest t = new ThreadLocalTest();
Thread t1 = new Thread(t, "Thread A");
Thread t2 = new Thread(t, "Thread B");
t1.start();
t2.start();
}
}
class Studen {
int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
( 5 )多线程共享数据
在Java传统线程机制中的共享数据方式,大致可以简单分两种情况:
多个线程行为一致,共同操作一个数据源。也就是每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,卖票系统就可以这么做。
多个线程行为不一致,共同操作一个数据源。也就是每个线程执行的代码不同,这时候需要用不同的Runnable对象。例如,银行存取款。
下面我们通过两个示例代码来分别说明这两种方式。
多个线程行为一致共同操作一个数据
如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,买票系统就可以这么做。
/**
* 共享数据类
**/
class ShareData {
private int num = 10;
public synchronized void inc() {
num++;
System.out.println(Thread.currentThread().getName() + ": invoke inc method num =" + num);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 多线程类
**/
class RunnableCusToInc implements Runnable {
private ShareData shareData;
public RunnableCusToInc(ShareData data) {
this.shareData = data;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
shareData.inc();
}
}
/**
* 测试方法
**/
public static void main(String[] args) {
ShareData shareData = new ShareData();
for (int i = 0; i < 4; i++) {
new Thread(new RunnableCusToInc(shareData), "Thread " + i).start();
}
}
}
2.
多个线程行为不一致共同操作一个数据
如果每个线程执行的代码不同,这时候需要用不同的Runnable对象,有如下两种方式来实现这些Runnable对象之间的数据共享:
1) 将共享数据封装在另外一个对象中,然后将这个对象逐一传递给各个Runnable对象。每个线程对共享数据的操作方法也分配到那个对象身上去完成,这样容易实现针对该数据进行的各个操作的互斥和通信。
ShareData shareData = new ShareData();
for (int i = 0; i < 4; i++) {
if (i % 2 == 0) {
new Thread(new RunnableCusToInc(shareData), "Thread " + i).start();
} else {
new Thread(new RunnableCusToDec(shareData), "Thread " + i).start();
}
}
补充:上面两种方式的组合:将共享数据封装在另外一个对象中,每个线程对共享数据的操作方法也分配到那个对象身上去完成,对象作为这个外部类中的成员变量或方法中的局部变量,每个线程的Runnable对象作为外部类中的成员内部类或局部内部类。
总之,要同步互斥的几段代码最好是分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现它们之间的同步互斥和通信。
( 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包的意义:
AtomicInteger类的boolean compareAndSet(expectedValue, updateValue);
AtomicIntegerArray类的int addAndGet(int i, int delta);
顺带解释volatile类型的作用,需要查看java语言规范。
volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。(具有可见性)
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();
public class Demo1 {
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()+"正在执行。。。");
}
}
从上面的运行来看,我们Thread类都是在线程池中运行的,线程池在执行execute方法来执行Thread类中的run方法。不管execute执行几次,线程池始终都会使用2个线程来处理。不会再去创建出其他线程来处理run方法执行。这就是固定大小线程池。
我们将上面的代码
//创建一个可重用固定线程数的线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
改为:
//创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。
ExecutorService pool = Executors.newSingleThreadExecutor();
运行结果看出,单任务线程池在执行execute方法来执行Thread类中的run方法。不管execute执行几次,线程池始终都会使用单个线程来处理。
补充:在java的多线程中,一但线程关闭,就会成为死线程。关闭后死线程就没有办法在启动了。再次启动就会出现异常信息:Exception in thread "main" java.lang.IllegalThreadStateException。那么如何解决这个问题呢?
我们这里就可以使用Executors.newSingleThreadExecutor()来再次启动一个线程。(面试)
//创建一个可重用固定线程数的线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
改为:
//创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。
ExecutorService pool = Executors.newCachedThreadPool();
运行结果看出,可变任务线程池在执行execute方法来执行Thread类中的run方法。这里execute执行多次,线程池就会创建出多个线程来处理Thread类中run方法。所有我们看到连接池会根据执行的情况,在程序运行时创建多个线程来处理,这里就是可变连接池的特点。
那么在上面的三种创建方式,Executors还可以在执行某个线程时,定时操作。那么下面我们通过代码简单演示下。
public class Demo2 {
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, 10, TimeUnit.MILLISECONDS);//t4和t5在10秒后执行
pool.schedule(t5, 10, TimeUnit.MILLISECONDS);
//关闭线程池
pool.shutdown();
}
}
class MyAThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行。。。");
}
}
ExecutorService执行器服务
java.util.concurrent.ExecutorService 接口表示一个异步执行机制,使我们能够在后台执行任务。因此一个 ExecutorService 很类似于一个线程池。实际上,存在于 java.util.concurrent 包里的 ExecutorService 实现就是一个线程池实现。
ExecutorService例子:
以下是一个简单的ExecutorService 例子:
//线程工厂类创建出线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
//执行一个线程任务
executorService.execute(new Runnable() {
public void run() {
System.out.println("Asynchronous task");
}
});
//线程池关闭
executorService.shutdown();
上面代码首先使用newFixedThreadPool() 工厂方法创建一个 ExecutorService。这里创建了一个十个线程执行任务的线程池。然后,将一个 Runnable 接口的匿名实现类传递给 execute() 方法。这将导致 ExecutorService 中的某个线程执行该 Runnable。这里可以看成一个任务分派,示例代码中的任务分派我们可以理解为:
一个线程将一个任务委派给一个ExecutorService 去异步执行。
一旦该线程将任务委派给ExecutorService,该线程将继续它自己的执行,独立于该任务的执行。
ExecutorService实现:
既然ExecutorService 是个接口,如果你想用它的话就得去使用它的实现类之一。
java.util.concurrent 包提供了 ExecutorService 接口的以下实现类:
ThreadPoolExecutor
ScheduledThreadPoolExecutor
ExecutorService创建:
ExecutorService 的创建依赖于你使用的具体实现。但是你也可以使用 Executors 工厂类来创建 ExecutorService 实例。代码示例:
ExecutorService executorService1 = Executors.newSingleThreadExecutor(); //之前Executors已介绍
ExecutorService executorService2 = Executors.newFixedThreadPool(10);
ExecutorService executorService3 = Executors.newScheduledThreadPool(10);
ExecutorService使用:
有几种不同的方式来将任务委托给ExecutorService 去执行:
execute(Runnable)
submit(Runnable)
submit(Callable)
invokeAny(…)
invokeAll(…)
接下来我们挨个看一下这些方法。
execute(Runnable)
execute(Runnable) 方法要求一个 java.lang.Runnable 对象,然后对它进行异步执行。以下是使用 ExecutorService 执行一个 Runnable 的示例:
//从Executors中获得ExecutorService
ExecutorService executorService = Executors.newSingleThreadExecutor();
//执行 ExecutorService 中的方法
executorService.execute(new Runnable() {
public void run() {
System.out.println("Asynchronous task");
}
});
//线程池关闭
executorService.shutdown();
特点:没有办法得知被执行的Runnable 的执行结果。如果有需要的话你得使用一个 Callable(以下将做介绍)。
submit(Runnable)
submit(Runnable) 方法也要求一个 Runnable 实现类,但它返回一个 Future 对象。这个 Future 对象可以用来检查 Runnable 是否已经执行完毕。以下是 ExecutorService submit() 示例:
//从Executors中获得ExecutorService
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future future = executorService.submit(new Runnable() {
public void run() {
System.out.println("Asynchronous task");
}
});
future.get(); //获得执行完run方法后的返回值,这里使用的Runnable,所以这里没有返回值,返回的是null。
executorService.shutdown();
submit(Runnable)
submit(Callable) 方法类似于 submit(Runnable) 方法,除了它所要求的参数类型之外。Callable 实例除了它的 call() 方法能够返回一个结果之外和一个 Runnable 很相像。Runnable.run() 不能够返回一个结果。Callable 的结果可以通过 submit(Callable) 方法返回的 Future 对象进行获取。
以下是一个ExecutorService Callable 示例:
//从Executors中获得ExecutorService
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future future = executorService.submit(new Callable(){
public Object call() throws Exception {
System.out.println("Asynchronous Callable");
return "Callable Result";
}
});
System.out.println("future.get() = " + future.get());
executorService.shutdown();
输出:
Asynchronous Callable
future.get() = Callable Result
invokeAny()
invokeAny() 方法要求一系列的 Callable 或者其子接口的实例对象。调用这个方法并不会返回一个 Future,但它返回其中一个 Callable 对象的结果。无法保证返回的是哪个 Callable 的结果 – 只能表明其中一个已执行结束。
如果其中一个任务执行结束(或者抛了一个异常),其他 Callable 将被取消。以下是示例代码:
ExecutorService executorService = Executors.newSingleThreadExecutor();
Set> callables = new HashSet>();
callables.add(new Callable() {
public String call() throws Exception {
return "Task 1";
}
});
callables.add(new Callable() {
public String call() throws Exception {
return "Task 2";
}
});
callables.add(new Callable() {
public String call() throws Exception {
return "Task 3";
}
});
String result = executorService.invokeAny(callables);
System.out.println("result = " + result);
executorService.shutdown();
上述代码将会打印出给定Callable 集合中的一个的执行结果。我自己试着执行了它几次,结果始终在变。有时是 “Task 1″,有时是 “Task 2″ 等等。
invokeAll()
invokeAll() 方法将调用你在集合中传给 ExecutorService 的所有 Callable 对象。invokeAll() 返回一系列的 Future 对象,通过它们你可以获取每个 Callable 的执行结果。记住,一个任务可能会由于一个异常而结束,因此它可能没有"成功"。无法通过一个 Future 对象来告知我们是两种结束中的哪一种。
以下是一个代码示例:
ExecutorService executorService = Executors.newSingleThreadExecutor();
Set> callables = new HashSet>();
callables.add(new Callable() {
public String call() throws Exception {
return "Task 1";
}
});
callables.add(new Callable() {
public String call() throws Exception {
return "Task 2";
}
});
callables.add(new Callable() {
public String call() throws Exception {
return "Task 3";
}
});
List> futures = executorService.invokeAll(callables);
for(Future future : futures){
System.out.println("future.get = " + future.get());
}
executorService.shutdown();
输出结果:
future.get = Task 3
future.get = Task 1
future.get = Task 2
Executors关闭:
使用shutdown和shutdownNow可以关闭线程池
两者的区别:
shutdown只是将空闲的线程interrupt() 了,shutdown()之前提交的任务可以继续执行直到结束。
shutdownNow 是interrupt所有线程, 因此大部分线程将立刻被中断。之所以是大部分,而不是全部 ,是因为interrupt()方法能力有限。
ThreadPoolExecutor线程池执行者
java.util.concurrent.ThreadPoolExecutor 是 ExecutorService 接口的一个实现。ThreadPoolExecutor 使用其内部池中的线程执行给定任务(Callable 或者 Runnable)。
ThreadPoolExecutor 包含的线程池能够包含不同数量的线程。池中线程的数量由以下变量决定:
corePoolSize
maximumPoolSize
当一个任务委托给线程池时,如果池中线程数量低于corePoolSize,一个新的线程将被创建,即使池中可能尚有空闲线程。如果内部任务队列已满,而且有至少 corePoolSize 正在运行,但是运行线程的数量低于 maximumPoolSize,一个新的线程将被创建去执行该任务。
创建ThreadPoolExecutor:
int corePoolSize = 5;
int maxPoolSize = 10;
long keepAliveTime = 5000;
ExecutorService threadPoolExecutor =
new ThreadPoolExecutor(corePoolSize,maxPoolSize,keepAliveTime,TimeUnit.MILLISECONDS,new LinkedBlockingQueue());
构造方法参数列表解释:
corePoolSize -池中所保存的线程数,包括空闲线程。
maximumPoolSize -池中允许的最大线程数。
keepAliveTime -当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
unit - keepAliveTime参数的时间单位。
workQueue -执行前用于保持任务的队列。此队列仅保持由 execute 方法提交的 Runnable 任务。
ScheduledPoolExecutor定时线程池执行者java.util.concurrent.ScheduledExecutorService 是一个 ExecutorService, 它能够将任务延后执行,或者间隔固定时间多次执行。 任务由一个工作者线程异步执行,而不是由提交任务给 ScheduledExecutorService 的那个线程执行。
ScheduledPoolExecutor例子:
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(5);
ScheduledFuture scheduledFuture =
scheduledExecutorService.schedule(new Callable() {
public Object call() throws Exception {
System.out.println("Executed!");
return "Called!";
}
},5,TimeUnit.SECONDS);//5秒后执行
首先一个内置5 个线程的 ScheduledExecutorService 被创建。之后一个 Callable 接口的匿名类示例被创建然后传递给 schedule() 方法。后边的俩参数定义了 Callable 将在 5 秒钟之后被执行。
ScheduledExecutorService的实现:
ScheduledExecutorService 是一个接口,你要用它的话就得使用 java.util.concurrent 包里对它的某个实现类。ScheduledExecutorService 具有以下实现类:ScheduledThreadPoolExecutor
创建一个ScheduledExecutorService:
如何创建一个ScheduledExecutorService 取决于你采用的它的实现类。但是你也可以使用 Executors 工厂类来创建一个 ScheduledExecutorService 实例。比如:
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
ScheduledExecutorService的使用:
一旦你创建了一个ScheduledExecutorService,你可以通过调用它的以下方法:
schedule (Callable task, long delay, TimeUnit timeunit)
schedule (Runnable task, long delay, TimeUnit timeunit)
scheduleAtFixedRate (Runnable, long initialDelay, long period, TimeUnit timeunit)
scheduleWithFixedDelay (Runnable, long initialDelay, long period, TimeUnit timeunit)
下面我们就简单看一下这些方法。
schedule (Callable task, long delay, TimeUnit timeunit)
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(5);
ScheduledFuture scheduledFuture =
scheduledExecutorService.schedule(new Callable() {
public Object call() throws Exception {
System.out.println("Executed!");
return "Called!";
}
},
5,
TimeUnit.SECONDS);
System.out.println("result = " + scheduledFuture.get());
scheduledExecutorService.shutdown();
输出结果:
Executed! result = Called!
schedule (Runnable task, long delay, TimeUnit timeunit)
这一方法规划一个任务将被定期执行。该任务将会在首个initialDelay 之后得到执行,然后每个 period 时间之后重复执行。
如果给定任务的执行抛出了异常,该任务将不再执行。如果没有任何异常的话,这个任务将会持续循环执行到ScheduledExecutorService 被关闭。
如果一个任务占用了比计划的时间间隔更长的时候,下一次执行将在当前执行结束执行才开始。计划任务在同一时间不会有多个线程同时执行。
scheduleAtFixedRate (Runnable, long initialDelay, long period, TimeUnit timeunit)
这一方法规划一个任务将被定期执行。该任务将会在首个initialDelay 之后得到执行,然后每个 period 时间之后重复执行。
如果给定任务的执行抛出了异常,该任务将不再执行。如果没有任何异常的话,这个任务将会持续循环执行到ScheduledExecutorService 被关闭。
如果一个任务占用了比计划的时间间隔更长的时候,下一次执行将在当前执行结束执行才开始。计划任务在同一时间不会有多个线程同时执行。
scheduleWithFixedDelay (Runnable, long initialDelay, long period, TimeUnit timeunit)
除了period 有不同的解释之外这个方法和 scheduleAtFixedRate() 非常像。scheduleAtFixedRate() 方法中,period 被解释为前一个执行的开始和下一个执行的开始之间的间隔时间。而在本方法中,period 则被解释为前一个执行的结束和下一个执行的结束之间的间隔。因此这个延迟是执行结束之间的间隔,而不是执行开始之间的间隔。
ScheduledExecutorService的关闭:
正如ExecutorService,在你使用结束之后你需要把 ScheduledExecutorService 关闭掉。否则他将导致 JVM 继续运行,即使所有其他线程已经全被关闭。
你可以使用从ExecutorService 接口继承来的 shutdown() 或 shutdownNow() 方法将 ScheduledExecutorService 关闭。参见 ExecutorService 关闭部分以获取更多信息。
ForkJoinPool合并和分叉(线程池)
ForkJoinPool 在 Java 7 中被引入。它和 ExecutorService 很相似,除了一点不同。ForkJoinPool 让我们可以很方便地把任务分裂成几个更小的任务,这些分裂出来的任务也将会提交给 ForkJoinPool。任务可以继续分割成更小的子任务,只要它还能分割。可能听起来有些抽象,因此本节中我们将会解释 ForkJoinPool 是如何工作的,还有任务分割是如何进行的。
合并和分叉的解释:
在我们开始看ForkJoinPool 之前我们先来简要解释一下分叉和合并的原理。分叉和合并原理包含两个递归进行的步骤。两个步骤分别是分叉步骤和合并步骤。
分叉:
一个使用了分叉和合并原理的任务可以将自己分叉(分割)为更小的子任务,这些子任务可以被并发执行。
通过把自己分割成多个子任务,每个子任务可以由不同的CPU 并行执行,或者被同一个 CPU 上的不同线程执行。只有当给的任务过大,把它分割成几个子任务才有意义。把任务分割成子任务有一定开销,因此对于小型任务,这个分割的消耗可能比每个子任务并发执行的消耗还要大。
什么时候把一个任务分割成子任务是有意义的,这个界限也称作一个阀值。这要看每个任务对有意义阀值的决定。很大程度上取决于它要做的工作的种类。
合并:
当一个任务将自己分割成若干子任务之后,该任务将进入等待所有子任务的结束之中。一旦子任务执行结束,该任务可以把所有结果合并到同一个结果。
当然,并非所有类型的任务都会返回一个结果。如果这个任务并不返回一个结果,它只需等待所有子任务执行完毕。也就不需要结果的合并啦。
所以我们可以将ForkJoinPool 是一个特殊的线程池,它的设计是为了更好的配合 分叉-和-合并 任务分割的工作。ForkJoinPool 也在 java.util.concurrent 包中,其完整类名为 java.util.concurrent.ForkJoinPool。
创建一个ForkJoinPool:
你可以通过其构造子创建一个ForkJoinPool。作为传递给 ForkJoinPool 构造子的一个参数,你可以定义你期望的并行级别。并行级别表示你想要传递给 ForkJoinPool 的任务所需的线程或 CPU 数量。以下是一个 ForkJoinPool 示例:
//创建了一个并行级别为 4 的 ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool(4);
提交任务到ForkJoinPool:
就像提交任务到ExecutorService 那样,把任务提交到 ForkJoinPool。你可以提交两种类型的任务。一种是没有任何返回值的(一个 “行动”),另一种是有返回值的(一个”任务”)。这两种类型分别由 RecursiveAction 和 RecursiveTask 表示。接下来介绍如何使用这两种类型的任务,以及如何对它们进行提交。
RecursiveAction:
RecursiveAction 是一种没有任何返回值的任务。它只是做一些工作,比如写数据到磁盘,然后就退出了。一个 RecursiveAction 可以把自己的工作分割成更小的几块,这样它们可以由独立的线程或者 CPU 执行。你可以通过继承来实现一个 RecursiveAction。示例如下:
public class MyRecursiveAction extends RecursiveAction {
private long workLoad = 0;
public MyRecursiveAction(long workLoad) {
this.workLoad = workLoad;
}
@Override
protected void compute() {
//if work is above threshold, break tasks up into smaller tasks
//翻译:如果工作超过门槛,把任务分解成更小的任务
if(this.workLoad > 16) {
System.out.println("Splitting workLoad : " + this.workLoad);
List subtasks = new ArrayList();
subtasks.addAll(createSubtasks());
for(RecursiveAction subtask : subtasks){
subtask.fork();
}
} else {
System.out.println("Doing workLoad myself: " + this.workLoad);
}
}
private List createSubtasks() {
List subtasks = new ArrayList();
MyRecursiveAction subtask1 = new MyRecursiveAction(this.workLoad / 2);
MyRecursiveAction subtask2 = new MyRecursiveAction(this.workLoad / 2);
subtasks.add(subtask1);
subtasks.add(subtask2);
return subtasks;
}
}
例子很简单。MyRecursiveAction 将一个虚构的 workLoad 作为参数传给自己的构造子。如果 workLoad 高于一个特定阀值,该工作将被分割为几个子工作,子工作继续分割。如果 workLoad 低于特定阀值,该工作将由 MyRecursiveAction 自己执行。你可以这样规划一个 MyRecursiveAction 的执行:
//创建了一个并行级别为 4 的 ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool(4);
//创建一个没有返回值的任务
MyRecursiveAction myRecursiveAction = new MyRecursiveAction(24);
//ForkJoinPool执行任务
forkJoinPool.invoke(myRecursiveAction);
运行结果:
Splitting workLoad : 24
Doing workLoad myself: 12
RecursiveTask:
RecursiveTask 是一种会返回结果的任务。它可以将自己的工作分割为若干更小任务,并将这些子任务的执行结果合并到一个集体结果。可以有几个水平的分割和合并。以下是一个 RecursiveTask 示例:
public class MyRecursiveTask extends RecursiveTask {
private long workLoad = 0;
public MyRecursiveTask(long workLoad) {
this.workLoad = workLoad;
}
protected Long compute() {
//if work is above threshold, break tasks up into smaller tasks
if(this.workLoad > 16) {
System.out.println("Splitting workLoad : " + this.workLoad);
List subtasks = new ArrayList();
subtasks.addAll(createSubtasks());
for(MyRecursiveTask subtask : subtasks){
subtask.fork();
}
long result = 0;
for(MyRecursiveTask subtask : subtasks) {
result += subtask.join();
}
return result;
} else {
System.out.println("Doing workLoad myself: " + this.workLoad);
return workLoad * 3;
}
}
private List createSubtasks() {
List subtasks = new ArrayList();
MyRecursiveTask subtask1 = new MyRecursiveTask(this.workLoad / 2);
MyRecursiveTask subtask2 = new MyRecursiveTask(this.workLoad / 2);
subtasks.add(subtask1);
subtasks.add(subtask2);
return subtasks;
}
}
除了有一个结果返回之外,这个示例和RecursiveAction 的例子很像。MyRecursiveTask 类继承自 RecursiveTask,这也就意味着它将返回一个 Long 类型的结果。
MyRecursiveTask 示例也会将工作分割为子任务,并通过 fork() 方法对这些子任务计划执行。此外,本示例还通过调用每个子任务的 join() 方法收集它们返回的结果。子任务的结果随后被合并到一个更大的结果,并最终将其返回。对于不同级别的递归,这种子任务的结果合并可能会发生递归。
你可以这样规划一个RecursiveTask:
//创建了一个并行级别为 4 的 ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool(4);
//创建一个有返回值的任务
MyRecursiveTask myRecursiveTask = new MyRecursiveTask(128);
//线程池执行并返回结果
long mergedResult = forkJoinPool.invoke(myRecursiveTask);
System.out.println("mergedResult = " + mergedResult);
注意: ForkJoinPool.invoke() 方法的调用来获取最终执行结果的。
B. 并发队列-阻塞队列
常用的并发队列有阻塞队列和非阻塞队列,前者使用锁实现,后者则使用CAS非阻塞算法实现。
PS:至于非阻塞队列是靠CAS非阻塞算法,在这里不再介绍,大家只用知道,Java非阻塞队列是使用CAS算法来实现的就可以。感兴趣的童鞋可以维基网上自行学习.
下面我们先介绍阻塞队列。
阻塞队列:
阻塞队列(BlockingQueue)是Java util.concurrent包下重要的数据结构,BlockingQueue提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。并发包下很多高级同步类的实现都是基于BlockingQueue实现的。
BlockingQueue阻塞队列
BlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。
一个线程往里边放,另外一个线程从里边取的一个BlockingQueue。
一个线程将会持续生产新对象并将其插入到队列之中,直到队列达到它所能容纳的临界点。也就是说,它是有限的。如果该阻塞队列到达了其临界点,负责生产的线程将会在往里边插入新对象时发生阻塞。它会一直处于阻塞之中,直到负责消费的线程从队列中拿走一个对象。负责消费的线程将会一直从该阻塞队列中拿出对象。如果消费线程尝试去从一个空的队列中提取对象的话,这个消费线程将会处于阻塞之中,直到一个生产线程把一个对象丢进队列。
BlockingQueue的方法:
BlockingQueue 具有 4 组不同的方法用于插入、移除以及对队列中的元素进行检查。如果请求的操作不能得到立即执行的话,每个方法的表现也不同。这些方法如下:
阻塞队列提供了四种处理方法:
方法\处理方式抛出异常返回特殊值一直阻塞超时退出
插入方法add(e)offer(e)put(e)offer(e,time,unit)
移除方法remove()poll()take()poll(time,unit)
检查方法element()peek()不可用不可用
四组不同的行为方式解释:
抛异常:如果试图的操作无法立即执行,抛一个异常。
特定值:如果试图的操作无法立即执行,返回一个特定的值(常常是 true / false)。
阻塞:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
超时:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(典型的是true / false)。
无法向一个BlockingQueue 中插入 null。如果你试图插入 null,BlockingQueue 将会抛出一个 NullPointerException.
BlockingQueue的实现类:
BlockingQueue 是个接口,你需要使用它的实现之一来使用BlockingQueue,
Java.util.concurrent包下具有以下 BlockingQueue 接口的实现类:
ArrayBlockingQueue:ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存储元素数量的上限。你可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改了(译者注:因为它是基于数组实现的,也就具有数组的特性:一旦初始化,大小就无法修改)。
DelayQueue:DelayQueue 对元素进行持有直到一个特定的延迟到期。注入其中的元素必须实现 java.util.concurrent.Delayed 接口。
LinkedBlockingQueue:LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。
PriorityBlockingQueue:PriorityBlockingQueue 是一个无界的并发队列。它使用了和类 java.util.PriorityQueue 一样的排序规则。你无法向这个队列中插入 null 值。所有插入到 PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。因此该队列中元素的排序就取决于你自己的 Comparable 实现。
SynchronousQueue:SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。据此,把这个类称作一个队列显然是夸大其词了。它更多像是一个汇合点。
ArrayBlockingQueue阻塞队列
ArrayBlockingQueue类图
ArrayBlockingQueue内部有个数组items用来存放队列元素,putindex下标标示入队元素下标,takeIndex是出队下标,count统计队列元素个数,从定义可知道并没有使用volatile修饰,这是因为访问这些变量使用都是在锁块内,并不存在可见性问题。另外有个独占锁lock用来对出入队操作加锁,这导致同时只有一个线程可以访问入队出队,另外notEmpty,notFull条件变量用来进行出入队的同步。另外构造函数必须传入队列大小参数,所以为有界队列,默认是Lock为非公平锁。
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
ps:
所谓公平锁:就是在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
非公平锁:比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式
ArrayBlockingQueue方法
offer方法
在队尾插入元素,如果队列满则返回false,否者入队返回true。
public boolean offer(E e) {
//e为null,则抛出NullPointerException异常
checkNotNull(e);
//获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//如果队列满则返回false
if (count == items.length)
return false;
else {
//否者插入元素
insert(e);
return true;
}
} finally {
//释放锁
lock.unlock();
}
}
private void insert(E x) {
//元素入队
items[putIndex] = x;
//计算下一个元素应该存放的下标
putIndex = inc(putIndex);
++count;
notEmpty.signal();
}
//循环队列,计算下标
final int inc(int i) {
return (++i == items.length) ? 0 : i;
}
这里由于在操作共享变量前加了锁,所以不存在内存不可见问题,加过锁后获取的共享变量都是从主内存获取的,而不是在CPU缓存或者寄存器里面的值,释放锁后修改的共享变量值会刷新会主内存中。
另外这个队列是使用循环数组实现,所以计算下一个元素存放下标时候有些特殊。另外insert后调用 notEmpty.signal();是为了激活调用notEmpty.await()阻塞后放入notEmpty条件队列中的线程。
Put操作
在队列尾部添加元素,如果队列满则等待队列有空位置插入后返回。
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
//获取可被中断锁
lock.lockInterruptibly();
try {
//如果队列满,则把当前线程放入notFull管理的条件队列
while (count == items.length)
notFull.await();
//插入元素
insert(e);
} finally {
lock.unlock();
}
}
需要注意的是如果队列满了那么当前线程会阻塞,知道出队操作调用了notFull.signal方法激活该线程。代码逻辑很简单,但是这里需要思考一个问题为啥调用lockInterruptibly方法而不是Lock方法。我的理解是因为调用了条件变量的await()方法,而await()方法会在中断标志设置后抛出InterruptedException异常后退出,所以还不如在加锁时候先看中断标志是不是被设置了,如果设置了直接抛出InterruptedException异常,就不用再去获取锁了。然后看了其他并发类里面凡是调用了await的方法获取锁时候都是使用的lockInterruptibly方法而不是Lock也验证了这个想法。
Poll操作
从队头获取并移除元素,队列为空,则返回null。
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//当前队列为空则返回null,否者
return (count == 0) ? null : extract();
} finally {
lock.unlock();
}
}
private E extract() {
final Object[] items = this.items;
//获取元素值
E x = this.cast(items[takeIndex]);
//数组中值值为null;
items[takeIndex] = null;
//队头指针计算,队列元素个数减一
takeIndex = inc(takeIndex);
--count;
//发送信号激活notFull条件队列里面的线程
notFull.signal();
return x;
}
Take操作
从队头获取元素,如果队列为空则阻塞直到队列有元素。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//队列为空,则等待,直到队列有元素
while (count == 0)
notEmpty.await();
return extract();
} finally {
lock.unlock();
}
}
需要注意的是如果队列为空,当前线程会被挂起放到notEmpty的条件队列里面,直到入队操作执行调用notEmpty.signal后当前线程才会被激活,await才会返回。
Peek操作
返回队列头元素但不移除该元素,队列为空,返回null。
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//队列为空返回null,否者返回头元素
return (count == 0) ? null : itemAt(takeIndex);
} finally {
lock.unlock();
}
}
final E itemAt(int i) {
return this.cast(items[i]);
}
Size操作
获取队列元素个数,非常精确因为计算size时候加了独占锁,其他线程不能入队或者出队或者删除元素。
public int size() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
ArrayBlockingQueue小结
ArrayBlockingQueue通过使用全局独占锁实现同时只能有一个线程进行入队或者出队操作,这个锁的粒度比较大,有点类似在方法上添加synchronized的意味。其中offer,poll操作通过简单的加锁进行入队出队操作,而put,take则使用了条件变量实现如果队列满则等待,如果队列空则等待,然后分别在出队和入队操作中发送信号激活等待线程实现同步。另外相比LinkedBlockingQueue,ArrayBlockingQueue的size操作的结果是精确的,因为计算前加了全局锁。
ArrayBlockingQueue示例
需求:在多线程操作下,一个数组中最多只能存入3个元素。多放入不可以存入数组,或等待某线程对数组中某个元素取走才能放入,要求使用java的多线程来实现。(面试)
代码实现:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueTest {
public static void main(String[] args) {
final BlockingQueue queue = new ArrayBlockingQueue(3);
for (int i = 0; i < 2; i++) {
new Thread() {
public void run() {
while (true) {
try {
Thread.sleep((long) (Math.random() * 1000));
System.out.println(Thread.currentThread().getName() + "准备放数据!");
queue.put(1);
System.out.println(Thread.currentThread().getName() + "已经放了数据," +
"队列目前有" + queue.size() + "个数据");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
new Thread() {
public void run() {
while (true) {
try {
// 将此处的睡眠时间分别改为100和1000,观察运行结果
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "准备取数据!");
System.err.println(queue.take());
System.out.println(Thread.currentThread().getName() + "已经取走数据," +
"队列目前有" + queue.size() + "个数据");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
输出结果:
Thread-0准备放数据!
Thread-0已经放了数据,队列目前有1个数据
Thread-0准备放数据!
Thread-0已经放了数据,队列目前有2个数据
Thread-1准备放数据!
Thread-1已经放了数据,队列目前有3个数据
Thread-2准备取数据!
Thread-2已经取走数据,队列目前有3个数据
Thread-0准备放数据!
Thread-1准备放数据!
Thread-2准备取数据!
Thread-2已经取走数据,队列目前有3个数据
LinkedBlockingQueue阻塞队列
LinkedBlockingQueue类图
LinkedBlockingQueue中也有两个Node分别用来存放首尾节点,并且里面有个初始值为0的原子变量count用来记录队列元素个数,另外里面有两个ReentrantLock的独占锁,分别用来控制元素入队和出队加锁,其中takeLock用来控制同时只有一个线程可以从队列获取元素,其他线程必须等待,putLock控制同时只能有一个线程可以获取锁去添加元素,其他线程必须等待。另外notEmpty和notFull用来实现入队和出队的同步。 另外由于出入队是两个非公平独占锁,所以可以同时又一个线程入队和一个线程出队,其实这个是个生产者-消费者模型。
/**通过take取出进行加锁、取出 */
private final ReentrantLock takeLock = new ReentrantLock();
/**等待中的队列等待取出 */
private final Condition notEmpty = takeLock.newCondition();
/*通过put放置进行加锁、放置*/
private final ReentrantLock putLock = new ReentrantLock();
/**等待中的队列等待放置 */
private final Condition notFull = putLock.newCondition();
/*记录集合中的个数(计数器) */
private final AtomicInteger count = new AtomicInteger(0);
队列的容量:
//队列初始容量,Integer最大值
public static final int MAX_VALUE = 0x7fffffff;
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
//初始化首尾节点
last = head = new Node(null);
}
如图默认队列容量为0x7fffffff;用户也可以自己指定容量。
LinkedBlockingQueue方法
ps:下面介绍LinkedBlockingQueue用到很多Lock对象。详细可以查找Lock对象的介绍
带时间的Offer操作-生产者
在ArrayBlockingQueue中已经简单介绍了Offer()方法,LinkedBlocking的Offer方法类似,在此就不过多去介绍。这次我们从介绍下带时间的Offer方法
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
//空元素抛空指针异常
if (e == null) throw new NullPointerException();
long nanos = unit.toNanos(timeout);
int c = -1;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
//获取可被中断锁,只有一个线程克获取
putLock.lockInterruptibly();
try {
//如果队列满则进入循环
while (count.get() == capacity) {
//nanos<=0直接返回
if (nanos <= 0)
return false;
//否者调用await进行等待,超时则返回<=0(1)
nanos = notFull.awaitNanos(nanos);
}
//await在超时时间内返回则添加元素(2)
enqueue(new Node(e));
c = count.getAndIncrement();
//队列不满则激活其他等待入队线程(3)
if (c + 1 < capacity)
notFull.signal();
} finally {
//释放锁
putLock.unlock();
}
//c==0说明队列里面有一个元素,这时候唤醒出队线程(4)
if (c == 0)
signalNotEmpty();
return true;
}
private void enqueue(Node node) {
last = last.next = node;
}
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
带时间的poll操作-消费者
获取并移除队首元素,在指定的时间内去轮询队列看有没有首元素有则返回,否者超时后返回null。
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
E x = null;
int c = -1;
long nanos = unit.toNanos(timeout);
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
//出队线程获取独占锁
takeLock.lockInterruptibly();
try {
//循环直到队列不为空
while (count.get() == 0) {
//超时直接返回null
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
//出队,计数器减一
x = dequeue();
c = count.getAndDecrement();
//如果出队前队列不为空则发送信号,激活其他阻塞的出队线程
if (c > 1)
notEmpty.signal();
} finally {
//释放锁
takeLock.unlock();
}
//当前队列容量为最大值-1则激活入队线程。
if (c == capacity)
signalNotFull();
return x;
}
首先获取独占锁,然后进入循环当当前队列有元素才会退出循环,或者超时了,直接返回null。
超时前退出循环后,就从队列移除元素,然后计数器减去一,如果减去1前队列元素大于1则说明当前移除后队列还有元素,那么就发信号激活其他可能阻塞到当前条件信号的线程。
最后如果减去1前队列元素个数=最大值,那么移除一个后会腾出一个空间来,这时候可以激活可能存在的入队阻塞线程。
put操作-生产者
与带超时时间的poll类似不同在于put时候如果当前队列满了它会一直等待其他线程调用notFull.signal才会被唤醒。
take操作-消费者
与带超时时间的poll类似不同在于take时候如果当前队列空了它会一直等待其他线程调用notEmpty.signal()才会被唤醒。
size操作-消费者
当前队列元素个数,如代码直接使用原子变量count获取。
public int size() {
return count.get();
}
peek操作
获取但是不移除当前队列的头元素,没有则返回null。
public E peek() {
//队列空,则返回null
if (count.get() == 0)
return null;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
Node first = head.next;
if (first == null)
return null;
else
return first.item;
} finally {
takeLock.unlock();
}
}
remove操作
删除队列里面的一个元素,有则删除返回true,没有则返回false,在删除操作时候由于要遍历队列所以加了双重锁,也就是在删除过程中不允许入队也不允许出队操作。
public boolean remove(Object o) {
if (o == null) return false;
//双重加锁
fullyLock();
try {
//遍历队列找则删除返回true
for (Node trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
if (o.equals(p.item)) {
unlink(p, trail);
return true;
}
}
//找不到返回false
return false;
} finally {
//解锁
fullyUnlock();
}
}
void fullyLock() {
putLock.lock();
takeLock.lock();
}
void fullyUnlock() {
takeLock.unlock();
putLock.unlock();
}
void unlink(Node p, Node trail) {
p.item = null;
trail.next = p.next;
if (last == p)
last = trail;
//如果当前队列满,删除后,也不忘记最快的唤醒等待的线程
if (count.getAndDecrement() == capacity)
notFull.signal();
}
开源框架的使用
tomcat中任务队列TaskQueue。
可知TaskQueue继承了LinkedBlockingQueue并且泛化类型固定了为Runnalbe.重写了offer,poll,take方法。
tomcat中有个线程池ThreadPoolExecutor,在NIOEndPoint中当acceptor线程接受到请求后,会把任务放入队列,然后poller 线程从队列里面获取任务,然后就把任务放入线程池执行。这个ThreadPoolExecutor中的的一个参数就是TaskQueue。
先看看ThreadPoolExecutor的参数如果是普通LinkedBlockingQueue是怎么样的执行逻辑: 当调用线程池方法execute() 方法添加一个任务时:
如果当前运行的线程数量小于corePoolSize,则创建新线程运行该任务
如果当前运行的线程数量大于或等于corePoolSize,则将这个任务放入阻塞队列。
如果当前队列满了,并且当前运行的线程数量小于maximumPoolSize,则创建新线程运行该任务;
如果当前队列满了,并且当前运行的线程数量大于或等于maximumPoolSize,那么线程池将会抛出RejectedExecutionException异常。
如果线程执行完了当前任务,那么会去队列里面获取一个任务来执行,如果任务执行完了,并且当前线程数大于corePoolSize,那么会根据线程空闲时间keepAliveTime回收一些线程保持线程池corePoolSize个线程。
首先看下线程池中exectue添加任务时候的逻辑:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//当前工作线程个数小于core个数则开新线程执行(1)
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//放入队列(2)
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//如果队列满了则开新线程,但是个数要不超过最大值,超过则返回false
//然后执行reject handler(3)
else if (!addWorker(command, false))
reject(command);
}
可知当当前工作线程个数为corePoolSize后,如果在来任务会把任务添加到队列,队列满了或者入队失败了则开启新线程。
然后看看TaskQueue中重写的offer方法的逻辑:
public boolean offer(Runnable o) {
//如果parent为null则直接调用父类方法
if (parent==null) return super.offer(o);
//如果当前线程池中线程个数达到最大,则无条件调用父类方法
if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
//如果当前提交的任务小于当前线程池线程数,说明线程用不完,没必要重新开线程
if (parent.getSubmittedCount()<(parent.getPoolSize())) return super.offer(o);
//如果当前线程池线程个数>core个数但是小于最大个数,则开新线程代替放入队列
if (parent.getPoolSize()
//到了这里,无条件调用父类
return super.offer(o);
}
可知parent.getPoolSize()
LinkedBlockingQueue安全分析总结
仔细思考下阻塞队列是如何实现并发安全的维护队列链表的,先分析下简单的情况就是当队列里面有多个元素时候,由于同时只有一个线程(通过独占锁putLock实现)入队元素并且是操作last节点(,而同时只有一个出队线程(通过独占锁takeLock实现)操作head节点,所以不存在并发安全问题。
考虑当队列为空的时候队列状态为:
这时候假如一个线程调用了take方法,由于队列为空,所以count.get()==0所以当前线程会调用notEmpty.await()把自己挂起,并且放入notEmpty的条件队列,并且释放当前条件变量关联的通过takeLock.lockInterruptibly()获取的独占锁。由于释放了锁,所以这时候其他线程调用take时候就会通过takeLock.lockInterruptibly()获取独占锁,然后同样阻塞到notEmpty.await(),同样会被放入notEmpty的条件队列,也就说在队列为空的情况下可能会有多个线程因为调用take被放入了notEmpty的条件队列。
这时候如果有一个线程调用了put方法,那么就会调用enqueue操作,该操作会在last节点后面添加新元素并且设置last为新节点。然后count.getAndIncrement()先获取当前队列元个数为0保存到c,然后自增count为1,由于c==0所以调用signalNotEmpty激活notEmpty的条件队列里面的阻塞时间最长的线程,这时候take中调用notEmpty.await()的线程会被激活await内部会重新去获取独占锁获取成功则返回,否者被放入AQS的阻塞队列,如果获取成功,那么count.get() >0因为可能多个线程put了,所以调用dequeue从队列获取元素(这时候一定可以获取到),然后调用c = count.getAndDecrement() 把当前计数返回后并减去1,如果c>1 说明当前队列还有其他元素,那么就调用 notEmpty.signal()去激活 notEmpty的条件队列里面的其他阻塞线程。
考虑当队列满的时候:
当队列满的时候调用put方法时候,会由于notFull.await()当前线程被阻塞放入notFull管理的条件队列里面,同理可能会有多个调用put方法的线程都放到了notFull的条件队列里面。
这时候如果有一个线程调用了take方法,调用dequeue()出队一个元素,c = count.getAndDecrement();count值减一;c==capacity;现在队列有一个空的位置,所以调用signalNotFull()激活notFull条件队列里面等待最久的一个线程。
LinkedBlockingQueue简单示例
并发库中的BlockingQueue是一个比较好玩的类,顾名思义,就是阻塞队列。该类主要提供了两个方法put()和take(),前者将一个对象放到队列中,如果队列已经满了,就等待直到有空闲节点;后者从head取一个对象,如果没有对象,就等待直到有可取的对象。
下面的例子比较简单,一个读线程,用于将要处理的文件对象添加到阻塞队列中,另外四个写线程用于取出文件对象,为了模拟写操作耗时长的特点,特让线程睡眠一段随机长度的时间。另外,该Demo也使用到了线程池和原子整型 (AtomicInteger),AtomicInteger可以在并发情况下达到原子化更新,避免使用了synchronized,而且性能非常高。由 于阻塞队列的put和take操作会阻塞,为了使线程退出,特在队列中添加了一个“标识”,算法中也叫“哨兵”,当发现这个哨兵后,写线程就退出。
当然线程池也要显式退出了。
import java.io.File;
import java.io.FileFilter;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
public class TestBlockingQueue {
static long randomTime() {
return (long) (Math.random() * 1000);
}
public static void main(String[] args) {
// 能容纳100个文件
final BlockingQueue queue = new LinkedBlockingQueue(100);
// 线程池
final ExecutorService exec = Executors.newFixedThreadPool(5);
final File root = new File("F:\\JavaLib");
// 完成标志
final File exitFile = new File("");
// 读个数
final AtomicInteger rc = new AtomicInteger();
// 写个数
final AtomicInteger wc = new AtomicInteger();
// 读线程
Runnable read = new Runnable() {
public void run() {
scanFile(root);
scanFile(exitFile);
}
public void scanFile(File file) {
if (file.isDirectory()) {
File[] files = file.listFiles(new FileFilter() {
public boolean accept(File pathname) {
return pathname.isDirectory() || pathname.getPath().endsWith(".java");
}
});
for (File one : files)
scanFile(one);
} else {
try {
int index = rc.incrementAndGet();
System.out.println("Read0: " + index + " " + file.getPath());
queue.put(file);
} catch (InterruptedException e) {
}
}
}
};
exec.submit(read);
// 四个写线程
for (int index = 0; index < 4; index++) {
// write thread
final int NO = index;
Runnable write = new Runnable() {
String threadName = "Write" + NO;
public void run() {
while (true) {
try {
Thread.sleep(randomTime());
int index = wc.incrementAndGet();
File file = (File) queue.take();
// 队列已经无对象
if (file == exitFile) {
// 再次添加"标志",以让其他线程正常退出
queue.put(exitFile);
break;
}
System.out.println(threadName + ": " + index + " " + file.getPath());
} catch (InterruptedException e) {
}
}
}
};
exec.submit(write);
}
exec.shutdown();
}
}
PriorityBlockingQueue无界阻塞优先级队列
PriorityBlockingQueue是带优先级的无界阻塞队列,每次出队都返回优先级最高的元素,是二叉树最小堆的实现,研究过数组方式存放最小堆节点的都知道,直接遍历队列元素是无序的。
PriorityBlockingQueue类图结构
如图PriorityBlockingQueue内部有个数组queue用来存放队列元素,size用来存放队列元素个数,allocationSpinLockOffset是用来在扩容队列时候做cas的,目的是保证只有一个线程可以进行扩容。
由于这是一个优先级队列所以有个比较器comparator用来比较元素大小。lock独占锁对象用来控制同时只能有一个线程可以进行入队出队操作。notEmpty条件变量用来实现take方法阻塞模式。这里没有notFull 条件变量是因为这里的put操作是非阻塞的,为啥要设计为非阻塞的是因为这是无界队列。最后PriorityQueue q用来搞序列化的。
如下构造函数,默认队列容量为11,默认比较器为null;
private static final int DEFAULT_INITIAL_CAPACITY = 11;
public PriorityBlockingQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
public PriorityBlockingQueue(int initialCapacity) {
this(initialCapacity, null);
}
public PriorityBlockingQueue(int initialCapacity,
Comparator comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.lock = new ReentrantLock();
this.notEmpty = lock.newCondition();
this.comparator = comparator;
this.queue = new Object[initialCapacity];
}
PriorityBlockingQueue方法
Offer操作
在队列插入一个元素,由于是无界队列,所以一直为成功返回true;
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
int n, cap;
Object[] array;
//如果当前元素个数>=队列容量,则扩容(1)
while ((n = size) >= (cap = (array = queue).length))
tryGrow(array, cap);
try {
Comparator cmp = comparator;
//默认比较器为null
if (cmp == null)(2)
siftUpComparable(n, e, array);
else
//自定义比较器(3)
siftUpUsingComparator(n, e, array, cmp);
//队列元素增加1,并且激活notEmpty的条件队列里面的一个阻塞线程
size = n + 1;(9)
notEmpty.signal();
} finally {
lock.unlock();
}
return true;
}
主流程比较简单,下面看看两个主要函数
private void tryGrow(Object[] array, int oldCap) {
lock.unlock(); //must release and then re-acquire main lock
Object[] newArray = null;
//cas成功则扩容(4)
if (allocationSpinLock == 0 &&
UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
0, 1)) {
try {
//oldGap<64则扩容新增oldcap+2,否者扩容50%,并且最大为MAX_ARRAY_SIZE
int newCap = oldCap + ((oldCap < 64) ?
(oldCap + 2) : // grow faster if small
(oldCap >> 1));
if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow
int minCap = oldCap + 1;
if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
throw new OutOfMemoryError();
newCap = MAX_ARRAY_SIZE;
}
if (newCap > oldCap && queue == array)
newArray = new Object[newCap];
} finally {
allocationSpinLock = 0;
}
}
//第一个线程cas成功后,第二个线程会进入这个地方,然后第二个线程让出cpu,尽量让第一个线程执行下面点获取锁,但是这得不到肯定的保证。(5)
if (newArray == null) // back off if another thread is allocating
Thread.yield();
lock.lock();(6)
if (newArray != null && queue == array) {
queue = newArray;
System.arraycopy(array, 0, newArray, 0, oldCap);
}
}
tryGrow目的是扩容,这里要思考下为啥在扩容前要先释放锁,然后使用cas控制只有一个线程可以扩容成功。我的理解是为了性能,因为扩容时候是需要花时间的,如果这些操作时候还占用锁那么其他线程在这个时候是不能进行出队操作的,也不能进行入队操作,这大大降低了并发性。
所以在扩容前释放锁,这允许其他出队线程可以进行出队操作,但是由于释放了锁,所以也允许在扩容时候进行入队操作,这就会导致多个线程进行扩容会出现问题,所以这里使用了一个spinlock用cas控制只有一个线程可以进行扩容,失败的线程调用Thread.yield()让出cpu,目的意在让扩容线程扩容后优先调用lock.lock重新获取锁,但是这得不到一定的保证,有可能调用Thread.yield()的线程先获取了锁。
那copy元素数据到新数组为啥放到获取锁后面那?原因应该是因为可见性问题,因为queue并没有被volatile修饰。另外有可能在扩容时候进行了出队操作,如果直接拷贝可能看到的数组元素不是最新的。而通过调用Lock后,获取的数组则是最新的,并且在释放锁前 数组内容不会变化。
具体建堆算法:
private static void siftUpComparable(int k, T x, Object[] array) {
Comparable key = (Comparable) x;
//队列元素个数>0则判断插入位置,否者直接入队(7)
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = array[parent];
if (key.compareTo((T) e) >= 0)
break;
array[k] = e;
k = parent;
}
array[k] = key;(8)
}
Poll操作
在队列头部获取并移除一个元素,如果队列为空,则返回null
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return dequeue();
} finally {
lock.unlock();
}
}
主要看dequeue
private E dequeue() {
//队列为空,则返回null
int n = size - 1;
if (n < 0)
return null;
else {
//获取队头元素(1)
Object[] array = queue;
E result = (E) array[0];
//获取对尾元素,并值null(2)
E x = (E) array[n];
array[n] = null;
Comparator cmp = comparator;
if (cmp == null)//cmp=null则调用这个,把对尾元素位置插入到0位置,并且调整堆为最小堆(3)
siftDownComparable(0, x, array, n);
else
siftDownUsingComparator(0, x, array, n, cmp);
size = n;(4)
return result;
}
}
private static void siftDownComparable(int k, T x, Object[] array,
int n) {
if (n > 0) {
Comparable key = (Comparable)x;
int half = n >>> 1; // loop while a non-leaf
while (k < half) {
int child = (k << 1) + 1; // assume left child is least
Object c = array[child];(5)
int right = child + 1;(6)
if (right < n &&
((Comparable) c).compareTo((T) array[right]) > 0)(7)
c = array[child = right];
if (key.compareTo((T) c) <= 0)(8)
break;
array[k] = c;
k = child;
}
array[k] = key;(9)
}
}
下面重点说说siftDownComparable这个屌屌的建立最小堆的算法:
首先说下思想,其中k一开始为0,x为数组里面最后一个元素,由于第0个元素为树根,被出队时候要被搞掉,所以建堆要从它的左右孩子节点找一个最小的值来当树根,子树根被搞掉后,会找子树的左右孩子最小的元素来代替,直到树节点为止。
Put操作
内部调用的offer,由于是无界队列,所以不需要阻塞
public void put(E e) {
offer(e); // never need to block
}
Take操作
获取队列头元素,如果队列为空则阻塞。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
E result;
try {
//如果队列为空,则阻塞,把当前线程放入notEmpty的条件队列
while ( (result = dequeue()) == null)
notEmpty.await();
} finally {
lock.unlock();
}
return result;
}
这里是阻塞实现,阻塞后直到入队操作调用notEmpty.signal 才会返回。
Size操作
获取队列元个数,由于加了独占锁所以返回结果是精确的
public int size() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return size;
} finally {
lock.unlock();
}
}
PriorityBlockingQueue小结
PriorityBlockingQueue类似于ArrayBlockingQueue内部使用一个独占锁来控制同时只有一个线程可以进行入队和出队,另外前者只使用了一个notEmpty条件变量而没有notFull这是因为前者是无界队列,当put时候永远不会处于await所以也不需要被唤醒。
PriorityBlockingQueue始终保证出队的元素是优先级最高的元素,并且可以定制优先级的规则,内部通过使用一个二叉树最小堆算法来维护内部数组,这个数组是可扩容的,当当前元素个数>=最大容量时候会通过算法扩容。
值得注意的是为了避免在扩容操作时候其他线程不能进行出队操作,实现上使用了先释放锁,然后通过cas保证同时只有一个线程可以扩容成功。
PriorityBlockingQueue示例
PriorityBlockingQueue类是JDK提供的优先级队列 本身是线程安全的 内部使用显示锁 保证线程安全。
PriorityBlockingQueue存储的对象必须是实现Comparable接口的 因为PriorityBlockingQueue队列会根据内部存储的每一个元素的compareTo方法比较每个元素的大小。这样在take出来的时候会根据优先级 将优先级最小的最先取出。
下面是示例代码
public static PriorityBlockingQueue queue = new PriorityBlockingQueue();
public static void main(String[] args) {
queue.add(new User(1,"wu"));
queue.add(new User(5,"wu5"));
queue.add(new User(23,"wu23"));
queue.add(new User(55,"wu55"));
queue.add(new User(9,"wu9"));
queue.add(new User(3,"wu3"));
for (User user : queue) {
try {
System.out.println(queue.take().name);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//静态内部类
static class User implements Comparable{
public User(int age,String name) {
this.age = age;
this.name = name;
}
int age;
String name;
@Override
public int compareTo(User o) {
return this.age > o.age ? -1 : 1;
}
}
SychronousQueue同步队列
SynchronousQueue是一个比较特别的队列,由于在线程池方面有所应用,为了更好的理解线程池的实现原理,此队列源码中充斥着大量的CAS语句,理解起来是有些难度的,为了方便日后回顾,本篇文章会以简洁的图形化方式展示该队列底层的实现原理。
SychronousQueue简单实用
经典的生产者-消费者模式,操作流程是这样的:
有多个生产者,可以并发生产产品,把产品置入队列中,如果队列满了,生产者就会阻塞;
有多个消费者,并发从队列中获取产品,如果队列空了,消费者就会阻塞;
SynchronousQueue 也是一个队列来的,但它的特别之处在于它内部没有容器,一个生产线程,当它生产产品(即put的时候),如果当前没有人想要消费产品(即当前没有线程执行take),此生产线程必须阻塞,等待一个消费线程调用take操作,take操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递),这样的一个过程称为一次配对过程(当然也可以先take后put,原理是一样的)。
我们用一个简单的代码来验证一下,如下所示:
import java.util.concurrent.SynchronousQueue;
public class SynchronousQueueDemo {
public static void main(String[] args) throws InterruptedException {
final SynchronousQueue queue = new SynchronousQueue();
Thread putThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("put thread start");
try {
queue.put(1);
} catch (InterruptedException e) {
}
System.out.println("put thread end");
}
});
Thread takeThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("take thread start");
try {
System.out.println("take from putThread: " + queue.take());
} catch (InterruptedException e) {
}
System.out.println("take thread end");
}
});
putThread.start();
Thread.sleep(1000);
takeThread.start();
}
}
一种输出结果如下:
put thread start
take thread start
take from putThread: 1
put thread end
take thread end
从结果可以看出,put线程执行queue.put(1) 后就被阻塞了,只有take线程进行了消费,put线程才可以返回。可以认为这是一种线程与线程间一对一传递消息的模型。
SychronousQueue实现原理
不像ArrayBlockingQueue、LinkedBlockingDeque之类的阻塞队列依赖AQS实现并发操作,SynchronousQueue直接使用CAS实现线程的安全访问。
队列的实现策略通常分为公平模式和非公平模式,接下来将分别进行说明。
公平模式下的模型:
公平模式下,底层实现使用的是TransferQueue这个内部队列,它有一个head和tail指针,用于指向当前正在等待匹配的线程节点。
接着我们进行一些操作:
1、线程put1执行 put(1)操作,由于当前没有配对的消费线程,所以put1线程入队列,自旋一小会后睡眠等待。
2、接着,线程put2执行了put(2)操作,跟前面一样,put2线程入队列,自旋一小会后睡眠等待。
3、这时候,来了一个线程take1,执行了 take操作,由于tail指向put2线程,put2线程跟take1线程配对了(一put一take),这时take1线程不需要入队,但是请注意了,这时候,要唤醒的线程并不是put2,而是put1。
为何?大家应该知道我们现在讲的是公平策略,所谓公平就是谁先入队了,谁就优先被唤醒,我们的例子明显是put1应该优先被唤醒。至于读者可能会有一个疑问,明明是take1线程跟put2线程匹配上了,结果是put1线程被唤醒消费,怎么确保take1线程一定可以和次首节点(head.next)也是匹配的呢?其实大家可以拿个纸画一画,就会发现真的就是这样的。
公平策略总结下来就是:队尾匹配队头出队。
执行后put1线程被唤醒,take1线程的 take()方法返回了1(put1线程的数据),这样就实现了线程间的一对一通信。
4、最后,再来一个线程take2,执行take操作,这时候只有put2线程在等候,而且两个线程匹配上了,线程put2被唤醒, take2线程take操作返回了2(线程put2的数据),这时候队列又回到了起点。
以上便是公平模式下,SynchronousQueue的实现模型。总结下来就是:队尾匹配队头出队,先进先出,体现公平原则。
非公平模式下的模型:
我们还是使用跟公平模式下一样的操作流程,对比两种策略下有何不同。非公平模式底层的实现使用的是TransferStack,一个栈,实现中用head指针指向栈顶,接着我们看看它的实现模型:
1、线程put1执行 put(1)操作,由于当前没有配对的消费线程,所以put1线程入栈,自旋一小会后睡眠等待。
2、接着,线程put2再次执行了put(2)操作,跟前面一样,put2线程入栈,自旋一小会后睡眠等待。
3、这时候,来了一个线程take1,执行了take操作,这时候发现栈顶为put2线程,匹配成功,但是实现会先把take1线程入栈,然后take1线程循环执行匹配put2线程逻辑,一旦发现没有并发冲突,就会把栈顶指针直接指向 put1线程。
4、最后,再来一个线程take2,执行take操作,这跟步骤3的逻辑基本是一致的,take2线程入栈,然后在循环中匹配put1线程,最终全部匹配完毕,栈变为空,恢复初始状态。
可以从上面流程看出,虽然put1线程先入栈了,但是却是后匹配,这就是非公平的由来。
SychronousQueue总结
SynchronousQueue由于其独有的线程一一配对通信机制,在大部分平常开发中,可能都不太会用到,但线程池技术中会有所使用,由于内部没有使用AQS,而是直接使用CAS,所以代码理解起来会比较困难,但这并不妨碍我们理解底层的实现模型,在理解了模型的基础上,有兴趣的话再查阅源码,就会有方向感,看起来也会比较容易,希望本文有所借鉴意义。
DeplayQueue延时无界阻塞队列
在谈到DelayQueue的使用和原理的时候,我们首先介绍一下DelayQueue,DelayQueue是一个无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的Delayed 元素。
DelayQueue阻塞队列在我们系统开发中也常常会用到,例如:缓存系统的设计,缓存中的对象,超过了空闲时间,需要从缓存中移出;任务调度系统,能够准确的把握任务的执行时间。我们可能需要通过线程处理很多时间上要求很严格的数据,如果使用普通的线程,我们就需要遍历所有的对象,一个一个的检 查看数据是否过期等,首先这样在执行上的效率不会太高,其次就是这种设计的风格也大大的影响了数据的精度。一个需要12:00点执行的任务可能12:01 才执行,这样对数据要求很高的系统有更大的弊端。由此我们可以使用DelayQueue。
下面将会对DelayQueue做一个介绍,然后举个例子。并且提供一个Delayed接口的实现和Sample代码。DelayQueue是一个BlockingQueue,其特化的参数是Delayed。(不了解BlockingQueue的同学,先去了解BlockingQueue再看本文)
Delayed扩展了Comparable接口,比较的基准为延时的时间值,Delayed接口的实现类getDelay的返回值应为固定值(final)。DelayQueue内部是使用PriorityQueue实现的。
DelayQueue = BlockingQueue +PriorityQueue + Delayed
DelayQueue定义和原理
DelayQueue的关键元素BlockingQueue、PriorityQueue、Delayed。可以这么说,DelayQueue是一个使用优先队列(PriorityQueue)实现的BlockingQueue,优先队列的比较基准值是时间。
他们的基本定义如下
public interface Comparable {
public int compareTo(T o);
}
public interface Delayed extends Comparable {
long getDelay(TimeUnit unit);
}
public class DelayQueue implements BlockingQueue {
private final PriorityQueue q = new PriorityQueue();
}
DelayQueue内部的实现使用了一个优先队列。当调用DelayQueue的offer方法时,把Delayed对象加入到优先队列q中。如下:
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E first = q.peek();
q.offer(e);
if (first == null || e.compareTo(first) < 0)
available.signalAll();
return true;
} finally {
lock.unlock();
}
}
DelayQueue的take方法,把优先队列q的first拿出来(peek),如果没有达到延时阀值,则进行await处理。如下:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek();
if (first == null) {
available.await();
} else {
long delay = first.getDelay(TimeUnit.NANOSECONDS);
if (delay > 0) {
long tl = available.awaitNanos(delay);
} else {
E x = q.poll();
assert x != null;
if (q.size() != 0)
available.signalAll(); // wake up other takers
return x;
}
}
}
} finally {
lock.unlock();
}
}
DelayQueue实例应用
Ps:为了具有调用行为,存放到DelayDeque的元素必须继承Delayed接口。Delayed接口使对象成为延迟对象,它使存放在DelayQueue类中的对象具有了激活日期。该接口强制执行下列两个方法。
一下将使用Delay做一个缓存的实现。其中共包括三个类
Pair
DelayItem
Cache
Pair类:
public class Pair {
public K first;
public V second;
public Pair() {}
public Pair(K first, V second) {
this.first = first;
this.second = second;
}
}
一下是对Delay接口的实现:
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
public class DelayItem implements Delayed {
/** Base of nanosecond timings, to avoid wrapping */
private static final long NANO_ORIGIN = System.nanoTime();
/**
* Returns nanosecond time offset by origin
*/
final static long now() {
return System.nanoTime() - NANO_ORIGIN;
}
/**
* Sequence number to break scheduling ties, and in turn to guarantee FIFO order among tied
* entries.
*/
private static final AtomicLong sequencer = new AtomicLong(0);
/** Sequence number to break ties FIFO */
private final long sequenceNumber;
/** The time the task is enabled to execute in nanoTime units */
private final long time;
private final T item;
public DelayItem(T submit, long timeout) {
this.time = now() + timeout;
this.item = submit;
this.sequenceNumber = sequencer.getAndIncrement();
}
public T getItem() {
return this.item;
}
public long getDelay(TimeUnit unit) {
long d = unit.convert(time - now(), TimeUnit.NANOSECONDS);
return d;
}
public int compareTo(Delayed other) {
if (other == this) // compare zero ONLY if same object
return 0;
if (other instanceof DelayItem) {
DelayItem x = (DelayItem) other;
long diff = time - x.time;
if (diff < 0)
return -1;
else if (diff > 0)
return 1;
else if (sequenceNumber < x.sequenceNumber)
return -1;
else
return 1;
}
long d = (getDelay(TimeUnit.NANOSECONDS) - other.getDelay(TimeUnit.NANOSECONDS));
return (d == 0) ? 0 : ((d < 0) ? -1 : 1);
}
}
以下是Cache的实现,包括了put和get方法
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
public class Cache {
private static final Logger LOG = Logger.getLogger(Cache.class.getName());
private ConcurrentMap cacheObjMap = new ConcurrentHashMap();
private DelayQueue>> q = new DelayQueue>>();
private Thread daemonThread;
public Cache() {
Runnable daemonTask = new Runnable() {
public void run() {
daemonCheck();
}
};
daemonThread = new Thread(daemonTask);
daemonThread.setDaemon(true);
daemonThread.setName("Cache Daemon");
daemonThread.start();
}
private void daemonCheck() {
if (LOG.isLoggable(Level.INFO))
LOG.info("cache service started.");
for (;;) {
try {
DelayItem> delayItem = q.take();
if (delayItem != null) {
//超时对象处理
Pair pair = delayItem.getItem();
cacheObjMap.remove(pair.first, pair.second); // compare and remove
}
} catch (InterruptedException e) {
if (LOG.isLoggable(Level.SEVERE))
LOG.log(Level.SEVERE, e.getMessage(), e);
break;
}
}
if (LOG.isLoggable(Level.INFO))
LOG.info("cache service stopped.");
}
//添加缓存对象
public void put(K key, V value, long time, TimeUnit unit) {
V oldValue = cacheObjMap.put(key, value);
if (oldValue != null)
q.remove(key);
long nanoTime = TimeUnit.NANOSECONDS.convert(time, unit);
q.put(new DelayItem>(new Pair(key, value), nanoTime));
}
public V get(K key) {
return cacheObjMap.get(key);
}
测试main方法:
//测试入口函数
public static void main(String[] args) throws Exception {
Cache cache = new Cache();
cache.put(1, "aaaa", 3, TimeUnit.SECONDS);
Thread.sleep(1000 * 2);
{
String str = cache.get(1);
System.out.println(str);
}
Thread.sleep(1000 * 2);
{
String str = cache.get(1);
System.out.println(str);
}
}
输出结果为:
aaaa
null
我们看到上面的结果,如果超过延时的时间,那么缓存中数据就会自动丢失,获得就为null。
并发(Collection)队列-非阻塞队列
非阻塞队列
首先我们要简单的理解下什么是非阻塞队列:
与阻塞队列相反,非阻塞队列的执行并不会被阻塞,无论是消费者的出队,还是生产者的入队。
在底层,非阻塞队列使用的是CAS(compare andswap)来实现线程执行的非阻塞。
非阻塞队列简单操作
与阻塞队列相同,非阻塞队列中的常用方法,也是出队和入队。
入队方法:
add():底层调用offer();
offer():Queue接口继承下来的方法,实现队列的入队操作,不会阻碍线程的执行,插入成功返回true;
出队方法:
poll():移动头结点指针,返回头结点元素,并将头结点元素出队;队列为空,则返回null;
peek():移动头结点指针,返回头结点元素,并不会将头结点元素出队;队列为空,则返回null;
非阻塞算法CAS
首先我们需要了解悲观锁和乐观锁
悲观锁:假定并发环境是悲观的,如果发生并发冲突,就会破坏一致性,所以要通过独占锁彻底禁止冲突发生。有一个经典比喻,“如果你不锁门,那么捣蛋鬼就回闯入并搞得一团糟”,所以“你只能一次打开门放进一个人,才能时刻盯紧他”。乐观锁:假定并发环境是乐观的,即,虽然会有并发冲突,但冲突可发现且不会造成损害,所以,可以不加任何保护,等发现并发冲突后再决定放弃操作还是重试。可类比的比喻为,“如果你不锁门,那么虽然捣蛋鬼会闯入,但他们一旦打算破坏你就能知道”,所以“你大可以放进所有人,等发现他们想破坏的时候再做决定”。
通常认为乐观锁的性能比悲观所更高,特别是在某些复杂的场景。这主要由于悲观锁在加锁的同时,也会把某些不会造成破坏的操作保护起来;而乐观锁的竞争则只发生在最小的并发冲突处,如果用悲观锁来理解,就是“锁的粒度最小”。但乐观锁的设计往往比较复杂,因此,复杂场景下还是多用悲观锁。
首先保证正确性,有必要的话,再去追求性能。
CAS
乐观锁的实现往往需要硬件的支持,多数处理器都都实现了一个CAS指令,实现“Compare And Swap”的语义(这里的swap是“换入”,也就是set),构成了基本的乐观锁。
CAS包含3个操作数:
[if !supportLists]n [endif]需要读写的内存位置V
[if !supportLists]n [endif]进行比较的值A
[if !supportLists]n [endif]拟写入的新值B
当且仅当位置V的值等于A时,CAS才会通过原子方式用新值B来更新位置V的值;否则不会执行任何操作。无论位置V的值是否等于A,都将返回V原有的值。
一个有意思的事实是,“使用CAS控制并发”与“使用乐观锁”并不等价。CAS只是一种手段,既可以实现乐观锁,也可以实现悲观锁。乐观、悲观只是一种并发控制的策略。下文将分别用CAS实现悲观锁和乐观锁?
ConcurrentLinkedQueue非阻塞无界链表队列
ConcurrentLinkedQueue是一个线程安全的队列,基于链表结构实现,是一个无界队列,理论上来说队列的长度可以无限扩大。
与其他队列相同,ConcurrentLinkedQueue也采用的是先进先出(FIFO)入队规则,对元素进行排序。当我们向队列中添加元素时,新插入的元素会插入到队列的尾部;而当我们获取一个元素时,它会从队列的头部中取出。
因为ConcurrentLinkedQueue是链表结构,所以当入队时,插入的元素依次向后延伸,形成链表;而出队时,则从链表的第一个元素开始获取,依次递增;
不知道,我这样形容能否让你对链表的入队、出队产生一个大概的思路!
ConcurrentLinkedQuere简单示例
值得注意的是,在使用ConcurrentLinkedQueue时,如果涉及到队列是否为空的判断,切记不可使用size()==0的做法,因为在size()方法中,是通过遍历整个链表来实现的,在队列元素很多的时候,size()方法十分消耗性能和时间,只是单纯的判断队列为空使用isEmpty()即可!!!
public class ConcurrentLinkedQueueTest {
public static int threadCount = 10;
public static ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue();
static class Offer implements Runnable {
public void run() {
//不建议使用queue.size()==0,影响效率。可以使用!queue.isEmpty()
if(queue.size()==0){
String ele = new Random().nextInt(Integer.MAX_VALUE)+"";
queue.offer(ele);
System.out.println("入队元素为"+ele);
}
}
}
static class Poll implements Runnable {
public void run() {
if(!queue.isEmpty()){
String ele = queue.poll();
System.out.println("出队元素为"+ele);
}
}
}
public static void main(String[] agrs) {
ExecutorService executorService = Executors.newFixedThreadPool(4);
for(int x=0;x
executorService.submit(new Offer());
executorService.submit(new Poll());
}
executorService.shutdown();
}
}
一种输出:
入队元素为313732926
出队元素为313732926
入队元素为812655435
出队元素为812655435
入队元素为1893079357
出队元素为1893079357
入队元素为1137820958
出队元素为1137820958
入队元素为1965962048
出队元素为1965962048
出队元素为685567162
入队元素为685567162
出队元素为1441081163
入队元素为1441081163
出队元素为1627184732
入队元素为1627184732
ConcurrentLinkedQuere类图
ConcurrentLinkedQueue中有两个volatile类型的Node节点分别用来存在列表的首尾节点,其中head节点存放链表第一个item为null的节点,tail则并不是总指向最后一个节点。Node节点内部则维护一个变量item用来存放节点的值,next用来存放下一个节点,从而链接为一个单向无界列表。public ConcurrentLinkedQueue() {
head = tail = new Node(null);
}
如上代码初始化时候会构建一个item为NULL的空节点作为链表的首尾节点。ConcurrentLinkedQuere方法
Offer操作
offer操作是在链表末尾添加一个元素,下面看看实现原理。
public boolean offer(E e) {
//e为null则抛出空指针异常
checkNotNull(e);
//构造Node节点构造函数内部调用unsafe.putObject,后面统一讲
final Node newNode = new Node(e);
//从尾节点插入
for (Node t = tail, p = t;;) {
Node q = p.next;
//如果q=null说明p是尾节点则插入
if (q == null) {
//cas插入(1)
if (p.casNext(null, newNode)) {
//cas成功说明新增节点已经被放入链表,然后设置当前尾节点(包含head,1,3,5.。。个节点为尾节点)
if (p != t) // hop two nodes at a time
casTail(t, newNode); // Failure is OK.
return true;
}
// Lost CAS race to another thread; re-read next
}
else if (p == q)//(2)
//多线程操作时候,由于poll时候会把老的head变为自引用,然后head的next变为新head,所以这里需要
//重新找新的head,因为新的head后面的节点才是激活的节点
p = (t != (t = tail)) ? t : head;
else
//寻找尾节点(3)
p = (p != t && t != (t = tail)) ? t : q;
}
}
从构造函数知道一开始有个item为null的哨兵节点,并且head和tail都是指向这个节点,然后当一个线程调用offer时候首先
如图首先查找尾节点,q==null,p就是尾节点,所以执行p.casNext通过cas设置p的next为新增节点,这时候p==t所以不重新设置尾节点为当前新节点。由于多线程可以调用offer方法,所以可能两个线程同时执行到了(1)进行cas,那么只有一个会成功(假如线程1成功了)。
add操作
add操作是在链表末尾添加一个元素,下面看看实现原理。其实内部调用的还是offer
public boolean add(E e) {
return offer(e);
}
poll操作
poll操作是在链表头部获取并且移除一个元素,下面看看实现原理。
public E poll() {
restartFromHead:
//死循环
for (;;) {
//死循环
for (Node h = head, p = h, q;;) {
//保存当前节点值
E item = p.item;
//当前节点有值则cas变为null(1)
if (item != null && p.casItem(item, null)) {
//cas成功标志当前节点以及从链表中移除
if (p != h) //类似tail间隔2设置一次头节点(2)
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
//当前队列为空则返回null(3)
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
//自引用了,则重新找新的队列头节点(4)
else if (p == q)
continue restartFromHead;
else//(5)
p = q;
}
}
}
final void updateHead(Node h, Node p) {
if (h != p && casHead(h, p))
h.lazySetNext(h);
}
peek操作
peek操作是获取链表头部一个元素(只读取不移除),下面看看实现原理。代码与poll类似,只是少了castItem.并且peek操作会改变head指向,offer后head指向哨兵节点,第一次peek后head会指向第一个真的节点元素。
public E peek() {
restartFromHead:
for (;;) {
for (Node h = head, p = h, q;;) {
E item = p.item;
if (item != null || (q = p.next) == null) {
updateHead(h, p);
return item;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
size操作
获取当前队列元素个数,在并发环境下不是很有用,因为使用CAS没有加锁所以从调用size函数到返回结果期间有可能增删元素,导致统计的元素个数不精确。
public int size() {
int count = 0;
for (Node p = first(); p != null; p = succ(p))
if (p.item != null)
//最大返回Integer.MAX_VALUE
if (++count == Integer.MAX_VALUE)
break;
return count;
}
//获取第一个队列元素(哨兵元素不算),没有则为null
Node first() {
restartFromHead:
for (;;) {
for (Node h = head, p = h, q;;) {
boolean hasItem = (p.item != null);
if (hasItem || (q = p.next) == null) {
updateHead(h, p);
return hasItem ? p : null;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
//获取当前节点的next元素,如果是自引入节点则返回真正头节点
final Node succ(Node p) {
Node next = p.next;
return (p == next) ? head : next;
}
remove操作
如果队列里面存在该元素则删除给元素,如果存在多个则删除第一个,并返回true,否者返回false
public boolean remove(Object o) {
//查找元素为空,直接返回false
if (o == null) return false;
Node pred = null;
for (Node p = first(); p != null; p = succ(p)) {
E item = p.item;
//相等则使用cas值null,同时一个线程成功,失败的线程循环查找队列中其他元素是否有匹配的。
if (item != null &&
o.equals(item) &&
p.casItem(item, null)) {
//获取next元素
Node next = succ(p);
//如果有前驱节点,并且next不为空则链接前驱节点到next,
if (pred != null && next != null)
pred.casNext(p, next);
return true;
}
pred = p;
}
return false;
}
contains操作
判断队列里面是否含有指定对象,由于是遍历整个队列,所以类似size 不是那么精确,有可能调用该方法时候元素还在队列里面,但是遍历过程中才把该元素删除了,那么就会返回false.
public boolean contains(Object o) {
if (o == null) return false;
for (Node p = first(); p != null; p = succ(p)) {
E item = p.item;
if (item != null && o.equals(item))
return true;
}
return false;
}
ConcurrentLinkedQuere的offer方法有意思的问题
offer中有个 判断 t != (t = tail)假如 t=node1;tail=node2;并且node1!=node2那么这个判断是true还是false那,答案是true,这个判断是看当前t是不是和tail相等,相等则返回true否者为false,但是无论结果是啥执行后t的值都是tail。
下面从字节码来分析下为啥。
一个例子
public static void main(String[] args) {
int t = 2;
int tail = 3;
System.out.println(t != (t = tail));
}
结果为:true
字节码文件
C:\Users\Simple\Desktop\TeacherCode\Crm_Test\build\classes\com\itheima\crm\util>javap -c Test001
警告:二进制文件Test001包含com.itheima.crm.util.Test001
Compiled from "Test001.java"
public class com.itheima.crm.util.Test001 {
public com.itheima.crm.util.Test001();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_2
1: istore_1
2: iconst_3
3: istore_2
4: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
7: iload_1
8: iload_2
9: dup
10: istore_1
11: if_icmpeq 18
14: iconst_1
15: goto 19
18: iconst_0
19: invokevirtual #22 // Method java/io/PrintStream.println:(Z)V
22: return
}
我们从上面标黄的字节码文件中分析
一开始栈为空:
第0行指令作用是把值2入栈栈顶元素为2
第1行指令作用是将栈顶int类型值保存到局部变量t中
第2行指令作用是把值3入栈栈顶元素为3第3行指令作用是将栈顶int类型值保存到局部变量tail中。
第4调用打印命令第7行指令作用是把变量t中的值入栈
第8行指令作用是把变量tail中的值入栈现在栈里面的元素为3、2,并且3位于栈顶
第9行指令作用是当前栈顶元素入栈,所以现在栈内容3,3,2
第10行指令作用是把栈顶元素存放到t,现在栈内容3,2第11行指令作用是判断栈顶两个元素值,相等则跳转 18。由于现在栈顶严肃为3,2不相等所以返回true.
第14行指令作用是把1入栈
然后回头分析下!=是双目运算符,应该是首先把左边的操作数入栈,然后在去计算了右侧操作数。
ConcurrentLinkedQuere总结
ConcurrentLinkedQueue使用CAS非阻塞算法实现使用CAS解决了当前节点与next节点之间的安全链接和对当前节点值的赋值。由于使用CAS没有使用锁,所以获取size的时候有可能进行offer,poll或者remove操作,导致获取的元素个数不精确,所以在并发情况下size函数不是很有用。另外第一次peek或者first时候会把head指向第一个真正的队列元素。
下面总结下如何实现线程安全的,可知入队出队函数都是操作volatile变量:head,tail。所以要保证队列线程安全只需要保证对这两个Node操作的可见性和原子性,由于volatile本身保证可见性,所以只需要看下多线程下如果保证对着两个变量操作的原子性。
对于offer操作是在tail后面添加元素,也就是调用tail.casNext方法,而这个方法是使用的CAS操作,只有一个线程会成功,然后失败的线程会循环一下,重新获取tail,然后执行casNext方法。对于poll也是这样的。
ConcurrentHashMap非阻塞Hash集合
ConcurrentHashMap是Java并发包中提供的一个线程安全且高效的HashMap实现,ConcurrentHashMap在并发编程的场景中使用频率非常之高,本文就来分析下ConcurrentHashMap的实现原理,并对其实现原理进行分析。
ConcurrentLinkedQuere类图
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
ConcurrentLinkedQuere实现原理
众所周知,哈希表是中非常高效,复杂度为O(1)的数据结构,在Java开发中,我们最常见到最频繁使用的就是HashMap和HashTable,但是在线程竞争激烈的并发场景中使用都不够合理。
HashMap :先说HashMap,HashMap是线程不安全的,在并发环境下,可能会形成环状链表(扩容时可能造成,具体原因自行百度google或查看源码分析),导致get操作时,cpu空转,所以,在并发环境中使用HashMap是非常危险的。
HashTable : HashTable和HashMap的实现原理几乎一样,差别无非是1.HashTable不允许key和value为null;2.HashTable是线程安全的。但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。
HashTable性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的"分段锁"思想
ConcurrentLinkedQuere源码解析
ConcurrentHashMap采用了非常精妙的"分段锁"策略,ConcurrentHashMap的主干是个Segment数组。
final Segment[] segments;
Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于不同Segment的数据进行操作是不用考虑锁竞争的。(就按默认的ConcurrentLeve为16来讲,理论上就允许16个线程并发执行,有木有很酷)
所以,对于同一个Segment的操作才需考虑线程同步,不同的Segment则无需考虑。Segment类似于HashMap,一个Segment维护着一个HashEntry数组
transient volatile HashEntry[] table;
HashEntry是目前我们提到的最小的逻辑处理单元了。一个ConcurrentHashMap维护一个Segment数组,一个Segment维护一个HashEntry数组。
static final class HashEntry {
final int hash;
final K key;
volatile V value;
volatile HashEntry next;
//其他省略
}
我们说Segment类似哈希表,那么一些属性就跟我们之前提到的HashMap差不离,比如负载因子loadFactor,比如阈值threshold等等,看下Segment的构造方法
Segment(float lf, int threshold, HashEntry[] tab) {
this.loadFactor = lf;//负载因子
this.threshold = threshold;//阈值
this.table = tab;//主干数组即HashEntry数组
}
我们来看下ConcurrentHashMap的构造方法
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
//MAX_SEGMENTS为1<<16=65536,也就是最大并发数为65536
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
//2的sshif次方等于ssize,例:ssize=16,sshift=4;ssize=32,sshif=5
int sshift = 0;
//ssize为segments数组长度,根据concurrentLevel计算得出
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
//segmentShift和segmentMask这两个变量在定位segment时会用到,后面会详细讲
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//计算cap的大小,即Segment中HashEntry的数组长度,cap也一定为2的n次方.
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
//创建segments数组并初始化第一个Segment,其余的Segment延迟初始化
Segment s0 =
new Segment(loadFactor, (int)(cap * loadFactor),
(HashEntry[])new HashEntry[cap]);
Segment[] ss = (Segment[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0);
this.segments = ss;
}
初始化方法有三个参数,如果用户不指定则会使用默认值,initialCapacity为16,loadFactor为0.75(负载因子,扩容时需要参考),concurrentLevel为16。
从上面的代码可以看出来,Segment数组的大小ssize是由concurrentLevel来决定的,
但是却不一定等于concurrentLevel,ssize一定是大于或等于concurrentLevel的最小的2的次幂。
比如:默认情况下concurrentLevel是16,则ssize为16;若concurrentLevel为14,ssize为16;若concurrentLevel为17,则ssize为32。
为什么Segment的数组大小一定是2的次幂?其实主要是便于通过按位与的散列算法来定位Segment的index。
其实,put方法对segment也会有所体现
public V put(K key, V value) {
Segment s;
//concurrentHashMap不允许key/value为空
if (value == null)
throw new NullPointerException();
//hash函数对key的hashCode重新散列,避免差劲的不合理的hashcode,保证散列均匀
int hash = hash(key);
//返回的hash值无符号右移segmentShift位与段掩码进行位运算,定位segment
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
从源码看出,put的主要逻辑也就两步:
1.定位segment并确保定位的Segment已初始化
2.调用Segment的put方法。
Ps:关于segmentShift和segmentMask
segmentShift和segmentMask这两个全局变量的主要作用是用来定位Segment,int j =(hash >>> segmentShift) & segmentMask。
segmentMask:段掩码,假如segments数组长度为16,则段掩码为16-1=15;segments长度为32,段掩码为32-1=31。这样得到的所有bit位都为1,可以更好地保证散列的均匀性
segmentShift:2的sshift次方等于ssize,segmentShift=32-sshift。若segments长度为16,segmentShift=32-4=28;若segments长度为32,segmentShift=32-5=27。而计算得出的hash值最大为32位,无符号右移segmentShift,则意味着只保留高几位(其余位是没用的),然后与段掩码segmentMask位运算来定位Segment。
ConcurrentLinkedQuere方法
Get操作
public V get(Object key) {
Segment s;
HashEntry[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;//先定位Segment,再定位HashEntry
if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry e = (HashEntry) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
get方法无需加锁,由于其中涉及到的共享变量都使用volatile修饰,volatile可以保证内存可见性,所以不会读取到过期数据。
来看下concurrentHashMap代理到Segment上的put方法,Segment中的put方法是要加锁的。只不过是锁粒度细了而已。
Put操作
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry node = tryLock() ? null :
scanAndLockForPut(key, hash, value);//tryLock不成功时会遍历定位到的HashEnry位置的链表(遍历主要是为了使CPU缓存链表),若找不到,则创建HashEntry。tryLock一定次数后(MAX_SCAN_RETRIES变量决定),则lock。若遍历过程中,由于其他线程的操作导致链表头结点变化,则需要重新遍历。
V oldValue;
try {
HashEntry[] tab = table;
int index = (tab.length - 1) & hash;//定位HashEntry,可以看到,这个hash值在定位Segment时和在Segment中定位HashEntry都会用到,只不过定位Segment时只用到高几位。
HashEntry first = entryAt(tab, index);
for (HashEntry e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry(hash, key, value, first);
int c = count + 1; //若c超出阈值threshold,需要扩容并rehash。扩容后的容量是当前容量的2倍。这样可以最大程度避免之前散列好的entry重新散列,具体在另一篇文章中有详细分析,不赘述。扩容并rehash的这个过程是比较消耗资源的。
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
ConcurrentLinkedQuere总结
ConcurrentHashMap作为一种线程安全且高效的哈希表的解决方案,尤其其中的"分段锁"的方案,相比HashTable的全表锁在性能上的提升非常之大。本文对ConcurrentHashMap的实现原理进行了详细分析,并解读了部分源码,希望能帮助到有需要的童鞋。
ConcurrentSkipListMap非阻塞Hash跳表集合
大家都是知道TreeMap,它是使用树形结构的方式进行存储数据的线程不安全的Map集合(有序的哈希表),并且可以对Map中的Key进行排序,Key中存储的数据需要实现Comparator接口或使用CompareAble接口的子类来实现排序。
ConcurrentSkipListMap也是和TreeMap,它们都是有序的哈希表。但是,它们是有区别的:
第一,它们的线程安全机制不同,TreeMap是非线程安全的,而ConcurrentSkipListMap是线程安全的。第二,ConcurrentSkipListMap是通过跳表实现的,而TreeMap是通过红黑树实现的。
那现在我们需要知道说明是跳表。
什么是SkipList
Skip list(跳表)是一种可以代替平衡树的数据结构,默认是按照Key值升序的。Skip list让已排序的数据分布在多层链表中,以0-1随机数决定一个数据的向上攀升与否,通过“空间来换取时间”的一个算法,在每个节点中增加了向前的指针,在插入、删除、查找时可以忽略一些不可能涉及到的结点,从而提高了效率。
从概率上保持数据结构的平衡比显示的保持数据结构平衡要简单的多。对于大多数应用,用Skip list要比用树算法相对简单。由于Skip list比较简单,实现起来会比较容易,虽然和平衡树有着相同的时间复杂度(O(logn)),但是skip list的常数项会相对小很多。Skip list在空间上也比较节省。一个节点平均只需要1.333个指针(甚至更少)。
SkipList性质
(1) 由很多层结构组成,level是通过一定的概率随机产生的。
(2) 每一层都是一个有序的链表,默认是升序,也可以根据创建映射时所提供的Comparator进行排序,具体取决于使用的构造方法。
(3) 最底层(Level 1)的链表包含所有元素。
(4) 如果一个元素出现在Level i 的链表中,则它在Level i 之下的链表也都会出现。
(5) 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。
什么是ConcurrentSkipListMap
ConcurrentSkipListMap提供了一种线程安全的并发访问的排序映射表。内部是SkipList(跳表)结构实现,在理论上能够在O(log(n))时间内完成查找、插入、删除操作。 注意,调用ConcurrentSkipListMap的size时,由于多个线程可以同时对映射表进行操作,所以映射表需要遍历整个链表才能返回元素个数,这个操作是个O(log(n))的操作。
ConcurrentSkipListMap数据结构
说明:可以看到ConcurrentSkipListMap的数据结构使用的是跳表,每一个HeadIndex、Index结点都会包含一个对Node的引用,同一垂直方向上的Index、HeadIndex结点都包含了最底层的Node结点的引用。并且层级越高,该层级的结点(HeadIndex和Index)数越少。Node结点之间使用单链表结构。
ConcurrentSkipListMap源码分析
ConcurrentSkipListMap主要用到了Node和Index两种节点的存储方式,通过volatile关键字实现了并发的操作
static final class Node {
final K key;
volatile Object value;//value值
volatile Node next;//next引用
……
}
static class Index {
final Node node;
final Index down;//downy引用
volatile Index right;//右边引用
……
}
ConcurrentSkipListMap查找操作
下面就是源码中的实现(get方法是通过doGet方法来时实现的)
public V get(Object key) {
return doGet(key);
}
//doGet的实现
private V doGet(Object okey) {
Comparable key = comparable(okey);
Node bound = null;
Index q = head;//把头结点作为当前节点的前驱节点
Index r = q.right;//前驱节点的右节点作为当前节点
Node n;
K k;
int c;
for (;;) {//遍历
Index d;
//依次遍历right节点
if (r != null && (n = r.node) != bound && (k = n.key) != null) {
if ((c = key.compareTo(k)) > 0) {//由于key都是升序排列的,所有当前关键字大于所要
查找的key时继续向右遍历
q = r;
r = r.right;
continue;
} else if (c == 0) {
//如果找到了相等的key节点,则返回该Node的value如果value为空可能是其他并发delete
导致的,于是通过另一种
//遍历findNode的方式再查找
Object v = n.value;
return (v != null)? (V)v : getUsingFindNode(key);
} else
bound = n;
}
//如果一个链表中right没能找到key对应的value,则调整到其down的引用处继续查找
if ((d = q.down) != null) {
q = d;
r = d.right;
} else
break;
}
//如果通过上面的遍历方式,还没能找到key对应的value,再通过Node.next的方式进行查找
for (n = q.node.next; n != null; n = n.next) {
if ((k = n.key) != null) {
if ((c = key.compareTo(k)) == 0) {
Object v = n.value;
return (v != null)? (V)v : getUsingFindNode(key);
} else if (c < 0)
break;
}
}
return null;
}
ConcurrentSkipListMap删除操作
下面就是源码中的实现(remove方法是通过doRemove方法来时实现的)
//remove操作,通过doRemove实现,把所有level中出现关键字key的地方都delete掉
public V remove(Object key) {
return doRemove(key, null);
}
final V doRemove(Object okey, Object value) {
Comparable key = comparable(okey);
for (;;) {
Node b = findPredecessor(key);//得到key的前驱(就是比key小的最大节点)
Node n = b.next;//前驱节点的next引用
for (;;) {//遍历
if (n == null)//如果next引用为空,直接返回
return null;
Node f = n.next;
if (n != b.next) //如果两次获得的b.next不是相同的Node,就跳转到第一层循环重新获得b和n
break;
Object v = n.value;
if (v == null) { //当n被其他线程delete的时候,其value==null,此时做辅助处理,并重新获取b和n
n.helpDelete(b, f);
break;
}
if (v == n || b.value == null) //当其前驱被delet的时候直接跳出,重新获取b和n
break;
int c = key.compareTo(n.key);
if (c < 0)
return null;
if (c > 0) {//当key较大时就继续遍历
b = n;
n = f;
continue;
}
if (value != null && !value.equals(v))
return null;
if (!n.casValue(v, null))
break;
if (!n.appendMarker(f) || !b.casNext(n, f))//casNext方法就是通过比较和设置b(前驱)的next节点的方式来实现删除操作
findNode(key); //通过尝试findNode的方式继续find
else {
findPredecessor(key); // Clean index
if (head.right == null) //如果head的right引用为空,则表示不存在该level
tryReduceLevel();
}
return (V)v;
}
}
}
ConcurrentSkipListMap插入操作
下面就是源码中的实现(put方法是通过doPut方法来时实现的)
//put操作,通过doPut实现
public V put(K key, V value) {
if (value == null)
throw new NullPointerException();
return doPut(key, value, false);
}
private V doPut(K kkey, V value, boolean onlyIfAbsent) {
Comparable key = comparable(kkey);
for (;;) {
Node b = findPredecessor(key);//前驱
Node n = b.next;
//定位的过程就是和get操作相似
for (;;) {
if (n != null) {
Node f = n.next;
if (n != b.next) //前后值不一致的情况下,跳转到第一层循环重新获得b和n
break;;
Object v = n.value;
if (v == null) { // n被delete的情况下
n.helpDelete(b, f);
break;
}
if (v == n || b.value == null) // b被delete的情况,重新获取b和n
break;
int c = key.compareTo(n.key);
if (c > 0) {
b = n;
n = f;
continue;
}
if (c == 0) {
if (onlyIfAbsent || n.casValue(v, value))
return (V)v;
else
break; // restart if lost race to replace value
}
// else c < 0; fall through
}
Node z = new Node(kkey, value, n);
if (!b.casNext(n, z))
break; // restart if lost race to append to b
int level = randomLevel();//得到一个随机的level作为该key-value插入的最高level
if (level > 0)
insertIndex(z, level);//进行插入操作
return null;
}
}
}
/**
*获得一个随机的level值
*/
private int randomLevel() {
int x = randomSeed;
x ^= x << 13;
x ^= x >>> 17;
randomSeed = x ^= x << 5;
if ((x & 0x8001) != 0) // test highest and lowest bits
return 0;
int level = 1;
while (((x >>>= 1) & 1) != 0) ++level;
return level;
}
//执行插入操作:如上图所示,有两种可能的情况:
//1.当level存在时,对level<=n都执行insert操作
//2.当level不存在(大于目前的最大level)时,首先添加新的level,然后在执行操作1
private void insertIndex(Node z, int level) {
HeadIndex h = head;
int max = h.level;
if (level <= max) {//情况1
Index idx = null;
for (int i = 1; i <= level; ++i)//首先得到一个包含1~level个级别的down关系的链表,
最后的inx为最高level
idx = new Index(z, idx, null);
addIndex(idx, h, level);//把最高level的idx传给addIndex方法
} else { //情况2 增加一个新的级别
level = max + 1;
Index[] idxs = (Index[])new Index[level+1];
Index idx = null;
for (int i = 1; i <= level; ++i)//该步骤和情况1类似
idxs[i] = idx = new Index(z, idx, null);
HeadIndex oldh;
int k;
for (;;) {
oldh = head;
int oldLevel = oldh.level;
if (level <= oldLevel) { // lost race to add level
k = level;
break;
}
HeadIndex newh = oldh;
Node oldbase = oldh.node;
for (int j = oldLevel+1; j <= level; ++j)
newh = new HeadIndex(oldbase, newh, idxs[j], j);//创建新的
if (casHead(oldh, newh)) {
k = oldLevel;
break;
}
}
addIndex(idxs[k], oldh, k);
}
}
/**
*在1~indexlevel层中插入数据
*/
private void addIndex(Index idx, HeadIndex h, int indexLevel) {
// insertionLevel代表要插入的level,该值会在indexLevel~1间遍历一遍
int insertionLevel = indexLevel;
Comparable key = comparable(idx.node.key);
if (key == null) throw new NullPointerException();
//和get操作类似,不同的就是查找的同时在各个level上加入了对应的key
for (;;) {
int j = h.level;
Index q = h;
Index r = q.right;
Index t = idx;
for (;;) {
if (r != null) {
Node n = r.node;
// compare before deletion check avoids needing recheck
int c = key.compareTo(n.key);
if (n.value == null) {
if (!q.unlink(r))
break;
r = q.right;
continue;
}
if (c > 0) {
q = r;
r = r.right;
continue;
}
}
if (j == insertionLevel) {//在该层level中执行插入操作
// Don't insert index if node already deleted
if (t.indexesDeletedNode()) {
findNode(key); // cleans up
return;
}
if (!q.link(r, t))//执行link操作,其实就是inset的实现部分
break; // restart
if (--insertionLevel == 0) {
// need final deletion check before return
if (t.indexesDeletedNode())
findNode(key);
return;
}
}
if (--j >= insertionLevel && j < indexLevel)//key移动到下一层level
t = t.down;
q = q.down;
r = q.right;
}
}
}
ConcurrentLinkedQuere示例
下面我们看下面示例输出的结果
import java.util.*;import java.util.concurrent.*;
/*
* ConcurrentSkipListMap是“线程安全”的哈希表,而TreeMap是非线程安全的。
*
*下面是“多个线程同时操作并且遍历map”的示例
* (01)当map是ConcurrentSkipListMap对象时,程序能正常运行。
* (02)当map是TreeMap对象时,程序会产生ConcurrentModificationException异常。
*
*/public class ConcurrentSkipListMapDemo1 {
// TODO: map是TreeMap对象时,程序会出错。
//private static Map map = new TreeMap();
private static Map map = new ConcurrentSkipListMap();
public static void main(String[] args) {
//同时启动两个线程对map进行操作!
new MyThread("a").start();
new MyThread("b").start();
}
private static void printAll() {
String key, value;
Iterator iter = map.entrySet().iterator();
while(iter.hasNext()) {
Map.Entry entry = (Map.Entry)iter.next();
key = (String)entry.getKey();
value = (String)entry.getValue();
System.out.print("("+key+", "+value+"), ");
}
System.out.println();
}
private static class MyThread extends Thread {
MyThread(String name) {
super(name);
}
@Override
public void run() {
int i = 0;
while (i++ < 6) {
// “线程名” + "序号"
String val = Thread.currentThread().getName()+i;
map.put(val, "0");
//通过“Iterator”遍历map。
printAll();
}
}
}
}
某一次的运行结果:
(a1, 0), (a1, 0), (b1, 0), (b1, 0),
(a1, 0), (b1, 0), (b2, 0),
(a1, 0), (a1, 0), (a2, 0), (a2, 0), (b1, 0), (b1, 0), (b2, 0), (b2, 0), (b3, 0),
(b3, 0), (a1, 0),
(a2, 0), (a3, 0), (a1, 0), (b1, 0), (a2, 0), (b2, 0), (a3, 0), (b3, 0), (b1, 0), (b4, 0),
(b2, 0), (a1, 0), (b3, 0), (a2, 0), (b4, 0),
(a3, 0), (a1, 0), (a4, 0), (a2, 0), (b1, 0), (a3, 0), (b2, 0), (a4, 0), (b3, 0), (b1, 0), (b4, 0), (b2, 0), (b5, 0),
(b3, 0), (a1, 0), (b4, 0), (a2, 0), (b5, 0),
(a3, 0), (a1, 0), (a4, 0), (a2, 0), (a5, 0), (a3, 0), (b1, 0), (a4, 0), (b2, 0), (a5, 0), (b3, 0), (b1, 0), (b4, 0), (b2, 0), (b5, 0), (b3, 0), (b6, 0),
(b4, 0), (a1, 0), (b5, 0), (a2, 0), (b6, 0),
(a3, 0), (a4, 0), (a5, 0), (a6, 0), (b1, 0), (b2, 0), (b3, 0), (b4, 0), (b5, 0), (b6, 0),
结果说明:
示例程序中,启动两个线程(线程a和线程b)分别对ConcurrentSkipListMap进行操作。以线程a而言,它会先获取“线程名”+“序号”,然后将该字符串作为key,将“0”作为value,插入到ConcurrentSkipListMap中;
接着,遍历并输出ConcurrentSkipListMap中的全部元素。 线程b的操作和线程a一样,只不过线程b的名字和线程a的名字不同。当map是ConcurrentSkipListMap对象时,程序能正常运行。如果将map改为TreeMap时,程序会产生ConcurrentModificationException异常。
java.util.concurrent.atomic包
AtomicBoolean原子性布尔
AtomicBoolean是java.util.concurrent.atomic包下的原子变量,这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程(或者说只是在硬件级别上阻塞了)。
AtomicBoolean,在这个Boolean值的变化的时候不允许在之间插入,保持操作的原子性。
下面将解释重点方法并举例:
boolean compareAndSet(expectedValue, updateValue),这个方法主要两个作用:
1. 比较AtomicBoolean和expect的值,如果一致,执行方法内的语句。其实就是一个if语句
2. 把AtomicBoolean的值设成update,比较最要的是这两件事是一气呵成的,这连个动作之间不会被打断,任何内部或者外部的语句都不可能在两个动作之间运行。为多线程的控制提供了解决的方案
下面我们从代码上解释:
首先我们看下在不使用AtomicBoolean情况下,代码的运行情况:
import java.util.concurrent.TimeUnit;
public class BarWorker implements Runnable {
//静态变量
private static boolean exists = false;
private String name;
public BarWorker(String name) {
this.name = name;
}
@Override
public void run() {
if (!exists) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
// do nothing
}
exists = true;
System.out.println(name + " enter");
try {
System.out.println(name + " working");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
// do nothing
}
System.out.println(name + " leave");
exists = false;
} else {
System.out.println(name + " give up");
}
}
public static void main(String[] args) {
BarWorker bar1 = new BarWorker("bar1");
BarWorker bar2 = new BarWorker("bar2");
new Thread(bar1).start();
new Thread(bar2).start();
}
}
运行结果:
bar1 enter
bar2 enter
bar1 working
bar2 working
bar1 leave
bar2 leave
从上面的运行结果我们可看到,两个线程运行时,都对静态变量exists同时做操作,并没有保证exists静态变量的原子性,也就是一个线程在对静态变量exists进行操作到时候,其他线程必须等待或不作为。等待一个线程操作完后,才能对其进行操作。
下面我们将静态变量使用AtomicBoolean来进行操作
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class BarWorker2 implements Runnable {
//静态变量使用AtomicBoolean 进行操作
private static AtomicBoolean exists = new AtomicBoolean(false);
private String name;
public BarWorker2(String name) {
this.name = name;
}
@Override
public void run() {
if (exists.compareAndSet(false, true)) {
System.out.println(name + " enter");
try {
System.out.println(name + " working");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
// do nothing
}
System.out.println(name + " leave");
exists.set(false);
} else {
System.out.println(name + " give up");
}
}
public static void main(String[] args) {
BarWorker2 bar1 = new BarWorker2("bar1");
BarWorker2 bar2 = new BarWorker2("bar2");
new Thread(bar1).start();
new Thread(bar2).start();
}
}
运行结果:
bar1 enter
bar1 working
bar2 give up
bar1 leave
可以从上面的运行结果看出仅仅一个线程进行工作,因为exists.compareAndSet(false, true)提供了原子性操作,比较和赋值操作组成了一个原子操作, 中间不会提供可乘之机。使得一个线程操作,其他线程等待或不作为。
下面我们简单介绍下AtomicBoolean的API
创建一个AtomicBoolean
你可以这样创建一个AtomicBoolean:
AtomicBoolean atomicBoolean = new AtomicBoolean();
以上示例新建了一个默认值为false 的 AtomicBoolean。如果你想要为 AtomicBoolean 实例设置一个显式的初始值,那么你可以将初始值传给 AtomicBoolean 的构造子:
AtomicBoolean atomicBoolean = new AtomicBoolean(true);
获得 AtomicBoolean的值
你可以通过使用get() 方法来获取一个 AtomicBoolean 的值。示例如下:
AtomicBoolean atomicBoolean = new AtomicBoolean(true);
boolean value = atomicBoolean.get();
设置AtomicBoolean的值
你可以通过使用set() 方法来设置一个 AtomicBoolean 的值。示例如下:
AtomicBoolean atomicBoolean = new AtomicBoolean(true);
atomicBoolean.set(false);
以上代码执行后AtomicBoolean 的值为 false。
交换AtomicBoolean的值
你可以通过getAndSet() 方法来交换一个 AtomicBoolean 实例的值。getAndSet() 方法将返回 AtomicBoolean 当前的值,并将为 AtomicBoolean 设置一个新值。示例如下:
AtomicBoolean atomicBoolean = new AtomicBoolean(true);
boolean oldValue = atomicBoolean.getAndSet(false);
以上代码执行后oldValue 变量的值为 true,atomicBoolean 实例将持有 false 值。代码成功将 AtomicBoolean 当前值 ture 交换为 false。
比较并设置AtomicBoolean的值
compareAndSet() 方法允许你对 AtomicBoolean 的当前值与一个期望值进行比较,如果当前值等于期望值的 话,将会对AtomicBoolean 设定一个新值。compareAndSet() 方法是原子性的,因此在同一时间之内有单个 线程执行它。因此compareAndSet() 方法可被用于一些类似于锁的同步的简单实现。以下是一个 compareAndSet() 示例:
AtomicBoolean atomicBoolean = new AtomicBoolean(true);
boolean expectedValue = true;
boolean newValue = false;
boolean wasNewValueSet = atomicBoolean.compareAndSet(expectedValue, newValue);
本示例对AtomicBoolean 的当前值与 true 值进行比较,如果相等,将 AtomicBoolean 的值更新为 false
AtomicInteger原子性整型
AtomicInteger,一个提供原子操作的Integer的类。在Java语言中,++i和i++操作并不是线程安全的,在使用的时候,不可避免的会用到synchronized关键字。而AtomicInteger则通过一种线程安全的加减操作接口。
我们先来看看AtomicInteger给我们提供了什么方法:
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
下面通过两个简单的例子来看一下AtomicInteger 的优势在哪: 普通线程同步:
class Test2 {
private volatile int count = 0;
public synchronized void increment() {
count++; //若要线程安全执行执行count++,需要加锁
}
public int getCount() {
return count;
}
}
使用AtomicInteger:
class Test2 {
private AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet();
}
//使用AtomicInteger之后,不需要加锁,也可以实现线程安全。
public int getCount() {
return count.get();
}
}
从上面的例子中我们可以看出:使用AtomicInteger是非常的安全的.而且因为AtomicInteger由硬件提供原子操作指令实现的。在非激烈竞争的情况下,开销更小,速度更快。AtomicInteger是使用非阻塞算法来实现并发控制的。AtomicInteger的关键域只有一下3个:
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset=unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex);
}
}
private volatile int value;
这里,unsafe是java提供的获得对对象内存地址访问的类,注释已经清楚的写出了,它的作用就是在更新操作时提供“比较并替换”的作用。实际上就是AtomicInteger中的一个工具。valueOffset是用来记录value本身在内存的便宜地址的,这个记录,也主要是为了在更新操作在内存中找到value的位置,方便比较。注意:value是用来存储整数的时间变量,这里被声明为volatile,就是为了保证在更新操作时,当前线程可以拿到value最新的值(并发环境下,value可能已经被其他线程更新了)。
优点:最大的好处就是可以避免多线程的优先级倒置和死锁情况的发生,提升在高并发处理下的性能。
下面我们简单介绍下AtomicInteger的API
创建一个AtomicInteger
创建一个AtomicInteger 示例如下:
AtomicInteger atomicInteger = new AtomicInteger();
本示例将创建一个初始值为0 的 AtomicInteger。如果你想要创建一个给定初始值的 AtomicInteger,你可以这样:
AtomicInteger atomicInteger = new AtomicInteger(123);
本示例将123 作为参数传给 AtomicInteger 的构造子,它将设置 AtomicInteger 实例的初始值为 123。
获得AtomicInteger的值
你可以使用get() 方法获取 AtomicInteger 实例的值。示例如下:
AtomicInteger atomicInteger = new AtomicInteger(123);
int theValue = atomicInteger.get();
设置AtomicInteger的值
你可以通过set() 方法对 AtomicInteger 的值进行重新设置。以下是 AtomicInteger.set() 示例:
AtomicInteger atomicInteger = new AtomicInteger(123);
atomicInteger.set(234);
以上示例创建了一个初始值为123 的 AtomicInteger,而在第二行将其值更新为 234。
比较并设置AtomicInteger的值
AtomicInteger 类也通过了一个原子性的 compareAndSet() 方法。这一方法将 AtomicInteger 实例的当前值与期望值进行比较,如果二者相等,为 AtomicInteger 实例设置一个新值。AtomicInteger.compareAndSet() 代码示例:
AtomicInteger atomicInteger = new AtomicInteger(123);
int expectedValue = 123;
int newValue = 234;
atomicInteger.compareAndSet(expectedValue, newValue);
本示例首先新建一个初始值为123 的 AtomicInteger 实例。然后将 AtomicInteger 与期望值 123 进行比较,如果相等,将 AtomicInteger 的值更新为 234。
增加AtomicInteger的值
AtomicInteger 类包含有一些方法,通过它们你可以增加 AtomicInteger 的值,并获取其值。这些方法如下:
public final int addAndGet(int addValue)//在原来的数值上增加新的值,并返回新值
public final int getAndIncrement()//获取当前的值,并自增
public final int incrementAndget() //自减,并获得自减后的值
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
第一个addAndGet() 方法给 AtomicInteger 增加了一个值,然后返回增加后的值。getAndAdd() 方法为 AtomicInteger 增加了一个值,但返回的是增加以前的 AtomicInteger 的值。具体使用哪一个取决于你的应用场景。以下是这两种方法的示例:
AtomicInteger atomicInteger = new AtomicInteger();
System.out.println(atomicInteger.getAndAdd(10));
System.out.println(atomicInteger.addAndGet(10));
本示例将打印出0 和 20。例子中,第二行拿到的是加 10 之前的 AtomicInteger 的值。加 10 之前的值是 0。第三行将 AtomicInteger 的值再加 10,并返回加操作之后的值。该值现在是为 20。你当然也可以使用这俩方法为 AtomicInteger 添加负值。结果实际是一个减法操作。getAndIncrement() 和 incrementAndGet() 方法类似于 getAndAdd() 和 addAndGet(),但每次只将 AtomicInteger 的值加 1。
减小AtomicInteger的值
AtomicInteger 类还提供了一些减小 AtomicInteger 的值的原子性方法。这些方法是:
public final int decrementAndGet()
public final int getAndDecrement()
decrementAndGet() 将 AtomicInteger 的值减一,并返回减一后的值。getAndDecrement() 也将 AtomicInteger 的值减一,但它返回的是减一之前的值。
AtomicIntegerArray原子性整型数组
java.util.concurrent.atomic.AtomicIntegerArray类提供了可以以原子方式读取和写入的底层int数组的操作,还包含高级原子操作。 AtomicIntegerArray支持对底层int数组变量的原子操作。 它具有获取和设置方法,如在变量上的读取和写入。 也就是说,一个集合与同一变量上的任何后续get相关联。 原子compareAndSet方法也具有这些内存一致性功能。
AtomicIntegerArray本质上是对int[]类型的封装。使用Unsafe类通过CAS的方式控制int[]在多线程下的安全性。它提供了以下几个核心API:
//获得数组第i个下标的元素
public final int get(int i)
//获得数组的长度
public final int length()
//将数组第i个下标设置为newValue,并返回旧的值
public final int getAndSet(int i, int newValue)
//进行CAS操作,如果第i个下标的元素等于expect,则设置为update,设置成功返回true
public final boolean compareAndSet(int i, int expect, int update)
//将第i个下标的元素加1
public final int getAndIncrement(int i)
//将第i个下标的元素减1
public final int getAndDecrement(int i)
//将第i个下标的元素增加delta(delta可以是负数)
public final int getAndAdd(int i, int delta)
下面给出一个简单的示例,展示AtomicIntegerArray使用:
public class AtomicIntegerArrayDemo {
static AtomicIntegerArray arr = new AtomicIntegerArray(10);
public static class AddThread implements Runnable{
public void run(){
for(int k=0;k<10000;k++)
arr.getAndIncrement(k%arr.length());
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] ts=new Thread[10];
for(int k=0;k<10;k++){
ts[k]=new Thread(new AddThread());
}
for(int k=0;k<10;k++){ts[k].start();}
for(int k=0;k<10;k++){ts[k].join();}
System.out.println(arr);
}
}
输出结果:
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
上述代码第2行,申明了一个内含10个元素的数组。第3行定义的线程对数组内10个元素进行累加操作,每个元素各加1000次。第11行,开启10个这样的线程。因此,可以预测,如果线程安全,数组内10个元素的值必然都是10000。反之,如果线程不安全,则部分或者全部数值会小于10000。
AtomicLong、AtomicLongArray原子性整型数组
AtomicLong、AtomicLongArray的API跟AtomicInteger、AtomicIntegerArray在使用方法都是差不多的。区别在于用前者是使用原子方式更新的long值和long数组,后者是使用原子方式更新的Integer值和Integer数组。两者的相同处在于它们此类确实扩展了Number,允许那些处理基于数字类的工具和实用工具进行统一访问。在实际开发中,它们分别用于不同的场景。这个就具体情况具体分析了,下面将举例说明AtomicLong的使用场景(使用AtomicLong生成自增长ID),其他就不在过多介绍。
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
public class AtomicLongTest {
/**
* @param args
*/
public static void main(String[] args) {
final AtomicLong orderIdGenerator = new AtomicLong(0);
final List orders = Collections
.synchronizedList(new ArrayList());
for (int i = 0; i < 10; i++) {
Thread orderCreationThread = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
long orderId = orderIdGenerator.incrementAndGet();
Item order = new Item(Thread.currentThread().getName(),
orderId);
orders.add(order);
}
}
});
orderCreationThread.setName("Order Creation Thread " + i);
orderCreationThread.start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Set orderIds = new HashSet();
for (Item order : orders) {
orderIds.add(order.getID());
System.out.println("Order name:" + order.getItemName()
+"----"+"Order id:" + order.getID());
}
}
}
class Item {
String itemName;
long id;
Item(String n, long id) {
this.itemName = n;
this.id = id;
}
public String getItemName() {
return itemName;
}
public long getID() {
return id;
}
}
输出:
Order name:Order Creation Thread 0----Order id:1
Order name:Order Creation Thread 1----Order id:2
Order name:Order Creation Thread 0----Order id:4
Order name:Order Creation Thread 1----Order id:5
Order name:Order Creation Thread 3----Order id:3
Order name:Order Creation Thread 0----Order id:7
Order name:Order Creation Thread 1----Order id:6
........
Order name:Order Creation Thread 2----Order id:100
从运行结果我们看到,不管是哪个线程。它们获得的ID是不会重复的,保证的ID生成的原子性,避免了线程安全上的问题。
java.util.concurrent.lock包
待续...
(三)多线程面试题
1.多线程的创建方式
(1)、继承Thread类:但Thread本质上也是实现了Runnable接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。例如:继承Thread类实现多线程,并在合适的地方启动线程
class MyThread extends Thread {
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread1 = new MyqThread();
MyThread myThread2 = new MyqThread();
myThread1.start();
myThread2.start();
(2)、实现Runnable接口的方式实现多线程,并且实例化Thread,传入自己的Thread实例,调用run( )方法
class MyqThread implements Runnable {
@Override
public void run() {
System.out.println("MyThread.run()");
}
}
MyqThread myThread = new MyqThread();
Thread thread = new Thread(myThread);
thread.start();
(3)、使用ExecutorService、Callable、Future实现有返回结果的多线程:ExecutorService、Callable、Future这个对象实际上都是属于Executor框架中的功能类。想要详细了解Executor框架的可以访问http://www.javaeye.com/topic/366591,这里面对该框架做了很详细的解释。返回结果的线程是在JDK1.5中引入的新特征,确实很实用,有了这种特征我就不需要再为了得到返回值而大费周折了,而且即便实现了也可能漏洞百出。可返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。下面提供了一个完整的有返回结果的多线程测试例子,在JDK1.5下验证过没问题可以直接使用。代码如下:
import java.util.concurrent.*;
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
/**
* 有返回值的线程
*/
@SuppressWarnings("unchecked")
public class Demo3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("----程序开始运行----");
Date date1 = new Date();
int taskSize = 5;
// 创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List list = new ArrayList();
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " ");
// 执行任务并获取Future对象
Future f = pool.submit(c);
// System.out.println(">>>" + f.get().toString());
list.add(f);
}
// 关闭线程池
pool.shutdown();
// 获取所有并发任务的运行结果
for (Future f : list) {
// 从Future对象上获取任务的返回值,并输出到控制台
System.out.println(">>>" + f.get().toString());
}
Date date2 = new Date();
System.out.println("----程序结束运行----,程序运行时间【" + (date2.getTime() - date1.getTime()) + "毫秒】");
}
}
class MyCallable implements Callable {
private String taskNum;
MyCallable(String taskNum) {
this.taskNum = taskNum;
}
public Object call() throws Exception {
System.out.println(">>>" + taskNum + "任务启动");
Date dateTmp1 = new Date();
Thread.sleep(1000);
Date dateTmp2 = new Date();
long time = dateTmp2.getTime() - dateTmp1.getTime();
System.out.println(">>>" + taskNum + "任务终止");
return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】";
}
}
2. 在java中wait和sleep方法的不同?
最大的不同是在等待时wait会释放锁,而sleep一直持有锁。wait通常被用于线程间交互,sleep通常被用于暂停执行。
3. synchronized和volatile关键字的作用
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
1.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
2.volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性
3.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
4.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
4.分析线程并发访问代码解释原因
class Counter {
private volatile int count = 0;
public void inc() {
try {
Thread.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
@Override
public String toString() {
return "[count=" + count + "]";
}
}
// ---------------------------------华丽的分割线-----------------------------
public class VolatileTest {
public static void main(String[] args) {
final Counter counter = new Counter();
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
counter.inc();
}
}).start();
}
System.out.println(counter);
}
}
上面的代码执行完后输出的结果确定为1000吗?
答案是不一定,或者不等于1000。这是为什么吗?
在java 的内存模型中每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。
也就是说上面主函数中开启了1000个子线程,每个线程都有一个变量副本,每个线程修改变量只是临时修改了自己的副本,当线程结束时再将修改的值写入在主内存中,这样就出现了线程安全问题。因此结果就不可能等于1000了,一般都会小于1000。
5. 什么是线程池,如何使用?
线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用new线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高的代码执行效率。
在JDK的java.util.concurrent.Executors中提供了生成多种线程池的静态方法。
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(4);
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(4);
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
然后调用他们的execute方法即可。
6.常用的线程池有哪些?
newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。
newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。
7. 请叙述一下您对线程池的理解?
(如果问到了这样的问题,可以展开的说一下线程池如何用、线程池的好处、线程池的启动策略)
合理利用线程池能够带来三个好处。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
8. 线程池的启动策略?
1、线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
2、当调用execute() 方法添加一个任务时,线程池会做如下判断:
a. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
b. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。
c. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务;
d. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。
3、当一个线程完成任务时,它会从队列中取下一个任务来执行。
4、当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
9. 如何控制某个方法允许并发访问线程的个数?
import java.util.concurrent.Semaphore;
public class SemaphoreTest {
static Semaphore semaphore = new Semaphore(5, true);
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
test();
}
}).start();
}
}
public static void test() {
try {
// 申请一个请求
semaphore.acquire();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "进来了");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "走了");
// 释放一个请求
semaphore.release();
}
}
可以使用Semaphore控制,第16行的构造函数创建了一个Semaphore对象,并且初始化了5个信号。这样的效果是控件test方法最多只能有5个线程并发访问,对于5个线程时就排队等待,走一个来一下。第33行,请求一个信号(消费一个信号),如果信号被用完了则等待,第45行释放一个信号,释放的信号新的线程就可以使用了。
10. 三个线程a、b、c并发运行,b,c需要a线程的数据怎么实现?
根据问题的描述,我将问题用以下代码演示,ThreadA、ThreadB、ThreadC,ThreadA用于初始化数据num,只有当num初始化完成之后再让ThreadB和ThreadC获取到初始化后的变量num。
分析过程如下:
考虑到多线程的不确定性,因此我们不能确保ThreadA就一定先于ThreadB和ThreadC前执行,就算ThreadA先执行了,我们也无法保证ThreadA什么时候才能将变量num给初始化完成。因此我们必须让ThreadB和ThreadC去等待ThreadA完成任何后发出的消息。
现在需要解决两个难题,一是让ThreadB和ThreadC等待ThreadA先执行完,二是ThreadA执行完之后给ThreadB和ThreadC发送消息。
解决上面的难题我能想到的两种方案,一是使用纯Java API的Semaphore类来控制线程的等待和释放,二是使用Android提供的Handler消息机制。
/**
* 三个线程a、b、c并发运行,b,c需要a线程的数据怎么实现(上海期学员提供)
*/
public class ThreadCommunication {
private static int num;// 定义一个变量作为数据
public static void main(String[] args) {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
try {
// 模拟耗时操作之后初始化变量num
Thread.sleep(1000);
num = 1;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "获取到num的值为:" + num);
}
});
Thread threadC = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "获取到num的值为:" + num);
}
});
// 同时开启个线程
threadA.start();
threadB.start();
threadC.start();
}
}
import java.util.concurrent.Semaphore;
public class Demo4 {
private static int num;
/**
* 定义一个信号量,该类内部维持了多个线程锁,可以阻塞多个线程,释放多个线程,线程的阻塞和释放是通过permit概念来实现的
* 线程通过semaphore.acquire()方法获取permit,如果当前semaphore有permit则分配给该线程,如果没有则阻塞该线程直到semaphore
* 调用release()方法释放permit。 构造函数中参数:permit(允许) 个数,
*/
private static Semaphore semaphore = new Semaphore(0);
public static void main(String[] args) {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
try {
// 模拟耗时操作之后初始化变量num
Thread.sleep(1000);
num = 1;
// 初始化完参数后释放两个permit
semaphore.release(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
try {
// 获取permit,如果semaphore没有可用的permit则等待,如果有则消耗一个
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "获取到num的值为:" + num);
}
});
Thread threadC = new Thread(new Runnable() {
@Override
public void run() {
try {
// 获取permit,如果semaphore没有可用的permit则等待,如果有则消耗一个
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "获取到num的值为:" + num);
}
});
// 同时开启3个线程
threadA.start();
threadB.start();
threadC.start();
}
}
11. 同一个类中的2个方法都加了同步锁,多个线程能同时访问同一个类中的这两个方法吗?
这个问题需要考虑到Lock与synchronized 两种实现锁的不同情形。因为这种情况下使用Lock 和synchronized 会有截然不同的结果。Lock可以让等待锁的线程响应中断,Lock获取锁,之后需要释放锁。如下代码,多个线程不可访问同一个类中的2个加了Lock锁的方法。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo5 {
private int count = 0;
private Lock lock = new ReentrantLock();// 设置lock锁
// 方法1
public Runnable run1 = new Runnable() {
public void run() {
lock.lock();// 加锁
while (count < 1000) {
try {
// 打印是否执行该方法
System.out.println(Thread.currentThread().getName() + " run1: " + count++);
} catch (Exception e) {
e.printStackTrace();
}
}
lock.unlock();
}
};
// 方法2
public Runnable run2 = new Runnable() {
public void run() {
lock.lock();
while (count < 1000) {
try {
System.out.println(Thread.currentThread().getName() + " run2: " + count++);
} catch (Exception e) {
e.printStackTrace();
}
}
lock.unlock();
}
};
public static void main(String[] args) throws InterruptedException {
Demo5 t = new Demo5(); // 创建一个对象
new Thread(t.run1).start();// 获取该对象的方法1
new Thread(t.run2).start();// 获取该对象的方法2
}
}
结果是:
Thread-0 run1: 0
Thread-0 run1: 1
Thread-0 run1: 2
Thread-0 run1: 3
Thread-0 run1: 4
Thread-0 run1: 5
Thread-0 run1: 6
........
而synchronized却不行,使用synchronized时,当我们访问同一个类对象的时候,是同一把锁,所以可以访问该对象的其他synchronized方法。代码如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo6 {
private int count = 0;
private Lock lock = new ReentrantLock();
public Runnable run1 = new Runnable() {
public void run() {
synchronized (this) { // 设置关键字synchronized,以当前类为锁
while (count < 1000) {
try {
// 打印是否执行该方法
System.out.println(Thread.currentThread().getName() + " run1: " + count++);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
};
public Runnable run2 = new Runnable() {
public void run() {
synchronized (this) {
while (count < 1000) {
try {
System.out.println(Thread.currentThread().getName()
+ " run2: " + count++);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
};
public static void main(String[] args) throws InterruptedException {
Demo6 t = new Demo6(); // 创建一个对象
new Thread(t.run1).start(); // 获取该对象的方法1
new Thread(t.run2).start(); // 获取该对象的方法2
}
}
结果为:
Thread-1 run2: 0
Thread-1 run2: 1
Thread-1 run2: 2
Thread-0 run1: 0
Thread-0 run1: 4 Thread-0 run1: 5 Thread-0 run1: 6
......
12.什么情况下导致线程死锁,遇到线程死锁该怎么解决?
11.1 死锁的定义:所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
11.2 死锁产生的必要条件:
互斥条件:线程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个线程所占有。此时若有其他线程请求该资源,则请求线程只能等待。
不剥夺条件:线程所获得的资源在未使用完毕之前,不能被其他线程强行夺走,即只能由获得该资源的线程自己来释放(只能是主动释放)。
请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
循环等待条件:存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个线程所请求。即存在一个处于等待状态的线程集合{Pl, P2, ..., pn},其中Pi等待的资源被P(i+1)占有(i=0, 1, ..., n-1),Pn等待的资源被P0占有,如图2-15所示。
11.3 产生死锁的一个例子
/**
* 一个简单的死锁类 当DeadLock类的对象flag==1时(td1),先锁定o1,睡眠500毫秒
* 而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒
* td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定; td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定;
* td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。
*/
public class DeadLock implements Runnable {
public int flag = 1;
// 静态对象是类的所有对象共享的
private static Object o1 = new Object(), o2 = new Object();
public void run() {
System.out.println("flag=" + flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("1");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("0");
}
}
}
}
public static void main(String[] args) {
DeadLock td1 = new DeadLock();
DeadLock td2 = new DeadLock();
td1.flag = 1;
td2.flag = 0;
// td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。
// td2的run()可能在td1的run()之前运行
new Thread(td1).start();
new Thread(td2).start();
}
}
11.4 如何避免死锁
在有些情况下死锁是可以避免的。两种用于避免死锁的技术:
加锁顺序(线程按照一定的顺序加锁)public class DeadLock1 {
public int flag = 1;
// 静态对象是类的所有对象共享的
private static Object o1 = new Object(), o2 = new Object();
public void money(int flag) {
this.flag = flag;
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("当前的线程是" +
Thread.currentThread().getName() + " " + "flag的值" + "1");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("当前的线程是" +
Thread.currentThread().getName() + " " + "flag的值" + "0");
}
}
}
}
public static void main(String[] args) {
final DeadLock1 td1 = new DeadLock1();
final DeadLock1 td2 = new DeadLock1();
td1.flag = 1;
td2.flag = 0;
// td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。
// td2的run()可能在td1的run()之前运行
final Thread t1 = new Thread(new Runnable() {
public void run() {
td1.flag = 1;
td1.money(1);
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
public void run() {
// TODO Auto-generated method stub
try {
// 让t2等待t1执行完
t1.join();// 核心代码,让t1执行完后t2才会执行
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
td2.flag = 0;
td1.money(0);
}
});
t2.start();
}
}
结果:
当前的线程是Thread-0 flag的值1
当前的线程是Thread-1 flag的值0
加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadLock2 {
public int flag = 1;
// 静态对象是类的所有对象共享的
private static Object o1 = new Object(), o2 = new Object();
public void money(int flag) throws InterruptedException {
this.flag = flag;
if (flag == 1) {
synchronized (o1) {
Thread.sleep(500);
synchronized (o2) {
System.out.println("当前的线程是" +
Thread.currentThread().getName() + " " + "flag的值" + "1");
}
}
}
if (flag == 0) {
synchronized (o2) {
Thread.sleep(500);
synchronized (o1) {
System.out.println("当前的线程是" +
Thread.currentThread().getName() + " " + "flag的值" + "0");
}
}
}
}
public static void main(String[] args) {
final Lock lock = new ReentrantLock();
final DeadLock2 td1 = new DeadLock2();
final DeadLock2 td2 = new DeadLock2();
td1.flag = 1;
td2.flag = 0;
// td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。
// td2的run()可能在td1的run()之前运行
final Thread t1 = new Thread(new Runnable() {
public void run() {
// TODO Auto-generated method stub
String tName = Thread.currentThread().getName();
td1.flag = 1;
try {
// 获取不到锁,就等5秒,如果5秒后还是获取不到就返回false
if (lock.tryLock(5000, TimeUnit.MILLISECONDS)) {
System.out.println(tName + "获取到锁!");
} else {
System.out.println(tName + "获取不到锁!");
return;
}
} catch (Exception e) {
e.printStackTrace();
}
try {
td1.money(1);
} catch (Exception e) {
System.out.println(tName + "出错了!!!");
} finally {
System.out.println("当前的线程是" + Thread.currentThread().getName() + "释放锁!!");
lock.unlock();
}
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
public void run() {
String tName = Thread.currentThread().getName();
// TODO Auto-generated method stub
td1.flag = 1;
try {
// 获取不到锁,就等5秒,如果5秒后还是获取不到就返回false
if (lock.tryLock(5000, TimeUnit.MILLISECONDS)) {
System.out.println(tName + "获取到锁!");
} else {
System.out.println(tName + "获取不到锁!");
return;
}
} catch (Exception e) {
e.printStackTrace();
}
try {
td2.money(0);
} catch (Exception e) {
System.out.println(tName + "出错了!!!");
} finally {
System.out.println("当前的线程是" + Thread.currentThread().getName() + "释放锁!!");
lock.unlock();
}
}
});
t2.start();
}
}
打印结果:
Thread-0获取到锁!
当前的线程是Thread-0 flag的值1
当前的线程是Thread-0释放锁!!
Thread-1获取到锁!
当前的线程是Thread-1 flag的值0
当前的线程是Thread-1释放锁!!
13. Java中多线程间的通信怎么实现?
线程通信的方式:
1.共享变量
线程间通信可以通过发送信号,发送信号的一个简单方式是在共享对象的变量里设置信号值。线程A在一个同步块里设置boolean型成员变量hasDataToProcess为true,线程B也在同步块里读取hasDataToProcess这个成员变量。这个简单的例子使用了一个持有信号的对象,并提供了set和get方法:
public class MySignal {
// 共享的变量
private boolean hasDataToProcess = false;
// 取值
public boolean getHasDataToProcess() {
return hasDataToProcess;
}
// 存值
public void setHasDataToProcess(boolean hasDataToProcess) {
this.hasDataToProcess = hasDataToProcess;
}
public static void main(String[] args) {
// 同一个对象
final MySignal my = new MySignal();
// 线程1设置hasDataToProcess值为true
final Thread t1 = new Thread(new Runnable() {
public void run() {
my.setHasDataToProcess(true);
}
});
t1.start();
// 线程2取这个值hasDataToProcess
Thread t2 = new Thread(new Runnable() {
public void run() {
try {
// 等待线程1完成然后取值
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
my.getHasDataToProcess();
System.out.println("t1改变以后的值:" + my.getHasDataToProcess());
}
});
t2.start();
}
}
结果:
t1改变以后的值:true
2.wait/notify机制
以资源为例,生产者生产一个资源,通知消费者就消费掉一个资源,生产者继续生产资源,消费者消费资源,以此循环。代码如下:
//资源类
class Resource {
private String name;
private int count = 1;
private boolean flag = false;
public synchronized void set(String name) {
// 生产资源
while (flag) {
try {
// 线程等待。消费者消费资源
wait();
} catch (Exception e) {
}
}
this.name = name + "---" + count++;
System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name);
flag = true;
// 唤醒等待中的消费者
this.notifyAll();
}
public synchronized void out() {
// 消费资源
while (!flag) {
// 线程等待,生产者生产资源
try {
wait();
} catch (Exception e) {
}
}
System.out.println(Thread.currentThread().getName() + "...消费者..." + this.name);
flag = false;
// 唤醒生产者,生产资源
this.notifyAll();
}
}
// 生产者
class Producer implements Runnable {
private Resource res;
Producer(Resource res) {
this.res = res;
}
// 生产者生产资源
public void run() {
while (true) {
res.set("商品");
}
}
}
// 消费者消费资源
class Consumer implements Runnable {
private Resource res;
Consumer(Resource res) {
this.res = res;
}
public void run() {
while (true) {
res.out();
}
}
}
public class ProducerConsumerDemo {
public static void main(String[] args) {
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
t1.start();
t2.start();
}
}
14. 线程和进程的区别
进程:具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位。
线程:是进程的一个实体,是cpu调度和分派的基本单位,是比进程更小的可以独立运行的基本单位。
特点:线程的划分尺度小于进程,这使多线程程序拥有高并发性,进程在运行时各自内存单元相互独立,线程之间内存共享,这使多线程编程可以拥有更好的性能和用户体验
注意:多线程编程对于其它程序是不友好的,占据大量cpu资源。
15.请说出同步线程及线程调度相关的方法?
wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
注意:java 5 通过Lock接口提供了显示的锁机制,Lock接口中定义了加锁(lock()方法)和解锁(unLock()方法),增强了多线程编程的灵活性及对线程的协调
16. 启动一个线程是调用run()方法还是start()方法?
启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM 调度并执行,这并不意味着线程就会立即运行。
run()方法是线程启动后要进行回调(callback)的方法。