写在前面
经过两天的学习,超梦终于将多线程的基础知识学了一遍,笔记做完之后,赶紧写篇博客再巩固一下。为了保证效果,博客也会像笔记一样的详细~(部分截图来自尚硅谷宋红康老师课件)
程序、进程、线程
程序:为了实现某个功能,用某种语言的一组指令组成的静态代码。
进程:正在运行中的程序。
线程:进程可进一步细化为线程,是一个程序内部的一条执行路径。
多线程:若一个进程同一时间并行执行多个线程,就是支持多线程的。
单核CPU与多核CPU
单核CPU:同一时间内只能执行一条线程。表现为“多线程”是假的,主要通过切换CPU对进程控制,切换速度过快让我们误认为是多线程。
多核CPU:真正可以执行多线程。
并行与并发
并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。
使用多线程的优点
1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
2. 提高计算机系统CPU的利用率
3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
何时需要多线程
1. 程序需要同时执行两个或多个任务。
2. 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
3. 需要一些后台运行的程序时。
线程的创建和使用(重点)
这里主要介绍JDK5.0之前的两种创建方式
关于Thread类
介绍
构造器
1.Thread():创建新的Thread对象
2.Thread(String threadname):创建线程并指定线程实例名
3.Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run方法
4.Thread(Runnable target, String name):创建新的Thread对象
常用方法
这些方法将会在后面的内容带大家慢慢熟悉,接下来进入重点:创建线程的两种方式!
方式一:继承Thread类创建线程
步骤:
- 定义子类继承Thread类。
- 子类中重写Thread类中的run方法。
- 创建Thread子类对象,即创建了线程对象。
- 调用线程对象start方法:启动线程,调用run方法。
代码示例
package com.deserts.demo01;
//1) 定义子类继承Thread类。
class Thread1 extends Thread {
// 2) 子类中重写Thread类中的run方法。
@Override
public void run() {
for (int i = 0; i < 30; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
}
}
class Thread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 30; i++) {
if (i % 2 != 0) {
System.out.println(i);
}
}
}
}
public class Thread01 {
public static void main(String[] args) {
// 3) 创建Thread子类对象,即创建了线程对象。
Thread1 t1 = new Thread1();
Thread2 t2 = new Thread2();
// 4) 调用线程对象start方法:启动线程,调用run方法。
t1.start();
t2.start();
}
}
运行结果:
我们也可以采用匿名内部类的方式
package com.deserts.demo01;
public class ThreadTest01 {
public static void main(String[] args) {
//创建匿名内部类的方式
new Thread() {
@Override
public void run() {
for (int i = 0; i < 30; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
}
}.start();
}
}
运行结果:
创建过程的注意点:
- 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
- run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
- 想要启动多线程,必须调用start方法。
- 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常“IllegalThreadStateException”。
方式二:实现Runnable接口的方式
步骤:
- 定义子类,实现Runnable接口。
- 子类中重写Runnable接口中的run方法。
- 通过Thread类含参构造器创建线程对象。
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
- 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
代码实现:
package com.deserts.demo01;
//1) 定义子类,实现Runnable接口。
class Thread02 implements Runnable {
//2) 子类中重写Runnable接口中的run方法。
@Override
public void run() {
for (int i = 0; i < 30; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
}
}
public class ThreadTest02 {
public static void main(String[] args) {
//4) 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
Thread02 t2 = new Thread02();
//3) 通过Thread类含参构造器创建线程对象。
Thread t1 = new Thread(t2);
//5) 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
t1.start();
}
}
运行结果:
关于为什么要将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中,我们可以看看源码:
这样我们可以知道:Runnable接口的子类实现了Runnable接口的run抽象方法,在传入参数时涉及到了多态,如:Runnable target = new Thread02(),Thread02是实现Runnable接口的子类,使用有参构造器创建Thread对象后调用start()方法时会调用run()方法,此时的run()方法已经Thread02重写过了!
两种方法的对比
线程的优先级
线程的分类
线程的生命周期
基本概念
过程图解
线程的同步(重点)
为什么要有线程的同步?
举个例子:你的银行账户有3000元,你和你的对象同时在不同地点都取出2000元,银行会先判断你是否余额充足,余额足了再扣钱,但是银行总不可能让你们两个人都·取了吧?这样岂不是亏了2000块!这样的问题可以理解为线程不安全,因为都操作了共享数据!
线程同步共有三种方法,每种方法又可以结合创建线程的两种方式来写,所以可以有6种写法,下面将通过卖票的例子来演示
Synchronized的两种方式
图解
方式一:同步代码块
同步监视器可以理解为一把锁,所有对象都可以作为锁,某一条线程操作共享数据时会持有这把锁,其他线程也就不能操作共享数据,等操作共享数据结束后该线程会释放这把锁。需要注意的是,多个线程必须共用同一把锁!
下面我们通过模拟两个窗口同时卖出30张票来理解这个过程!
继承类使用同步代码块方式
package com.deserts.demo02;
public class Window01 {
public static void main(String[] args) {
Sell01 s1 = new Sell01();
Sell01 s2 = new Sell01();
s1.setName("窗口1");
s2.setName("窗口2");
s1.start();
s2.start(