菜鸟学习笔记:Java提升篇7(线程1——进程、程序、线程的区别,Java多线程实现,线程的状态)
进程、程序、线程的区别
在讲解之前先明确两个概念:
并发:多个任务在同一个CPU核上,按细分的时间片轮流执行,从逻辑上看任务是同时执行的;
并行:多个处理器或多核处理器同时处理多个任务。
进程与程序的区别
进行和程序的区别我们可以用一个例子来形容:计算机要实现一个功能好比你买一一个东西要使用,首先计算机并不直到如何实现这个功能,就好比你不会用你买的东西,这时候你就需要一本说明书告诉你这个东西怎么用,同样的道理程序编译得到的机器码就是写给计算机如何实现这一功能的说明书。有了说明书之后你就会根据说明书中所说的内容进行操作,同理计算机也会根据机器码所表达内容进行相应的操作,这个过程就叫做进程。根据这个例子的讲解我们把视线回到程序执行过程上,一般情况下Java程序的执行分为两个大部分:
首先编译器将我们的程序翻译成一条一条的机器指令存储到内存中,递送给操作系统进行处理。这一步主要工作是把Java文件翻译成class文件,再将class文件中的类加载到内存中,对所有变量、地址引用完成初始化,对程序执行需要的堆、栈、方法区等空间经行初始化。总的来说就是为程序的执行做出准备。
在准备完成之后,执行引擎会按照随着程序计数器的节拍,将main函数放到方法区栈中,并按照顺序一条条的去读取代码对应的指令放入CPU中执行,直到执行完最后一条指令为止程序结束。
可能Java初学的同学们对以上过程不是非常理解,但无所谓,我主要要说明所谓进程就是上面的完整过程,也就是从编译、初始化、再到运行这一完整过程,而程序就是指明这个过程中要进行什么操作的具体说明书。
进程与线程的区别
还是先举个例子,csdn博客对应的就是浏览器的一个进程,我们都知道计算机中执行机器指令是按照顺序依次执行的,那也就说明我在写csdn博客时左边的输入框需要接收我的键盘输入才能往下继续执行,但为什么我们在写博客的同时可以放大缩小编辑窗口、切换目录和帮助窗口,甚至打开其他网页查看资料那?这就是线程在作用,每个进程在执行时会对进程进行再一次分解,进而得到一个个线程,每个线程的执行相互独立(有自己的栈、程序计数器),但共享部分数据区(堆空间、方法区),各个进程独立执行。每个进程必须有一个主线程与之对应。
为什么要有线程的概念那?首先要明确多个线程在微观上是并发执行并不是并行执行的,CPU只能一条一条的执行指令,各个线程也只能按照时间片轮流的执行。但是CPU的执行速度是非常快的,基本不会发生等待,我们程序执行发生等待的情况基本都是申请外部资源时。如果加入了线程的概念,那么需要申请外部资源时CPU并不会等待,而是去执行另外一个线程。从而让对外部设备的访问就可以并行完成。大大加快了我们程序执行效率。
这样进程和线程之间的区别也大概明确了,最后总结一下。线程就是进程中的一个执行过程。
进程、程序和线程的三者的区别也是面试常考问题,需要大家理解记忆。
Java中实现多线程
方式一Thread类
Java中实现多线程的一种方式主要是通过Java.lang.Thread这个类来完成,我们只需要继承这个类并重写其中的run方法就可以实现多线程操作。我们通过多线程可以模拟一下龟兔赛跑的过程:
首先创建兔子类和乌龟类继承Thread类,重写run方法
public class Rabbit extends Thread {
@Override
public void run() {
//线程体
for(int i=0;i<100;i++){
System.out.println("兔子跑了"+i+"步");
}
}
}
class Tortoise extends Thread {
@Override
public void run() {
//线程体
for(int i=0;i<100;i++){
System.out.println("乌龟跑了"+i+"步");
}
}
}
之后我们在调用时执行start方法(注意不是run方法)就可以实现多线程运行。
public static void main(String[] args) {
//创建子类对象
Rabbit rab = new Rabbit();
//Tortoise tor =new Tortoise();
//调用start 方法
rab.start(); //不要调用run方法
//tor.start();
//主线程中也运行
for(int i=0;i<1000;i++){
System.out.println("main==>"+i);
}
}
这样就会得到这样的结果:
可以看到CPU在不同时间片内会执行不同类中的程序,也就说明两个线程类并发执行。
方式二Runnable 接口
由于Java是单继承的,那么一个线程继承了Thread类就无法在继承其他类了,我们最好可以通过实现接口的方式来实现多线程操作。所以通过Runnable来实现多线程操作更为常用。
定义过程是先定义线程类并实现Runnable接口,之后重写run方法。
public class Web12306 implements Runnable {
private int num =50;
@Override
public void run() {
while(true){
if(num<=0){
break; //跳出循环
}
System.out.println(Thread.currentThread().getName()+"抢到了"+num--);
}
}
}
在实现过程中采用静态代理的方法,首先定义线程类对象,之后用Thread(Runnable Target)构造器定义对象,并执行start方法。
public static void main(String[] args) {
//真实角色
Web12306 web = new Web12306();
//代理
Thread t1 =new Thread(web,"路人甲");
Thread t2 =new Thread(web,"黄牛已");
Thread t3 =new Thread(web,"攻城师");
//启动线程
t1.start();
t2.start();
t3.start();
}
程序执行结果:
方式三Callable接口(了解)
该接口的优点是创建的线程可以有返回值,但是创建过程非常繁琐,大家做了解即可,实现代码如下:
public class Call {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//创建线程
ExecutorService ser=Executors.newFixedThreadPool(2);
Race tortoise = new Race("乌龟",1000);
Race rabbit = new Race("小兔子",500);
//获取值
Future<Integer> result1 =ser.submit(tortoise) ;
Future<Integer> result2 =ser.submit(rabbit) ;
Thread.sleep(2000); //主线程暂停2秒,两个子线程继续执行
tortoise.setFlag(false); //停止线程体循环
rabbit.setFlag(false);
int num1 =result1.get();
int num2 =result2.get();
System.out.println("乌龟跑了-->"+num1+"步");
System.out.println("小兔子跑了-->"+num2+"步");
//停止服务
ser.shutdownNow();
}
}
//泛型定义的类型就是返回值类型
class Race implements Callable<Integer>{
private String name ; //名称
private long time; //延时时间
private boolean flag =true;
private int step =0; //步
public Race() {
}
public Race(String name) {
super();
this.name = name;
}
public Race(String name,long time) {
super();
this.name = name;
this.time =time;
}
//线程方法
@Override
public Integer call() throws Exception {
while(flag){
Thread.sleep(time); //延时
step++;
}
return step;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public int getStep() {
return step;
}
public void setStep(int step) {
this.step = step;
}
}
线程重要概念
线程状态
线程状态一共有5个,分别是:
新建:new 线程对象后。
可运行(就绪):调用了线程对象的start方法,该状态的线程位于可运行线程池中,等待被线程调度选中。
运行:可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
阻塞:线程因为某种原因放弃了cpu 使用权(比如执行sleep方法、、join方法、IO流中readLine方法),暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。
死亡:线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。
它们之间的关系如下:
线程停止
停止线程的方法一般有两种,一种是等到线程体自己执行完成自然停止,另一种是通过外部干涉,在类中定义标识并在线程方法中使用,在外边通过改变标识的方法终止线程类的执行。实例如下:
package com.bjsxt.thread.status;
public class StopDemo01 {
public static void main(String[] args) {
Study s =new Study();
new Thread(s).start();
//外部干涉
for(int i=0;i<100;i++){
if(50==i){ //外部干涉
s.stop();
}
System.out.println("main.....-->"+i);
}
}
}
class Study implements Runnable{
//1)、线程类中 定义 线程体使用的标识
private boolean flag =true;
@Override
public void run() {
//2)、线程体使用该标识
while(flag){
System.out.println("study thread....");
}
}
//3)、对外提供方法改变标识
public void stop(){
this.flag =false;
}
}
也可以通过调用destory方法或者stop方法停止线程,但这两个方法有安全隐患,不建议使用。
线程阻塞
Java中可以通过几个方法使运行中的线程变成阻塞状态。
join方法
join方法的主要作用是合并线程,说白了一个线程调用该方法后该线程执行过程不会再有其他线程加入,直到该线程执行完其他线程才会执行。它会让其他所有线程进入阻塞状态。
public static void main(String[] args) throws InterruptedException {
JoinDemo01 demo = new JoinDemo01();
Thread t = new Thread(demo); //新生
t.start();//就绪
//cpu调度 运行
for(int i=0;i<1000;i++){
if(50==i){
t.join(); //main阻塞...
}
System.out.println("main...."+i);
}
}
@Override
public void run() {
for(int i=0;i<1000;i++){
System.out.println("join...."+i);
}
}
}
此程序会在join执行完之后才会继续执行main:
yield方法
它是一个静态方法,调用它的线程会发生阻塞。
public class YieldDemo01 extends Thread {
public static void main(String[] args) {
YieldDemo01 demo = new YieldDemo01();
Thread t = new Thread(demo); //新生
t.start();//就绪
//cpu调度 运行
for(int i=0;i<1000;i++){
if(i%20==0){
//暂停本线程 main
Thread.yield();
}
System.out.println("main...."+i);
}
}
@Override
public void run() {
for(int i=0;i<1000;i++){
System.out.println("yield...."+i);
}
}
}
由于cpu调度我们不能控制,而且暂停时间非常短,所以即使线程暂停也可能之后马上又被调度,所以该方法效果不是太明显这里不做演示,只给大家示例代码,大家只需要直到该方法会让一个线程进入阻塞状态就可以了。
Sleep方法
该方法最为常用,也是一个static方法,调用时需要传入一个数字代表暂停的毫秒数,它会让调用该方法的线程在传入的时间内进入阻塞状态。
Sleep方法有一个特点,它会给被暂停的线程加上排他锁(该线程占用的资源其他线程不能访问)直到时间结束。
//从当前时间开始倒计时10秒
public class SleepDemo01 {
public static void main(String[] args) throws InterruptedException {
Date endTime =new Date(System.currentTimeMillis()+10*1000);
long end =endTime.getTime();
while(true){
//输出
System.out.println(new SimpleDateFormat("mm:ss").format(endTime));
//等待一秒
Thread.sleep(1000);
//构建下一秒时间
endTime =new Date(endTime.getTime()-1000);
//10秒以内 继续 否则 退出
if(end-10000>endTime.getTime()){
break;
}
}
}
}
程序结果:
上一篇:菜鸟学习笔记:Java提升篇6(IO流2——数据类型处理流、打印流、随机流)
下一篇:菜鸟学习笔记:Java提升篇8(线程2——线程的基本信息、线程安全、死锁、生产者消费者模式、任务调度)