课堂总结

1线程的创建与启动

1.1 进程与线程

一、什么是进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

二、什么是线程

线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪阻塞运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。

线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称多线程

三、进程和线程的区别

1、进程是资源分配的最小单位,线程是程序执行的最小单位。

2、地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。

3、资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程

4、执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

5、线程是处理器调度的基本单位,但是进程不是。

6、两者均可并发执行。

1.2 Java中的ThreadRunnable

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类提供了另外一种构造方法ThreadRunnable target),其中Runnable是一个接口,它只有一个run()方法。当通过ThreadRunnable target)构造方法创建一个线程对象时,只需该方法传递一个实现了Runnable接口的实例对象,这样创建的线程将调用实现了Runnable接口中的run()方法作为运行代码,二不需要调用Thread类中的run()方法。

1. package test;

2.  

3. public class example {

4.       public static void main(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. }

1.3 三种创建线程的办法

一、Java中创建线程主要有三种方式:

1、继承Thread类创建线程类

通过继承Thread类来创建并启动多线程的一般步骤如下

1d定义Thread类的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。

2创建Thread子类的实例,也就是创建了线程对象

3启动线程,即调用线程的start()方法

代码实例

public class MyThread extends Thread{//继承Thread

public void run(){ //重写run方法

}

}

public class Main {

public static void main(String[] args){

new MyThread().start();//创建并启动线程

}

}

 

线程的执行流程很简单,当执行代码start()时,就会执行对象中重写的void run();方法,该方法执行完成后,线程就消亡了。

2、实现Runnable接口创建进程

通过实现Runnable接口创建并启动线程一般步骤如下:

1定义Runnable接口的实现类,一样要重写run()方法,这个run()方法和Thread中的run()方法一样是线程的执行体

2创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象

3第三部依然是通过调用线程对象的start()方法来启动线程

代码实例:

public class MyThread2 implements Runnable {//实现Runnable接口

public void run(){

//重写run方法

}

}

public class Main {

public static void main(String[] args){

//创建并启动线程

MyThread2 myThread=new MyThread2();

Thread thread=new Thread(myThread);

thread().start();

//或者    new Thread(new MyThread2()).start();

}

}

3、使用CallableFuture创建线程

创建并启动有返回值的线程的步骤如下:

1创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lamada表达式创建Callable对象)。

2使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值

3使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)

4调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

代码实例:

public class Main {

public static void main(String[] args){

MyThread3 th=new MyThread3();

//使用Lambda表达式创建Callable对象

  //使用FutureTask类来包装Callable对象

FutureTask<Integer> future=new FutureTask<Integer>(

(Callable<Integer>)()->{

return 5;

}

 );

new Thread(task,"有返回值的线程").start();//实质上还是以Callable对象来创建并启动线程

 try{

System.out.println("子线程的返回值:"+future.get());//get()方法会阻塞,直到子线程执行结束才返回

 }catch(Exception e){

ex.printStackTrace();

}

}

}

二、创建线程的三种方式的对比

1、采用实现RunnableCallable接口的方式创建多线程时,

优势是:

线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。

在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

劣势是:

编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

2、使用继承Thread类的方式创建多线程时

优势是:

编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

劣势是:

线程类已经继承了Thread类,所以不能再继承其他父类。

3RunnableCallable的区别

(1) Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。

(2) Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。

(3) call方法可以抛出异常,run方法不可以。

(4) 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

4实现Runnable和实现Callable接口的方式基本相同,不过是后者执行call()方法有返回值,后者线程执行体run()方法无返回值,因此可以把这两种方式归为一种这种方式与继承Thread类的方法之间的差别如下:

1、线程只是实现Runnable或实现Callable接口,还可以继承其他类。

2、这种方式下,多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。

3、但是编程稍微复杂,如果需要访问当前线程,必须调用Thread.currentThread()方法。

4、继承Thread类的线程类不能再继承其他父类(Java单继承决定)。

注:一般推荐采用实现接口的方式来创建多线程

 

2 线程简单同步(同步块)

2.1 同步的概念和必要性

1、概念:同步(英语:Synchronization),指对在一个系统中所发生的事件(event)之间进行协调,在时间上出现一致性与统一化的现象。在系统中进行同步,也被称为及时(in time)、同步化的(synchronousin sync)。

2、线程同步的几种方式 
1)互斥锁 
1.概念:实现线程访问临界资源的同步控制。 如果一个线程在临界区开始时,给互斥锁加锁, 那么其他的线程就必须等待线程解锁, 才能接着运行, 并访问资源。 
2.操作: 初始化, 加锁、 解锁、 销毁锁

锁初始化:

int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr);

加锁:

int pthread_mutex_lock(pthread_mutex_t *mutex);

解锁:

int pthread_mutex_unlock(pthread_mutex_t *mutex);

销毁锁:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

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方法。如:  public synchronized 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;

static Object lock = new Object();

public static void main(String[] args) {

Thread[] thread = new Thread[1000];

for(int i=0;i<1000;i++) {

final int index = i;

thread[i] = new Thread(()->{

synchronized(lock) {

System.out.println("thread"+index+"enter");

int a = c;//获取c的值

a++;//将值加一

try {//模拟复杂处理过程

Thread.sleep((long)(Math.random()*1000));

}

catch(InterruptedException e) {

e.printStackTrace();

}

c=a;//存回去

System.out.println("thread"+index+"leave");

}

});

thread[i].start();//线程开始

}

for(int i=0;i<1000;i++) {

try {

thread[i].join();//等待thread i完成

}catch(InterruptedException e) {

e.printStackTrace();

}

}//循环后所有的线程都完成了

System.out.println("c="+c);//输出c的结果

 

}

}

3 生产者消费者问题

3.1 问题表述

1:生产者-消费者问题描述:

生产者消费者问题,也称有限缓冲问题,是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

3.2 实现思路

我们这里利用一个一个数组buffer来表示这个n个缓冲区的缓冲池,用输入指针和输出指针+1来表示在缓冲池中存入或取出一个产品。由于这里的缓冲池是循环缓冲的,故应把inout表示成: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 org.young;

 

import java.util.concurrent.locks.Condition;

import java.util.concurrent.locks.Lock;

 

/**

 * 生产者

 */

public class Producer implements Runnable {

private Queue q;

private Condition isFull; //信号量 , 如果满了则等待

private Condition isEmpty; //信号量 , 如果空了则等待

private Lock lock;

private int index; //生产者的编号

public Producer(int index,Queue q,Lock lock,Condition isFull,Condition isEmpty) {

this.index = index;

this.q= q;

this.isFull = isFull;

this.isEmpty = isEmpty;

this.lock = lock;

}

@Override

public void run() {

lock.lock();

if(q.isFull()) {

try {

isFull.await(); //如果队列为慢,则等待

} catch (InterruptedException e) {

return;

}

}

//生产并入队

int a = (int) (Math.random()*1000);

q.EnQueue(a);

//生产完后

isEmpty.signalAll();//把消费者唤醒。

lock.unlock();

}

}

3.4 测试

3.4.1当生产能力超出消费能力时的表现

当生产能力超出消费能力时,生产者线程生产物品时没有空缓冲区可用,生产者线程必须等待消费者线程释放出一个空缓冲区。

3.4.2当生产能力弱于消费能力时的表现

生产能力弱于消费能力时,消费者线程消费物品,如果没有满的缓冲区,那么消费者线程将被阻塞,直到新的物品被生产者线程生产出来

4 总结

通过这几节课的学习,我学会了多线程执行,Runnable的实现类是线程执行的主体 run函数是入口,三个线程交替执行,还有在同步问题中用同步块实现,以及生产者消费者问题的概述、实现思路等,收获很多,Java语言简洁明了,对我们在软件开发等有很大帮助。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值