多线程引入
1、进程和线程介绍
多线程技术主要是解决的是让多个程序能够同时执行起来。可以提高程序的执行效率。
1.1、进程
正在执行的程序。当我们运行系统上安装的应用程序之后,这个应用程序就会被加载内存中,并且在内存中开始运行。
这个应用程序被加载到内存中,它需要在内存中分配内存空间。这时就需要分配内存空间,而这个内存空间就是一个进程。这个空间专门负责当前这个应用程序的执行。
进程其实是专门负责当前这个应用程序的内存空间的分配和管理,以及程序的执行过程的任务调度。
进程它们是独立的运行单元,在内存中是不会互相影响。
1.2、线程
一个应用程序肯定是由多部分代码组成。而这些代码在当前这个进程中要执行。
当一个应用程序被启动之后,这个应用程序所占的内存空间又会被划分成多个区域,多个区域来负责运行当前进程中不同功能。
而负责运行这个功能的那些单独执行空间(执行路径)就称为每个线程。
一个进程中最少要有一个线程。线程才是真正执行应用程序的执行空间。而现在大部分的程序都是多线程程序。这样可以保证当前程序的执行效率。
2、cpu执行线程简介
cpu(中央处理器)它是整个电脑的大脑,所有数据的运行都由它完成。它是负责计算和调度正常电脑系统的运行以及其他程序的运行。
cpu执行程序:
真正cpu执行程序,在某个时刻某个时间点上,cpu只能执行一个线程。而不是同时在执行多个程序。cpu在执行应用程序的时候,它是以时间碎片为概念在多个程序中的多个线程之间来回切换造成。并且cpu的切换速度非常快,导致我们感觉好像是多个程序在同时运行。
是不是在程序开的线程越多越好?
不是,在cpu的有效的处理能力范围内,多开线程,可以提高效率。
3、主线程介绍
在我们书写任何程序中都一个启动的线程,这个线程主线程。
在Java中我们书写的程序主要从main方法开始运行。然后当main方法执行完之后这个程序就结束了。
DemoA2.java——多线程程序引入,主要介绍主线程:
public class DemoA2 { public static void show() { for( int i=0;i<20;i++ ) { System.out.println("show i="+i); } } public static void main(String[] args) { show(); for( int i=0;i<20;i++ ) { System.out.println("main i="+i); } System.out.println("main over...."); } } /* 当我们在dos窗口中输入了java DemoA2回车之后, 会启动JVM,这时就会在JVM运行进程中划分出一片区域用来运行 main方法中的代码。如果main方法中调用了其他的方法,其他的方法也会在当前分配的区域中运行。 这时这些执行的区域,或者称为执行的路径都称为主线程所在的路径。 */
4、线程的实现方式一
线程也是属于一类事物,Java对这个事物就会有自己的描述。我们需要在api中找Java是使用哪个类来描述线程这类事物。
Java中使用Thread这个类来描述线程:这个类在java.lang包中。
Thread类是专门用来描述线程这类事物。它可以负责在程序执行过程单独的开辟出一片内存区域用来运行当前单独分配的任务。
创建新执行线程有两种方法。一种方法是将类声明为 Thread
的子类。该子类应重写 Thread
类的 run
方法。接下来可以分配并启动该子类的实例。
4.1、实现线程的第一种方式:
1、书写一个类,必须继承Thread
2、重写Thread类中的run方法
3、创建子类的对象
4、通过子类对象开启线程
ThreadDemo.java——创建线程的第一种方式 继承Thread ★★★★★
//定义一个类继承Thread class ThreadDemo extends Thread { //重写Thread类中的run方法 public void run() { for( int i=0;i<20;i++ ) { System.out.println("run i="+i); } } //程序都是从main方法开始运行 public static void main(String[] args) { ThreadDemo d = new ThreadDemo(); //启动这个线程 d.start(); /* 创建的子类对象之后调用start方法 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 */ for( int i=0;i<20;i++ ) { System.out.println("main i="+i); } System.out.println("main over...."); } }
4.2、为什么继承Thread和复写run方法
为什么要继承Thread类?
因为在Java中使用Thread类描述线程这类事物。也就是说Thread类具备了操作线程这类事物的基本行为(功能)。当书写了一个类继承了Thread这个类,那么我们写的这个子类也就变成线程类了。那么我们书写的这个类也会具备线程的继承操作行为(这些行为其实是从Thread类中继承到的)。我们就可以开启我们自己写的这个线程对象类。
我们定义线程目的是什么?
目的是希望在内存中开辟出多条执行路径,让多部分代码能够(并发)同时执行。如果我们直接new Thread类,而这个Thread是api中已经写好的类,我们无法把自己想让线程执行的代码交给Thread类。那么我们就直接写个类继承Thread,那么我们自己的类也就变成线程类了,那么就可以在自己的类中书写线程要并发执行的代码。
为什么要复写run方法?
run方法也是Thread类中写好的方法。这个方法是在调用start方法开启线程的时候,有JVM自动调用run方法。JVM调用run方法的目的是去执行我们给run方法中分配的需要线程并发执行的那些代码。复写run方法的目的就是在run方法书写当前线程要执行的任务代码。
如果是不调用start方法,而是创建一个线程对象,直接通过这个对象去调用run方法和使用start间接的去调用run方法有什么区别?
ThreadDemo2 d = new ThreadDemo2();
//启动这个线程
//d.start();
d.run();
在这里new ThreadDemo2 的确是创建了线程对象。但是这个对象直接调用run方法的话,就是前面我们所学习对象调用方法是一样的,这时并没有在内存把这个线程开启,也就是线程存在了,但是它不运行。
ThreadDemo2.java——调用start和run方法区别 ★★★★★
5、多线程运行图解
多线程执行过程:
线程内存执行图解:
线程中的异常问题:
当我们在运行多线程程序的时候,如果那个线程发生了异常,那么这个线程所在的代码就会停止运行,但是其他线程不受影响。
红色部分表示异常发生在哪个线程上。黄色部分异常的名字以及异常的发生原因
这时在Thread-0线程上发生了异常,就会导致Thread-0线程停止运行。但是其他线程依然可以正常运行
ThreadDemo3.java——重复启动线程,以及异常问题
//定义一个类继承Thread class Demo extends Thread { //复写run方法 public void run() { for( int i=0;i<10;i++ ) { System.out.println( getName()+"....run i="+i); } } } class ThreadDemo3 { public static void main(String[] args) { //创建子类对象 Demo d = new Demo(); Demo d2 = new Demo(); System.out.println("......"+d.getName()); System.out.println("=================="+d2.getName()); //d.setName("小强"); //d2.setName("旺财"); //开启线程 d.start(); d2.start(); //d2.start(); // java.lang.IllegalThreadStateException //发生异常的原因是线程已经处于运行状态,不能再次开启 //d2.run(); for( int i=0;i<10;i++ ) { System.out.println(Thread.currentThread().getName()+ ".................main i="+i); } System.out.println("over.................."); } }
6、获取线程名字和线程状态
6.1、获取线程的名字:
Thread类是是描述线程本身的,而现在我们又需要获取线程的名字,猜测有这个方法,猜测返回值可能是String。方法:getName();
在Thread类中的确有getName方法返回当前线程的名字;
在Java中如果我们没有手动的指定线程的名字,那么JVM会自动给我们线程分配名字,名字是 Thread-x x从0开始;
上面报错的原因是ThreadDemo3这个类中就没有getName()方法。而我们又想在ThreadDemo3中的main方法中获取当前正在运行的主线程的名字。这时可以使用Thread类中的静态的方法currentThread就可以获取当前正在运行的这个线程对象。从而就可以获取到当前这个线程的名字
获取线程名字的方法:
先使用Thread类中的currentThread方法获取到当前正在运行的线程对象,然后再调用getName方法获取线程的名字。
6.2、线程状态
7、线程的实现方式二
1、定义类实现Runnable接口
2、实现run方法,这个run方法是接口中的方法
3、创建实现类对象
4、创建Thread类对象,把实现类对象作为参数传递
5、开启线程
当我们要定义一个线程时,目的是给这个线程分配线程运行时要执行的任务代码。Java在设计的时候使用Thread类来描述线程这个事物,这时在Thread类中定义了一个run方法。而这个run方法是专门用来存放线程要执行的任务。Java在设计Thread类的时候,让线程本身对象和线程要执行的任务严重的耦合在一起。于是就把这个任务单独的抽离出来,放到一个接口中,这样就可以保证Thread类专门负责线程这个事物,而Runnble接口专门负责线程要执行的任务。
当我们要让线程执行某个任务时,首先我们可以创建Thread本身对象,然后在把任务传递给Thread对象。这样Thread对象就可以执行我们传递的这个任务。
由于Java只支持单继承,如果有一个类中有部分代码需要多线程来执行,而这个类已经继承了其他的类,这时这个类无法在继承Thread类,可是其中需要多线程执行的任务没有办法交给Thread类。这时就可以让这个类实现Runnable接口,然后创建这个类对象,在把这个类对象交给Thread,那么Thread 就可以执行其中的任务。
RunnableDemo.java——创建线程的第二种方式 实现Runnable接口 ★★★★★
//定义一个类,实现Runnable接口 class Demo implements Runnable { //实现run方法 public void run() { for (int i=0;i<20 ;i++ ) { System.out.println(Thread.currentThread().getName()+"...i..."+i); } } } class RunnableDemo { public static void main(String[] args) { //创建实现类对象 Demo d = new Demo(); //这里仅仅创建了线程要执行的任务对象 //创建Thread对象 目的是创建线程本身对象 Thread t = new Thread( d ); Thread t2 = new Thread( d ); Thread t3 = new Thread( d ); t.start(); t2.start(); t3.start(); } }
8、线程练习
模拟售票窗口:
一般情况下火车站有多个窗口负责售票。这些窗口都会同时(并发执行)售票。使用多线程技术来模拟窗口售票的现象。
可以把售票的这个动作认为是线程要操作的任务。这个任务什么时候结束?当把某个趟列车上的票售完之后,窗口就不能在售这趟列车上的票。
我们现在模拟假设有100张票,4个窗口同时来卖。只要有任何一个窗口售完最后一张票,其他窗口保包含当前窗口不能在继续售票。
ThreadTest.java——使用thread 完成售票的功能 ★★★★★
//定义类来模拟售票的案例,售票的动作要被多线程执行 class Ticket extends Thread{ //定义一个变量用来记录当前的总票数 //由于我们当前的类本身就是线程对象类,在创建当前类对象的时候 //num没有被静态,每个对象中都会有自己的num那么在run方法运行的时候 //就会出现每个run中都有使用自己当前这个对象中的num //把num变成静态之后,那么所有的Ticket对象就共享这个num成员变量 static int num = 50; //售票的动作要被多线程执行,就需要把售票的动作放在run方法中 public void run() { //书写死循环的目的是让任何一个线程进来之后,就不断的售票 //由于是死循环,就会导致线程在不断的售票 while( true ) { //当num为0的时候,就说明票已经卖完,就不应该在打印数据了 if( num > 0 ) { //每打印一次,就代表售出一张票 //让任何线程执行到这里都休眠5毫秒 System.out.println(Thread.currentThread().getName()+"....."+num); num--; } } } } class ThreadTest { public static void main(String[] args) { //创建四个线程对象 Ticket t1 = new Ticket(); Ticket t2 = new Ticket(); Ticket t3 = new Ticket(); Ticket t4 = new Ticket(); //开启线程 t1.start(); t2.start(); t3.start(); t4.start(); } }