多任务与多线程
- 多任务(进程):操作系统的一种能力, 可以在同一时刻运行多个程序,例如,在编辑或者下载文件的同时可以打印文件
- 多线程:单个程序可以同时完成多个任务,每个任务在一个线程中执行
多线程程序在更低一层扩展了多任务的概念
在多线程中,每个进程都拥有这自己的一整套变量,而线程则是共享变量的
共享变量使线程之间的通信比进程之间的通信更有效
启动线程
- 将执行这个任务的代码放在一个类的run方法当中,这个类要实现Runnable接口 public interface Runable{
void run();
}Runnable是一个函数式接口,可以用一个lambda表达式来代替\Runable r = ()->{…}; - 从这个Runnable构造一个Thread对象var t = new Thread();
- 启动线程t.start();比如:public class ThredTest {
public static final int DELAY = 10;
public static final int STEPS = 100;
public static final double MAX_AMOUNT = 1000;
public static void main(String args[]){
Runnable r1 = () ->{
try {
double amount = MAX_AMOUNT * Math.random();
System.out.println(amount);
Thread.sleep(((int)(DELAY * Math.random())));
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Runnable r2 = () ->{
try {
double amount = MAX_AMOUNT * Math.random();
System.out.println(amount);
Thread.sleep(((int)(DELAY * Math.random())));
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(r1).start();
new Thread(r2).start();
}
}
线程状态
线程可以以下有六种状态:
- New(新建)
- Runnable(可运行)
- Blocked(阻塞)
- Waiting(等待)
- Timed waiting(计时等待)
- Terminated(终止)
New(新建)
使用new操作符创建一个线程时,这个线程就处于New状态
代表这个线程还没有开始运行线程中的代码,在线程运行前还需要有一些准备工作要做
Runnable(可运行)
在对线程调用start()方法之后,线程就处于可运行的状态,但是,这并不意味着该线程中的代码正在被执行
一个可运行的线程可能在运行也可能没在运行
事实上,运行中的线程有时需要暂停,让其他线程有机会运行,线程调度的细节取决于操作系统提供的服务
比如在桌面及服务器操作系统中,系统分配给每一个可运行线程一个时间片来执行任务,当时间片用完时,操作系统便剥夺该线程的运行权,并给另一个线程一个机会来运行,这种调度方式成为抢占式调度系统
而在像手机这样的小型设备中,更多的采用一种名为协作式调度的调度方式,在这样的设备中,一个线程只有在调用yield方法或者被阻塞即等待时才会失去控制权
Blocked(阻塞)&Waiting(等待)&Timed waiting(计时等待)
当线程处于阻塞或者等待状态时,它暂时是不活动的,而且消耗最少的资源
当线程进入这些状态后,要有线程调度器重新激活这个线程,具体细节取决于他是怎样达到非活动状态的
- 当线程试图获得一个内部的对象锁,而这个锁目前被其他线程占有,该线程就会被阻塞,当其他线程释放这个锁,该线程就会变为非阻塞状态
- 当线程等待另一个线程告知调度器出现了某个条件时,这个线程就会进入等待状态
- 当调用一些有超时参数的方法时,会使线程进入计时等待状态,该状态将一直持续到超时期满或者接收到适当的通知,比如Thread.sleep()
Terminated(终止)
线程会由于以下两个原因之一而终止
- run方法正常退出,线程自然终止
- 发生异常,线程意外终止
线程属性
中断线程
在Java的早期版本中,使用stop()方法来使一个线程来强制终止,但是这个方法在Java后续的版本中遭到了废弃,所以现在的Java已经无法强制终止一个线程,但是,Java提供了一个可以请求终止一个进程的方法
interrupt()
实际上,在每个线程的内部都有着一个boolean标志,该标志代表着这个线程是否处于中断状态,每个线程都会是不是检查这个标志,以判断线程是否中断
而在对一个线程调用这个方法时,就会设置线程当中的这个变量
如果想要查询是否设置了中断状态,首先调用Thread.currentThread方法获得当前线程,然后调用isInterrupted方法
while(!Thread.currentThread.isTerrupted()&&...){
...
}
另外要注意的是,如果一个线程被阻塞或者处于等待状态,就无法检查中断状态,比如如果在设置了中断状态后,对一个线程调用sleep()方法,这个线程就会抛出一个InterruptedException
对于可能抛出的InterruptedException异常,最好的操作是将其捕获,通常有两种合理的选择
- 将其放入throws列表,继续抛出给调用者
- 在catch子句中调用Thread.currentThread.interrupt()来设置中断状态,这样一来调用者就可以检测中断状态
守护线程
可以通过调用
setDaemon(true)
方法来将一个线程转换为守护线程,守护线程相对于普通线程并没有太多的意义,其实更多时候,它只是起着标志的作用,守护线程标志着该线程为其他线程提供服务,所以当程序中只剩下守护线程时,虚拟机就会退出
线程优先级
每一个线程都有一个优先级
默认的情况下,一个线程会继承构造它的那个线程的优先级
当然,也可以使用setPriority方法手动的提高或者降低一个线程的优先级
每当线程调度器有机会选择新线程时,首先选择具有较高优先级的线程
但是,在有些系统中,优先级不被操作系统所采用,比如在Oracle为linux设计的Java虚拟机中,所有的线程都具有相同的优先级
线程同步
在实际的多线程应用中,往往结构复杂且工作繁琐,这时就有可能出现两个或两个以上的线程在运行时同时对一段数据进行了读写,一个不好的情况就是,他们两个的读写结果会造成冲突,也就是相互覆盖,可能会导致对象被破坏,这种情况通常称为竞态条件
比如Thread1与Thread2同时读写account变量,并将他们各自的修改结果输出,Thread1将account变量增加100,而Thread变量将account变量减少100,在Thread1先执行完对account变量的增加后,让Thread进入休眠10s,此时,同时执行的Thread2又将account变量减少了100,所以,Thread在经过休眠后恢复再输出的修改结果就不是原account变量增加100的结果了
再比如下面的代码
public class ThredTest {
public static final int DELAY = 10;
public static final int STEPS = 100;
public static final double MAX_AMOUNT = 1000;
public static void main(String args[]){
var bank = new Bank(4,100000);
while(true) {
Runnable r = () -> {
try {
int toAccount = (int) (bank.size() * Math.random());
int fromAccount = (int) (bank.size() * Math.random());
double amount = MAX_AMOUNT * Math.random();
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((int) (DELAY * Math.random()));
} catch (InterruptedException e) {
}
};
var t = new Thread(r);
t.start();
}
}
}
import java.util.Arrays;
public class Bank {
private final double[] accounts;
public Bank(int n,double initialBalance){
accounts = new double[n];
Arrays.fill(accounts,initialBalance);//fill填充相同默认值
}
public void transfer(int from,int to,double amount){
if(accounts[from] < amount) return;
System.out.println(Thread.currentThread());
accounts[from] -= amount;
System.out.printf("%10.2f from %d to %d ",amount,from,to);
accounts[to] += amount;
System.out.printf("Total Balance: %10.2f%n",getTotalBalance());
}
public double getTotalBalance(){
double sum = 0;
for(double a:accounts) sum += a;
return sum;
}
public int size(){
return accounts.length;
}
}
即使在Bank类中,Balance的值应该是不变的,但是在多线程对accounts数组并发访问之下,Balance的变量也会渐渐发生变化
当然,如果减少线程在休眠前所做的工作,对应的风险就会降低,但是
在一个或多个内核的现代处理器上,由于存在大量运行中的线程,即使没有处理特别繁重的任务,出现这种情况的概率仍然是很高的
所以,为了避免多线程破坏共享数据,必须学习如何同步数据存取
这里有两种方法解决这种问题,并分别引入了锁对象和条件对象的概念
锁对象(Lock)
在Java5中,为了防止多线程破坏共享数据,引入了ReentrantLock类,利用这个类可以设计出一种类似于锁定,解锁的结构形式
mylock.lock();
try{
...
}
finally{
...
mylock.unlock();
}
这个结构确保任何时刻都只有一个线程进入临界区,一旦一个线程锁定了锁对象,其他任何线程都无法通过lock语句,当其他线程调用lock时,他们会暂停,知道第一个线程释放这个锁对象
所以,要把unlock操作包括在finally子句中.否则,有可能会出现锁对象无法被释放的情况,其他线程也将永远阻塞
实例:
public class Bank{
private var bankLock = new ReentrantLock();
...
public void transfer(int from , int to ,int amount){
bankLock.lock();
try{
System.out.println(Thread.currentThread());
}
finally{
bankLock.unlock();
}
}
}
假设第一个线程调用了transfer,但是在执行结束前被抢占,再假设第二个线程也调用了transfer,由于第二个线程不能获得锁,将在调用lock方法时被阻塞,它会暂停,必须等待第一个线程执行完transfer方法,当第一个线程释放锁时,第第二个线程才能开始运行
这个结构也是为什么把这种方法成为锁的原因,ReentrantLock的实例就像一把所在对象的锁,在线程要执行的方法内部把锁芯锁住,同时也只能在外部开启
但是需要注意的是每个对象都有自己单独的锁,如果两个线程访问同一个对象,那么锁就可以保证串行化访问,不过,如果两个线程访问不同的两个对象,每个线程都可以得到不同的锁对象,两个线程也就不会阻塞
条件对象(Condition)
有时候,光靠一个锁对象并不能满足我们的目的,比如,我们可能需要线程进入临界区之后还需要判断是否满足某个条件,不满足则不能继续执行,当然,你可以选择if-else语句来做这件事,但是如果这个条件需要运行另一个线程或者是另一个线程的运行结果时,就无法通过if-else语句来完成了,这时,可以使用一个条件对象来管理那些已经获得了一个锁但不能做有用工作的线程
比如说,银行的模拟程序,我们希望账户余额如果低于提款金额的话,就无法转出资金,而作为一个银行,如果没有锁机制的话,在多线程的处理之下,很可能会导致余额错误,所以,必须确保在检查余额与转账活动之间没有其他线程修改金额
我们可以这样写
private ReentrantLock bankLock = new ReentrantLock();
public void transfer(int from,in to,int amount){
bankLock.lock();
try{
while(accounts[from] < amount){
...//wait
}
}
finally{
bankLock.unLock();
}
}
现在,当账户中没有足够资金的时候,线程就会直接结束,但是这样的话,当日后再往里存入资金的时候,就需要重新进行转账操作,我们不希望看到这样,我们更希望的是在资金不够的时候,可以等待账户里汇入足够资金后继续完成转账的操作
而这里这个线程以及被锁对象锁定,所以,当这个线程进行时,其他线程无法获得这个类的访问权,因此,别的线程就没有存款的机会
这时候就需要用到条件对象了
class Bank{
private Condition sufficientFunds;
...
public Bank(){
...
sufficientFunds = bankLock.newCondition();
}
}
调用锁对象的newCondition()方法可以获得一个该锁所在对象的条件对象,一个锁对象可以一个或者多个关联的条件对象
当你需要用到条件对象进行等待时,调用条件对象的awit()方法,当前线程就会暂停,并放弃锁,这就运行另一个线程执行,因为类上始终都只有一个线程在运行,所以不会发生变量不同步的情况
而这个被暂停的线程将一直保持非活动状态,直到另一线程在同一条件对象上调用signalAll方法
sufficientFunds.await();
sufficientFuinds.signalALL();
这个调用会激活等待这个条件的所有线程,同时重新测试条件
所以,将signalAll方法添加到另一个线程当中是至关重要的,否则,原来的线程就永远再运行了
总结
- 锁用来保护代码片段,一次只能有一个线程执行被保护代码
- 锁可以管理试图进入被保护代码的片段
- 一个锁可以有一个或多个相关联的条件对象
- 每个条件对象管理那些已经进入被保护代码段但还不能运行的线程
synchronized关键字
public synchronized void method(){
...
}
等价于
public void method(){
this.intrinsicLock.lock();
try{
...
}
finally{
this.intrinsicLock.unLock();
}
}
示例代码
import java.util.Arrays;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Bank {
private final double[] accounts;
private Lock bankLock;
private Condition sufficientFunds;
public Bank(int n,double initialBalance){
accounts = new double[n];
Arrays.fill(accounts,initialBalance);//fill填充相同默认值
bankLock = new ReentrantLock();
sufficientFunds = bankLock.newCondition();
}
public void transfer(int from,int to,double amount) {
bankLock.lock();
try {
while(accounts[from] < amount)
sufficientFunds.await();
System.out.println(Thread.currentThread());
accounts[from] -= amount;
System.out.printf("%10.2f from %d to %d ", amount, from, to);
accounts[to] += amount;
System.out.printf("Total Balance: %10.2f%n", getTotalBalance());
}
catch (InterruptedException e) {
e.printStackTrace();
}
finally {
bankLock.unlock();
}
}
public double getTotalBalance(){
double sum = 0;
for(double a:accounts) sum += a;
return sum;
}
public int size(){
return accounts.length;
}
}
Thread方法
**代表可选
.start()
开启一个线程
.join(** long millis)
等待终止指定的线程或者等待millis毫秒
.State getState()
得到这个线程的状态