Java多线程笔记
这篇笔记是总结我看的《Java核心技术》并发一章。
要说线程,首先要说进程,操作系统运行程序的基本单位是进程,如今的操作系统早已是多任务操作系统,在同一时刻运行多个任务。操作系统将CPU的时间片分配每一个进程,给人并行处理的感觉。线程在低层次上扩展了进程,一个进程通常包含多个线程。区别在于进程拥有自己的一整套变量,线程则共享数据。线程之间的通信比进程更有效,容易。创建,销毁一个线程比进程开销小。
创建线程的方式
继承自Thread类
class MyThread extends Thread {
@Override
public void run() {
super.run();
//your code
}
}
实现Runnable接口
Runnable myRunnable = () -> {
//your code }; new Thread(myRunnable).start();
中断线程
当线程的run方法执行完方法体最后一条语句后,或出现了在方法中没有捕获的异常时,线程将终止。
没有可以强制线程终止的方法,interrupt()方法可以用来请求终止线程。调用interrupt(),线程的中断状态被置位,中断状态是一个boolean标志。通过调用isInterrupted()方法来判断当前线程是否被中断。
当在一个被阻塞的线程(调用sleep()或者wait()方法)上调用interrupt()时,阻塞调用会被InterruptedException异常中断
如果在中断状态被置位时调用sleep(),线程不会休眠,会清除这一状态,然后抛出InterruptedException异常。
interrupted()和isInterrupted()方法的区别
共同点:两个方法都用来检测当前线程是否被中断不同点:interrupted()是Thread的静态方法,在检测的同时会请求中断状态。isInterrupted()是实例方法,在检测时不会清除中断状态。
线程状态
线程可以有以下6种状态:
new,用new操作符创建一个新线程时,该线程还没有开始运行,该线程是new状态。
runnable,调用start()方法,线程处于runnable状态。处于runnable状态的线程可能正在运行,也可能没有在运行,取决于操作系统给线程提供运行的时间。
blocked,当线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态。当其他所有线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程变为非阻塞状态。
waiting,当线程等待另一个线程通知调度器一个条件时,自己进入等待状态。
timed waiting,给等待添加一个超时参数,等待状态保持到超时期满或接收到适当通知。
terminated,线程被终止,不再运行。
线程因为如下两个原因被终止:
run方法执行完自然退出
因为一个未捕获的异常终止了run方法
线程属性
线程优先级
Java语言中,每个线程都有一个优先级。默认情况下,继承自父类的优先级。
调用setPriority()进行设置优先级,从MAX_PRIORITY(10)到MIN_PRIORITY(1),NORM_PRIROTY为5
每当线程调度器有机会选择新线程时,首先选择具有较高优先级的线程。
线程优先级高度依赖于系统,Java线程的优先级映射到宿主机平台的优先级上。Windows有7个优先级,Linux中线程具有相同的优先级。
如果有几个高优先级的线程没有进入非活动状态,低优先级的线程可能永远也不能执行。
守护线程
调用setDaemon(true)将线程转为守护线程
唯一用途是为其他线程提供服务
当只剩下守护线程时,虚拟机就退出
未捕获异常处理器
run()方法抛出的unchecked异常被传递到未捕获异常处理器中,这个处理是实现了Thread.UncaughtExceptionHandler接口的类
调用setUncaughtExceptionHandler()为线程安装一个处理器,也可以调用setDefaultUncaughtExceptionHandler()为所有线程安装处理器
同步
多线程访问同一个资源会出现race condition(条件竞争),为防止出现并发访问的问题,代码块需要加锁
ReentrantLock
保护代码块的基本结构如下,保证了任何时刻只有一个线程执行代码块。只有一个线程可以获得锁,其他线程会被阻塞。
myLock.lock();
try { ... } finally { myLock.unlock(); }
ReentrantLock是可重入锁,线程可以重复获得已持有的锁。ReentrantLock有一个hold count(持有计数)来跟踪lock()的嵌套调用。
线程每次调用lock()是都需要调用unlock()来释放锁
条件对象
线程进入并行代码块,却发现在某一条件满足下才能执行。这时需要条件对象,管理那些已经获得锁但不能做有用工作的线程。
一个锁对象可以有一个或多个相关的条件对象,通过以下方式来创建。
Condition condition = myLock.newCondition();
当线程发现条件不满足时,当前线程进入阻塞状态,并放弃锁,进入该条件对象的等待集,等待其他线程激活。
condition.await();
condition.sigal();
condition.sigalAll();
sigalAll()会激活因为这一条件而等待的所有线程,一旦成为可用的,将从await()返回,从被阻塞的地方继续执行。
如果没有其他线程来激活等待的线程,将导致死锁(deadlock)。
synchronized
从Java 1.0开始,每个Java对象都有一个内部锁,如果一个方法用synchronized声明,将获取对象的内部锁来保护该方法。
内部对象锁只有一个条件对象,wait()将线程添加到等待集中,notifyAll(),notify()解除线程的阻塞状态
wait() notifyAll() notify()是Object类的final方法,所以Condition对应的方法名改为await() signalAll() signal()
synchronized的另一个用法是同步代码块,线程进入是需要获取obj的内部锁
synchronized(obj) {
...
}
volatile关键字
如果一个字段被声明为volatile,那么编译器和虚拟机就知道该字段可能被另一个线程并发更新
volatile实现了可见性和
final
final修改的字段也可以被多线程安全的访问。其他线程只会在构造函数完成构造后才会看到变量的值。
原子性
java.util.concurrent.atomic包中很多类使用了高效的机器级指令来保证操作的原子性,不会有并发问题。
复杂的更新可以使用compareAndSet()方法,
乐观更新需要多次重试,性能会大幅下降
LongAdder
包括多个变量,可以有多个线程来更新加数,总和为当前值。
longAdder.add(x);
longAdder.sum();
LongAccumulator
LongAccumulator将这种思想扩展到其他操作上,在定义LongAccumulator时定义操作和零元素,get()获取当前值
LongAccumulator longAccumulator = new LongAccumulator(Math::max, 0);
longAccumulator.get();