1 线程的创建与启动
1.1 进程与线程
1、线程
定义:线程是进程中的一个实体,作为系统调度和分派的基本单位。
2、进程
定义:指在系统中能独立运行并作为资源分配的基本单位,它是由一组机器指令、数据和堆栈等组成的,是一个能独立运行的活动实体。
3、进程与线程的联系与差别
一个进程包含一个或多个线程。
进程是被分给并拥有资源的基本单元。同一进程内的多个线程共享该进程的资源,线程并不拥有资源,只是使用他们。
1.2 Java中的Thread和Runnable类
java提供了两种线程方式:
一种是继承java.lang包下的Thread类,覆写Thread类的run()方法,在run()方法中实现运行在线程上的代码。弊端:通过继承Thread类实现多线程有一定的局限性,因为java中只支持单继承,一个类一旦继承了某个父类就无法再继承Thread类了
第二种是实现Runnable接口创建多线程。Runnable接口提供了run()方法的原型,因此创建新的线程类时,只要实现此接口,既只要特定的程序代码实现Runnable接口中的run()方法,就可以完成新线程类的运行
1.3 三种创建线程的办法
实验过程:
1. 打开Eclipse Java,点击File-> new-> JavaProject,建立名为os 的project。
2. 在os下,点击File-> new-> Package,建立名为org的包。
3.在org 下,点击File-> new-> class,分别建立名为TestThread、TestThread2、TestThread3的三个类。
4.分别在三个类中使用以下三种方法来创建线程:
方法一:
package org;
class MyR implementsRunnable{
private String msg;
public MyR(String msg) {
this.msg=msg;
}
//线程的入口
publicvoid run() {
while(true) {
try {
Thread.sleep(1000);
System.out.println(msg);
}catch(InterruptedException e) {
e.printStackTrace();
break;
}
}
}
}
publicclassTestThread {
publicstaticvoid main(String[] args) {
Thread thread1=new Thread(newMyR("hello"));
thread1.start();//启动了线程
Thread thread2=new Thread(newMyR("wuwu"));
thread2.start();//启动了线程
}
}
运行结果: Hello与wuwu同步出现
方法二:
package org;
publicclassTestThread2 {
publicstaticvoid main(String[] args) {
//匿名信匿名类引用就是指针
TestThread2testThread2=new TestThread2();
//runnable 是一个接口 new后面应该跟类名所以创建了一个匿名类
Runnablerunnable=new Runnable() {
publicvoid run() {
while(true) {
try {
System.out.println("haha");
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
break; }
}
}
};
Threadthread=newThread(runnable);
thread.start();
}
}
运行结果:
方法三:
package org;
publicclassTestThread3 {
publicstaticvoid main(String[] args) {
/*
//匿名类经典写法1
newThread(new Runnable() {
publicvoid run() {
while(true){
try{
System.out.println("haha");
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
}).start();
//注意:都在括号里
*/
//经典写法2: Lamda表达式(复杂变简单) JAVA1.8以上支持 语法tang
new Thread(()-> {
System.out.println("haha");
}).start();
}
}
/*等价于
newThread(new Runnable() {
publicvoid run() {
System.out.println("haha");
}
}).start();
}
*/
运行结果:
2 线程简单同步(同步块)
2.1 同步的概念和必要性
同步指协同步调,协助、相互配合。就是在发出一个功能调用时,在没有得到返回结果之前一直在等待,不会继续往下执行。比如说线程A和线程B一块完成某个功能,线程A执行到某个步骤是需要线程B的执行结果,于是就停下来示意线程B执行,线程B得到结果时,唤醒线程A继续执行
某个线程可以修改变量,而其他线程也可以读取或修改这个变量的时候,就需要对这些线程进行同步,以确保它们在访问变量的存储内容时不会访问到无效的数值。当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。
举个例子,如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块。假设账户原本有0块,如果取钱线程和存钱线程同时发生,竟无法判断结果,取钱不成功,账户余额是100.取钱成功了,账户余额是0.出现混乱。
所以若没有线程同步,当一个线程修改变量时,其他线程在读取这个变量的值时就可能会看到不一致的数据。
2.2 synchronize关键字和同步块
在JAVA中实现线程同步使用synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。synchronized 关键字,它包括两种用法:synchronized 方法和 synchronized 块。
1. synchronized 方法:通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。控制对类成员变量的访问:每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例
2.synchronized 块:通过synchronized关键字来声明synchronized 块。
synchronized 块是这样一个代码块,其中的代码必须获得对象 syncObject (可以是类实例或类)的锁方能执行。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。
2.3 实例
实验一
package org;
publicclassTestSync {
staticintc=0;
publicstaticvoid main(String[] args) {
Thread[]threads=newThread[1000];
for(inti=0;i<1000;i++) {
threads[i] =new Thread(()-> {
inta=c;//获取c的值
a++;//值加一
try {//模拟复杂处理过程
Thread.sleep((long)(Math.random()*1000));
}catch(InterruptedException e) {
e.printStackTrace();
}//异常处理两种方式:1.try catch 2.throw
c=a;//存回去
});
threads[i].start();//线程开始
}
for(inti=0;i<1000;i++) {
try {
threads[i].join();//等待thread i 完成
}catch(InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}//循环后,所有进程都完成了
System.out.println("c="+c);//输出c 的结果
}
}
运行结果:
实验二:
package org;
publicclassTestSync {
staticintc=0;
static Object lock=new Object();//随便建立了一个变量,作为锁变量
publicstaticvoid main(String[] args) {
Thread[]threads=newThread[1000];
for(inti=0;i<1000;i++) {
finalintindex = i;//建立了一个final变量,放在lamba中使用
threads[i] =new Thread(()-> {
synchronized (lock) {//创建一个同步块,需要一个锁
System.out.println("thread "+index+"enter");
inta=c;//获取c的值
a++;//值加一
try {//模拟复杂处理过程
Thread.sleep((long)(Math.random()*10));
}catch(InterruptedException e) {
e.printStackTrace();
}//异常处理两种方式:1.try catch 2.throw
c=a;//存回去
System.out.println("thread "+index+"leave");
}//这是块的终结
});
threads[i].start();//线程开始
}
for(inti=0;i<1000;i++) {
try {
threads[i].join();//等待thread i 完成
}catch(InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}//循环后,所有进程都完成了
System.out.println("c="+c);//输出c 的结果
}
}
运行结果:
3 生产者消费者问题
3.1 问题表述
有一个队列
生产者负责将元素加入队列中,谓之生产
消费者负责将元素从队列中移出,谓之消费
3.2 实现思路
唤醒的时机? 当有消费者消费了队列中的元素,队列不再满,就可以唤醒生产者让其生产
当队列为空时,让消费者等待,当生产者生产了元素,队列不再为空,则唤醒消费者。
3.3 Java实现该问题的代码
创建队列Queue类:
package org;
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//建立一个锁,俩信号量
publicclassQueue {//队列
private Lock lock=new ReentrantLock();
private Condition fullC;
private Condition emptyC;
privateintsize;
public Queue(intsize) {
this.size=size;
//为信号量赋值
fullC=lock.newCondition();
emptyC=lock.newCondition();
}
LinkedList<Integer>list= newLinkedList<Integer>();
/*
* 入队
*/
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;
}
/**
* 出队
*/
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;
}
}
创建生产者Producer类:
package org;
/**
* 生产者
*/
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
publicclassProducer implementsRunnable{
private Queue q;
private Condition isFull;//信号量,如果满了则等待
private Condition isEmpty;//信号量,如果空了则等待
private Lock lock;
privateintindex;//生产者的编号
public Producer (intindex,Queue q,Condition isFull,Condition isEmpty) {
this.q=q;
this.isFull=isFull;
this.isEmpty=isEmpty;
this.lock=lock;
this.index=index;
}
publicvoid run() {
lock.lock();
if(q.isFull()) {
try {
isFull.await();//如果队列为空,则等待
}catch(InterruptedException e) {
return;
}
}
//生产并入队
inta=(int) (Math.random()*1000);
q.EnQueue(a);
//生产完
isEmpty.signalAll();//把消费者唤醒
lock.unlock();
}
}
创建测试TestPC类:
package org;
import java.util.concurrent.ThreadLocalRandom;
publicclassTestPC {
static Queue queue =new Queue(5);
publicstaticvoid main(String[] args) {
//创建三个生产者
for(inti=0;i<3;i++) {
finalintindex=i;
new Thread(()->{
while(true) {
intdata=(int)(Math.random()*1000);
System.out.printf("producer thread %d want to EnQueue %d\n",index,data);
queue.EnQueue(data);
System.out.printf("producer thread %d EnQueue %d Success\n",index,data);
sleep();
}
}).start();
}
//创建消费者
for(inti=0;i<3;i++) {
finalintindex=i;
new Thread(()->{
while(true) {
System.out.printf("consumer thread %d want to DeQueue\n",index);
intdata=queue.DeQueue();
System.out.printf("consumer thread %d DeQueue %d Success\n",index,data);
sleep2();//随机休息一段时间
}
}).start();
}
}
//sleep随机时间
publicstaticvoid sleep() {
intt=(int)(Math.random()*100);
try {
Thread.sleep(t);
}catch(InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
publicstaticvoid sleep2() {
intt=(int)(Math.random()*1000);
try {
Thread.sleep(t);
}catch(InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
运行结果:
3.4 测试
3.4.1 当生产能力超出消费能力时的表现
当生产能力超出消费能力时,生产者线程生产物品时,没有缓冲区可用,生产者线程必须等待消费者线程释放出一个空缓冲区。
3.4.2 当生产能力弱于消费能力时的表现
当生产能力弱于消费能力时,消费者线程消费物品,如果没有满的缓冲区,那么消费者线程将被阻塞,直到新的物品被生产者线程生产出来。
此次课程设计包括三方面内容,分别是线程的创建与启动、线程简单同步(同步块)和生产者消费者问题。
通过学习创建线程的三种方法,对进程与线程的概念和差别有了进一步理解,并且熟悉了Java中的Thread类和Runnable类。通过学习线程的同步问题,我更深的了解了线程同步的必要性,并且学会了使用JAVA中的synchronize关键字和同步块。通过使用编程实现生产者消费者问题,让我对这个问题理解的更透彻,进一步明确了实现思路。
本次操作系统编程实践我认为重点难点在于使用JAVA编程实现生产者消费者问题。生产者是一堆线程,消费者是另一堆线程,内存缓冲区可以使用队列。关键是如何处理多线程之间的协作。所以内存缓冲区为空的时候消费者必须等待,而内存缓冲区满的时候,生产者必须等待。多线程对临界区资源的操作时候必须保证在读写中只能存在一个线程,所以需要设计锁的策略。
在本次设计中的编写、调试、执行的过程中,也发现了编程时出现的种种问题,像是打字速度跟不上,对关键字的使用不熟悉等,这都会是很好的经验教训。