线程通信
先了解一下最经典的程序设计模式之一的生产者-消费者模型
日常生活中,每当我们缺少某些生活用品时,我们都会去超市进行购买,那么,你有没有想
过,你是以什么身份去的超市呢?
相信大部分人都会说自己是 消费者,确实如此,那么既然我们是消费者,又是谁替我们生产
各种各样的商品呢?
当然是超市的各大供货商,自然而然地也就成了我们的 生产者。
如此一来,生产者有了,消费者也有了,那么将二者联系起来的超市又该作何理解呢?
诚然,它本身是作为一座 交易场所而诞生。
将上述场景类比到我们实际的软件开发过程中,经常会见到这样一幕:
代码的某个模块负责生产数据(供货商),
而生产出来的数据却不得不交给另一模块(消费者)来对其进行处理,
在两个程序模块之间我们必须要有一个类似上述超市的东西来存储数据(超市),这就抽象
出了我们的生产者/消费者模型。
其中,产生数据的模块,就形象地称为 生产者;
而处理数据的模块,就称为 消费者;
生产者和消费者之间的中介就叫做 缓冲区,一般就是一个队列。
在生产者-消费者模型中,当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,
而 在等待的期间内,生产者必须释放对队列的占用权。
因为生产者如果不释放对 队列的占用权,
那么 消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。
因此,一般情况下,当队列满时,会让生产者交出对 队列的占用权,并进入挂起状态。
然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。
同样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。
这种互相通信的过程就需要线程间的协作。
Java中线程通信协作方式有好多种,这里讲下最基础常用的两个方式,其他方式我们遇到的时候在讲:
1.配合synchronized加锁实现的线程同步。
2.配合ReentrantLock加锁实现的线程同步。
3.利用管道流实现通信,在后面的流的课程里讲解。
4.前面讲的JUC里的线程交换器,也是一种通信的方式。
- 配合synchronized加锁实现的线程同步
可以借助Object类的三个方法wait()/notify()/notifyAll()
1)wait()、notify()和notifyAll()方法是本地方法(底层C++写的),并且为final方法,无法被重写。
2)调用某个对象的wait()方法能让当前线程阻塞,相当于让当前线程交出(释放)此同步锁,然后进入等待状态,等待后续再次获得此同步锁。
3)调用某个对象的notify()方法能够唤醒一个正在等待这个同步锁对象的线程,如果有多个线程都在等待这个同步锁对象,则只能唤醒其中一个线程;
4)调用notifyAll()方法能够唤醒所有正在等待这个同步锁对象的线程;
----这里要注意一点: notify()和notifyAll()方法只是唤醒等待该同步锁的线程,并不决定哪个线程能够获取到锁。
public class ThreadTest1 {
public static void main(String args[]) {
Object lock = new Object();
P p = new P(lock);
C c=new C(lock);
p.start();
c.start();
}
}
//定义一个类,模拟生产者和消费者之间的缓冲区
class ValueObject{
public static String value=""; //模拟的缓冲区,静态变量的会让所有对象共享之
}
//实现生产者的线程类
class P extends Thread{
private Object lock; //锁生产者与肖费者同一把锁
public P(Object lock) {
this.lock = lock;
}
@Override
public void run() {
while(true) {
try {
synchronized (lock) {
//作为生产者的基本功能,生产数据,是判 断缓冲区(超市)没有数据就生产否则通知消费者消费
if(!ValueObject.value.equals("")) { //"=="号不能做字符串内之容相同之判断
//缓冲区里有数据的情况下,生产者不生产
lock.wait(); //让当线线程阻塞,把当前线程的同步锁释放出去
}
//这时我们才真正的开始生产数据
System.out.println("缓冲区没数据,这时我们才真正的开始生产数据!");
ValueObject.value = System.currentTimeMillis()+"";
//这个生产数据完毕,放进了缓冲区,通知消费者,消费可以了
lock.notify(); //通知消费者消费
}
}catch(Exception e) {
}
}
}
}
//实现消费者线程类
class C extends Thread{
private Object lock;
public C(Object lock) {
this.lock = lock;
}
@Override
public void run() {
while(true) {
try {
synchronized (lock) {
if(ValueObject.value.equals("")) {
lock.wait();
}
//没阻塞,说明缓冲区里有数据
System.out.println("缓冲区里有数据,现在把数据消费掉");
ValueObject.value="";
lock.notify(); //没有数据了,通知生产者
}
}catch(Exception e) {
}
}
}
}
-----------------------------------------------------------------------------------------------------------------------------------------------
缓冲区里有数据,现在把数据消费掉
缓冲区没数据,这时我们才真正的开始生产数据!
缓冲区里有数据,现在把数据消费掉
缓冲区没数据,这时我们才真正的开始生产数据!
缓冲区里有数据,现在把数据消费掉
缓冲区没数据,这时我们才真正的开始生产数据!
---------------------循环下去
多个生产者多个消费者
实现和上面1对1基本一样,只是在测试代码中,多new几个生产者,几个消费者。
只需注意一个问题:假死
问题描述:所有线程都被wait,这个项目就停止运行了。
问题原因:代码中使用wait/notify进行通信,不能保证notify唤醒的是异类,
比如生产者唤醒生产者,消费者唤醒消费者,就可能导致都在等待的状态。
问题解决:其实很简单,就是唤醒的时候同类异类都唤醒, 把notify()改为natifyAll()就解决了。
- ReentrantLock类加锁的线程的Condition类的
await()/signal()/signalAll()来实现通信
Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说,线程通信的实现比较推荐使用Condition
Condition是个接口,基本的方法就是await()和signal()方法;
Condition依赖于Lock接口,生成一个Condition的基本代码是 lock.newCondition()
调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
Conditon中的await()对应Object的wait();
Condition中的signal()对应Object的notify();
Condition中的signalAll()对应Object的notifyAll()。
代码示例:
------只是把示例1的代码稍稍换了一把锁而已。
public class ThreadTest2 {
public static void main(String args[]) {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
P1 p1 = new P1(lock, condition);
C1 c1 =new C1(lock, condition);
p1.start();
c1.start();
}
}
// 定义一个类,模拟生产者和消费者之间的缓冲区
class ValueObject1 {
public static String value = ""; // 模拟的缓冲区,静态变量的会让所有对象共享之
}
// 实现生产者的线程类
class P1 extends Thread {
private Lock lock; // 锁生产者与肖费者同一把锁 ,这时的锁不同于1例中的锁了,1例 中是Object类型的锁
private Condition condition;
public P1(Lock lock, Condition condition) {
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
while (true) {
try {
lock.lock();
// 作为生产者的基本功能,生产数据,是判 断缓冲区(超市)没有数据就生产否则通知消费者消费
if (!ValueObject1.value.equals("")) { // "=="号不能做字符串内之容相同之判断
// 缓冲区里有数据的情况下,生产者不生产
condition.await(); // 让当线线程阻塞,把当前线程的同步锁释放出去
}
// 这时我们才真正的开始生产数据
System.out.println("缓冲区没数据,这时我们才真正的开始生产数据!");
ValueObject1.value = System.currentTimeMillis() + "";
// 这个生产数据完毕,放进了缓冲区,通知消费者,消费可以了
condition.signal(); // 通知消费者消费
} catch (Exception e) {
} finally {
lock.unlock(); // 释放锁
}
}
}
}
// 实现消费者线程类
class C1 extends Thread {
private Lock lock;
Condition condition;
public C1(Lock lock, Condition condition) {
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
while (true) {
try {
lock.lock();
if (ValueObject1.value.equals("")) {
condition.wait();
}
// 没阻塞,说明缓冲区里有数据
System.out.println("缓冲区里有数据,现在把数据消费掉");
ValueObject1.value = "";
condition.signal(); // 没有数据了,通知生产者
} catch (Exception e) {
} finally {
lock.unlock();
}
}
}
}
线程控制的补充内容
Thread类中interrupt()、interrupted()和isInterrupted()方法
interrupt()方法
--------其作用是中断 此线程(此线程不一定是当前线程,而是指调用该方法的Thread实例所代表的线程),但实际上只是给线程设置一个中断标志,线程仍会继续运行。
interrupted()方法
-------作用是测试 当前线程是否被中断(检查中断标志),有标记的话,返回一个ture并清除中断状态,第二次再调用时中断状态已经被清除,将返回一个false。
isInterrupted()方法
-------作用是只测试 此线程(调用者代表的线程)是否被中断 ,不清除中断状态。
public class ThreadControllDemo {
public static void main(String args[]) {
Thread myt = new MyThread1();
myt.start();
myt.interrupt(); //打一个中断标志, 并不中断线程,线程会继续运行下去。
System.out.println("线程是打了中断标志吗? "+myt.isInterrupted()); //如果没有 Thread.currentThread().interrupt();而是myt.interrupt()它可能是主线程,因为它是静态方法
System.out.println("线程是打了中断标志吗? "+myt.isInterrupted()); //interrupted();会清除中断标记,所以第二次打印为false
System.out.println("线程是活的吗?"+myt.isAlive());
/**
* 注意:myt线程调用interrupted()并不一定是当前线程打了中断标记
* interrupted()测试 此线程(调用者代表的线程)是否被中断 ,不清除中断状态。
* 确保万无一失的情况下,要Thread.currentThread.interrupt()调 用才是当前线程调用
*/
Thread.currentThread().interrupt();
System.out.println(Thread.interrupted()); //如果没有 Thread.currentThread().interrupt();而是myt.interrupt()它可能是主线程,因为它是静态方法
System.out.println(Thread.interrupted());
System.out.println("线程是活的吗?"+myt.isAlive());
//或 下面代码执行结果一样的
/*Thread.currentThread().interrupt();
System.out.println(myt.interrupted());
System.out.println(myt.interrupted());
System.out.println("线程是活的吗?"+myt.isAlive());*/
}
}
class MyThread1 extends Thread{
@Override
public void run() {
for(int i = 0;i<100;i++) {
System.out.println(Thread.currentThread().getName()+"--"+i);
}
}
}
-----------------------------------------------------------------------------------------------------------------------------------------------
线程是打了中断标志吗? true
线程是打了中断标志吗? true
线程是活的吗?true
true
false
线程是活的吗?true
Thread-0--0
Thread-0--1
Thread-0--2
Thread-0--3
Thread-0--4
Thread-0--5
Thread-0--6
.................................
下面代码有坑
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
thread.interrupt();
System.out.println("第一次调用thread.isInterrupted():" + thread.isInterrupted());
System.out.println("第二次调用thread.isInterrupted():" + thread.isInterrupted());
// 测试interrupted()函数
System.out.println("第一次调用thread.interrupted():" + thread.interrupted());
System.out.println("第二次调用thread.interrupted():" + thread.interrupted());
System.out.println("thread是否存活:" + thread.isAlive());
}
从输出结果看,可能会有疑惑,为什么后面两个interrupted方法输出的都是false,而不是预料中的一个true一个false?
注意!!!这是一个坑!!!上面说到,interrupted()方法测试的是当前线程是否被中断,当前线程!!!当前线程!!!这里当前线程是main线程,而thread.interrupt()中断的是thread线程,这里的此线程就是thread线程。所以当前线程main从未被中断过,尽管interrupted()方法是以thread.interrupted()的形式被调用,但它检测的仍然是main线程而不是检测thread线程,所以thread.interrupted()在这里相当于main.interrupted()。
------只打标记似乎没有作用呀,别急,我们可以在run()方法获取标记值,再用return;等真正中断线程运行。
public class ThreadControllDemo {
public static void main(String args[]) {
Thread myt = new MyThread1();
myt.start();
myt.interrupt(); //打一个中断标志, 并不中断线程,线程会继续运行下去。
System.out.println("线程是打了中断标志吗? "+myt.isInterrupted());
System.out.println("线程是打了中断标志吗? "+myt.isInterrupted());
System.out.println("线程是活的吗?"+myt.isAlive());
}
}
class MyThread1 extends Thread{
@Override
public void run() {
for(int i = 0;i<100;i++) {
System.out.println(Thread.currentThread().getName()+"--"+i);
if(Thread.currentThread().isInterrupted()) { // 或 if(Thread.interrupted())
return;
}
}
}
}
-----------------------------------------------------------------------------------------------------------------------------------------------
线程是打了中断标志吗? true
线程是打了中断标志吗? true
线程是活的吗?true
Thread-0--0
//后面没有了,绝大多数情况下只执行了一条语句
面试题:线程按序交替
-------编写一个程序,开启三个线程,这三个线程的ID分别为A、B、C,每个线程将自己的ID在屏幕上打印10遍,要求输出的结果必须按顺序显示,如:ABCABCABC……依次递归。
三把锁
public class ABCABCTest {
public static void main(String args[]) {
ABCABCThread t = new ABCABCThread();
//A线程
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 1;i<11;i++) {
t.loopA(i);
}
}
},"A").start();
//B线程
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 1;i<11;i++) {
t.loopB(i);
}
}
},"B").start();
//C线程
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 1;i<11;i++) {
t.loopC(i);
}
}
},"C").start();
}
}
class ABCABCThread{
private int number = 1;
private Lock lock = new ReentrantLock();
private Condition condition1= lock.newCondition();
private Condition condition2= lock.newCondition();
private Condition condition3= lock.newCondition();
//A
public void loopA (int totalLoop) {
lock.lock();
try {
if(number != 1) {
condition1.await();
}
//System.out.println(Thread.currentThread().getName()+"\t"+totalLoop);
System.out.print(Thread.currentThread().getName());
number = 2;
condition2.signal(); //唤醒第二个线程
}catch(Exception e) {
}finally {
lock.unlock();
}
}
//B
public void loopB (int totalLoop) {
lock.lock();
try {
if(number != 2) {
condition2.await();
}
//System.out.println(Thread.currentThread().getName()+"\t"+totalLoop);
System.out.print(Thread.currentThread().getName());
number = 3;
condition3.signal(); //唤醒第二个线程
}catch(Exception e) {
}finally {
lock.unlock();
}
}
//c
public void loopC (int totalLoop) {
lock.lock();
try {
if(number != 3) {
condition3.await();
}
//System.out.println(Thread.currentThread().getName()+" "+totalLoop);
System.out.print(Thread.currentThread().getName());
//System.out.println();
number = 1;
condition1.signal(); //唤醒第二个线程
}catch(Exception e) {
}finally {
lock.unlock();
}
}
}
-----------------------------------------------------------------------------------------------------------------------------------------------
ABCABCABCABCABCABCABCABCABCABC
线程池
--------java线程的创建、销毁和线程间切换是一件比较耗费计算机资源的事。如果我们需要用多线程处理任务,并频繁的创建、销毁线程会造成计算机资源的无端浪费,因此我们在真正的项目中,使用的是线程池技术。
实现线程池的好处,总结以下几点:
1.降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
2.提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
3.提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池结构
线程池生命周期
看看ThreadPoolExecutor的源码:
这几种状态的转换过程:
1、线程池在构造前(new操作)是初始状态,一旦构造完成线程池就进入了执行状态RUNNING。
--------严格意义上讲线程池构造完成后并没有线程被立即启动,只有进行“预启动”或者接收到任务的时候才会启动线程。
但是线程池是出于运行状态,随时准备接受任务来执行。
2、线程池运行中可以通过shutdown()和shutdownNow()来改变运行状态。
--------线程池Executor是异步的执行任务,因此任何时刻不能够直接获取提交的任务的状态。这些任务有可能已经完成,也有可能正在执行或者还在排队等待执行。shutdown()是一个平缓的关闭过程,线程池停止接受新的任务,同时等待已经提交的任务执行完毕,包括那些进入队列还没有开始的任务,这时候线程池处于SHUTDOWN状态;shutdownNow()是一个立即关闭过程,线程池停止接受新的任务,同时线程池取消所有执行的任务和已经进入队列但是还没有执行的任务,这时候线程池处于STOP状态。
3、一般情况下我们认为shutdown()或者shutdownNow()执行完毕,线程池就进入TERMINATED状态,此时线程池就结束了。当然,在shutdown/stop到TERMINATED状态之间还存在一个TIDYING状态。
总结以上:
这几个状态的转化关系为:
1、调用shundown()方法线程池的状态由RUNNING——>SHUTDOWN
2、调用shutdowNow()方法线程池的状态由RUNNING——>STOP
3、当任务队列和线程池均为空的时候 线程池的状态由STOP/SHUTDOWN——–>TIDYING
4、当terminated()方法被调用完成之后,线程池的状态由TIDYING———->TERMINATED状态
说线程池的状态转换就要进一步了解线程池中控制线程池生命周期的几个方法:
isShutdown()描述的是非RUNNING状态,也就是SHUTDOWN/STOP/TERMINATED三种状态。
isTerminated()描述的是TERMINATED状态。
awaitTermination()描述的是等待线程池关闭的时间,如果等待时间线程池还没有关闭将会抛出一个超时异常。
- 线程池的创建
线程池的创建可以通过创建 ThreadPoolExecutor 对象或者调用 Executors 的工厂方法来
创建线程池。
但是在阿里巴巴的java开发手册中提到:
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明: Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM(内存溢出)。
2) CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE, 可能会创建大量的线程,从而导致 OOM。
ThreadPoolExecutor的一个构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
1.corePoolSize:核心线程池大小, 新的任务到线程池后,线程池会创建新的线程
(即使有空闲线程),直到核心线程池已满。
2.maximumPoolSize:最大线程池大小,顾名思义,线程池能创建的线程的最大数目
3.keepAliveTime:线程池的工作线程空闲后,保持存活的时间,时间单位为4说明.
4.TimeUnit: 时间单位
5.BlockingQueue
6.threadFactory:线程工厂
7.RejectedExecutionHandler: 当队列和线程池都满了时拒绝任务的策略
重要参数的说明:
corePoolSize 和 maximumPoolSize
--------默认情况下线程池中的线程初始时为 0, 当有新的任务到来时才会创建新线程,当线程数目到达 corePoolSize 的数量时,新的任务会被缓存到 workQueue 队列中。
--------如果不断有新的任务到来,队列也满了的话,线程池会再新建线程直到总的线程数目达到maximumPoolSize。如果还有新的任务到来,则要根据 RejectedExecutionHandler对新的任务进行相应拒绝处理。
BlockingQueue
一个阻塞队列,用来存储等待执行的任务,常用的有如下几种:
1.ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
2.LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
3.SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
4.PriorityBlockingQueue:一个具有优先级得无限阻塞队列。
RejectedExecutionHandler
当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。有下面四种JDK提供的策略:
1.AbortPolicy,表示无法处理新任务时抛出异常, 默认策略--一般用它。
2.CallerRunsPolicy:用调用者所在线程来运行任务。
3.DiscardOldestPolicy: 该策略将丢弃最老的一个请求,也就是丢弃即将被执行的任务,并尝试再次提交当前任务。
4.DiscardPolicy:不处理,丢弃掉除了这些JDK提供的策略外,还可以自己实现 RejectedExecutionHandler 接口定义策略。
public class ThreadPoolTest1 {
public static void main(String args[]) {
//设置核心池大小
int corePoolSize = 5;
//设置线程池最大能接受多少条线程
int maximumPoolSize = 10;
//当前线程数大于corePoolSize时,小于maximumPoolSize时,超出corePoolSize的线程数的生命周期
long keepActiveTime = 200;
//设置时间单位为秒,即上面的200时间为200秒
TimeUnit timeUnit = TimeUnit.SECONDS;
//设置线程池缓存队列的排队策略为FIFO,先进先出,并且指定缓存队列大小为5
BlockingQueue workQueue =new ArrayBlockingQueue(5);
//根据前面我们的设置参数,来创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepActiveTime, timeUnit, workQueue);
for(int i = 1;i<16;i++) {
PoolThread pt=new PoolThread(i);
executor.execute(pt);
System. out .println( "线程池中线程数目:" +executor.getPoolSize() + ",队列中等待执行的任务数目:"+executor.getQueue().size() + ",已执行完的任务数目:"+executor.getCompletedTaskCount());
}
//使用线程池,一定要关掉线程池
executor.shutdown();
}
}
//线程池中的线程对象要用的类
class PoolThread implements Runnable{
//构造方法,初始化一个数num,用于循环创建线程对象( for i)
private int num;
public PoolThread(int num) {
this.num = num;
}
@Override
public void run() {
System.out.println("任务"+num+"开始执行");
try {
Thread.sleep(1000); //任务是深睡1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务"+num+"执行完比");
}
}
ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor实现了 ScheduledExecutorService接口,
该接口定义了 可延时执行异步任务和可周期执行异步任务的特有功能,相应的方法分别为:
//达到给定的延时时间后,执行任务。这里传入的是实现Runnable接口的任务,
//因此通过ScheduledFuture.get()获取结果为null
public ScheduledFuture> schedule(Runnable command,
long delay, TimeUnit unit);
//达到给定的延时时间后,执行任务。这里传入的是实现Callable接口的任务,
//因此,返回的是任务的最终计算结果
public ScheduledFuture schedule(Callable callable,long delay, TimeUnit unit);
//是以上一个任务开始的时间计时,period时间过去后,
//检测上一个任务是否执行完毕,如果上一个任务执行完毕,
//则当前任务立即执行,如果上一个任务没有执行完毕,则需要等上一个任务执行完毕后立即执行,周期性执行任务。
public ScheduledFuture> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
//当达到延时时间initialDelay后,任务开始执行。上一个任务执行结束后到下一次
//任务执行,中间延时时间间隔为delay。以这种方式,周期性执行任务。
public ScheduledFuture> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
再来一个代码案例,实现周期执行某事的多线程程序,也可以说是进行线程调度:
public class TestScheduledTread {
public static void main(String[] args) {
ScheduledThreadPoolExecutor scheduled = new ScheduledThreadPoolExecutor(10);
scheduled.scheduleAtFixedRate( new Runnable() {
@Override
public void run() {
System. out .println(Thread. currentThread ().getName());
}
}, 0, 1, TimeUnit.SECONDS );
//0表示首次执行任务的延迟时间,
// 1表示每次执行任务的间隔时间为1秒,
// TimeUnit.MILLISECONDS执行的时间间隔数值单位
}
}
--------------------------------------------------------------------------------------------------------------------
pool-1-thread-2
pool-1-thread-1
pool-1-thread-3
pool-1-thread-3
pool-1-thread-4
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,一直循环下去,但只有10个线程
读写锁ReadWriteLock
① ReadWriteLock同Lock一样也是一个接口,
提供了readLock和writeLock两种锁的操作机制,一个是读锁,一个是写锁。
----读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的(排他的)。每次只能有一个写线程,但是可以有多个线程并发地读数据。
所有读写锁的实现必须确保写操作对读操作的内存影响。
----换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。
理论上,读写锁比互斥锁允许对于共享数据更大程度的并发。
----与互斥锁相比,读写锁是否能够提高性能取决于读写数据的频率、读取和写入操作的持续时间、以及读线程和写线程之间的竞争。
② 使用场景
假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。
例如,最初填充有数据,然后很少修改的集合,同时频繁搜索(例如某种目录)是使用读写锁的理想候选项。
在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。
但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写。这就需要一个读/写锁来解决这个问题。
③ 互斥原则:
读-读能共存,读-写不能共存,写-写不能共存。
public class ThreadTest {
public static void main(String args[]) {
ReadWriteLockTest rwlt = new ReadWriteLockTest();
// 开启10条线程读
for (int i = 1; i < 11; i++) {
new Thread(new Runnable() {
@Override
public void run() {
rwlt.get();
}
}, "A").start();
}
// 开启一条线程来写。读线程很多,写线程很少甚至只一条
new Thread(new Runnable() {
@Override
public void run() {
rwlt.set(12);
}
}, "B").start();
}
}
class ReadWriteLockTest {
// 定义一个线程访问的共享数据
private int num = 0;
// 用到读写锁
ReadWriteLock rwl = new ReentrantReadWriteLock();
// 定义两个方法
public void get() {
rwl.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + ":" + num);
} finally {
rwl.readLock().unlock();
}
}
public void set(int num) {
// 用写锁
rwl.writeLock().lock();
try {
this.num = num;
System.out.println(Thread.currentThread().getName() + ":" + num);
} finally {
rwl.writeLock().unlock();
}
}
}
--------------------------------------------------------------------------------------------------------------------
A:0
A:0
A:0
A:0
A:0
B:12
A:12
A:12
A:12
A:12
A:12 //如果没有读写锁,那么B后面的A全为0
ReentrantReadWriteLock的进一步分析
ReadWriteLock是接口,我们实现读写锁实质是用接口的实现类 ReentrantReadWriteLock
① ReentrantReadWriteLock拥有的特性
1.1获取顺序(公平和非公平)
ReentrantReadWriteLock不会为锁定访问强加读或者写偏向顺序,但是它确实是支持可选的公平策略。
非公平模式(默认)比公平锁更高的吞吐量。
1.2可重入
写锁(写线程)可以在不释放已经拥有的写锁的情况下,重新获取读锁,但是不允许读锁(读
线程)获取写锁。
1.3锁降级
可重入特性还允许从写锁降级到读锁—通过获取写锁,然后获取读锁,然后释放写锁。但
是,从读锁到写锁的升级是不可能的。
当前线程拥有写锁,然后将其释放,最后再获取读锁,这种并不能称之为锁降级,锁降级指的是把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前有用的)写锁的过程。
通过这种重入,可以减少一步流程——释放写锁后 再次 获取读锁。
使用了锁降级,就可以减去 释放写锁的步骤。直接获取读锁。效率更高。
1.4锁获取的中断
在读锁和写锁的获取过程中支持中断 。
1.5支持Condition
写锁提供了Condition实现,ReentrantLock.newCondition;读锁不支持Condition。
1.6监控
该类支持确定锁是否持有或争用的方法。这些方法是为了监视系统状态而设计的,而不是用
于同步控制。
② 特性使用实例
2.1锁降级代码
//锁降级,不是锁切换,锁不能升级
public class ThreadTest2 {
public static void main(String args[]) {
CacheData cd = new CacheData();
for(int i=1;i<=10;i++) {
new Thread(new Runnable() {
@Override
public void run() {
cd.putCacheData(Thread.currentThread().getName()+"放入的新数据");
}
},"t"+i).start();
}
}
}
class CacheData{
//定义一个线程共享的变量
private String data = "共享变量原有的值";
//定义一个标记,是新值,还是过期值
private volatile boolean isUpdate; //volatile使变量是透明的
//产生读写锁对象
private ReadWriteLock rwl = new ReentrantReadWriteLock();
//模拟放置数据到缓存
public void putCacheData(String data) {
//获取写锁之前,先获取读锁读缓存内容
rwl.readLock().lock();
if(!isUpdate) {
rwl.readLock().unlock(); //缓存中不是更新的,则要去更新数据,拿写锁,这不是升级,因为读锁现在没有了
//获取写锁
rwl.writeLock().lock();
try {
if(!isUpdate) {
//更新缓存
this.data = data;
isUpdate = true;
}
//拥有写锁的情况下,是可以重入读锁
rwl.readLock().lock();
}finally {
rwl.writeLock().unlock(); //里写的时候有读锁,这才叫锁降级
}
}
//使用读锁,来读缓存
try {
System.out.println("最新的缓存数据打印一下:"+this.data);
}finally {
rwl.readLock().unlock();
}
}
}
-----------------------------------------------------------------------------------------------------------------------------------------------
最新的缓存数据打印一下:t1放入的新数据
最新的缓存数据打印一下:t1放入的新数据
最新的缓存数据打印一下:t1放入的新数据
最新的缓存数据打印一下:t1放入的新数据
最新的缓存数据打印一下:t1放入的新数据
最新的缓存数据打印一下:t1放入的新数据
最新的缓存数据打印一下:t1放入的新数据
最新的缓存数据打印一下:t1放入的新数据
最新的缓存数据打印一下:t1放入的新数据
最新的缓存数据打印一下:t1放入的新数据 //"放入的新数据"为data的字符串值由写锁锁定后写入的
2.2集合使用场景代码
通常可以在集合使用场景中看到ReentrantReadWriteLock的身影。
不过只有在集合比较大,读操作比写操作多,操作开销大于同步开销的时候才是值得的。
//集合场景中读写锁的应用,只有读操作比写操作多的时候才值的
public class ThreadTest3 {
public static void main(String args[]) {
}
}
class CollectionRWLTest{
//线程共享的集合
private Map map = new TreeMap<>(); //该对象的键值对,会按键排序
//获取读写锁,先读锁,再写锁
private ReadWriteLock rw = new ReentrantReadWriteLock();
Lock r = rw.readLock();
Lock w = rw.writeLock();
//通过集合的key获取集合的值
public Object get(String key) {
r.lock();
try {
return map.get(key);
}finally {
r.unlock();
}
}
//获取集合里的所有的key
public String[] allKeys() {
r.lock();
try {
Set rsSet = map.keySet();
return rsSet.toArray(new String[rsSet.size()]);
}finally {
r.unlock();
}
}
//放置进入集合
public Object put(String key,Object val) {
w.lock();
try {
return map.put(key, val);
}finally {
w.unlock();
}
}
}
ForkJoinPool 分支合并框架
ForkJoinPool 这个工具从Java7 才开始提供的, 优势在于,可以充分利用多cpu,多核cpu的优势,把一个任务拆分成多个“小任务”,把多个“小任务”放到多个处理器核心上并行执行;
当多个“小任务”执行完成之后,再将这些执行结果合并起来即可。最终,实现用少量的线程,完成大数量的任务。
ForkJoinPool是ExecutorService的实现类,因此是一种特殊的线程池
使用方法:
创建了ForkJoinPool实例之后,就可以调用ForkJoinPool的submit(ForkJoinTask
其中ForkJoinTask代表一个可以并行、合并的任务。ForkJoinTask是一个抽象类,它还有两个抽象子类:RecusiveAction和RecusiveTask。其中RecusiveTask代表有返回值的任务,而RecusiveAction代表没有返回值的任务。
- 例:大任务:打印 1--300之间的整数,对半拆分,递归执行
public class ThreadTest1 {
public static void main(String args[]) {
//创建大任务
PrintTask pt = new PrintTask(1, 300);
//获取工具类
ForkJoinPool fjp = new ForkJoinPool(); //构造的参数,可以自已传入线程数量,默认空的,默认值会自动设置为计算机的cpu个数
fjp.submit(pt);
try {
fjp.awaitTermination(2, TimeUnit.SECONDS); //让线程池的关闭,等待2秒后再动作,这是为了保证线程执行完
}catch(InterruptedException e) {
e.printStackTrace();
}
fjp.shutdown(); //关闭线程池
}
}
//ForkJoinTask是一个抽象类,它还有两个抽象子类:RecusiveAction和RecusiveTask。其中RecusiveTask代表有返回值的任务,而RecusiveAction代表没有返回值的任务。
//大任务:打印 1--300之间的整数
class PrintTask extends RecursiveAction{
private static final long serialVersionUID = 1L;
//定义一个极限值
private static final int THRESHOLD = 30; //单独的一条线程,一次最多打印30个数
private int start; //小任务打印的开头的数
private int end; //小任条打印的尾数
public PrintTask(int start,int end) { //构造器
this.start = start;
this.end = end;
}
@Override
protected void compute() { //主要作用就是实现拆分任务,使用递归的方式,把大任务一级一级的拆分成小任务
if((end-start)
- 通过RecursiveTask的返回值,来对一个长度为300的数组元素进行累加。
//把大任,分成左右,两半多个任务执行求和,有合并功能,再返回值(join()来做)
public class ThreadTest2 {
public static void main(String args[]) throws InterruptedException, ExecutionException {
// 大任务是求300元素的数组里的,成员之和
// 构造一个数组
int[] arr = new int[300];
int sum=0; //验证结果对不对用到的变量
Random random = new Random();
for (int i = 0; i < arr.length; i++) {
int tmp =random.nextInt(20); //返回0-19的随机正整数
// System.out.println(tmp);
arr[i] = tmp;
sum+=arr[i];
}
System.out.println("主线程单独做的求和: "+sum);
//用工具类SumTask来实现求和
SumTask st = new SumTask(arr, 0, arr.length-1);
ForkJoinPool pool = ForkJoinPool.commonPool(); //获取线程池对象
Future future = pool.submit(st); //返回一个ForkJoinPool,
System.out.println("多子任任多Cpu最终的数组元素所有之和为: "+future.get());
pool.shutdown();
}
}
//该类有返回值的任务拆分合并功能
class SumTask extends RecursiveTask{
private static final long serialVersionUID = 1L;
private static final int THRESHOLD = 20 ; //设一个极限值
private int[] arr;
private int start;
private int end;
public SumTask(int[] arr,int start,int end) {
this.arr= arr;
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
if(end - start <= THRESHOLD) { //一序列数个数小于极限值时,直接(主方法)做
for(int i = start;i<=end;i++) {
sum+= arr[i];
}
return sum;
}else {
int middle = (start+end)/2;
SumTask left = new SumTask(arr, start, middle);
SumTask right = new SumTask(arr, middle+1, end);
left.fork();
right.fork();
//要把结果进行合并,
return left.join()+right.join();
}
}
}
-----------------------------------------------------------------------------------------------------------------------------------------------
主线程单独做的求和: 2906
多子任任多Cpu最终的数组元素所有之和为: 2906
分析总结
在Java 7中引入了一种新的线程池:ForkJoinPool。
它同ThreadPoolExecutor一样,也实现了Executor和ExecutorService接口。
它使用了一个无限队列来保存需要执行的任务,而线程的数量则是通过构造函数传入,如果没有向构造函数中传入希望的线程数量,那么当前计算机可用的CPU数量会被设置为线程数量作为默认值。
-----那么使用ThreadPoolExecutor或者ForkJoinPool,会有什么性能的差异呢?
首先,使用ForkJoinPool能够使用 数量有限的线程来完成非常多的具有父子关系的任务,比如使用4个线程来完成超过200万个任务。但是,使用ThreadPoolExecutor时,是不可能完成的,因为ThreadPoolExecutor中的Thread无法选择优先执行子任务,需要完成200万个具有父子关系的任务时,也需要200万个线程,显然这是不可行的。