进程与线程概念
进程:一个或多个程序,在一组数据上的一次执行过程,被称为进程。每一个进程有一套独立的数据。
线程:线程是“轻量级”进程;线程是由进程创建的,但线程不再另外申请计算机资源,所以,线程要比进程“轻”。就是说,线程不需要像进程那样拥有庞大的资源表,也不需要像进程那样对资源进行严格的管理;线程所i使用的资源都是由进程申请的;多个线程的状态切换也比进程更简单、更省时;线程也可以再生成新的线程,称为子线程。
进程是计算机资源的竞争者。
打开任务管理器可以发现,很多的任务“同时”运行。我们可以这样理解,每个任务都是一个进程,而这个进程是可以是多个线程完成的。
进程的管理是需要消耗计算机时间片段和计算机内存资源的。
进程的管理与调度
多道程序的调度使得每一个程序的执行(占有CPU)变得很随机。
为了更好地理解进程,我们可以将进程分为5种不同的状态:创建态、就绪态、运行态、阻塞态和销毁态。
注:
1.进程创建后并不是立刻进入运行态;
2.而是先进入就绪态,和其他进程竞争CPU(一个CPU一次只能执行一条机器语言);
3.就绪态存在一个进程队列;
4.只有在就绪态队列中的进程才有资格竞争CPU;
5.就绪态进程除了CPU外,不需要等待其他计算机资源;
6.阻塞态往往有多个阻塞队列;
7.阻塞态的进程只能由运行态的进程唤醒;
8.被唤醒后不是立刻进入运行态的,而是先进入就绪态,和其他进程竞争CPU;
9.进程是在运行态执行的情况下,进入阻塞态的;
10.将进程阻塞起来的是进程自己。
结合上图可以很好的理解;
临界资源
在很多情况下,我们需要多个进程相互配合,共同完成某个编程任务,他们之间通常以“共享数据”的方式来建立联系。
为了更好地处理具有关联关系的多个进程之间的并发执行问题,人们提出“临界资源”这个概念。
临界资源:进程中的代码段;
锁
对临界资源加“锁”,我们就可以使进入到临界资源的进程顺利完成其临界资源中的所有语句;
注:
假设A先执行,当其执行完“之前语句”后,时间片段未用完,当其进入“临界资源”之前,要先检查“锁的状态”;
若是“锁开状态”,A则先使锁变为“锁闭状态”,然后进入临界资源。
假设A在临界资源内运行的过程中,时间片段耗尽了,A进入就绪态,等待调度
这是若是B获得了执行权利,B运行完了“之前语句”,但B 的时间片段未耗尽,这时B遇到锁。
这时,B先检查“锁”的状态,发现锁处于“锁闭状态”,于是B进入“这个锁的阻塞队列”,当B没有A的唤醒前,B将不再执行任何代码。
这就意味着:A可以顺利地完成所有“锁内”的临界资源。当A运行完锁内的所有代码后,遇到’}’,将执行“开锁”操作,唤醒阻塞队列中的所有进程(这时B在其中)
B被唤醒后,与A一起竞争CPU。
重要声明:
锁必须是所有相关进程共享的,即,大家都知道这个锁的存在。
多个相关进程检查/打开/关闭的应该是“同一把锁”;
进程(线程)遇到锁,一定是先查看锁的状态;
Java线程编程
Java提供了一个Thread类,通过继承这个类,可以实现线程编程。
MyFirstThread类:
package com.mec.about_thread.test;
public class MyFirstThread extends Thread {
public MyFirstThread() {
System.out.println("准备创建线程:");
this.start();
System.out.println(System.currentTimeMillis() + "线程创建完毕!");
try {
this.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程执行完毕!");
}
@Override
public void run() {
System.out.println(System.currentTimeMillis() + "线程真正开始执行!");
for (int i = 0; i < 10; i++) {
try {
//停顿1秒钟
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
}
}
测试类——Test类:
public class Test {
public static void main (String[] args) {
new MyFirstThread();
}
}
结果:
还有第二种创建线程的方法,使用Runnable接口:
package com.mec.about_thread.test;
public class MyFirstThread implements Runnable {
public MyFirstThread() {
System.out.println("准备创建线程:");
new Thread(this).start();
System.out.println(System.currentTimeMillis() + "线程创建完毕!");
}
@Override
public void run() {
System.out.println(System.currentTimeMillis() + "线程真正开始执行!");
for (int i = 0; i < 10; i++) {
try {
//停顿1秒钟
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
}
}
运行结果和继承Tread方法的运行结果一样,推荐使用接口;
Java支持的是单继承;
两个相关线程
TwoRelationThread类:
package com.mec.about_thread.test;
public class TwoRelationThread implements Runnable {
private static int num;
private String threadName;
public TwoRelationThread(String threadName) {
this.threadName = threadName;
new Thread(this, threadName).start();
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//这里只是为了让计算过程复杂一些,增加被中断的概率(用尽时间片段)
num = num + 30;
num = num + 1;
num = num + 20;
num = num - 50;
System.out.println(threadName + "(" + i + "):" + num);
}
}
}
Test类:
package com.mec.about_thread.test;
public class Test {
public static void main(String[] args) {
new TwoRelationThread("A");
new TwoRelationThread("B");
System.out.println("主线程主函数执行完毕!");
}
}
执行结果:
不难发现这个执行结果是不是我们与预期的那样,而是杂乱无章的,是随机的。
现在我们在试试用到对象锁后的样子。
package com.mec.about_thread.test;
public class TwoRelationThread implements Runnable {
private static int num;
private String threadName;
private static final Object lock = new Object();
public TwoRelationThread(String threadName) {
this.threadName = threadName;
new Thread(this, threadName).start();
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (lock) {
//这其中的代码就是临界资源,我们给临界资源“加锁”
num = num + 30;
num = num + 1;
num = num + 20;
num = num - 50;
System.out.println(threadName + "(" + i + "):" + num);
}
}
}
}
private static final Object lock = new Object();
//这里定义了一个静态常量对象(只读对象),Object类的lock,将其实例化,并把它当成锁。事实上,我们可以将任何非八大基本类型的对象当成锁。
synchronized (lock) {
//临界资源
}
//锁拦截的是不同的线程
synchronized是Java关键字,其本意是同步,在这里所起作用的就是“门”和“锁”。
门就是其后的一对大括号;这对大括号就是临界资源的起止边界。
锁就是synchronized后面的“()”中的对象;
被这个锁拦截的是不同的线程。
注:当线程A进入
synchronized (lock) {
//临界资源
}
之前,会检查lock是否被锁上(这里是有JVM内部操作完成的);如果是没锁上的,那就对lock先上锁,然后进入“同步块”执行其中的语句。
当线程B来到锁前,发现锁的状态是锁上的,线程B就会进入“阻塞态”,不会执行同步块中的语句。
当线程A顺利完成同步块中的语句,将锁打开,并唤醒所有在该锁上阻塞的线程,让他们进入就绪态,等待调度执行。
执行结果:
从上面的结果来看,num的输出再也没有出现打断的现象。但是线程A和线程B并没有严格的交替执行。
现在给一个场景:两个线程,线程A是生产者,线程B是消费者,A生产一个数据B消费一个数据。如果A没有生产,B就不消费,等待A的生产。如果发现生产的数据B没有消费,那A就停止生产,等B消费完,再继续生产。
生产者——消费者问题就是如何让两个线程交替执行的问题。
下一章Java线程编程——生产者消费者问题分析将对该问题进一步讲解。