2020/9/1
多任务(multitasking)是操作系统的一项能力,看起来可以在同一时间运行多个程序。
实际上,并发执行的进程数目并不受CPU数量限制,操作系统会为每个进程分配CPU时间片,给人并行处理的感觉。
多线程(multithreaded)则是更低一层的概念,单个程序看起来在同一时间完成多个任务,每个任务在一个线程(thread)中执行。线程是控制线程的简称。
多线程与多进程的区别在于,每个进程都有自己的一整套变量,而线程则共享数据。因此,线程之间存在风险,但是线程之间的通信更有效,更容易。线程“更轻量级”,创建撤销的开销都比进程小。
并发和并行的区别
1、并发(Concurrent):指两个或多个事件在同一时间间隔内发生,即交替做不同事的能力,多线程是并发的一种形式。例如垃圾回收时,用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
2、并行(Parallel):指两个或者多个事件在同一时刻发生,即同时做不同事的能力。例如垃圾回收时,多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
什么是线程
一个简单的创建一个线程的过程:
1,将任务代码放在一个类的run方法中,这个类需要实现一个函数式接口Runnable。
public interface Runnable{
void run();
}
2,从这个Runnable构造一个Thread对象:
var t = new Thread(r);
3,启动线程:
t.start();
综上,一个转账线程如下:
Runnable r = () ->{
try{
for (int i = 0; i < STEPS; i++){
double amount = MAX_AMOUNT * Math.random();
bank.transfer(0, 1, amount);
Thread.sleep((int)(DELAY * Math.random()));
}
}
catch(InterruptedException e){};
};
Thread t = new Thread(r);
t.start();
也可以通过建立一个Thread的子类来定义线程:
class MyThread extends Thread{
public void run(){
task code
}
}
然后构造一个实例(构造这个实例的过程其实就是分配了一个线程),并调用start方法来运行,不要调用run方法,run方法只会在本线程中执行这个任务。
现在不再推荐这种方法,应当把并行运行的任务与运行机制解耦。
线程状态
有以下六种状态:
New(新建)
Runnable(可运行)
Blocked(阻塞)
Waiting(等待)
Timed waiting(计时等待)
Terminated(终止)
新建线程
当用new Thread(r)创建一个新线程时,这个线程还没有运行,处于新建状态。
可运行线程
一旦调用start方法,线程就处于可运行(runnable)状态。可运行代表进入了排队,并不表示正在运行,可能在运行也可能没有在运行。
操作系统可能提供抢占式调度系统,它会给每一个可运行线程一个时间片来执行任务。当时间片用完,操作系统就会剥夺该线程的运行权,并给另一个线程一个机会来运行。
像手机这样的小型设备,可能会使用协作式调度:一个线程只有再调用yield方法或者被阻塞或等待时才失去控制权。
阻塞和等待线程
阻塞和等待线程会退出排队,需要线程调度器重新激活才会进入到排队。两个区别在于怎样退出排队
阻塞线程:当一个线程视图获取一个内部的对象的锁,但是锁被其他线程占有,这个线程就会被阻塞。当所有的线程都释放了锁,并且线程调度器允许这个线程持有这个锁,它将变成可运行状态。
等待线程:当线程等待另一个线程通知调度器出现一个条件时,这个线程就会进入等待状态。
计时等待:有几个方法有超时参数,调用这些方法,线程就会进入计时等待状态。例如Tread.sleep()
终止线程
run方法正常推出或者出现了一个没有被捕获的异常而意外终止。
线程属性
线程属性
中断线程
线程有一个中断状态,可以调用interrupt方法来设置它。书上讲的不是很好,这里引用别人的文章。
守护线程
可以通过调用
t.setDaemon(boolean isDaemon);
将一个线程转换为守护线程(daemon thread)。这样一个线程没什么用。唯一用途是为其它线程提供服务。
在Java中有两类线程:用户线程 (User Thread)、守护线程 (Daemon Thread)。
所谓守护 线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。
线程名
这里的线程名不是指线程实例的标识符,可以通过
t.setName("Web crawler");
来设置名字,这在线程转储时可能很有用。
未捕获异常的处理器
我们经常使用try..catch进行异常处理,但是对于Uncaught Exception是没办法捕获的。对于这类异常如何处理呢?
回顾一下thread的run方法,有个特别之处,它不会抛出任何检查型异常,但异常会导致线程终止运行。这非常糟糕,我们必须要“感知”到异常的发生。比如某个线程在处理重要的事务,当thread异常终止,我必须要收到异常的报告(email或者短信)。
在jdk 1.5之前貌似无法直接设置thread的Uncaught Exception Handler(具体未验证过),但从1.5开始可以对thread设置handler。
1. As the handler for a particular thread
当 uncaught exception 发生时, JVM寻找这个线程的异常处理器. 可以使用如下的方法为当前线程设置处理器
public class MyRunnable implements Runnable{
public void run(){
// Other Code
Thread.currentThread().setUncaughtExceptionHandler(myHandler);
// Other Code
}
}
2. As the handler for a particular thread group
如果这个线程属于一个线程组,且线程级别并未指定异常处理器(像上节As the handler for a particular thread中那样),jvm则试图调用线程组的异常处理器。
线程组(ThreadGroup)实现了Thread.UncaughtExceptionHandler接口,所以我们只需要在ThreadGroup子类中重写实现即可
public class ThreadGroup implements Thread.UncaughtExceptionHandler
java.lang.ThreadGroup 类uncaughtException默认实现的逻辑如下:
如果父线程组存在, 则调用它的uncaughtException方法.
如果父线程组不存在, 但指定了默认处理器 (下节中的As the default handler for the application), 则调用默认的处理器
如果默认处理器没有设置, 则写错误日志.但如果 exception是ThreadDeath实例的话, 忽略。
对应JDK的源码:
public void uncaughtException(Thread t, Throwable e) {
if (parent!=null) {
parent.uncaughtException(t, e);
}else{
Thread.UncaughtExceptionHandler ueh = Thread.getDefaultUncaughtExceptionHandler();
if(ueh !=null) {
ueh.uncaughtException(t, e);
}elseif(!(einstanceofThreadDeath)) {
System.err.print("Exception in thread \""+ t.getName() +"\" "); e.printStackTrace(System.err);
}
}
}
3. As the default handler for the application (JVM)
Thread.setDefaultUncaughtExceptionHandler(myHandler);
线程优先级
在Java程序设计语言中,每一个线程都有一个优先级。默认情况下,一个线程会继承创建它的那个线程的优先级。可以用setPriority方法来设置线程。
Java中优先级可以设置为MIN_PRIORITY(1)与MAX_PRIORITY(10)和NORM_PRIORITY(5)。
但是,优先级高度依赖于操作系统,不同的操作系统有不同个数的优先级,虚拟机要映射到平台的优先级。因此现在不要使用优先级。
同步
在大多数程序中,会有多个线程共享统一数据,如果多个线程对其进行修改,就会导致数据被破坏,这种情况叫做竞态条件。
为了避免多线程破坏共享数据,就需要学会同步存取
有个常见但是容易出错的例子:
多个线程执行
i += 1;
问题在于,这条语句并不是一个原子操作,它的执行步骤分为三布:
1,首先取出i的值,将其加载到寄存器
2,执行i+1
3,将结果写回 i 。
相当于 i = i +1
当一个线程执行1,2两步,这时运行权被转移,另一个线程开始执行,就会出现问题。会有一条线程覆盖结果导致数据破坏。
在后面的voliate关键词时会提到。
锁对象
有两种机制可以防止并发访问代码块:synchronized关键词和ReentranLock类。
这里先说ReentranLock类,基本结构如下
myLock.lock() ;//myLock是一个ReentranLock类的实例
try {
critical section
}
finally {
myLock.unlock() ;
}
可以用来保护一个类的方法,如下
public class Bank{
private ReentrantLock banklock = new ReentrantLock();
...
public void transfer(int from, int to, int amount){
banklock.lock();
try{
...
}
finally{
banklock.unlock();
}
}
}
每个线程再调用transfer之前,都要查看是否有锁。
ReentrantLock类称为重用(reentrant)锁,因为可以在一段有锁的代码中调用另一段有锁的代码。锁有一个持有计数来跟踪对lock方法的嵌套调用。
例如,transfer方法调用getTotalBalance方法,第二个方法也会封锁bankLock对象,这是bankLock的持有计数就会加一,变成2(因为bankLock是Bank类中的私有变量)。直到锁的计数变为零,才会释放锁。
条件对象
可以使用一个条件对象来管理那些已经获得了一个锁却不能做有用工作的线程。由于历史原因条件对象经常被称为条件变量。
可以给锁对象关联一个条件对象condition。这个对象的功能是可以将一个线程进入等待状态,如果满足条件就可以在其他线程中恢复等待的线程,使用方法如下:
public class Bank{
...
private Lock bankLock;
private Condition sufficientFunds;
public Bank(int n, double initialBalance) {
...
bankLock = new ReentrantLock();
sufficientFunds = bankLock.newCondition();
}
public void transfer(int from, int to, double amount) throws InterruptedException {
bankLock.lock();
try{
while (accounts[from] < amount)
sufficientFunds.await();
...
sufficientFunds.signalAll();
}
finally {
bankLock.unlock();
}
}
synchronized关键字
先对锁进行总结:
·锁用来保护代码片段,一次只能有一个线程执行被保护的片段。
·锁可以管理试图进入被保护代码段的线程
·一个锁可以一个或多个相关联的条件对象
·每个条件对象,管理那些已经进入被保护代码段但还不能与逆行的线程。
Lock和Condition两个接口已经允许程序员充分控制锁定,但是大部分情况下这样比较麻烦。可以使用一种Java语言的内置机制,synchronized关键字。
Java中的每个对象都有一个内部锁,这个锁只有一个关联条件,可以直接调用wait()和notifyAll来代替condition.await()和condition.signalAll();
原理是一样的,用synchronized的内部锁来封锁整个方法。
结构类似。
内部锁和条件存在一些限制:
·不能中断一个正在尝试获得锁的线程
·不能指定尝试获得锁时的超时时间。
·每个锁仅有一个条件可能是不够的
在代码中使用哪种做法有一些建议:
·最好既不使Lock/Condition也不使用synchronized关键字。在许多情况下,可以使用java.util.concurrent包中的某种机制,它会为你处理所有的锁定。例如,使用阻塞队列来同步完成一个共同任务的线程。还应当研究并行流
·如果synchronized关键字适合你的程序,那么尽量使用这种做法,这样可以减少编写的代码量。
·如果特别需要Lock/Condition结构提供的额外能力则使用Lock/Condition
同步块
区别:
同步方法默认用this或者当前类class对象作为锁;
同步代码块可以选择以什么来加锁,比同步方法要更细颗粒度,我们可以选择只同步会发生同步问题的部分代码而不是整个方法;
同步方法使用关键字 synchronized修饰方法,而同步代码块主要是修饰需要进行同步的代码,用 synchronized(object){代码内容}进行修饰;
同步代码块,即有synchronized修饰符修饰的语句块,被该关键词修饰的语句块,将加上内置锁。实现同步。
例:synchronized(Object o ){}
同步是高开销的操作,因此尽量减少同步的内容。通常没有必要同步整个方法,同步部分代码块即可。
同步方法默认用this或者当前类class对象作为锁。
同步代码块可以选择以什么来加锁,比同步方法要更颗粒化,我们可以选择只同步会发生问题的部分代码而不是整个方法。
监视器概念
监视器(monitor)是一个相互排斥且具备同步能力的对象。监视器中的一个时间点上,只能有一个线程执行一个方法。线程通过获取监视器上的锁进入监视器,并且通过释放锁退出监视器。任意对象都可能是一个监视器。一旦一个线程锁住对象,该对象就成为监视器。加锁是通过在方法或块上使用synchronized关键字来实现的。在执行同步方法或块之前,线程必须获得锁。如果条件不适合线程继续在监视器内执行,县城可能在监视器中等待。可以对监视器调用wait()方法来释放锁,这样其他的一些监视器中的线程就可以获取它,也就有可能改变监视器中的状态。当条件满足时,另一线程可以调用notify()方法或notifyAll()方法来通知一个或所有的等待线程重新获取锁并且恢复执行。
关键词volatile
也可以用volatile来进行同步,但是,volatile并不保证原子性。
volatile可以用来修饰线程之间共享的变量,如果只是对这个变量进行原子性操作,例如赋值,就可以用这个来避免使用锁。
这里的原子性:
volatile int i = 1;
...
i = 2;//是原子性操作
i += 1;//不是原子性操作,分三步
i = atomiclong.incrementAndGet();//是原子性,这是一个用机器码写的自增方法,没有用锁。
final变量
final某些方面也可以保证能安全的访问一个字段
final var accounts = new HashMap<String, Double>();
上面这个声明的作用,是保证一个线程在创建accounts时,其他线程不会读到null,只会访问到新构造的HashMap。但这不是线程安全的,注意。
死锁
例如转账程序,有几种可能会导致死锁的情况。
比如各账户转账,但是每个账户的余额都不够;或者signalAll方法改为signal,随机唤醒一个等待中的线程,但是这个线程的条件仍然不满足,继续转账导致钱不够,运行的线程也等待,导致所有线程等待。
线程局部变量