1线程的创建与启动
一、什么是进程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
二、什么是线程
线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
三、进程和线程的区别
1、进程是资源分配的最小单位,线程是程序执行的最小单位。
2、进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
3、线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
4、但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。
1、Thread类实现了Runnable接口,在Thread类中,有一些比较关键的属性,比如name是表示Thread的名字,可以通过Thread类的构造器中的参数来指定线程名字,priority表示线程的优先级(最大值为10,最小值为1,默认值为5),daemon表示线程是否是守护线程,target表示要执行的任务。
下面是Thread类中常用的方法:
以下是关系到线程运行状态的几个方法:
1)start方法
start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。
2)run方法
run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。
3)sleep方法
sleep方法有两个重载版本:
sleep(long millis) //参数为毫秒
sleep(long millis,int nanoseconds) //第一参数为毫秒,第二个参数为纳秒
sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。
但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。
4)yield方法
调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。
5)join方法
join方法有三个重载版本:
join()
join(long millis) //参数为毫秒
join(long millis,int nanoseconds) //第一参数为毫秒,第二个参数为纳秒
假如在main线程中,调用thread.join方法,则main方法会等待thread线程执行完毕或者等待一定的时间。如果调用的是无参join方法,则等待thread执行完毕,如果调用的是指定了时间参数的join方法,则等待一定的事件。
2、实现Runnable接口创建多线程
通过继承Thread类实现多线程,但是这种方式有一定的局限性。因为在java中只支持单继承,一个类一旦继承了某个父类就无法再继承Thread类,比如学生类Student继承了person类,就无法再继承Thread类创建的线程。为了克服这种弊端,Thread类提供了另外一种构造方法Thread(Runnable target),其中Runnable是一个接口,它只有一个run()方法。当通过Thread(Runnable target)构造方法创建一个线程对象时,只需该方法传递一个实现了Runnable接口的实例对象,这样创建的线程将调用实现了Runnable接口中的run()方法作为运行代码,二不需要调用Thread类中的run()方法。
1. package test;
2.
3. public class example {
4. public static voidmain(String[] args){
5. MyThread myThread=new MyThread();
6. Thread thread=new Thread(myThread);
7. thread.start();
8. while(true)
9. {
10. System.out.println("Main方法在运行");
11. }
12. }
13.}
14.
15.class MyThread implements Runnable{
16. public void run(){
17. while(true){
18. System.out.println("MyThread类的run()方法在运行");
19. }
20. }
21.}
一、Java中创建线程主要有三种方式:
1、继承Thread类创建线程类
(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动该线程。
package com.thread;
publicclass FirstThreadTest extends Thread{
int i = 0;
//重写run方法,run方法的方法体就是现场执行体
publicvoid run()
{
for(;i<100;i++){
System.out.println(getName()+" "+i);
}
}
publicstaticvoid main(String[] args)
{
for(int i = 0;i< 100;i++)
{
System.out.println(Thread.currentThread().getName()+" : "+i);
if(i==20)
{
new FirstThreadTest().start();
new FirstThreadTest().start();
}
}
}
}
上述代码中Thread.currentThread()方法返回当前正在执行的线程对象。GetName()方法返回调用该方法的线程的名字。
2、通过Runnable接口创建线程类
(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建 Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来启动该线程。
示例代码为:
package com.thread;
publicclass RunnableThreadTest implements Runnable
{
privateint i;
publicvoid run()
{
for(i = 0;i <100;i++)
{
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
publicstaticvoid main(String[] args)
{
for(int i = 0;i < 100;i++)
{
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20)
{
RunnableThreadTest rtt = new RunnableThreadTest();
new Thread(rtt,"新线程1").start();
new Thread(rtt,"新线程2").start();
}
}
}
}
线程的执行流程很简单,当执行代码start()时,就会执行对象中重写的void run();方法,该方法执行完成后,线程就消亡了。
3、通过Callable和Future创建线程
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
publicinterface Callable
{
V call() throws Exception;
}
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。(FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了Future和Runnable接口。)
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
实例代码:
package com.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
publicclass CallableThreadTest implements Callable<Integer>
{
publicstaticvoid main(String[] args)
{
CallableThreadTest ctt = new CallableThreadTest();
FutureTask<Integer> ft = new FutureTask<>(ctt);
for(int i = 0;i < 100;i++)
{
System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);
if(i==20)
{
new Thread(ft,"有返回值的线程").start();
}
}
try
{
System.out.println("子线程的返回值:"+ft.get());
} catch (InterruptedException e)
{
e.printStackTrace();
} catch (ExecutionException e)
{
e.printStackTrace();
}
}
@Override
public Integer call() throws Exception
{
int i = 0;
for(;i<100;i++)
{
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
}
}
1、采用实现Runnable、Callable接口的方式创建多线程时,
优势是:
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势是:
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
2、使用继承Thread类的方式创建多线程时,
优势是:
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势是:
线程类已经继承了Thread类,所以不能再继承其他父类。
3、Runnable和Callable的区别
(1) Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
(2) Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
(3) call方法可以抛出异常,run方法不可以。
(4) 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
1. 概念:线程同步就是线程排队。同步就是排队。线程同步的目的就是避免线程“同步”执行。
2. 线程同步的几种方式
(1)互斥锁
1.概念:实现线程访问临界资源的同步控制。如果一个线程在临界区开始时,给互斥锁加锁,那么其他的线程就必须等待线程解锁,才能接着运行,并访问资源。
2.操作:初始化,加锁、解锁、销毁锁
锁初始化:
intpthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr);
加锁:
intpthread_mutex_lock(pthread_mutex_t *mutex);
解锁:
intpthread_mutex_unlock(pthread_mutex_t *mutex);
销毁锁:
intpthread_mutex_destroy(pthread_mutex_t *mutex);
(2)信号量
1.信号量—-二进制信号量
2.操作:初始化, P 操作,V 操作,销毁
函数:#include <semaphore.h>
int sem_init(sem_t *sem, int shared, int val); 初始化
int sem_wait(sem_t *sem); P 操作
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem); V 操作
int sem_destroy(sem_t *sem); 销毁
3、必要性
必要性:不管是多线程还是多进程,涉及到共享相同的内存时,需要确保好同步问题。对线程来说,需要确保每个线程看到一致的数据视图。如果每个线程使用的变量都是其他线程不会读取和修改的,那么就不存在一致性问题,同样的,如果变量是只读的,多个线程同时读取该变量也不会有一致性问题。但是如果其中的某个线程去改变该变量,其他线程也能读取或者修改的时候,我们就需要对这些线程进行同步,确保他们访问变量的存储内容时不会访问到无效的值。当线程修改变量的时候,其他线程在读取这个变量时可能会看到一个不一致的值,在变量修改时间多于一个存储器访问周期的处理器结构中,当存储器读与写这两个周期交叉,不一致就会出现。
2.2 synchronize关键字和同步块
synchronized 关键字,它包括两种用法:synchronized 方法和 synchronized 块。
1. synchronized 方法:通过在方法声明中加入 synchronized关键字来声明 synchronize方法。如: publicsynchronized void accessVal(int newVal);
synchronized 方法控制对类成员变量的访问:每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized)。在 Java 中,不光是类实例,每一个类也对应一把锁,这样我们也可将类的静态成员函数声明为 synchronized ,以控制其对类的静态成员变量的访问。
2. synchronized 块:通过 synchronized关键字来声明synchronized 块。语法如下:
synchronized(syncObject) {
//允许访问控制的代码
}
synchronized 块是这样一个代码块,其中的代码必须获得对象 syncObject (如前所述,可以是类实例或类)的锁方能执行,具体机制同前所述。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。
2.3 实例
package zheng;
import com.sun.media.jfxmedia.events.NewFrameEvent;
public class testhread {
static int c=0;
staticObject lock = new Object();
publicstatic void main(String[] args) {
Thread[]thread = new Thread[1000];
for(inti=0;i<1000;i++) {
finalint index = i;
thread[i]= new Thread(()->{
synchronized(lock){
System.out.println("thread"+index+"enter");
inta = c;//获取c的值
a++;//将值加一
try{//模拟复杂处理过程
Thread.sleep((long)(Math.random()*1000));
}
catch(InterruptedExceptione) {
e.printStackTrace();
}
c=a;//存回去
System.out.println("thread"+index+"leave");
}
});
thread[i].start();//线程开始
}
for(inti=0;i<1000;i++) {
try{
thread[i].join();//等待thread i完成
}catch(InterruptedExceptione) {
e.printStackTrace();
}
}//循环后所有的线程都完成了
System.out.println("c="+c);//输出c的结果
}
}
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作为临界资源处理,即令生产者进程和消费者进程互斥的访问它。
package org.young;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* 生产者
*/
publicclass Producer implements Runnable {
private Queue q;
private Condition isFull; //信号量,如果满了则等待
private Condition isEmpty; //信号量,如果空了则等待
private Lock lock;
privateintindex; //生产者的编号
public Producer(intindex,Queue q,Lock lock,Condition isFull,Condition isEmpty) {
this.index = index;
this.q= q;
this.isFull = isFull;
this.isEmpty = isEmpty;
this.lock = lock;
}
@Override
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();
}
}
3.4.1当生产能力超出消费能力时的表现
当生产能力超出消费能力时,生产者线程生产物品时没有空缓冲区可用,生产者线程必须等待消费者线程释放出一个空缓冲区。
3.4.2当生产能力弱于消费能力时的表现
生产能力弱于消费能力时,消费者线程消费物品,如果没有满的缓冲区,那么消费者线程将被阻塞,直到新的物品被生产者线程生产出来。
4 总结
操作系统是现在计算机系统中必不可少的基本系统软件,通过这次实践我们学到了很多课堂没有学到的知识和学习方法,帮助我们更进一步的了解了操作系统这门课程,因此操作系统的学习与实践是必不可少的。
本文介绍了线程的基本概念,包括进程与线程的区别、Java中创建线程的三种方法及其实现细节。此外,还详细讲解了线程同步的重要性及其实现方式,包括互斥锁、信号量和synchronized关键字的使用。

被折叠的 条评论
为什么被折叠?



