进程和线程
线程依赖于进程而存在
进程:正在运行的程序 1、是系统进行资源分配和调用的独立单位 2、每一个进程都有它自己的内存空间和系统资源
线程:是进程中的单个顺序控制流,是一条执行路径 1、单线程指的是一个进程如果只有一条执行路径,则称为单线程程序 2、多线程指的是一个进程如果只有多条执行路径,则称为多线程程序 注意:进程是计算机中资源分配的最小单位,而线程是进程的执行单位。一个进程可以包含多个线程 举例:记事本程序是单线程,扫雷程序是多线程
继承Thread类的方式实现多线程
Thread类在java.lang包下。该类是具体类,继承自Object,实现自Runnable接口
线程是程序中执行的线程,即Thread类可以理解为是一个线程。Java虚拟机允许应用程序同时运行多个执行线程
线程是如何创建出来的呢,有两种方式可以创建新的执行线程。如下
方式一:继承Thread类,步骤如下 1、定义一个类MyThread继承Thread类 2、在MyThread类中重写run()方法 3、创建MyThread类的对象 4、启动线程
Thread类里面有一个start方法,作用是使此线程开始执行; Java虚拟机调用此线程的run方法
为什么要重写run方法,原因:因为run()是用来封装被线程执行的代码
run()方法和start()方法的区别: 1、run():封装线程执行的代码,直接调用,相当于普通方法的调用 2、start():做了两件事,第一件事是启动线程,第二件事是由JVM调用此线程的run()方法
继承Thread类来实现多线程的练习
xxxxxxxxxx
package ch21;
public class a_2_1测试 {
public static void main(String[] args) {
//第一种方式创建新的执行线程 继承Thread类
//3、创建a_2_2MyThread类的对象。因为是多线程程序,所以我们就创建两个对象
a_2_2MyThread my1 = new a_2_2MyThread();
a_2_2MyThread my2 = new a_2_2MyThread();
//4、启动线程。注意run方法是我们在a_2_2MyThread里面重写的方法,run方法里面我们写的是一个循环
//my1.run(); //实验预期:my1调用的run方法应该和my2调用的run方法共同参与执行,而不是等到my1执行结束后再执行my2
//my2.run(); //输出没有达到我们的预期,先my1执行结束后再执行my2,说明程序设计的还不是多线程
//原因:直接调用run方法并没有启动线程。解决:调用start方法,才是启动线程。原理:start方法的底层实际上是调用了run方法。解决如下
my1.start();
my2.start(); //此时就达到我们的预期,输出就是即有my1调用run方法的内容,又有my1调用run方法的内容
}
}
xxxxxxxxxx
package ch21;
//1、定义一个类MyThread继承Thread类
public class a_2_2MyThread extends Thread {
//2、在MyThread类中重写run()方法
//为什么要重写,理由:我们的MyThread类中可能有其它代码,并不是所有代码都需要被线程执行。为了区分需不需要被线程执行,java就提
//供了一个run方法来封装需要被线程执行的代码
public void run() { //快捷生成重写run方法:输入run,再直接回车。注意把run()后面那个花括号里面的代码改成自己写的
for(int i=0; i<=50; i++){
System.out.println(i);
}
}
}
设置和获取线程名称
Thread类中设置和获取线程名称的方法 1、void setName(String name): 将此线程的名称更改为等于参数name 2、String getName():返回此线程的名称 3、通过构造方法也可以设置线程名称
如何获取main方法所在的线程名称,如下 第一步:public static Thread currentThread():返回当前正在执行的线程对象 第二步:Thread.currentThread().getName():用线程对象来调用getName才能获取到线程的名称
设置和获取线程名称的练习
xxxxxxxxxx
package ch21;
//线程类,继承了Thread类
public class a_3_1MyThread extends Thread {
//带参的
//思考:使用带参构造方法会怎么样,如下
//首先在'a_3_1MyThread'里面写一个无参构造方法MyThread(){},再提供一个带参构造方法MyThread(String name){},如下
public a_3_1MyThread(){}
public a_3_1MyThread(String name){
super(name);//且通过super访问父类中的带参构造方法
}
//----------------------------------------------------------------------------------------------------------------
public void run() { //快捷生成重写run方法:输入run,再直接回车。注意把run()后面那个花括号里面的代码改成自己写的
for(int i=0; i<=50; i++){
//System.out.println(i); //当双线程代码,在控制台同时参与输出,我们无法辨认哪些数据是哪个线程的。优化如下
//-------------------------------------------------------------------------------------------------------------------
//String getName():返回此线程的名称。注意我们还没有设置此线程的名称。注意谁调用这个for方法,'此'就指的谁。比如我们这里就是my1和my2
System.out.println(getName()+":"+i); //getName是线程名称
//总结:如果不设置线程名称,直接获取,系统会默认给一个线程名称,比如这里的Thread-0和Thread-1
/*为什么系统会给默认线程名称呢,理由:该类即'a_3_1MyThread'类没有写无参构造方法,那么就会去访问父类即Thread类的无参构造
方法,即父类的无参构造方法给的默认线程名称
具体原因可选中最上面的'Thread',再按Ctrl+B,查看源码。可查看到Thread类的无参构造方法的源码。如下
private String name;//给这里成员变量赋值的方式,即下面部分的源码的setName设置值,getName获取值
public Thread() {
this(null, null, "Thread-" + nextThreadNum(), 0);
}
分析父类即Thread类的构造方法的源码,选中最上面的Thread,按Ctrl+B,查看源码,如下。注意这行是对应的是带参的,不是无参的
public Thread(String name) {
this(null, null, name, 0);
}
再选中上面源码的nextThreadNum,再按Ctrl+B,查看源码,如下
private static int threadInitNumber; //第二次到这里才是1。第三次到这里才是2,...
private static synchronized int nextThreadNum() {
return threadInitNumber++; //后++,所以第一次返回的是0.第二次到这里时,第二次返回的是1,...
}
再选中上上面的this,继续看源码,如下
public Thread(ThreadGroup group, Runnable target, String name, long stackSize) {
this(group, target, name, stackSize, null, true);
}
在选中上面的this,继续看源码,如下
private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
this.name = name;
}
注意:下面那个是setName方法做的事情
public final synchronized void setName(String name) {
this.name = name; //给线程名字重新赋值命名
}
继续看getName做的事情
public final String getName() {
return name;
}
*/
}
}
}
xxxxxxxxxx
package ch21;
//测试类。注意:主要的代码知识点在线程类里面写了笔记,即在'a_3_1MyThread'类。这里只是简单地调用'a_3_1MyThread'类的方法
public class a_3_2测试 {
//注意,这节课的测试类中有4个线程。无参构造方法2个,带参构造方法2个
public static void main(String[] args) {
//无参的
//通过无参构造方法创建线程对象
a_3_1MyThread my1 = new a_3_1MyThread();
a_3_1MyThread my2 = new a_3_1MyThread();
//带参的
//首先在'a_3_1MyThread'里面写一个无参构造方法MyThread(){},再提供一个带参构造方法MyThread(String name){}
//不然下面那行报错
a_3_1MyThread my11 = new a_3_1MyThread("第11线程");
a_3_1MyThread my22 = new a_3_1MyThread("第22线程");
//无参的
//把下面的setName方法注释掉,那么线程名字就会使用系统默认的
//void setName(String name): 将此线程的名称更改为等于参数name
my1.setName("第1线程");//原本上面通过无参构造方法,给线程一个默认的名称。现在我们对线程用setName重新赋值
my2.setName("第2线程");
//调用方法
my1.start();
my2.start();
my11.start();
my22.start();
//为什么要设置和获取线程名称,原因:例如上面的双线程代码,在控制台同时参与输出,我们无法辨认哪些数据是哪个线程的
//在Thread类中有一个静态方法currentThread(),作用是返回当前正在执行的线程对象
System.out.println(Thread.currentThread().getName()); //用线程对象来调用getName才能获取到线程的名称
//上面那行输出main,即main方法是在一个名字叫做'main'的线程中执行
}
}
线程优先级
线程调度:线程有两种调度模型 1、分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片 2、抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些
Java使用的是抢占式调度模型
假如计算机只有一个CPU,那么CPU在某一时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。 所有说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的
思考: 我们能不能控制线程的优先级呢。答案是能,如下 Thread类中设置和获取线程优先级的方法: 1、public final int getPriority() 作用是返回此线程的优先级 2、public final void setPriority(int newPriority) 作用是更改此线程的优先级
结论: 1、线程默认优先级是5,线程优先级的范围是1~10 2、线程优先级高仅仅表示线程获取的CPU时间片的几率高。但是要在次数比较多,或者多次运行的时候才能看到你想要的效果
线程优先级的练习
xxxxxxxxxx
package ch21;
//线程类
public class a_4_1ThreadPriority extends Thread {
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(getName() + "," + i);
}
}
}
xxxxxxxxxx
package ch21;
//测试类
public class a_4_2测试 {
public static void main(String[] args) {
//创建三个线程对象
a_4_1ThreadPriority tp1 = new a_4_1ThreadPriority();
a_4_1ThreadPriority tp2 = new a_4_1ThreadPriority();
a_4_1ThreadPriority tp3 = new a_4_1ThreadPriority();
//为三个线程命名
tp1.setName("第1线程");
tp2.setName("第2线程");
tp3.setName("第3线程");
//-------------------------------------------------------------------------------------------------------------------
/*//public final int getPriority() 作用是返回此线程的优先级。注意:我们还没有设置线程的优先级
System.out.println(tp1.getPriority());//输出5
System.out.println(tp2.getPriority());//输出5
System.out.println(tp3.getPriority());//输出5。结论:线程默认的优先级是5
System.out.println("------------------------");
//--------------------------------------------------------------------------------------------------------------------
//public final void setPriority(int newPriority) 作用是更改此线程的优先级
//tp1.setPriority(10000); //优先级设置为10000。控制台报错,非法参数异常IllegalArgumentException。这行报错,先注释了。自行解开注释
//上面那行报错的原因:设置的优先级不在规定的优先级范围内
//如果知道规定的范围是多少呢,如下
System.out.println(Thread.MAX_PRIORITY);//输出10
System.out.println(Thread.MIN_PRIORITY);//输出1
System.out.println(Thread.NORM_PRIORITY);//默认的线程优先级。输出5
System.out.println("------------------------");*/
//设置正确的优先级
tp1.setPriority(1);//处理效率最低的是"第1线程"
tp2.setPriority(10);//优先处理"第2线程"
tp3.setPriority(5);
//注意:优先级只是决定线程抢到时间片的几率,并不是优先级越高就越先运行
//-------------------------------------------------------------------------------------------------------------------
//启动线程 。建议:把这里启动线程的3行注释掉,不然控制台输出太乱,不方便看下面的哪些代码演示
//设置好正确的线程优先级之后,就必须解开注释,不然看不了设置好优先级后的输出效果
tp1.start();
tp2.start();
tp3.start();
}
}
线程控制_sleep
方法名 | 说明 |
---|---|
static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数 |
void join() | 等待这个线程死亡 |
void setDaemon(boolean on) | 将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出 |
线程控制_sleep的练习
xxxxxxxxxx
package ch21;
//Thread的线程类
public class a_5_1ThreadSleep extends Thread {
public void run() {
for(int i=0; i<50; i++){
System.out.println(getName()+","+i);
try {
Thread.sleep(1000);//每输出一次就会使线程休眠一秒1000毫秒。报红线就选中sleep,按Alt+Enter捕获异常
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
xxxxxxxxxx
package ch21;
//static void sleep(long millis) 使当前正在执行的线程停留(暂停执行)指定的毫秒数
//ThreadSleep的测试类
public class a_5_2测试 {
public static void main(String[] args) {
a_5_1ThreadSleep ts1 = new a_5_1ThreadSleep();
a_5_1ThreadSleep ts2 = new a_5_1ThreadSleep();
a_5_1ThreadSleep ts3 = new a_5_1ThreadSleep();
ts1.setName("线程1");
ts2.setName("线程2");
ts3.setName("线程3");
ts1.start();
ts2.start();
ts3.start();
//思考:如何使3个线程都有机会每隔3次输出必有且只有执行一次。比如前3次输出必须保证那3个线程都有一次输出机会。比如前6次输出必须
//保证那3个线程都有两次输出机会。
//解决:在'a_5_1ThreadSleep'类里面使用sleep方法即可
//注意:每3次输出后,线程会休眠1秒,然后继续又输出3次。在3次之内,输出哪个线程是没有规律的。保证了那3个线程在3次输出内必抢占到一次时间片
}
}
线程控制_join
方法名 | 说明 |
---|---|
static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数 |
void join() | 等待这个线程死亡 |
void setDaemon(boolean on) | 将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出 |
线程控制_join的练习
xxxxxxxxxx
package ch21;
//Thread的线程类
public class a_6_1ThreadJoin extends Thread{
public void run() {
for(int i=0; i<30; i++){
System.out.println(getName()+":"+i);
}
}
}
xxxxxxxxxx
package ch21;
public class a_6_2测试 {
public static void main(String[] args) {
a_6_1ThreadJoin ts1 = new a_6_1ThreadJoin();
a_6_1ThreadJoin ts2 = new a_6_1ThreadJoin();
a_6_1ThreadJoin ts3 = new a_6_1ThreadJoin();
ts1.setName("线程1");
ts2.setName("线程2");
ts3.setName("线程3");
ts1.start();
try {
ts1.join(); //join会报红线就选中,解决是选中join,按Alt+Enter捕获异常,注意不是抛出异常,是捕获一次,选那个try...catch
} catch (InterruptedException e) {
e.printStackTrace();
}
ts2.start();
ts3.start();
//思考:如何让线程1先执行完,再让线程2和线程3开始执行呢
//解决:在'a_6_2测试'类里面使用join方法。如上
}
}
线程控制_setDaemon
方法名 | 说明 |
---|---|
static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数 |
void join() | 等待这个线程死亡 |
void setDaemon(boolean on) | 将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出 |
线程控制_setDaemon的练习
xxxxxxxxxx
package ch21;
public class a_7_1ThreadDaemon extends Thread{
public void run() {
for(int i=0; i<100; i++){
System.out.println(getName()+":"+i);
}
}
}
xxxxxxxxxx
package ch21;
public class a_7_2测试 {
public static void main(String[] args) {
a_7_1ThreadDaemon td1 = new a_7_1ThreadDaemon();
a_7_1ThreadDaemon td2 = new a_7_1ThreadDaemon();
td1.setName("线程1");
td2.setName("线程2");
//思考:如何设置一个主线程呢,且主线程的名字叫"主线程",如下
Thread.currentThread().setName("主线程");
//思考:如何实现当主线程结束后,另外两个分线程不再继续执行。注意:不要理解为另外两个分线程会马上停止,分线程会继续执行一点点,然后才停
//解决:把那两个分线程设置为守护线程。如下
td1.setDaemon(true);
td2.setDaemon(true);
//注意:启动线程写的位置也很重要。不能写在守护线程的上面,估计代码应该是从上往下执行的
td1.start();
td2.start();
//给主线程安排一下事情。注意这里主线程是只执行到9就停止。上面两个分线程是执行到99才停止
for(int i=0; i<10; i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
线程的生命周期
生命周期如下:
1、创建线程对象 2、调用start方法,此时线程开始启动 3、线程进入就绪状态,即有执行资格却没有执行权。原因:线程启动后不会马上就抢到CPU的执行权 4、抢到CPU的执行权 5、线程进入运行状态,即有执行资格也有执行权 6、当run()结束或调用stop方法,线程就会死亡
总结:新建、就绪、运行、死亡
注意:一个线程在运行状态时如果被其他线程抢走CPU的执行权,那这个线程就会回到就绪状态,即这个线程会继续抢占CPU的执行权
注意:一个线程在运行状态,突然调用了sleep方法,那这个线程就会进入阻塞状态,即没有执行资格且没有执行权。只有等待到sleep方法结束,这个线程会回到就绪状态,即有执行资格却没有执行权
实现Runnable接口的方式实现多线程
创建线程有两种方式,我们之前在ch21的a_2_0~a_8_0学习了通过继承Thread类的方式来创建线程。 现在,我们来学习另一种创建线程的方式,如下
即定义一个实现Runnable接口的类,接着在这个定义的类里面重写run方法和分配类的实例 最后把这个定义的类的对象作为Thread构造方法的参数传递并启动线程
具体步骤如下: 1、定义一个类MyRunnable实现Runnable接口 2、在MyRunnable类中重写run方法 3、创建MyRunnable类的对象 4、创建Thread类的对象,把MyRunnable对象作为构造方法的参数 5、启动线程
注意上面的第四步创建创建Thread类的对象需要用到Thread类的构造方法,如下 Thread(Runnable target) 分配新的Thread对象
多线程的实现方案有两种: 1、继承Thread类 2、实现Runnable接口
创建接口的两种方式,我们这节课学的'通过实现Runnable接口'方式,相比之前学的'继承Thread类'方式有如下优点: 1、避免了Java单继承的局限性 2、适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,较好的体现了面向对象的设计思想
实现Runnable接口来实现多线程的练习
xxxxxxxxxx
package ch21;
public class a_9_1MyRunnable implements Runnable {
public void run() {
for(int i=0; i<10; i++){
//System.out.println(getName()+":"+i);//注意这里的getName报红字。原因:getName是Thread类的方法,不是Runnable接口里面的方法
//如果非要使用上面那行的getName怎么才能做到,如下
System.out.println(Thread.currentThread().getName()+":"+i);//Thread.currentThread()是当前正在执行的线程
}
}
}
xxxxxxxxxx
package ch21;
public class a_9_2测试 {
public static void main(String[] args) {
//3、创建MyRunnable类的对象
a_9_1MyRunnable my = new a_9_1MyRunnable();
//4、创建Thread类的对象,把MyRunnable对象作为构造方法的参数 Thread(Runnable target)
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);//这样就创建了两个线程
//启动线程
t1.start();
t2.start();
//当没有给线程设置名称时,线程会使用系统给的默认线程名称,例如Thread-0、Thread-1
//思考如何给线程起名字呢,用Thread的这个构造方法即可 Thread(Runnable target, String name)
Thread t3 = new Thread(my,"第3线程");
Thread t4 = new Thread(my,"第4线程");
//启动线程
t3.start();
t4.start();
//总结:我们在'a_9_1MyRunnable'类里面使用实现Runnable接口的方式来创建多线程,'a_9_1MyRunnable'类并没有继承Thread类
//优点:'a_9_1MyRunnable'类在以后的开发中,可以有自己的父类
}
}