【JAVA 提高班之七】多线程互斥和协作

多线程是个庞大的话题,从不同的角度有不同的讲述,本文专注于从多线程的协作角度,有互斥关系,合作关系,父子关系,线程池。

线程状态图的演进

一.线程基本状态图

这里写图片描述
普通的Thread对象通过调用Start()方法进入就绪态(Runnable),在Runnable状态下是可以被CPU调度的,即获取CPU时间片进入运行态调用Run方法,在获取到的时间片结束之后可以继续返回到就绪态。但是如果在运行态发生阻塞式的操作,比如硬盘IO,网络IO等,线程在等待结果时会导致CPU空转,因此操作系统会把线程调入阻塞态不参与线程调度,直到线程获取到返回结果之后重新进入就绪态。在线程的Run方法执行完成之后线程就会完成任务,进入死亡状态。有一篇文章我是一个线程用拟人的手法讲述了这个过程,大家可以看一下。

这里有几个关于阻塞式和非阻塞式、同步和异步的概念进行一下澄清。

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时线程所处的状态

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication),同步就是由调用者主动等待这个调用的结果,无论是不断轮训还是阻塞等待的实现方式。而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

二、互斥同步的线程状态图

【JAVA 提高班之二】Volatile用法详解 中提到过在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题。通过使用Java提供的synchronized和Lock机制是可以做到以上三点的。而具体是怎么实现的呢?

Java中的每个对象都有一个锁(lock),或者叫做监视器(monitor),当一个线程访问某个对象的synchronized方法时,线程就会进入该对象的监视器的Entry Set尝试获取该对象上的锁,一旦获取到锁,其他任何线程都无法再去访问该对象的synchronized方法了(这里是指所有的同步方法,而不仅仅是同一个方法),直到之前的那个线程执行方法完毕后(或者是抛出了异常,或者是主动Release锁),才将该对象的锁释放掉,其他线程才有可能再去访问该对象的synchronized方法。

监视器的结构

这里写图片描述

监视器是一种同步结构,它允许线程同时互斥(使用锁)和协作,即使用等待集(wait-set)使线程等待某些条件为真的能力。

互斥

使用比较形象的说明,监视器就像一个包含一个特殊房间(对象实例)的建筑物,每次只能占用一个线程。这个房间通常包含一些需要防止并发访问的数据。从一个线程进入这个房间到它离开的时间,它可以独占访问房间中的任何数据。进入监控的建筑被称为“进入监控监视器。”进入建筑内部特殊的房间叫做“获取监视器”。房间占领被称为“拥有监视器”,离开房间被称为“释放监视器。”让整个建筑被称为“退出监视器。”

当一个线程访问受保护的数据(进入特殊的房间)时,它首先在建筑物接收(entry-set)中排队。如果没有其他线程在等待(拥有监视器),线程获取锁并继续执行受保护的代码。当线程完成执行时,它释放锁并退出大楼(退出监视器)。

如果当一个线程到达并且另一个线程已经拥有监视器时,它必须在接收队列中等待(entry-set)。当当前所有者退出监视器时,新到达的线程必须与在入口集中等待的其他线程竞争。只有一个线程能赢得竞争并拥有锁。

这里没有wait-set的事。

合作

一般来说,只有当多个线程共享数据或其他资源时,互斥才是重要的。如果两个线程不处理任何公共数据或资源,它们通常不能互相干扰,也不需要以互斥的方式执行。尽管互斥有助于防止线程在共享数据时互相干扰,但合作有助于线程共同努力实现一些共同目标。

合作在当一个线程需要的数据改变为在一个特定的状态时很重要,另一个线程负责将数据该入状态,如生产者/消费者的问题,读线程需要缓冲去在一个“不空”的状态才可以从缓冲区中读取任何数据。如果读线程发现缓冲区为空,则必须等待。写线程负责用数据填充缓冲区。一旦写入线程完成了更多的写入操作,读线程可以进行更多的读取。它有时也称为“Wait and Notify”或“Signal and Continue”监视器,因为它保留对监视器的所有权,并继续执行监视区域(如果需要的话继续)。在稍后的时间内,通知线程释放监视器,等待线程重新恢复拥有锁。

所以整体上说引入了同步的概念之后线程的状态更加的复杂了,这一小节讲一下互斥情况下的状态图。
这里写图片描述

三、相互协作的线程状态图

在第二节里面已经描述的在Monitor这个Building里面还有和协作相关的几个集合。这里面的状态迁移和Object的几个方法相关。

    public final native void notify();

    public final native void notifyAll();

    public final native void wait(long timeout) throws InterruptedException;

wait()、notify()、notifyAll()是三个定义在Object类里的方法,可以用来控制线程的状态。
这三个方法最终调用的都是jvm级的native方法。随着jvm运行平台的不同可能有些许差异。

  • 如果对象调用了wait方法就会使持有该对象的线程把该对象的控制权交出去,然后处于等待状态(进入对象的Wait Set)。
  • 如果对象调用了notify方法就会通知某个正在等待这个对象的控制权的线程可以继续运行(具体唤醒哪一个是由JVM确定的),此时被唤醒的线程会重新进入Entry Set尝试获取锁。
  • 如果对象调用了notifyAll方法就会通知所有等待这个对象控制权的线程继续运行。

sleep():在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),该线程不丢失任何监视器的所属权,sleep() 是 Thread 类专属的静态方法,针对一个特定的线程。

join() 方法定义在 Thread 类中,所以调用者必须是一个线程,join() 方法主要是让调用该方法的 Thread 完成 run() 方法里面的东西后,再执行 join() 方法后面的代码。

具体使用参照下例。

package org.HelloWorld;

public class Test1 {
    /*
     * 两个线程,一个打印1-100的奇数,一个打印1-100的偶数;要求:线程1打印1个之后,线程2开始打印,线程2打印1个之后,线程1再开始打印,以此循环。
     */
    private static int state = 1;
    private static int num1 = 1;
    private static int num2 = 2;

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        final Test1 t = new Test1();
        new Thread(new Runnable() {
            public void run() {
                while (num1 < 100) {
                    // 两个线程都用t对象作为锁,保证每个交替期间只有一个线程在打印
                    synchronized (t) {
                        // 如果state!=1, 说明此时尚未轮到线程1打印, 线程1将调用t的wait()方法, 直到下次被唤醒
                        if (state != 1) {
                            try {
                                t.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        System.out.println(num1);
                        num1 += 2;
                        // 线程1打印完成后, 将state赋值为2, 表示接下来将轮到线程2打印
                        state = 2;
                        // notifyAll()方法唤醒在t上wait的线程2, 同时线程1将退出同步代码块, 释放t锁
                        t.notifyAll();
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                while (num2 < 100) {
                    synchronized (t) {
                        if (state != 2) {
                            try {
                                t.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }

                        System.out.println(num2);
                        num2 += 2;

                        state = 1;
                        t.notifyAll();
                    }

                }
            }
        }).start();
    }
}

这里写图片描述

父子线程

对于父子线程,有两种情况:

  • 第一种是父线程是进程的主线程,子线程由主线程创建;
  • 第二种情况是父线程为进程主线程创建的一个子线程,而这个子线程又创建了一个孙线程,这种情况大多被称为子孙线程。

父线程调用Thread的Start方法之后会马上返回,面对第一种情况,如果主线程退出,相关的子线程也会退出;而在第二种情况下,由于孙线程并没有使用任何子线程的资源,如果子线程退出并不会影响孙线程的运行。

如果父线程需要子线程的运算结果时,可以采用Join方法进行同步,具体可见下面的例子:

package org.HelloWorld;

public class JoinThread extends Thread {
    private static Integer n = 0;

    public void run() {
        synchronized (JoinThread.class) {
            for (int i = 0; i < 10; i++) {
                n++;
            }
        }
    }

    public static void main(String[] args) throws Exception {
        Thread threads[] = new Thread[100];
        for(int j = 0; j<10; j++, JoinThread.n = 0) {
        for (int i = 0; i < threads.length; i++) // 建立100个线程
            threads[i] = new JoinThread();
        for (int i = 0; i < threads.length; i++) // 运行刚才建立的100个线程
            threads[i].start();
        if (args.length > 0)
            for (int i = 0; i < threads.length; i++) // 100个线程都执行完后继续
                threads[i].join();
        System.out.println("n=" + JoinThread.n);
        }
    }
}

线程池

线程是稀缺资源,如果被无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,合理的使用线程池对线程进行统一分配、调优和监控,有以下好处:
1、降低资源消耗;
2、提高响应速度;
3、提高线程的可管理性。

Java1.5中引入的Executor框架把任务的提交和执行进行解耦,只需要定义好任务,然后提交给线程池,而不用关心该任务是如何执行、被哪个线程执行,以及什么时候执行。

demo
这里写图片描述

  • 1、Executors.newFixedThreadPool(10)初始化一个包含10个线程的线程池executor;
  • 2、通过executor.execute方法提交20个任务,每个任务打印当前的线程名;
  • 3、负责执行任务的线程的生命周期都由Executor框架进行管理;

ThreadPoolExecutor
Executors是java线程池的工厂类,通过它可以快速初始化一个符合业务需求的线程池,如Executors.newFixedThreadPool方法可以生成一个拥有固定线程数的线程池。
这里写图片描述
其本质是通过不同的参数初始化一个ThreadPoolExecutor对象,具体参数描述如下:

corePoolSize

线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。

maximumPoolSize

线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize;

keepAliveTime

线程空闲时的存活时间,即当线程没有任务执行时,继续存活的时间;默认情况下,该参数只在线程数大于corePoolSize时才有用;

unit

keepAliveTime的单位;

workQueue

用来保存等待被执行的任务的阻塞队列,且任务必须实现Runable接口,在JDK中提供了如下阻塞队列:
1、ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务;
2、LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene;
3、SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene;
4、priorityBlockingQuene:具有优先级的无界阻塞队列;

实现原理
除了newScheduledThreadPool的内部实现特殊一点之外,其它几个线程池都是基于ThreadPoolExecutor类实现的。

线程池内部状态
线程内部状态
其中AtomicInteger变量ctl的功能非常强大:利用低29位表示线程池中线程数,通过高3位表示线程池的运行状态:

  • 1、RUNNING:-1 << COUNT_BITS,即高3位为111,该状态的线程池会接收新任务,并处理阻塞队列中的任务;
  • 2、SHUTDOWN: 0 << COUNT_BITS,即高3位为000,该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
  • 3、STOP : 1 << COUNT_BITS,即高3位为001,该状态的线程池不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
  • 4、TIDYING : 2 << COUNT_BITS,即高3位为010;
  • 5、TERMINATED: 3 << COUNT_BITS,即高3位为011;

任务提交流程
线程池框架提供了两种方式提交任务,根据不同的业务需求选择不同的方式。
Executor.execute()
这里写图片描述
通过Executor.execute()方法提交的任务,必须实现Runnable接口,该方式提交的任务不能获取返回值,因此无法判断任务是否执行成功。

ExecutorService.submit()
这里写图片描述
通过ExecutorService.submit()方法提交的任务,可以获取任务执行完的返回值。

任务执行
这里写图片描述
当向线程池中提交一个任务,线程池会如何处理该任务?
execute实现
这里写图片描述
具体的执行流程如下:
1、workerCountOf方法根据ctl的低29位,得到线程池的当前线程数,如果线程数小于corePoolSize,则执行addWorker方法创建新的线程执行任务;否则执行步骤(2);
2、如果线程池处于RUNNING状态,且把提交的任务成功放入阻塞队列中,则执行步骤(3),否则执行步骤(4);
3、再次检查线程池的状态,如果线程池没有RUNNING,且成功从阻塞队列中删除任务,则执行reject方法处理任务;
4、执行addWorker方法创建新的线程执行任务,如果addWoker执行失败,则执行reject方法处理任务;

addWorker实现
从方法execute的实现可以看出:addWorker主要负责创建新的线程并执行任务,代码实现如下:
这里写图片描述

这只是addWoker方法实现的前半部分:

  • 1、判断线程池的状态,如果线程池的状态值大于或等SHUTDOWN,则不处理提交的任务,直接返回;
  • 2、通过参数core判断当前需要创建的线程是否为核心线程,如果core为true,且当前线程数小于corePoolSize,则跳出循环,开始创建新的线程,具体实现如下:

这里写图片描述

线程池的工作线程通过Woker类实现,在ReentrantLock锁的保证下,把Woker实例插入到HashSet后,并启动Woker中的线程,其中Worker类设计如下:
1、继承了AQS类,可以方便的实现工作线程的中止操作;
2、实现了Runnable接口,可以将自身作为一个任务在工作线程中执行;
3、当前提交的任务firstTask作为参数传入Worker的构造方法;

这里写图片描述

从Woker类的构造方法实现可以发现:线程工厂在创建线程thread时,将Woker实例本身this作为参数传入,当执行start方法启动线程thread时,本质是执行了Worker的runWorker方法。

runWorker实现
这里写图片描述

runWorker方法是线程池的核心:
1、线程启动之后,通过unlock方法释放锁,设置AQS的state为0,表示运行中断;
2、获取第一个任务firstTask,执行任务的run方法,不过在执行任务之前,会进行加锁操作,任务执行完会释放锁;
3、在执行任务的前后,可以根据业务场景自定义beforeExecute和afterExecute方法;
4、firstTask执行完成之后,通过getTask方法从阻塞队列中获取等待的任务,如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源;

getTask实现
这里写图片描述
整个getTask操作在自旋下完成:
1、workQueue.take:如果阻塞队列为空,当前线程会被挂起等待;当队列中有任务加入时,线程被唤醒,take方法返回任务,并执行;
2、workQueue.poll:如果在keepAliveTime时间内,阻塞队列还是没有任务,则返回null;

所以,线程池中实现的线程可以一直执行由用户提交的任务。

总结

线程池中的线程数在一定范围内是按需增加的,也可以做到按需减少。

线程池中的每个线程其实都是在执行一个从任务队列取任务的自旋,有任务就调用Run方法(不需要去封装Runnable对象接口);如果工作队列为空,则线程自动挂起,有任务时线程被唤醒。

线程被阻塞可能是由于下面五方面的原因:(《Thinking in Java》)

  1.调用sleep(毫秒数),使线程进入睡眠状态。在规定时间内,这个线程是不会运行的。

  2.用suspend()暂停了线程的执行。除非收到resume()消息,否则不会返回“可运行”状态。

  3.用wait()暂停了线程的执行。除非线程收到notify()或notifyAll()消息,否则不会变成“可运行”状态。

  4.线程正在等候一些IO操作完成。

  5.线程试图调用另一个对象的“同步”方法,但那个对象处于锁定状态,暂时无法使用。

参考文献:
http://www.cnblogs.com/hongten/archive/2013/03/27/hongten_thread_5.html
http://www.cnblogs.com/mengdd/archive/2013/02/20/2917966.html
http://www.cnblogs.com/mengdd/archive/2013/02/16/2913806.html
http://www.jianshu.com/p/87bff5cc8d8c

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值