实际上关于多线程的基础知识,前面自己已经总结过一部分,但是每一个阶段对于同样知识点的学习侧重点是不一样的,前面的Java基础总结八之多线程(一)和 Java基础总结九之多线程(二)是对JDK5以前多线程相关基础知识的一个简单总结,今天本文将偏重于JDK5提供的并发库进行学习总结。
首先,从一个简单的多线程demo引入(包括内容为JDK5之前的synchronized关键字及通过wait方法和notify方法控制的等待唤醒机制):
/**
* @author: Is-Me-Hl
* @Description: 传统的多线程互斥和等待唤醒机制
* 给定两个线程依次循环五十次,一个线程每次取数据十次,期间不可以被打断,另一个每次取数据二十次,期间不可打断,二者交替执行
*/
public class Test {
private static ThreadTest threadTest = new ThreadTest();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 50; i++) {
threadTest.thread1(i);
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 50; i++) {
threadTest.thread2(i);
}
}
}).start();
}
}
class ThreadTest {
private boolean flag;
public synchronized void thread1(int i) {
while (flag) {
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for (int j = 1; j <= 10; j++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "第" + i
+ "次取到的数据为:" + j);
}
flag = true;
this.notify();
}
public synchronized void thread2(int i) {
while (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for (int j = 1; j <= 100; j++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "第" + i
+ "次取到的数据为:" + j);
}
flag = false;
this.notify();
}
}
通过使用synchronized关键字保证方法的原子性,通过Object的wait和notify方法的等待与唤醒机制保证其交替执行。以上就是传统线程同步的低配版demo,是入门多线程的一个总结。那么JDK5给开发者提供了些什么呢?整整一个Concurrent并发库,有Lock锁对象,衍生而出的读锁,写锁,有Executors线程池对象等等...下面按照笔者想到什么就总结什么的思路来进行知识的总结(皮一下很开心~):
(一)原子操作类:
很经典的一个问题,i++是原子操作吗?答案:显然不是。原子操作:通俗的讲,操作是不可分割的,要么执行,要么不执行,不会说执行到一半而被打断。那i++为什么不是原子操作呢?看着不就一条语句吗?实际上是不是原子操作和是不是一条语句没有必然的联系,i++实际上是多步操作,先把i的值拿过来,然后再加1,然后又赋值给i。所以是非原子操作。那么如果想要这个语句变成原子操作是不是我这个语句还要加synchronized锁起来呢?可以这么做,但是JDK5开始有了更为简便的方法。
从JDk5开始,Java提供了java.util.concurrent.atomic包供数据进行原子性操作。
(1)关于基本类型的原子更新类:AtomicBoolean、AtomicInteger、AtomicLong,下面以AtomicInteger举例:
import java.util.concurrent.atomic.AtomicInteger;
/**
*
* @author: Is-Me-Hl
* @Description: 关于原子类操作 AtomicInteger类举例。
*/
public class Test2 {
public static void main(String[] args) {
AtomicInteger ai=new AtomicInteger(100);
System.out.println("当前数据为: "+ai.get());
System.out.println("相加得到的数据为: "+ai.addAndGet(100));
System.out.println("当前值减去1返回之前的值为: "+ai.getAndDecrement());
System.out.println("当前值加上1返回之前的值为: "+ai.getAndIncrement());
}
}
//执行结果:
当前数据为: 100
相加得到的数据为: 200
当前值减去1返回之前的值为: 200
当前值加上1返回之前的值为: 199
上面是一个简单的举例,实际上只要有了API文档,里面的方法自然也就能顺利使用了。看到这,我想应该马上就会有人说你这个demo只是应用,我还是没有看到其是怎么保证原子性更新的,请不要急,笔者在读其他博主文章的时候发现了一篇相对详细的文章介绍了这个包里面的所有类,包括基本类型的原子更新和引用类型的原子更新。Java原子操作类汇总
(二)Executors线程池:
(1) newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
(2) newCachedThreadPool:创建一个可缓存线程池,应用中存在的线程数可以无限大。
(3)newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
(4) newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/*
* Executors使用
*/
public class ThreadPoolTest {
public static void main(String[] args) {
// 第一种线程池
// ExecutorService threadPool =
// Executors.newFixedThreadPool(3);//固定大小的线程池
// 第二种线程池
// ExecutorService threadPool =
// Executors.newCachedThreadPool();//如果没有可用的线程池=会自己创建新的线程出来供使用,过一定时间后自己自动销毁
// 第三种线程池
ExecutorService threadPool = Executors.newSingleThreadExecutor();// 只有一个线程,类似单线程,但是好处是,该池中一直维持着一个线程,一个线程死了,有马上产生一个
for (int i = 1; i <= 10; i++) {
final int task = i;
threadPool.execute(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ " is looping " + i + " for task " + task);
}
}
});
}
// threadPool.shutdownNow();//与shutdown()的区别就是Now指的是马上关闭线程池,不管你运行到哪里,不带now的是指等所有任务都执行完之后再关闭它,而不是一直等待任务进来
//可定期执行的
Executors.newScheduledThreadPool(3).schedule(new Runnable() {
@Override
public void run() {
System.out.println("boming");
}
}, 10, TimeUnit.SECONDS);
}
}
(三)Callable 和 Future
/*
* callable和future的应用
*/
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableAndFuture {
public static void main(String[] args) {
// 使用callable提交一个任务
ExecutorService threadPool = Executors.newSingleThreadExecutor();
Future<String> future = threadPool.submit(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(2000);
return "hello";
}
});
System.out.println("等待结果:");
try {
System.out.println("拿到结果:" + future.get());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 使用callable一次性提交一组任务
ExecutorService threadPool2 = Executors.newFixedThreadPool(10);// 创建一个固定的大小的线程池
CompletionService<Integer> completionService = new ExecutorCompletionService<Integer>(
threadPool2);
for (int i = 1; i <= 10; i++) {
final int taskNum = i;
completionService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
Thread.sleep(new Random().nextInt(5000));
return taskNum;
}
});
}
for (int i = 1; i <= 10; i++) {
try {
System.out.println(completionService.take().get());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
Future是提交了Callable后返回的结果对象。通过get()方法能够接收到返回的结果,当然在一组任务提交后,Future阻塞得到的结果可能是乱序的,谁先计算好返回给我结果,我就先拿到谁的结果。
(四)lock锁对象
Lock提供了比使用synchronized方法和语句更广泛的锁定操作。它允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的Condition对象。 锁是用于控制多个线程对共享资源访问的工具。通常,对共享资源的所有访问都要求首先获取锁,锁提供了对共享资源的独占访问,一次只能有一个线程可以获取锁。 但是,一些锁可能允许并发访问共享资源,如ReadWriteLock的读锁。 首先,下面先给出一个lock锁的demo,之后再总结读写锁的知识:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
*
* @author: Is-Me-Hl
* @date: 2018年11月8日
* @Description: 主线程和子线程使用lock锁保证运行时候的互斥。
*/
public class Test2 {
private static MyThread myThread = new MyThread();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
myThread.sub(i);
}
}
}).start();
for (int i = 1; i <= 100; i++) {
myThread.main(i);
}
}
}
class MyThread {
private Lock lock = new ReentrantLock();
public void sub(int i) {
lock.lock();
try {
for (int j = 1; j <= 10; j++) {
System.out.println(Thread.currentThread().getName() + "---" + i
+ "---" + j);
}
} finally {
lock.unlock();
}
}
public void main(int i) {
lock.lock();
try {
for (int j = 1; j <= 10; j++) {
System.out.println(Thread.currentThread().getName() + "---" + i
+ "---" + j);
}
} finally {
lock.unlock();
}
}
}
从上面的例子中可以看出来,lock锁同样能够有synchronized的互斥效果,但是如果lock锁仅仅有这样的效果JDK为什么还要再推出来呢?显然lock锁还有其他的作用,如能让多个线程同时访问共享变量的读锁。在高并发时,有效的提高了数据共享的效率。下面是读写锁的demo,也算是通过读写锁实现缓存机制的一个demo:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/*
* 缓存机制Demo,读锁与写锁的使用,一个不错的读写锁demo
* 读锁和写锁机制是什么呢?读和读互不影响,读和写互斥,写和写互斥,提高读写的效率
*/
public class CacheDemo {
private static Map<String, Object> cache = new HashMap<>();
public static void main(String[] args) {
cache.put("key", null);
long start = System.currentTimeMillis();
for (int i = 0; i < 50; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i < 100000; i++) {
System.out.println("线程名:"
+ Thread.currentThread().getName()
+ " 线程所取得的数据:" + getData("key"));
}
}
}).start();
}
try {
Thread.sleep(240000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("时间花费:" + (end - start));
}
private static ReadWriteLock rwl = new ReentrantReadWriteLock();
public static Object getData(String key) {
// 这里可以加入snychronized关键字进行线程互斥。但是这里有更好的处理方法,
// 就是如果所有的线程过来都是读的话,那么,加个读锁让所有线程进来就可以,没有必要排斥其他一块来读数据的线程
// 好方法就是加读写锁就可以了
rwl.readLock().lock();// 一进来就加上读锁,这样所有读的线程就都可以进来
Object value = null;
try {
value = cache.get(key);
if (value == null) {
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
if (value == null) {
value = "Is-Me-Hl";// 这里模拟的是去从数据库中取数据,存放到value中
}
} finally {
rwl.writeLock().unlock();
}
rwl.readLock().lock();
}
} finally {
rwl.readLock().unlock();
}
return value;
}
}
由上面的代码可以看出来,通过读写锁能很好地实现缓存,同时也能提高各路线程对共享数据的读取速率。多个读锁不互斥,读锁和写锁互斥,写锁和写锁互斥,这个过程是由JVM自己控制的,开发者只需要上号相应的锁就可以。
(五)Condition条件(条件队列或条件变量)
Lock替换synchronized方法和语句的使用,Condition取代了对象监视器方法的使用。
Condition的功能类似在传统线程技术中的Object.wait和Object.notify的功能。但是读者在看到这句话的时候又可能会问,既然是一样的,出现的意义在哪里?在传统的线程机制中一个监视器对象上只能有一路等待和通知,要想实现多路通知,必须嵌套多个同步监视器对象。因此Condition对象就派上了用场。(ps:一个Condition实例本质上要绑定到一个锁。如果要获得特定Condition实例,请使用lock对象的newCondition()方法。)
一个简单的Condition应用的demo:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionDemo {
public static void main(String[] args) {
final Business business = new Business();
new Thread(
new Runnable() {
@Override
public void run() {
for(int i=1;i<=50;i++){
business.sub(i);
}
}
}
).start();
for(int i=1;i<=50;i++){
business.main(i);
}
}
static class Business {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
private boolean bShouldSub = true;
public void sub(int i){
lock.lock();
try{
while(!bShouldSub){
try {
condition.await();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for(int j=1;j<=10;j++){
System.out.println("sub thread sequence of " + j + ",loop of " + i);
}
bShouldSub = false;
condition.signal();
}finally{
lock.unlock();
}
}
public void main(int i){
lock.lock();
try{
while(bShouldSub){
try {
condition.await();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for(int j=1;j<=100;j++){
System.out.println("main thread sequence of " + j + ",loop of " + i);
}
bShouldSub = true;
condition.signal();
}finally{
lock.unlock();
}
}
}
}
官方API文档中给出的一个通过lock锁和Condition条件实现的阻塞队列demo:
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock(); try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally { lock.unlock(); }
}
public Object take() throws InterruptedException {
lock.lock(); try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally { lock.unlock(); }
}
}
需要注意的是官方demo中使用了两个Condition,有什么用呢?是多路通知的体现。自己上面所写的demo实际上是不完善的,假如要唤醒指定的线程,官方demo是能做到的,而自己的demo会随机唤醒一个,这个是不太完善的地方,也是官方文档的官方性的表现。另外一个注意点是Condition需要始终让他放在循环中,防止虚假唤醒。总结:好的Coffee需要细品!(ps:这里大概读者就会有个思路,那我终于能理解阻塞队列的实现了,以后我就这么写,那笔者告诉你,大可不必。这只是个demo,官方既然给了你实现,为什么不给你封装好个类供你使用呢?BlockingQueue阻塞队列,这是个大知识点,笔者准备留着以后有时间学的相对完善,有好的demo再总结一下。如果您有需要了解请移步百度)
(六)Semaphere(与synchronized类似)
一个计数信号量。在概念上,信号量维持一组许可证。如果有必要,每个acquire()都会阻塞,直到许可证可用,然后才能使用它。每个release()添加许可证,潜在地释放阻塞获取方。但是,没有使用实际的许可证对象; Semaphore只保留可用数量的计数,并相应地执行。
synchronized保证了,我管理的那部分代码同一时刻只有一个线程能访问。Semaphore保证了,我管理的那部分代码同一时刻最多可以有n个线程访问。Semaphore可以维护当前访问自身的线程个数,并提供了同步机制。例子:停车场总共五个车位,但是我是门口看车场的大叔,我说,外面的十辆车听着,里面五个车位车位已经满了,只有当里面车位空了出来,你们再进来替换,至于谁进来,我给你们定个规则,公平或者非公平,然后你们依据规则进来,谢谢大家配合。(ps:案例demo略)
(七)CyclicBarrier
CyclicBarrier是一个同步工具类,它允许一组线程互相等待,直到到达某个公共屏障点。barrier在释放等待线程后可以重用,所以称它为循环(Cyclic)的屏障(Barrier)。例子:幼稚园小芳老师告诉小朋友们明天去郊游,早上八点学校出发,那么小朋友在八点前从自己的家,不同的方向,不同的地点,不同的时间点到达这里,早到的同学要等晚到的同学,等到了八点,老师带着同学们到了郊外,说大家自由活动,中午十二点回学校,那么小朋友们有各自忙各自的,到十二点,大家都相互等着结伴回家。(ps:案例demo略)
(八)CountDownLatch
CountDownLatch是一个同步工具类,它允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。一个CountDownLatch用给定的计数初始化。await方法阻塞,直到由于countDown()方法的调用而导致当前计数达到零,之后所有等待线程被释放,并且任何后续的await 调用立即返回。这是一个一次性的现象:计数无法重置。如果您需要重置计数的版本,请考虑使用CyclicBarrier 。举例:一个赛跑比赛:裁判员吹响哨子,运动员们就开始跑,这是倒计时。在终点处,裁判一个人也可以等到所有的运动员们冲过终点,再报出比赛中的冠亚季军。实际上,这个例子:也就说明多个线程可以等一个线程一声号令就开始行动,那么一个线程也可以等所以线程都已经下达了命令,该线程开始行动。(ps:案例demo略)
(九)Exchanger
Exchanger是一个同步工具类,线程可以在成对内配对和交换元素的同步点。每个线程在输入exchange方法时提供一些对象,与合作者线程匹配,并在返回时接收其合作伙伴的对象。交换器可以被视为一个的双向形式SynchronousQueue。交换器在诸如遗传算法和管道设计的应用中可能是有用的。 举例:一个黑帮大哥和军贩子要交易,一个带上钱一个带上武器,约定好某个时间点在某个地方,啪,瞬间交换。(ps:案例demo略)
(十)同步集合类
在之前的Java基础总结五之集合中总结乐意系列集合,典型的有hashMap,hashSet等等,但是之前也强调了这些是线程不安全的,也介绍了要获得线程安全的集合我们可以使用Collections工具类,将线程不安全的集合传入,得到的就是安全的集合:实现方式是通过代理的方式:(源码如下)
public static <T> Collection<T> synchronizedCollection(Collection<T> c) {
return new SynchronizedCollection<>(c);
}
static <T> Collection<T> synchronizedCollection(Collection<T> c, Object mutex) {
return new SynchronizedCollection<>(c, mutex);
}
/**
* @serial include
*/
static class SynchronizedCollection<E> implements Collection<E>, Serializable {
private static final long serialVersionUID = 3053995032091335093L;
final Collection<E> c; // Backing Collection
final Object mutex; // Object on which to synchronize
SynchronizedCollection(Collection<E> c) {
this.c = Objects.requireNonNull(c);
mutex = this;
}
SynchronizedCollection(Collection<E> c, Object mutex) {
this.c = Objects.requireNonNull(c);
this.mutex = Objects.requireNonNull(mutex);
}
public int size() {
synchronized (mutex) {return c.size();}
}
public boolean isEmpty() {
synchronized (mutex) {return c.isEmpty();}
}
public boolean contains(Object o) {
synchronized (mutex) {return c.contains(o);}
}
public Object[] toArray() {
synchronized (mutex) {return c.toArray();}
}
public <T> T[] toArray(T[] a) {
synchronized (mutex) {return c.toArray(a);}
}
public Iterator<E> iterator() {
return c.iterator(); // Must be manually synched by user!
}
public boolean add(E e) {
synchronized (mutex) {return c.add(e);}
}
public boolean remove(Object o) {
synchronized (mutex) {return c.remove(o);}
}
public boolean containsAll(Collection<?> coll) {
synchronized (mutex) {return c.containsAll(coll);}
}
public boolean addAll(Collection<? extends E> coll) {
synchronized (mutex) {return c.addAll(coll);}
}
public boolean removeAll(Collection<?> coll) {
synchronized (mutex) {return c.removeAll(coll);}
}
public boolean retainAll(Collection<?> coll) {
synchronized (mutex) {return c.retainAll(coll);}
}
public void clear() {
synchronized (mutex) {c.clear();}
}
public String toString() {
synchronized (mutex) {return c.toString();}
}
// Override default methods in Collection
@Override
public void forEach(Consumer<? super E> consumer) {
synchronized (mutex) {c.forEach(consumer);}
}
@Override
public boolean removeIf(Predicate<? super E> filter) {
synchronized (mutex) {return c.removeIf(filter);}
}
@Override
public Spliterator<E> spliterator() {
return c.spliterator(); // Must be manually synched by user!
}
@Override
public Stream<E> stream() {
return c.stream(); // Must be manually synched by user!
}
@Override
public Stream<E> parallelStream() {
return c.parallelStream(); // Must be manually synched by user!
}
private void writeObject(ObjectOutputStream s) throws IOException {
synchronized (mutex) {s.defaultWriteObject();}
}
}
那么JDK5之后,并发库提供了满足并发安全需求的集合:
这些集合都是安全的,也在JDK5之后并发中常用。至于使用的demo,我想读者如果能学到并发这里,自己看看API文档,百度一下的技能还是具备的,作为自己总结的笔记,自己就懒懒地忽略掉了~
注:以上文章仅是个人学习过程总结,若有不当之处,望不吝赐教。