Java并发基础

作者: _Once1 | 来源:发表于2019-02-17 20:39 被阅读0次

    并发基础


    线程

    表示一条单独的执行流,有自己自己单独的程序计数器和栈;

    1.1 创建方法

    • 继承Thread类
    • 实现Runnable接口
      如果不是调用Thread.start开启线程,而是直接调用其run方法,那就不会有开启一个新线程的作用,这种情况下,run方法只是作为一个普通方法被调用的;

    1.2 基本属性

    • id和name
      id是一个递增的整数,每创建一个线程就会加一;
    • 优先级
      Java中1-10,默认为5;
      这里需要注意,设置优先级对于操作 系统而言只是一个建议,编程时不要过分依赖优先级
    • 状态
      可以用Thread的getState()方法得到线程的状态,得到的值是一个枚举类型,如下:

    NEW:没有调用start的线程
    RUNNABLE:调用start后,正在执行run方法并且没有阻塞的状态;注意:线程在运行或者具备运行条件,只是在等待操作系统调度
    BLOCKED:线程在等待锁,视图进入同步块
    WAITING: 在等待某个条件
    TIMED_WAITING: 在等待超时
    TERMINATED: 运行结束后的状态

    1.3 基本方法

    • isAlive()
      启动后,run方法运行结束前,返回都是true
    • isDaemon()
      先看下什么是守护线程,对于一般的线程,程序在所有线程都结束后,才会退出,但是对于守护线程,当整个程序剩下的都是daemon线程时,就会退出;
      daemon线程一般是其他线程的辅助线程
    • sleep()
      让线程睡眠指定时间,睡眠期间,该线程会让出CPU;
      注意:这里传入的时间不一定会精准;
    • yield()
      该方法会建议调度器,目前当前线程不着急执行,可以先让其他线程运行
    • join()
      可以让调用join的线程(例如主线程)等待该线程(执行计算的子线程)执行结束

    1.4 多线程可能存在的问题

    • 竞态条件:指执行结果不确定,和执行时序有关
      可通过synchroniezd关键字、使用显示锁、使用原子变量解决
    • 内存可见性
      造成这种问题的原因是,数据会被存储在各种高速缓存中,当访问/修改一个变量时,不一定会直接从内存中读取/写入,这就可能导致一个线程对值的修改,另一个线程无法及时更新到;
      可以通过volatile、synchronized关键字或者显示锁方式解决

    1.5 优缺点

    • 优点:充分利用CPU和硬件资源,保证GUI及时刷新等
    • 缺点:
      创建线程需要耗费系统资源,为线程创建程序计数器,栈等都是需要开销的;
      线程的切换也是有成本的,主要是上下文切换带来的成本, 当切换时,需要保存当前线程的上下文状态(包括程序计数器的值,CPU寄存器的值等)到内存中

    synchronized的理解

    2.1 用法

    可用于修饰类的:

    • 静态方法
      保护的是当前的类对象
    • 实例方法
      保护的是这个实例,这里需要注意:多个线程是可以同时执行同一个synchronized修饰的实例方法的,只要它们针对的是不同的对象即可。
      因此,需要明确一点:

    synchronized修饰实例方法,保护的是当前的实例对象,即this;每一个对象都有一个锁和等待队列,同一时间,锁只能被一个线程所持有;具体执行synchronized实例方法的过程如下:
    1) 尝试获得锁,若得到,则执行,否则,加入等待队列,阻塞并等待唤醒
    2) 执行方法
    3) 释放锁,如果等待队列有线程,则取一个并将其唤醒;注意,如果有多个等待线程,则唤醒哪一个是不一定的,不保证公平性

    • 代码块
      任意对象都有一个锁和等待队列,也就是说任何对象都可以作为锁对象

    注意: synchronized关键字保护的是对象而不是具体的代码,理解这一点是很重要的。只要访问的是同一个对象的synchronized方法,即使是不同的代码,也会被保证同步顺序方法。
    并且,只能保证加了synchronized修饰的方法同步执行,synchronized方法无法保证非synchronized方法被同时执行;因此,在保护变量时,需要在所有访问该变量的方法上加synchronized修饰。

    2.2 特点

    • 可重入性:当其获得了锁后,当进入需要同样锁的代码时,可以直接进入,而无需再等待
    • 提供内存可见性:如果只是为了获得可见性的话,优先考虑更加轻量的volatile关键字
    • 可能产生死锁

    当使用synchronized时,需要特别注意修饰的对象是否是同一个,即是否使用了相同的锁;


    线程间的协作

    3.1 wait/notify

    除了用于锁的等待队列, 线程还有另一个等待队列, 表示条件队列,用于线程间的协作。
    当调用了wait之后,就会把当前线程加入条件队列并阻塞, 表示当前线程执行不下去了,需要等待一个条件,这个条件自己改变不了,需要其他线程改变,当其他线程改变了条件后,应该调用notify方法。

    wait/notify方法只能在synchronized代码块内被调用,否则会抛异常。

    wait的具体过程

    1. 把当前的线程加入条件队列,释放对象锁,阻塞等待,线程状态变为WAITING或者TIME_WAITING
    2. 等待时间到或者被其他线程调用notify/notifyAll从条件队列中移除,这时,需要重新竞争对象锁:
      a) 可以获得,线程状态变为RUNNABLE,从wait调用中返回
      b) 无法获得,该线程会加入对象锁等待队列,线程状态变为BLOCKED,获得锁后才会从wait调用中返回;

    从wait调用中返回后,不代表其等待条件就一定成立,需要重新检查等待条件:

    synchronized (obj) {
        while(条件不成立) {
            obj.wait();
        }
        // do sth
    }
    

    调用notify后,并不会释放对象锁,只有在包含notify的synchronized代码块执行结束后,等待的线程才会从wait调用中返回

    总结:wait/notify被不同的线程调用,但是二者共享相同的锁和条件等待队列(即相同锁对象的synchronized代码块内),二者围绕一个共享的条件变量进行协作,这个变量是程序自己维护的,当不满足时,wait并进入条件等待队列,另一个线程修改了该条件变量并调用了notify,然后调用wait的线程被唤醒,该线程需要重新检查条件变量。在使用wait/notify时,需要明确协作的共享变量和条件是什么。

    3.2 生产者/消费者模式

    Java提供的阻塞队列有:

    • BlockingQueue
    • ArrayBlockingQueue
    • LinkedBlockingQueue等

    3.3 同时开始

    其他线程都先wait,条件满足后notifyAll即可

    3.4 等待结束

    以未就绪线程数量为条件,一个线程就绪后,将条件-1,当条件为0时,notifyAll即可
    Java提供了CountDownLatch用于这种情况

    3.5 异步结果

    Java提供的主要涉及到的是:

    • 表示异步结果的接口Future和其实现FutureTask
    • 用于执行异步任务的接口Executor,和具有更多功能的子接口ExecutorService
    • 创建上面两种Executor的工厂类Executors

    3.5 集合点

    当所有线程都执行结束后,到达集合点,交换数据并进行下一步动作;这种和等待结束是类似的;
    Java提供了CyclicBarrier


    线程的中断

    4.1 中断

    主要用到的机制是中断,下面来看下中断。
    中断并不是强迫终止一个线程,它是一种协作机制,是传递给线程一个取消信号,但何时退出是由线程来决定的。
    Java主要提供了下面几个方法:

    • isInterrupted():返回当前线程的中断标志位是否为true
    • interrupt():中断对应的线程
    • static interrupted():返回当前线程的中断标志位是否为true,并清空中断标志位为false

    4.2 线程对中断的反应

    根据线程当前的状态:调用interrupt()后的变化如下:

    • RUNNABLE:只是设置中断标志位,线程应该自己检查该标志位的状态,例如,如果它是true那就应该退出循环
    • WAITING/TIMED_WAITING:会清空中断标志位,并抛出InterruptedException,该异常是受检查异常,必须处理:
      1) 向上传递
      2) 无法传递时(例如在run方法中),则需要进行合适的清理工作,并调用interrupt方法设置中断标志位,让其他代码知道其发生了中断
    • BLOCKED:只是设置标志位,线程状态不会变化。
      这里需要注意,使用synchronized关键字获取锁的过程中,不会相应中断请求,这时synchronized的局限性,如果这对程序是个问题,那就应该使用显示锁
    • NEW/TERMINATE:无效,标志位也不会变化

    4.3 如何取消/关闭线程

    如果不清楚线程在做什么,不要贸然使用interrupt方法;
    具体的取消方法可以参考原生实现:Future接口的cancle()、ExecutorService的shutdown(),shutdownNow()等

    相关文章

      网友评论

        本文标题:Java并发基础

        本文链接:https://www.haomeiwen.com/subject/phjneqtx.html