1:Java多线程的概述
- 定义:进程和线程是现代操作系统中两个必不可少的运行模型,在操作系统中可以有很多进程,包括系统进程和用户进程,一个进程中可以有多个线程 。
注:
1:进程:是一个正在执行中的程序(每一个进程执行都有一个执行顺序(或者叫执行路径或者叫控制单元)。
2: 线程:就是进程中的一个独立的控制单元(线程控制着进程的执行)。
3: 一个进程中至少有一个一个线程 。例如: JVM(进程)启动时会有一个进程java.exe,该进程中至少一个线程负责Java程序的执行,这个线程的运行代码存在于主函数中(主线程)
4:jvm启动不止一个线程,还有负责回收垃圾机制的线程。 - 普通方法调用和多线程
说起进程,就不得不说下程序。程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。而进程则是执行程序的一次执行过程,它是一个动态的概念。是系统资源分配的单位。通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程是CPU调度和执行的的单位。
注意:很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下, 在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错局。
总结:
-
线程就是独立的执行路径;
-
在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程, gc线程;main()称之为主线程,为系统的入口,用于执行整个程序;
-
在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能认为的干预的。
-
对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
-
线程会带来额外的开销,如cpu调度时间,并发控制开销。
-
每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
2:创建线程的方法
创建线程有三种方法:1:继承Thread类 2:实现Runable接口 3:Callable接口(了解)
如下都会介绍
引入问题:如何在自定义代码中,自定义一个线程呢?通过对API文档查找,java已经提供了对线程(对象)这类事物的描述,Thread类。
注:JAVA API文档提供了很多官方的介绍和类、方法、变量的解释。一般很系统,涉及所有的方面,如果开发人员对正在使用的类不熟悉,想查看类里面的变量或者方法,就可以打开JavaAPI文档进行阅读和查看。
2.1 创建线程的方法1:继承Thread类
步骤:
- 1:定义类继承Thread
- 2:复写Thread类中的run()方法
- 3:调用start()方法 方法作用:导致此线程开始执行; Java虚拟机调用此线程的run方法。
class Demo extends Thread{
//1:定义类继承Thread
public void run() {
//2:复写Thread类中的run()方法
for(int i=0;i<10;i++) {
System.out.println("demo run"+i);
}
}
}
public class SetUpThreadMethods1 {
public static void main(String[] args) {
Demo demo =new Demo();//创建好一个线程(对象)
demo.start(); //3:调用start()方法 方法作用:导致此线程开始执行; Java虚拟机调用此线程的run方法。
for(int i=0;i<10;i++) {
System.out.println("hello java"+i);
}
}
}
/* 该结果可以说明:
* 1.这个代码中出现了两个线程
* 2.cpu在某一时刻只能执行一个程序,比如CPU在执行网易云以后,又执行网游,反复执行,CPU在这里面做快速切换(速度快)CPU其实在切换每一个进程中的线程,所以程序越多就越卡
* 3.发现每次运行结果都不同,因为多个线程都获取CPU的执行权,CPU执行谁,谁就运行。
* 多线程的特性:随机性。(线程)谁抢到,谁执行
* 教师:CPU 教室:内存 教师越多教室越大。
*/
2.2 创建线程的方法2:实现Runable接口
步骤:
1:定义类实现Runable接口。
2:覆盖Runable接口中的run()方法(将线程要运行的代码存放到run()方法中)。
3:通过Thread类建立线程对象。
4:将runable接口的子对象作为实际参数传递给Thread的构造函数。
理由:自定义的run方法()所属的对象是runable接口的子对象。所以要让线程去指定指定对象的run()方法,就必须明确该run()方法所属对象
Thread(Runnable target) //Thread类的构造方法 作用: 分配一个新的 Thread对象。
5:调用thread类的start()方法开启线程并调用runnable接口子类的run方法
2.3 两种创建多线程方式: 实现(接口)方式和继承(类) 方式的区别:
- 继承Thread类
-
子类继承Thread类具备多线程能力
-
启动线程:子类对象.start()
-
不建议使用:避免OOP单继承局限性
- 实现Runnable接口
-
实现接口Runnable具有多线程能力
-
启动线程:传入目标对象+ Thread对象,start()
-
推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用
案例说明
class Tickets implements Runnable{
// 1:定义类实现Runable接口。
private int num=10;
public void run() {
// 2:覆盖Runable接口中的run()方法(将线程要运行的代码存放到run()方法中)。
while(num>0) {
System.out.println(Thread.currentThread().getName()+" "+"tickets:"+num--);
}
//}
}
}
public class SetUpThreadMethods2 {
public static void main(String[] args) {
Tickets a=new Tickets();
Thread s1=new Thread(a);// 3:通过Thread类建立线程对象。 4:将runable接口的子对象作为实际参数传递给Thread的构造函数。
//Thread类的构造方法 作用: 分配一个新的 Thread对象。
Thread s2=new Thread(a);
Thread s3=new Thread(a);
s1.start(); // 5:调用thread类的start()方法开启线程并调用runnable接口子类的run方法
s2.start();
s3.start();
}
}
/*
* 该运行结果说明,三个窗口去卖了10张票(三个线程去指定同一个runable接口的子对象)
* 发现问题:多个线程操作同一个资源的情况下,线程不安全,数据紊乱
*/
2.4 巩固多线程案例
龟兔赛跑
public class RaceTurtleAndRabblt implements Runnable{
private static String winner;
@Override
public void run() {
//模拟兔子休息
if (Thread.currentThread().getName().equals("兔子")){
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i=0;i<=100;i++){
boolean flag=gameOver(i);
if (flag){
break;
}
System.out.println(Thread.currentThread().getName()+"-->跑了"+i+"步");
}
}
public boolean gameOver(int step){
if (winner!=null){
return true;//已经存在胜利者
}
if (step>=100){
winner=Thread.currentThread().getName();
System.out.println("获胜者:"+winner);
return true;
}
return false;
}
public static void main(String[] args) {
RaceTurtleAndRabblt raceTurtleAndRabblt=new RaceTurtleAndRabblt();
new Thread(raceTurtleAndRabblt,"乌龟").start();
new Thread(raceTurtleAndRabblt,"兔子").start();
}
}
...
乌龟-->跑了96步
兔子-->跑了0步
........
乌龟-->跑了99步
兔子-->跑了24步
获胜者:乌龟
3:线程的状态
1. 新建状态:
实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了新建状态。
2.可运行状态(就绪状态)
1.只是说你资格运行,调度程序没有挑选到你,你就永远是可运行状态。
2.调用线程的start()方法,此线程进入可运行状态。
3.当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入可运行状态。
4.当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入可运行状态。
5.锁池里的线程拿到对象锁后,进入可运行状态。
3. 运行状态
线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。