我们知道,计算机中每个任务在一个线程中执行,如果一个程序可以运行多个线程,那么称这个程序是多线程的。
多线程与多进程的区别在于,每个进程都可以拥有自己的一整套变量,而线程则共享数据。
01什么是线程
建立线程的方法:
1.实现Runable接口,构造Thread对象,再启动(start方法)线程。
2.建立Thread类的一个子类来定义线程。
3.也可以使用匿名内部类
通常推荐第一种,因为Java是单继承,多实现的。
注意:不要调用run方法,直接调用run方法只会在同一个线程中执行这个任务,而没有启动新的线程。
02线程状态
线程有六种状态:
New新建
Runnable可运行
Blocked阻塞
Waiting等待
Timed Waiting计时等待
Terminated终止
我们通过new来新建一个线程,如new Thread(r)。
可运行线程:当我们调用了start方法,线程就处于可运行状态,一个可运行线程可能正在运行也可能没有运行。需要由操作系统提供具体的运行时间(Java中,一个正在运行的线程也处于可运行状态)。当由多个线程时,操作系统会进行线程调度。抢占式调度系统给每一个可运行线程一个时间片来执行任务。当时间片用完时,操作系统会剥夺该线程的运行权,并给另一个线程一个机会来运行。当选择下一个线程时,操作系统会考虑现成的优先级别。一些小型设备会采用协作式调度,一个线程只有在调用yield或者被阻塞或者等待时才失去控制权,
阻塞和等待线程:当线程处于阻塞或等待状态时,它暂时是不活动的。不运行任何代码,且消耗最少资源。常见有:
线程获得了内部锁,而这个锁被其他线程占用时,其会被阻塞;
通知线程等待时;
含有超时参数进入计时等待;
终止线程:两个原因会终止线程:
run方法正常退出;
因为一个没有捕获的异常终止了run方法,使线程意外终止。
03线程属性
中断线程:当线程run方法执行方法体最后一条语句后再执行return语句返回时,或者出现了方法中没有捕获的异常,线程终止。当对一个线程调用interrupt方法时,就会设置线程的中断状态。这是每个线程都有的boolean状态。每个线程都该时不时检查这个状态。不过当线程被阻塞,要引入一个InterruptedException异常。如我们调用sleep或者wait时。(注:interrupted和isInterrupted方法区别,前者是一个静态方法,检查线程状态,并清除中断状态,后者是一个实例方法,不会改变中断状态)
守护线程:调用t.setDaemon(true);将一个线程转换为守护线程,其唯一用途是为其他线程提供服务。
线程名:运用setName方法设置。
未捕获异常的处理器:非检查型异常可能会导致线程终止,在这种情况下,线程会死亡。不过对于可传播异常,并没有任何catch子句。实际上,在线程死亡之前,异常会传递到一个用于处理未捕获异常的处理器。
线程优先级:Java程序中,每个线程有一个优先级。默认情况下,一个线程会继承构造它的那个线程的优先级。线程优先级会高度依赖于系统。
04同步
在大多数实际多线程应用中,两个或两个以上的线程需要共享对同一数据的存取。如果存取同一个对象,可能会出现两个线程的相互覆盖。取决于线程访问次序,可能会导致对象被破坏。
竞态条件:例如银行转账业务,如果有两个线程同时对一个账户进行操作,假设线程1执行了对账户1转出100元准备存入账户2,此时如果这个线程的进行被抢占了,线程2同样执行转出任务并更新了账户2金额,然后线程1 被唤醒,更新了账户2的金额.我们会发现,线程2 的所有操作都会被抹去了。这样,我们账户信息就会出现差错。
锁对象:有两种机制可以防止并发访问代码块。synchronized关键字,以及ReetrantLock类。使用ReetrantLock代码块首先提供一个锁。myLock.lock();然后try/catch后在finally中,myLock.unlock();释放锁。这能确保任何时刻只有一个线程进入临界区。一旦一个线程锁定了锁对象,其他任何线程都无法通过lock语句,直到这个线程释放锁。注意unlock()放在finally中是为了若临界区代码抛出一个异常,锁必须释放,否则会产生阻塞。
条件对象:线程进入临界区后却发现只有满足了某个条件之后其才能执行。即我们可以使用一个条件对象来管理那些已经获得了一个锁却不能做有用工作的线程。
await方法,一个线程调用了该方法,其会进入等待集。当锁可用时,该线程并不会变成可运行状态,直到另一线程在同一条件上调用signalAll方法。这个调用会重新激活等待这个条件的所有所有进程(如转账发现资金不足条件直到另一个线程转账过来)。不过await方法会导致死锁。此外signal方法会随机选择等待集中的线程,解除阻塞状态。
锁条件总结:
锁用来保护代码片段,一次只能有一个线程执行被保护代码
锁可以管理试图进入被保护代码段的线程
一个锁可以有一个或者多个关联的条件对象
每个条件对象管理那些已经进入被保护代码段但还不能运行的线程
synchronized关键字:一个方法声明时有该关键字,那么对象的锁将保护整个方法。也就是说调用该方法,线程必须获得内部锁。内部对象锁只有一个关联条件。wait方法将一个线程增加到等待集,notifyAll/notify方法可以解除等待线程阻塞。内部锁和条件存在一些限制:
不能中断一个正在尝试获得锁的线程
不能指定尝试获得锁时的超时时间
每个锁仅有一个条件是不够的。
同步块:线程可以通过进入同步块的方式获得锁。如下形式:
synchronized(obj){
critical section
}
监视器:监视器可以不要求程序员显示锁就可以保证多线程安全性的一个解决方案。
volatile字段:该关键字为实例字段的同步访问提供了一种免锁机制。声明一个字段为volatile,那么编译器和虚拟机就知道该字段可能被另一个线程并发更新。
05线程安全的集合
阻塞队列:当试图向队列添加元素而队列已满,或者移除元素而队列为空,阻塞队列将导致线程阻塞。
高效的映射、集和队列:Java.util.concurrent包提供了映射、有序集和队列的高效实现,这些集合使用复杂的算法,允许并发的访问数据结构的不同部分尽可能减少竞争。
对并发散列映射的批操作:批操作会遍历映射,处理遍历过程中找到的元素。这里不会冻结映射的当前快照。除非你恰好知道操作运行时映射不会被修改,否则就要把结果看作是映射状态的一个近似。
集合库中提供了一种机制,任何集合类都可以通过使用同步包装器变成线程安全的。
06任务和线程池
线程池中包含有许多准备运行的线程,为线程池提供一个Runnable,就会有一个线程调用run方法。当run方法退出时,这个线程不会死亡而是归还给线程池。
Callable和Future:Callable可以看作与runnable类似但是有返回值。该接口是一个参数化类型,只有一个方法call。Future保存异步计算的结果。可以启动一个计算,将future对象交给某个线程,然后忘掉它。这个future对象的所有者在结果计算好之后就可以获得结果。
执行器:执行器(Executors)类有许多静态工厂方法来构造线程池。为了得到最优的运行速度,并发线程数等于处理器内核数。在这个情况下,就应当使用固定线程池,即并发线程总数有一个上限。使用连接池所要做的工作:
使用执行器静态方法
调用submit提交
保存返回future对象
不想再提交任何任务时调用shutdown
fork-join框架:即用来处理一些应用可能对处理器内核分别使用一个线程,已完成计算密集型任务。在后台这种框架使用了工作密取的方法来平衡可用线程的工作负载。每个工作线程都有一个双端队列来完成任务。一个工作线程将子任务压入其双端队列的队头。一个工作线程空闲时,他会从另一个双端队列的队尾密取一个任务。
07异步计算
利用future对象可以完成异步计算,但调用get会产生阻塞。直到值可用。我们可以通过CompleteableFuture类实现Future接口,来注册一个回调,一旦结果可用,就会利用该结果调用这个回调。
此外这个类提供了可以将异步任务组合为一个处理管线。
08进程
可以使用ProcessBuilder来建立一个进程,如:
var builder = new ProcessBuilder(“gcc”,“myapp.c”);
其第一个字符串必须是可执行命令。
可以调用start方法来启动进程。并把输入、输出和错误流配置为管道,现在可以写入输入流并读取输出和错误流。