1 线程的创建与启动
1.1 进程与线程
进程:
进行进程切换就是从正在运行的进程中收回处理器,然后再使待运行进程来占用处理器。
这里所说的从某个进程收回处理器,实质上就是把进程存放在处理器的寄存器中的中间数据找个地方存起来,从而把处理器的寄存器腾出来让其他进程使用。那么被中止运行进程的中间数据存在何处好呢?当然这个地方应该是进程的私有堆栈。
让进程来占用处理器,实质上是把某个进程存放在私有堆栈中寄存器的数据(前一次本进程被中止时的中间数据)再恢复到处理器的寄存器中去,并把待运行进程的断点送入处理器的程序指针PC,于是待运行进程就开始被处理器运行了,也就是这个进程已经占有处理器的使用权了。
这就像多个同学要分时使用同一张课桌一样,所谓要收回正在使用课桌同学的课桌使用权,实质上就是让他把属于他的东西拿走;而赋予某个同学课桌使用权,只不过就是让他把他的东西放到课桌上罢了。
在切换时,一个进程存储在处理器各寄存器中的中间数据叫做进程的上下文,所以进程的切换实质上就是被中止运行进程与待运行进程上下文的切换。在进程未占用处理器时,进程的上下文是存储在进程的私有堆栈中的。
进程执行时的间断性,决定了进程可能具有多种状态。事实上,运行中的进程可能具有以下三种基本状态。
1)就绪状态(Ready):
进程已获得除处理器外的所需资源,等待分配处理器资源;只要分配了处理器进程就可执行。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。
2)运行状态(Running):
进程占用处理器资源;处于此状态的进程的数目小于等于处理器的数目。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。
3)阻塞状态(Blocked):
由于进程等待某种条件(如I/O操作或进程同步),在条件满足之前无法继续执行。该事件发生前即使把处理器资源分配给该进程,也无法运行。
线程:线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
进程和线程都是由操作系统所体现的程序运行的基本单元,系统利用该基本单元实现系统对应用的并发性。 进程和线程的区别在于: 简而言之,一个程序至少有一个进程,一个进程至少有一个线程. 线程的划分尺度小于进程,使得多线程程序的并发性高。另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位. 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源. 一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.
1.2 Java中的Thread和Runnable类
Java中线程的创建有两种方式:
1. 通过继承Thread类,重写Thread的run()方法,将线程运行的逻辑放在其中
2. 通过实现Runnable接口,实例化Thread类
一、通过继承Thread类实现多线程
二、通过继承Runnable接口实现多线程
1.3 三种创建线程的办法
(1)
package aaa;
/**
*
* @author Administrator
*
*/
class MyR implementsRunnable{
private String msg;
public MyR(String msg) {
this.msg=msg;
}
@Override
publicvoidrun() {
while(true){
try {
Thread.sleep(1000);
System.out.println(msg);;
}catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
}
publicclasstest {
publicstaticvoid main(String[] args) {
Thread thread1 = new Thread(new MyR("hello"));
thread1.start();
Thread thread2 = new Thread(new MyR("wuwuwuwu"));
thread2.start();
}
}
(2)
package aaa;
publicclasstest3 {
publicstaticvoid main(String[] args) {
test3testThread2=newtest3();
// 匿名信匿名类
Runnable runnable = new Runnable() {
@Override
publicvoid run() {
while(true) {
try {
Thread.sleep(1000);
System.out.println("qiaonima");
}catch(InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
break;
}
}
}
};
Thread thread = new Thread(runnable);
thread.start();
}
}
(3)
package aaa;
import javax.xml.stream.events.StartDocument;
publicclasstest4 {
publicstaticvoid main(String[] args) {
new Thread(new Runnable() {
@Override
publicvoid run() {
// TODO Auto-generated method stub
while(true) {
try {
Thread.sleep(1000);
System.out.println("qiaonima");
}catch(InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
break;
}
}
}
}).start();
new Thread(()->{
System.out.println("qiaonima");
}).start();
}
}
2 线程简单同步(同步块)
2.1 同步的概念和必要性
1.进程同步:就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事.就像早上起床后,先洗涮,然后才能吃饭,不能在洗涮没有完成时,就开始吃饭.按照这个定义,其实绝大多数函数都是同步调用(例如sin,isdigit等)。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。最常见的例子就是
sendmessage。该函数发送一个消息给某个窗口,在对方处理完消息之前,这个函数不返回。当对方处理完毕以后,该函数才把消息处理函数所返回的lresult值返回给调用者。
我们可以在计算机上运行各种计算机软件程序。每一个运行的程序可能包括多个独立运行的线程(Thread)。
线程(Thread)是一份独立运行的程序,有自己专用的运行栈。线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等。
当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团。
同步这个词是从英文synchronize(使同时发生)翻译过来的。我也不明白为什么要用这个很容易引起误解的词。既然大家都这么用,咱们也就只好这么将就。
线程同步的真实意思和字面意思恰好相反。线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。
因此,关于线程同步,需要牢牢记住的第一点是:线程同步就是线程排队。同步就是排队。线程同步的目的就是避免线程“同步”执行。这可真是个无聊的绕口令。
关于线程同步,需要牢牢记住的第二点是 “共享”这两个字。只有共享资源的读写访问才需要同步。如果不是共享资源,那么就根本没有同步的必要。
关于线程同步,需要牢牢记住的第三点是,只有“变量”才需要同步访问。如果共享的资源是固定不变的,那么就相当于“常量”,线程同时读取常量也不需要同步。至少一个线程修改共享资源,这样的情况下,线程之间就需要同步。
关于线程同步,需要牢牢记住的第四点是:多个线程访问共享资源的代码有可能是同一份代码,也有可能是不同的代码;无论是否执行同一份代码,只要这些线程的代码访问同一份可变的共享资源,这些线程之间就需要同步。
为了加深理解,下面举几个例子。
有两个采购员,他们的工作内容是相同的,都是遵循如下的步骤:
(1)到市场上去,寻找并购买有潜力的样品。
(2)回到公司,写报告。
这两个人的工作内容虽然一样,他们都需要购买样品,他们可能买到同样种类的样品,但是他们绝对不会购买到同一件样品,他们之间没有任何共享资源。所以,他们可以各自进行自己的工作,互不干扰。
这两个采购员就相当于两个线程;两个采购员遵循相同的工作步骤,相当于这两个线程执行同一段代码。
下面给这两个采购员增加一个工作步骤。采购员需要根据公司的“布告栏”上面公布的信息,安排自己的工作计划。
这两个采购员有可能同时走到布告栏的前面,同时观看布告栏上的信息。这一点问题都没有。因为布告栏是只读的,这两个采购员谁都不会去修改布告栏上写的信息。下面增加一个角色。一个办公室行政人员这个时候,也走到了布告栏前面,准备修改布告栏上的信息。如果行政人员先到达布告栏,并且正在修改布告栏的内容。两个采购员这个时候,恰好也到了。这两个采购员就必须等待行政人员完成修改之后,才能观看修改后的信息。如果行政人员到达的时候,两个采购员已经在观看布告栏了。那么行政人员需要等待两个采购员把当前信息记录下来之后,才能够写上新的信息。
上述这两种情况,行政人员和采购员对布告栏的访问就需要进行同步。因为其中一个线程(行政人员)修改了共享资源(布告栏)。而且我们可以看到,行政人员的工作流程和采购员的工作流程(执行代码)完全不同,但是由于他们访问了同一份可变共享资源(布告栏),所以他们之间需要同步。
2.2 synchronize关键字和同步块
关键字synchronize拥有锁重入的功能,也就是在使用synchronize时,当一个线程的得到了一个对象的锁后,再次请求此对象是可以再次得到该对象的锁。
当一个线程请求一个由其他线程持有的锁时,发出请求的线程就会被阻塞,然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由她自己持有的锁,那么这个请求就会成功,“重入” 意味着获取锁的操作的粒度是“线程”,而不是调用。
即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
代码如:
synchronized(object){
}
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
2.3 实例
package T1;
import java.util.concurrent.locks.Lock;
import javax.swing.text.StyledEditorKit.ForegroundAction;
import com.sun.media.jfxmedia.events.NewFrameEvent;
publicclassTestSync {
staticintc=0;
privatestaticObject lock=newObject();//创建一个锁变量
publicstaticvoid main(String[] args) {
Thread[]threads= newThread[1000];
for(inti=0;i<1000;i++) {
finalintindex =i;
threads[i]= new Thread(()->{
synchronized(lock) {//创建一个同步块,需要一个锁
System.out.println("thread "+index+"enter");//输出
inta = c;
a++;
try {
Thread.sleep((long) (Math.random()*1000));
}catch(Exception e) {
e.printStackTrace();
}
c=a;//存回去
System.out.println("thread"+index+"leave");
}
});
threads[i].start();
}
for(inti=0;i<1000;i++) {
try {
threads[i].join();
}catch(InterruptedException e) {
e.printStackTrace();
}
}
System.out.print("c="+c);
}
}
3 生产者消费者问题
3.1 问题表述
问题描述:一群生产者进程在生产产品,并将这些产品提供给消费者去消费。为了使生产者进程与消费者进程能够并发进行,在两者之间设置一个具有n个缓冲区的缓冲池,生产者进程将产品放入一个缓冲区中;消费者可以从一个缓冲区取走产品去消费。尽管所有的生产者进程和消费者进程是以异方式运行,但它们必须保持同步:当一个缓冲区为空时不允许消费者去取走产品,当一个缓冲区满时也不允许生产者去存入产品。
3.2 实现思路
我们这里利用一个一个数组buffer来表示这个n个缓冲区的缓冲池,用输入指针和输出指针+1来表示在缓冲池中存入或取出一个产品。由于这里的缓冲池是循环缓冲的,故应把in和out表示成:in = (in +1 ) % n (或把out表示为 out = ( out +1 ) % n )当( in +1) % n= out的时候说明缓冲池满,in = out 则说明缓冲池空。在这里还要引入一个整型的变量counter(初始值0),每当在缓冲区存入或取走一个产品时,counter +1或-1。那么问题的关键就是,把这个counter作为临界资源处理,即令生产者进程和消费者进程互斥的访问它。
3.3 Java实现该问题的代码
package aaa;
import java.util.concurrent.ThreadLocalRandom;
publicclasstest7 {
statictest5 queue= newtest5(5);
publicstaticvoid main(String[] args) {
for(inti=0;i<3;i++) {
finalintindex = i;
new Thread(()->{
intdata =(int)(Math.random()*1000);
System.out.printf("produce thread %d want to EnQueue %d\n",index,data);
queue.EnQueue(data);
System.out.printf("produce thread %d EnQueue %d Success\n",index,data);
sleep();
}).start();
}
for(inti=0;i<3;i++) {
finalintindex=i;
new Thread(()->{
System.out.printf("customer thread %d want to EnQueue\n",index);
intdata = queue.DeQueue();
System.out.printf("customer thread %d EnQueue %d Success\n",index,data);
}).start();
}
}
publicstaticvoid sleep() {
intt =(int)(Math.random()*100);
try {
Thread.sleep(t);
}catch(Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
package aaa;
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
publicclasstest5 { //队列建立一个锁,两个信号量
private Lock lock = new ReentrantLock();
private Condition fullC;//信号量
private Condition emptyC;//信号量
privateintsize;
public test5(intsize) {
this.size = size;
fullC=lock.newCondition();
emptyC=lock.newCondition();
}
LinkedList<Integer>list= newLinkedList<Integer>();
/**
* 入队
* @return
*/
publicboolean EnQueue(intdata) {
lock.lock();
while(list.size()>=size) {
try {
fullC.await();
}catch(InterruptedException e) {
lock.unlock();
returnfalse;
}
}
list.addLast(data);
emptyC.signalAll();
lock.unlock();
returntrue;}
}
/**
* 出队
* @return
*/
publicint DeQueue() {
lock.lock();
while(list.size()==0) {
try {
emptyC.await();
}catch(InterruptedException e) {
lock.unlock();
return -1;
}
}
intr=list.removeFirst();
fullC.signalAll();
lock.unlock();
returnr;
}
publicboolean isFull() {
returnlist.size()>=size;
}
publicboolean isEmpty() {
returnlist.size()==0;
}
}
3.4 测试
3.4.1 当生产能力超出消费能力时的表现
3.4.2 当生产能力弱于消费能力时的表现
4 总结
课程设计终于结束了,我们的程序也通过了,这次的课程设计给我们很大的收获,使我对操作系统的基本知识有了进一步的深化,开始的时候总觉得很简单的课程设计,后来在分析时,才发现老师的要求蛮高的,但明白了一个道理,简单的应付是不容易完成的。