Java并发基础

并发基础


线程

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

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并发基础)