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的具体过程
- 把当前的线程加入条件队列,释放对象锁,阻塞等待,线程状态变为WAITING或者TIME_WAITING
- 等待时间到或者被其他线程调用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()等