一、 概述
1、 进程与线程
-
进程:指的是正在运行的程序
-
线程:线程是进程中的一个执行单元,
- 是程序使用CPU的基本单位(调度)。
- 负责当前进程中程序的执行。
- 是进程中单个顺序控制流(执行路径),是一条单独执行的路径
-
一个程序运行后,至少有一个进程,一个进程可包含多个线程
-
在操作系统中,进程是资源分配的基本单位,线程是调度的基本单位
在没有出现进程之前,进程既是操作系统进行资源分配的基本单位,又是调度的基本单位
-
单线程程序:有多个任务只能依次执行并且上一个任务执行结束后下一个任务才能开始执行。程序只有一条执行路径
-
多线程程序:有多个任务可以同时执行。程序有多条执行路径。
操作系统发展:单道批处理操作——>多道批处理操作系统——>分时操作系统(多进程的)——>线程
批处理:程序在执行过程中,不会响应用户的请求
单道批处理操作:一次只能运行一个程序,如果要在计算机运行多个程序,这多个程序,只能一个一个的顺序执行,如果这个正在运行的程序,在运行过程中,执行了一些非常耗时的IO操作(传输数据的过程是没有使用到cpu),这样一来,cpu就闲下来了,但是cpu是计算机中,最为昂贵的
多道批处理操作系统:同时运行多个程序,显著的提高了cpu的利用率,但是我们一旦一个,程序运行起来,都是是需要使用计算机资源的
分时操作系统:每一个进程,有一个固定的时间片,在运行一个固定的时间片后,紧接着轮到下一个进程运行(切换)
- 线程出现的原因: 进程切换的代价过高,每一次进程切换,都需要付出不小的额外的代价。为了减小进程切换的代价,引入了线程,提高了CPU的利用率
2、 线程调度
- 分时调度:所有线程轮流使用CPU,平均分配每个线程占用CPU的时间
- 抢占式调度:优先让优先级高的线程使用CPU,若线程的优先级相同,则会随机选择一个(线程的随机性),Java使用的是抢占式调度
- 体现: 程序运行的不确定性
- CPU 使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
- 多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
3、 线程控制
-
线程睡眠:
- static void sleep(long millis) :在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
- //让主线程,休眠3秒 Thread.sleep(3000);
- //sleep新的写法 TimeUnit.SECONDS.sleep(3);
-
线程加入:
-
public final void join():(让当前线程)等待该线程(新加入的线程终止)终止。
SecondThread sec = new SecondThread("zs"); SecondThread thr = new SecondThread("lsii"); SecondThread fou = new SecondThread("wangwu"); //启动这三个线程对象 sec.start(); //sec线程上调用join方法 ,让当前线程(main)等待sec线程执行完毕 sec.join(); thr.start(); fou.start();
-
-
线程礼让:
- public static void yield():暂停当前正在执行的线程对象,并执行其他线程
- 只是让当前线程放弃cpu执行权,但是不能阻止它放弃后继续抢夺cpu执行权
-
后台线程(守护线程)
- public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。
- 当正在运行的线程都是守护线程时,Java 虚拟机退出;该方法必须在启动线程前调用。
SecondThread sec = new SecondThread("zs"); SecondThread thr = new SecondThread("lsii"); //将sec和thr线程设置为守护线程 sec.setDaemon(true); thr.setDaemon(true); //启动 sec.start(); thr.start(); //之后发现sec和thr是守护线程,就会中断
-
中断线程:
-
public void interrupt():让一个线程控制另外一个线程(有条件的:受阻)
-
可以利用该方法终止另一个线程的运行
-
如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 等阻塞方法处于阻塞状态,它还将收到一个 InterruptedException
-
中断一个不处于活动状态的线程不需要任何作用。
中断一个不处于阻塞状态的线程,没有其他任何效果
-
public class Test { public static void main(String[] args) { FiveThread fiveThread = new FiveThread(); fiveThread.start(); //在主线程中,终止fiveThread,休眠状态 fiveThread.interrupt(); } } // 睡5秒之后,再让该线程输出一句话 class FiveThread extends Thread{ @Override public void run() { try { //假如申请了很多的系统资源 TimeUnit.SECONDS.sleep(5); System.out.println("FiveThread 睡醒了"); } catch (InterruptedException e) { e.printStackTrace(); //异常的意义:即使我的线程被异常终止,我也可以保证资源的正常释放 } } } //会抛出 java.lang.InterruptedException: sleep interrupted
-
4、 Java程序的运行原理
- Java命令会启动JVM,即启动了一个进程,该进程会启动一个主线程,主线程调用某个类的main方法 ---->main方法运行在主线程中
- JVM启动后,比如有一个执行路径(线程)从main方法开始,一直执行到main方法结束,这个线程在Java中被称为主线程
- 当程序的主线程执行时,若遇到了循环而导致程序在指定位置停留时间过长,则无法马上执行下面的程序,则需要等待循环结束后才能够执行
- 方法在哪个线程中被调用,则会在哪个进程中运行
- JVM是一个多线程程序,每个Java进程都分配一个JVM实例
public class ThreadDemo {
public static void main(String[] args) {
// 利用垃圾回收器来证明
while(true) {
// 这里虽然一直在堆空间中,创建数组对象,
// 但是始终没有耗尽堆空间,就是因为垃圾回收器,
// 在另外一个线程中,帮我们回收垃圾,所以才不会耗尽heap内存
// 从而证明,jvm是线程的
int[] ints = new int[1024];
ints = null;
}
}
}
二、 Thread类
1、 概述
- Thread是程序中的执行线程。Java虚拟机允许应用程序并发地运行多个执行线程
- 不是抽象类
2、 构造方法
- Thread(): 分配新的Thread对象
- Thread(String name): 分配新的Thread对象,将指定的name作为其线程名称
3、 常用方法
- void start(): 使该线程开始执行,Java虚拟机调用线程的run方法
- void run(): 该线程将要执行的操作
- static void sleep(long millis): 在指定毫秒内让当前正在执行的线程休眠,暂停执行
- static Threa currentThread(): 返回当前正在执行的线程对象的引用 Thread.currentThread()
4、 创建新执行线程的两种方法
- 将类声明为Thread的子类
- 该子类应重写Thread类的run方法。
- 创建对象,开启线程。
- run方法相当于其他线程的main方法
- 声明一个实现Runnable接口的类
- 该类实现 run 方法。
- 创建Runnable的子类对象,传入到某个线程的构造方法中,开启线程
实现线程有两种方式,从客观来讲,线程本身只代表独立的执行路径,执行的具体内容其实是Task本身,和执行路径的实现本身没有联系;只是开发者,想把一个task放在某条独立的执行路径(Thread类对象,也就是一个线程中)来运行
三、 创建线程:继承Thread类
创建线程的步骤:
- 定义一个类继承Thread
- 重写run方法
- 创建子类对象,就是创建线程对象
- 调用start方法,开启线程并让线程执行,同时会告诉JVM去调用run方法
线程对象调用run方法–不开启线程,仅是对象调用方法
线程对象调用start开启线程,并让JVM调用run方法在开启的线程中执行
public class Test {
public static void main(String[] args) {
//创建自定义线程对象
MyThread mt = new MyThread("新的线程!");
//错误启动线程,这只是普通的方法调用
//firstThread.run();
//开启新线程
mt.start();
//再次启动一个线程,会抛异常IllegalThreadStateException
//因为一个线程对象只能启动一次,
// 如果同一个线程对象,启动多次,就会抛出异常
//firstThread.start();
//只能创建一个新的对象
new MyThread("第二个线程!").start();
//获取主线程的名字
System.out.println(Thread.currentThread().getName()+":主线程!");
}
}
//自定义线程
class MyThread extends Thread {
//定义指定线程名称的构造方法
public MyThread(String name) {
//调用父类的String参数的构造方法,指定线程的名称
super(name);
}
//重写run方法,完成该线程执行的逻辑
@Override
public void run() {
//获取线程的名字getName()
System.out.println(getName() + ":正在执行!");
}
};
四、 创建线程:实训Runnable接口
1、 Runnable接口的构造方法
- Thread(Runnable target): 分配新的Thread对象,以便将target作为其运行对象
- Thread(Runnable target, String name): 分配新的Thread对象,以便将target作为其运行对象;并将指定的name作为其名称
2、 创建线程的步骤
- 定义类实现Runnable接口
- 覆盖接口中的run方法
- 创建Thread类的对象
- 将Runnable接口的子类对象作为参数传递给Thread类的构造方法
- 调用Thread类的start()开启线程
public class Test {
public static void main(String[] args) {
// 创建实现 Runnable 接口的子类对象
MyRunnable myrunnable = new MyRunnable();
// 创建Thread实例,在Thread的构造方法中传递Runnable实例
// Runnable就代表在 Thread 上运行的任务
Thread thread = new Thread(myrunnable);
// 开启线程
thread.start();
for (int i = 0; i < 10; i++) {
System.out.println("main线程:正在执行!"+i);
}
}
}
// 自定义线程执行任务类
class MyRunnable implements Runnable{
// 定义线程要执行的run方法逻辑
// run方法,不能抛出编译时异常
// run方法,没有参数没有返回值
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("我的线程:正在执行!"+i);
}
}
}
3、 实现Runnable接口的原理
为什么需要定一个类去实现Runnable接口呢?继承Thread类和实现Runnable接口有啥区别呢?
只有创建Thread类的对象才可以创建线程。线程任务已被封装到Runnable接口的run方法中,而这个run方法所属于Runnable接口的子类对象,所以将这个子类对象作为参数传递给Thread的构造函数,这样,线程对象创建时就可以明确要运行的线程的任务
4、 两种方式的比较
继续Thread类方法
- 若某个类已经有父类,则无法再继承Thread类
实现Runnable接口方式
- 解决了单继承的局限性
- 便于多线程共享数据
- 实现Runnable接口的方式,更加符合面向对象;线程分为两部分,一部分线程对象,一部分线程任务
继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,则其既是线程对象,又有线程任务
实现Runnable接口, 将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦
五、 线程优先级
1、 概述
我们可以通过Thread类中:
- getPriority方法: 获取线程的优先级
- setPriority方法: 设置线程的优先级
2、 线程优先级的范围
若设置了线程优先级的范围,超出规定范围则会抛出异常
MAX_PRIORITY 10 // 最大优先级
MIN_PRIORITY 1 // 最小优先级
NORM_PRIORITY 5 // 默认优先级
//使用
thread1.setPriority(Thread.MAX_PRIORITY); //设置
thread2.setPriority(Thread.MIN_PRIORITY);
System.out.println(thread1.getPriority());//获取
Warning
Thread类中的优先级,对于Jvm而言不起决定性作用!
在JVM实际调度线程的时候,它使用的优先级可能不仅仅包含给每个线程所设置的静态优先级,可能还考虑了其他的很多因素(各个线程的运行状态),所以我们所设置的优先级对于JVM只起一个参考作用
六、 线程的生命周期
1、 新建状态
创建线程对象
2、 就绪状态
start()之后
sleep()睡醒之后
yield()之后
有执行资格,等待CPU调度
3、 运行状态
取得执行权,开始执行
4、 阻塞状态
无执行资格, 无执行权
5、 死亡状态
执行完毕,等待垃圾回收
七、 同步代码块
1、 问题引入–线程安全问题
发生线程安全问题的三个条件:
- 多线程运行环境
- 多线程访问线程共享数据(存在共享数据)
- 访问共享数据的操作不是原子操作(不可分割的操作,==一次性完成的操作)
当三个条件同时满足时才会发生多线程的数据安全问题
解决多线程的线程安全问题:如何实现线程对共享资源的排他性访问(只有我访问完毕后别才能修改)
----> 使用同步代码块
2、 同步代码块
-
同步代码块实现线程同步
-
线程同步: 利用锁对象,完成多线程运行环境中,对共享资源的排他性访问
-
优点: 解决了多线程的安全问题
-
缺点: 消耗资源(线程很多时,每个线程运行时都需要去判断同步锁)
线程异步: 线程间互不干扰,各自独立运行
同步代码块中所使用的对象,称之为锁对象 ——> 锁的角色
锁对象中,有一个标志位:可以表示两种状态,加锁 和 解锁持有锁的是线程(一个线程给一个锁对象加锁,我们就说这个线程持有了锁对象)
同步代码块何时给锁对象加锁?进入同步代码块的同时,就给锁对象加锁
线程何时释放持有的锁? 当执行完同步代码块的时候,就会释放同步代码块的锁对象
判断和修改锁对象标志位的操作,这是由 jvm 保证一定是原子操作
** 锁对象,究竟是什么对象? java语言中任意一个对象**,都可以充当锁对象的角色(因为任意一个对象中都有一个表示加锁,解锁状态的标志位)
Warning
虽然锁对象可以是任意对象,但是针对同一个(同样的多个共享变量)的所有操作,都必须保证在同步代码块中,使用同一个锁对象,才能避免线程安全问题。
3、 同步方法
- 当同步代码块的范围扩大到整个方法的方法体的时候,我们可以将整个方法定义成同步方法,
在方法声明上加上synchronized
- 普通的同步代码块:
synchronized(锁对象) {}
- 同步方法:
void synchronized 方法名(){}
- 同步方法的锁对象就是
this
静态方法是否可以定义为同步方法?
静态的同步方法, 在方法声明上加上static synchronized
是原子操作
6. ** 锁对象,究竟是什么对象? java语言中任意一个对象**,都可以充当锁对象的角色(因为任意一个对象中都有一个表示加锁,解锁状态的标志位)
Warning
虽然锁对象可以是任意对象,但是针对同一个(同样的多个共享变量)的所有操作,都必须保证在同步代码块中,使用同一个锁对象,才能避免线程安全问题。
3、 同步方法
- 当同步代码块的范围扩大到整个方法的方法体的时候,我们可以将整个方法定义成同步方法,
在方法声明上加上synchronized
- 普通的同步代码块:
synchronized(锁对象) {}
- 同步方法:
void synchronized 方法名(){}
- 同步方法的锁对象就是
this
静态方法是否可以定义为同步方法?
静态的同步方法, 在方法声明上加上static synchronized
它的所使用的锁对象是:当前类的字节码文件对象
:类名.class