进程就是正在运行的程序,它会占用对应的内存区域,由CPU进行执行与计算。
独立性
进程是系统中独立存在的实体,它可以拥有自己独立的资源,每个进程都拥有自己私有的地址空间,在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间
动态性
进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合,程序加入了时间的概念以后,称为进程,具有自己的生命周期和各种不同的状态,这些概念都是程序所不具备的.
并发性
多个进程可以在单个处理器CPU上并发执行,多个进程之间不会互相影响.并发执行的进程数目并不受限于CPU数目。操作系统会为每个进程分配CPU时间片,给人并行处理的感觉。
线程是控制线程的简称。线程是操作系统OS能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位.如果一个进程可以同时运行多个线程,则称这个进程是多线程的(multithreaded)。一个进程可以开启多个线程,其中有一个主线程来调用本进程中的其他线程。
我们看到的进程的切换,切换的也是不同进程的主线程
多线程可以让同一个进程同时并发处理多个任务,相当于扩展了进程的功能。
一个操作系统中可以有多个进程,一个进程中可以包含一个线程(单线程程序),也可以包含多个线程(多线程程序)
每个线程在共享同一个进程中的内存的同时,又有自己独立的内存空间.
所以想使用线程技术,得先有进程,进程的创建是OS操作系统来创建的,一般都是C或者C++完成
多线程与多进程的区别:每个线程都拥有自己的一整套变量,而线程则共享数据。共享变量使线程之间的通信比进程更有效、更容易。此外,在有些操作系统中,与进程相比较,线程更"轻量级",创建、撤销一个线程比启动新进程的开销要小得多。
我们宏观上觉得多个进程是同时运行的,但实际的微观层面上,一个CPU【单核】只能执行一个进程中的一个线程。
那为什么看起来像是多个进程同时执行呢?
是因为CPU以纳秒级别甚至是更快的速度高效切换着,超过了人的反应速度,这使得各个进程从看起来是同时进行的,也就是说,宏观层面上,所有的进程看似并行【同时运行】,但是微观层面上是串行的【同一时刻,一个CPU只能处理一件事】。
串行是指同一时刻一个CPU只能处理一件事,类似于单车道
并行是指同一时刻多个CPU可以处理多件事,类似于多车道
时间片,即CPU分配给各个线程的一个时间段,称作它的时间片,即该线程被允许运行的时间,如果在时间片用完时线程还在执行,那CPU将被剥夺并分配给另一个线程,将当前线程挂起,如果线程在时间片用完之前阻塞或结束,则CPU当即进行切换,从而避免CPU资源浪费,当再次切换到之前挂起的线程,恢复现场,继续执行。
注意:我们无法控制OS选择执行哪些线程,OS底层有自己规则,如:
在有多个处理器的机器上,每一个处理器运行一个线程,可以有多个线程并行运行。当然,如果线程的数目多于处理器的数目,调度器还是需要分配时间片。
由于线程状态比较复杂,我们由易到难,先学习线程的三种基础状态及其转换,简称”三态模型” :
就绪 → 执行:为就绪线程分配CPU即可变为执行状态" 执行 → 就绪:正在执行的线程由于时间片用完被剥夺CPU暂停执行,就变为就绪状态 执行 → 阻塞:由于发生某事件,使正在执行的线程受阻,无法执行,则由执行变为阻塞 (例如线程正在访问临界资源,而资源正在被其他线程访问) 反之,如果获得了之前需要的资源,则由阻塞变为就绪状态,等待分配CPU再次执行
我们可以再添加两种状态:
PCB(Process Control Block):为了保证参与并发执行的每个线程都能独立运行,OS配置了特有的数据结构PCB来描述线程的基本情况和活动过程,进而控制和管理线程
线程生命周期,主要有五种状态:
新建状态(New) : 当线程对象创建后就进入了新建状态.如:Thread t = new MyThread();
可运行状态(Runnable):当调用线程对象的start()方法,线程即为进入可运行状态.
处于状态的线程,只是说明线程已经做好准备,随时等待CPU调度执行,并不是执行了t.start()此线程立即就会执行,要由操作系统为线程提供具体的执行时间。
运行状态(Running):当CPU调度了处于就绪状态的线程时,此线程才是真正的执行,即进入到运行状态
就绪状态是进入运行状态的唯一入口,也就是线程想要进入运行状态状态执行,先得处于就绪状态(不过,Java规范并没有把正在运行状态作为一个单独的状态,一个正在运行的线程仍然处于可运行状态)
注:一旦一个线程开始运行,它不一定始终保持运行。事实上,运行中的线程有时需要暂停,让其他线程有机会运行。线程调度的细节依赖于操作系统提供的服务。抢占式调度系统给每一个可运行线程一个时间片来执行任务。当时间片用完后,操作系统剥夺该线程的运行权,并给另一个线程一个机会来运行。当选择下一个线程时,操作系统会考虑线程的优先级。
阻塞状态(Blocked):处于运状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入就绪状态才有机会被CPU选中再次执行.
根据阻塞状态产生的原因不同,阻塞状态又可以细分成三种:
等待阻塞:当线程等待另一个线程通知调度器出现一个条件,或当该线程调用wait()方法时,这个线程会进入等待状态。有几个方法有超时参数,调用这些方法会让线程进入计时等待(time waiting)状态。这一状态将一直保持到超时期满或者接收到适当的通知。
同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态。当所有其他线程都释放了这个锁,并且线程调度器运行该线程持有这个锁时,它将变成非阻塞状态。
其他阻塞:调用线程的sleep()或者join()或发出了I/O请求时,线程会进入到阻塞状态.当sleep()状态超时.join()等待线程终止或者超时或者I/O处理完毕时线程重新转入就绪状态
终止状态(Dead):run方法正常退出或者因为一个没有捕获的异常终止了run方法使线程意外终止,该线程结束生命周期。具体来说,可以调用线程的stop方法杀死一个线程。该方法抛出一个ThreadDeath错误对象,这会杀死线程。不过,stop方法已经废弃,不要在你的代码中调用该方法。
注:当一个线程阻塞或等待时(或终止时),可以调度另一个线程运行。当一个线程被重新激活(例如,因为超时期满或成功地获得了一个锁)调度器检查它是否具有比当前运行线程更高的优先级。如若这样,调度器会剥夺某个当前运行线程的运行权,选择一个新线程运行。
当该线程的run方法执行方法体中最后一条语句后再执行return语句返回时,或者出现了一个没有捕获的异常时,线程将终止。
除了已经废弃的stop方法,没有办法可以强制线程终止。不过,interrupt方法可以用来请求终止一个线程。当对一个线程调用interrupt方法时,就会设置线程的中断状态。这是每个线程都有的boolean标志。每个线程都应不时地检查这个标志,以判断线程是否被中断。可以通过调用
Thread.currentThread().isInterrupted()
但如果线程被阻塞,就无法检查中断状态。当在一个被sleep或wait调用阻塞的线程上调用interrupt方法时,将会抛出InterruptedException异常。
注: 没有任何语言要求被中断的线程应当终止。中断一个线程只是要引起它的注意。被中断的线程可以决定如何响应中断。某些线程非常重要,所以应该处理这个异常,然后再继续执行。但是,更普遍的情况是,线程只希望将中断解释为一个终止请求。
如果设置了中断状态,此时倘若调用sleep方法,它不会休眠,而是会清除中断状态并抛出InterruptedException异常。因此,如果你的循环调用了sleep,不要检测中断状态,而应该捕获InterruptedException异常。
API java.lang.Thread
void interrupt()
向线程发送中断请求。线程的中断状态将设置为true。如果当前该线程被一个sleep调用阻塞,则抛出一个InterruptedException异常。
static boolean interrupt()
测试当前线程(即正在执行这个指令的线程)是否被中断。这个调用有一个副作用——它将当前线程的中断状态重置为false。
boolean isInterrupted()
与static interrupt不同,这个调用不改变线程的中断状态。
static Thread currentThread()
返回表示当前正在执行的线程的Thread对象
可以通过调用
t.setDaemon(ture)
将一个线程转换为守护线程(daemon thread)。守护线程的唯一用途是为其他线程提供服务。例如计时器线程,它定时发送"计时器嘀嗒"信号给其他线程,另外清空过时缓存项的线程也是守护线程。当只剩下守护线程时,虚拟机就会退出。因为如果只剩下守护线程,就没必要继续运行程序了。
API java.lang.Thread
void setDaemon(boolean isDaemon)
标识该线程为守护线程或用户线程。这一方法必须在线程启动之前调用
Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例
启动线程的唯一方法就是通过Thread类的start()实例方法
start()方法是一native方法,它将通知底层操作系统,.最终由操作系统启动一个新线程,操作系统将执行run()
这种方式实现的多线程很简单,通过自己的类直接extends Thread,并重写run()方法,就可以自动启动新线程并执行自己定义的run()方法
模拟开启多个线程,每个线程调用run()方法.
如果自己的类已经extends另一个类,就无法多继承,此时,可以实现一个Runnable接口
API java.lang.Thread
构造方法
Thread() 构造一个新线程
Thread(String name) 构造一个新线程,并设置该Thread对象名
Thread(Runnable target) 构造一个新线程,调用指定目标的run()方法
Thread(Runnable target,String name) 构造一个新线程,调用指定目标的run()方法,并设置该Thread对象名
普通方法
static Thread currentThread( )
返回对当前正在执行的线程对象的引用
void setName(String name)
调用该方法为线程设置名字,在线程转储中可能很有用
long getId()
返回该线程的标识
String getName()
返回该线程的名称
void run()
调用相关Runnable的run方法
void start()
使该线程开始执行:Java虚拟机调用该线程的run()。启动这个线程,从而调用run()方法。这个方法会立即返回。新线程会并发运行
static void sleep(long mills)
让该线程(执行状态)休眠指定的毫秒数(暂停执行)
API java.lang.Runnable
void run()
必须覆盖这个方法,提供你希望执行的任务指令
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;
/**
* Constructs the bank
* @param n the number of accounts
* @param initialBalance the initial balance for each account
*/
public Bank(int n,double initialBalance)
{
accounts=new double[n];
Arrays.fill(accounts,initialBalance);
bankLock=new ReentrantLock();
sufficientFunds=bankLock.newCondition();
}
/**
* Transfers money from one account to another
* @param from the account to transfer from
* @param to to the account to transfer to
* @param amount the amount to transfer
*/
public void transfer(int from,int to,double amount) throws InterruptedException {
while (accounts[from] < amount) {
System.out.println("因 accounts["+from+"] ("+accounts[from]+")< amount ("+amount+") "+Thread.currentThread().getName()+"进入WAIT状态");
wait();
}
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());
System.out.println("重新激活所有线程");
notifyAll();
}
/**
* Gets the sum of all account balance
* @return the total balance
*/
public double getTotalBalance()
{
try {
double sum = 0;
for (double a : accounts)
sum += a;
return sum;
}
}
public void getEachBalance()
{
for (int i=0;i<size();i++)
{
System.out.println(accounts[i]);
}
}
/**
* Gets the number of accounts in the bank
* @return the number of accounts
*/
public int size()
{
return accounts.length;
}
}
public class ThreadTest {
public static final int DELAY = 10;
public static final int STEPS = 20;
public static final double INITIAL_BALANCE = 10000;
public static final double MAX_AMOUNT = 20000;
/**
* 线程的并发运行测试
*/
public static void main(String[] args) {
var bank = new Bank(4, INITIAL_BALANCE);
Runnable task1 = () -> {
try {
Thread.currentThread().setName("Thread-1");
for (int i = 0; i < STEPS; i++) {
double amount = MAX_AMOUNT * Math.random();
bank.transfer(1, 2, amount);
Thread.sleep((int) (DELAY));
}
System.out.println(" task1 :");
bank.getEachBalance();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
Runnable task2 = () -> {
try {
Thread.currentThread().setName("Thread-2");
for (int i = 0; i < STEPS; i++) {
double amount = MAX_AMOUNT * Math.random();
bank.transfer(2, 1, amount);
Thread.sleep((int) (DELAY ));
}
System.out.println(" task2 :");
bank.getEachBalance();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
//以多线程的方式启动任务1,将当前线程变为就绪状态
//执行的时候start()底层会自动调用我们给定Runnable对象重写的run()方法,因Runnable为函数式接口,可用lambda表达式来重写run()方法
new Thread(task1).start();
new Thread(task2).start();
}
}
警告:不要调用Thread类或Runnable对象的run方法。直接调用run方法只会在同一个线程中执行这个任务——而没有启动新的线程。实际上,应当调用
Thread.start()
这会创建一个执行run方法的新线程
注:我可以通过建立Thread类的一个子类开定义线程,构造这个子类的一个对象并调用它的start方法。不过,现在不再推荐这种方法。应当把要并行运行的任务与运行机制解耦合。如果有多个任务,为每个任务分别创造一个单独的线程开销会太大。实际上,可以使用一个线程池。
在大多数实际的多线程应用中,两个或两个以上的线程需要共享对同一数据的存取。如果两个线程存取同一个对象,并且每个线程分别调用了一个修改该对象状态的方法,会发生什么呢?可以想见,这两个线程会相互覆盖。这有可能破坏共享数据。在这里的测试程序运行时,可以清楚地看见余额有轻微变化,有时可能需要很长时间才能发现这个错误。在现实的银行存取中,你肯定不希望看到自己的余额莫名其妙便少了,当然也有可能变多(可以试试)。为了防止这种不稳定情况出现我们需要防止并发访问这块代码,一种是使用synchronnized关键字,另外也可以使用锁。这一部分请参阅我的下一篇文章。
并发中的同步