Java多线程初步学习笔记
多线程
多线程技术概括
-
程序
⽤某种编程语⾔(java、python等)编写,能够完成⼀定任务或者功能
的代码集合,是指令和数据的有序集合,是⼀段静态代码。 -
进程
内存中的一个应用程序,拥有单独的内存空间,每个进程中至少有一个线程。在Java程序中,默认会创建一个线程用于执行 main 方法。 -
线程
进程中的一条独立路径,共享同一个内存空间,线程之间可以自由切换,线程的状态会在其所属进程中保存 -
多线程
由cpu分配时间片给线程,如果时间片结束时进程还在运行,则暂停该线程,并 cpu 分配给另一个线程。
但事实上,对于单核 cpu 来说,任意时刻都只有一个任务在占用 cpu 资源,也可以说只是 cpu 通过快速切换线程给人一种多个线程在同时执行的感觉,所以多线程不能提高程序的运行速度,但能提高程序运行效率,让 cpu 的使用率更高
-
线程调度策略
指 cpu 分配线程的方式,一般有以下两种:- 分时调度:所有线程轮流使用 cpu 的使用权,平均分配给每一个线程
- 抢占式调度:优先级越高的线程抢到 cpu 资源的概率越大,优先级相同则由 cpu 随机选择
-
线程的并发与并行
- 并发:指两个或多个事件在同一个时间段内发生。
- 并行:指两个或多个事件在同一时刻发生(同时发生)。
-
线程阻塞
- 线程在某个地方等待继续执行如等待用户输入
- 线程进行某种耗时操作,例如读取文件
多线程相关类与接口
在Java中,使用多线程的方式有以下三种:
- 继承
Thread类,并重写run⽅法; - 实现
Runnable接⼝的run⽅法; - 实现
Callable接口的call方法;
线程类 Thread
- 使用方法:重写其 run 方法,该方法为线程需要执行的任务代码,该方法在对象调用start 方法开辟一个新线程时执行
- 示例代码
MyThread类:
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "-" + i);
}
}
}
Test 类:
public class Test {
public static void main(String[] args){
//new 一个MyThrad对象
MyThread myThread = new MyThread();
myThread.start();
}
}
控制台输出结果:

- run 方法,该方法为线程需要执行的任务代码,该方法在对象调用start 方法开辟一个新线程时执行
- start 方法:启动一个子线程
- 优势:通过可以匿名类实现
注意:每个线程都拥有自己的栈空间,共用一份堆内存,即每个线程中调用的方法都是在自己的栈空间中执行
Runnable 接口
- 使用方法:创建一个任务对象,然后通过任务对象 new 一个
Thread对象,调用 、该对象的start方法即可 - 示例代码如下
MyRunnable类:
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "-" + i);
}
}
}
Test 类:
public class Test {
public static void main(String[] args){
//Runnable对象作为参数传给Thread的构造方法
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
控制台输出结果:

从控制台输出结果我们可以看出实现 Runnable 接口与继承线程 Thread 类都能实现多线程操作
- 实现
Runnable接口与继承线程Thread类相比优势如下:- 通过创建任务对象,然后给线程分配的方式实现的多线程,更适合多个线程同时执行相同任务的情况
- 避免了单继承的局限性
- 任务和线程分离,提高程序的健壮性
- 线程池只接受
Runnable的任务,不接受Thread线程
Lambda 的简单使用
- 使用 Lambda 表达式可以大大简化通过实现
Runnable接口来实现多线程的操作 - 函数式编程思想:只关注结果不关注过程,Lambda 表达式的编程思想
- 面向对象编程思想: 创建对象调用方法解决问题,Java的编程思想
- 如果接口只有一个抽象方法,可以通过
(参数) -> {代码块}代替接口的实现类对象 - 示例:使用 Lambda 表达式简化
Runnable接口实现多线程的代码
Thread thread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
});
thread.start();
此时我们可以看到 Lambda 表达式大大简化操作步骤,省去了我们创建 Runnable 接口的实现类和 new 实现对象的过程
如果代码块中只有一句代码,可以省略{}
Callable 接口、Future 接口与 FutureTask 类
- 返回值的线程,可以实现和主线程并发执行,也可以先执行该线程再执行主线程,相当于主线程分派任务给该线程
- 常用方法
- get 方法:运行该方法则主线程等待其运行完成,可以传入毫秒数表示最长等待时间
- isDone 方法:判断线程是否还在执行
- cancel 方法:取消线程,返回true表示取消成功,返回fakse表示该线程不在执行或已经结束
Thread类解析
- 构造方法:
- Thread(Runnable target):参数 target 为
Runnable的实现类 - Thread(String name):参数 name 为线程名称
- Thread(Runnable target, String name):即同时给上面两个参数赋值
- Thread(Runnable target):参数 target 为
- 常用方法
-
getName 方法:返回线程名称
-
getId 方法:返回线程的标识符
-
getPriority 方法:返回线程的优先级
-
setName 方法:修改线程名称
-
currentThread 方法:静态方法,返回当前线程对象
-
interrupt 方法
- 为什么会出现 interrupt 方法:线程中断
- 因为强制中断可能会导致资源未关闭等情况,所以线程的结束应该由其自己决定
- 为能正确关闭进程,我们可以在任务中定义标记变量,以此来判断线程是否应该中断
- 该方法的功能就是为线程自身添加中断标记,然后在中断标记,在调用 wait 方法,sleep,interrupt,interrupted 方法检查是否存在中断标记,如果存在则抛出异常,程序员应使用 try-catch 捕捉,在 catch 块中决定是否死亡和关闭资源
- 代码示例
控制台输出如下:public class Test { public static void main(String[] args){ Thread thread = new Thread(() -> { //线程 thread 循环5次 for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + "-" + i); try { //睡眠2秒 Thread.sleep(2000); } catch (InterruptedException e) { System.out.println("发现中断标记,线程中断"); //如果不return线程会继续执行 return; } } }); //thread 线程开启 thread.start(); //主线程循环3次 for (int i = 0; i < 3; i++) { System.out.println(Thread.currentThread().getName() + "-" +i); try { //睡眠1秒 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } //为线程 thread 添加中断标记 thread.interrupt(); } }
![image]()
- 为什么会出现 interrupt 方法:线程中断
-
sleep 方法:静态方法,线程休眠指定毫秒数,即暂停线程
-
setDaemon 方法:将该线程标记为 守护线程 或 用户线程 ,true表示该线程为守护线程,线程默认为用户进程
- 线程分类:用户线程和守护线程
- 用户线程:线程中的任务完成才会死亡
- 守护线程:所有用户线程死亡,守护线程才死亡
- 示例代码
public class Test { public static void main(String[] args){ //创建一个 thread 线程,实现 Runnable 中的 run 方法 Thread thread = new Thread(() -> { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + "-" + i); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }); //设置 thread 线程为守护线程 thread.setDaemon(true); thread.start(); for (int i = 0; i < 3; i++) { System.out.println(Thread.currentThread().getName() + "-" +i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("主线程死亡,守护线程紧接着死亡"); } }控制台输出如下:
![image]()
- 线程分类:用户线程和守护线程
-
- static属性
- MAX_PRIORITY:线程的最大优先级
- MIN_PRIORITY:线程的最小优先级
- NORM_PRIORITY:线程的默认优先级
线程之间的通信
有时候,我们需要线程之间互相协作完成任务时,此时就需要在线程之间进行通信。
Java中线程的通信方式有以下几种:
- 线程同步与锁
- 线程的等待和唤醒
线程同步
-
线程的同步与异步
- 同步:线程排队执行,效率低但是安全
- 异步:线程同时执行,效率高但是数据不安全
-
线程不安全
- 指在多个线程需要操作同一个值时,在一个线程使用该值时,该线程时间片结束,cpu 切换了线程,切换后的线程也操作了该值,导致该值在原线程中不符合预期的情况。
- 示例代码
public class Test { public static void main(String[] args){ //创建一个 Runnable 的实现类 MyRunnable myRunnable = new MyRunnable(); //启动两个线程,都传入 myRunnable 对象 new Thread(myRunnable).start(); new Thread(myRunnable).start(); } //实现 Runnable 接口的静态内部类 static class MyRunnable implements Runnable { private int count = 10; @Override public void run() { while (true) { if(count > 0){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩:"+count); } else { break; } } } } }控制台输出:
![image]()
- 通过观察控制台输出我们可以发现售票时出现了剩余票数重复以及出现剩余票数为-1等不合理的情况,原因如图:
![image]()
- 解决方案:当多个线程需要操作同一个值的代码使用线程同步
线程同步的三种实现方式
-
同步代码块
- 格式:synchronized(锁对象){}
- 示例代码:线程不安全的示例代码使用同步代码块,修改后的run方法如下
@Override public void run() { while (true) { //同步代码块 synchronized (this) { if (count > 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩:" + count); } else { break; } } } }控制台输出:
![image]()
从控制台输出我们也看到了剩余票数重复和剩余票数为负值的情况被解决,但在使用同步代码块时,很容易造成第一个线程抢占到资源的线程一直都能抢占到资源,其他线程无法抢占到资源的情况
- 锁对象:任何对象都可以作为锁对象,在某个线程执行同步代码块时,该对象就会被打上锁的标记,直到该线程执行完毕,其他线程对象会观察锁对象,如果锁对象存在锁标记,则会排队等待
注意:所有线程都应该使用同一个锁对象
-
同步方法
- 给方法加
synchronized关键字 - 示例代码:在线程不安全的示例代码的基础上修改了 MyRunnable 类
static class MyRunnable implements Runnable { private int count = 10; @Override public void run() { while (true) { if (!saleOfTickets()){ break; } } } //同步方法,返回票数是否大于0 public synchronized boolean saleOfTickets(){ if (count > 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩:" + count); return true; } return false; } }控制台输出:
![image]()
可以看出,同步方法同样也保证了线程安全- 如果是静态方法,则锁对象是this的类对象,如果不是,则锁对象是this
注意:如果同步代码块和同步方法都使用了同一把锁,则会出现同步代码块在执行时则同步方法不执行,同步方法在执行时则同步代码块不执行的情况
- 给方法加
-
显式锁
Lock子类ReentrantLock- 示例代码
static class MyRunnable implements Runnable { private int count = 10; Lock lock = new ReentrantLock(); @Override public void run() { while (true) { //给下面的代码加锁 lock.lock(); if (count > 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩:" + count); } else { lock.unlock(); break; } //解锁代码 lock.unlock(); } } }控制台输出:
![image]()
显式锁 lock 锁也解决了线程不安全问题
- 它和
synchronized这种隐式锁的区别是- Lock 更符合锁的概念
- Lock 锁更加灵活
- 构造方法:fair参数,为true是公平锁,否则为非公平锁,默认为 false
- 公平锁和非公平锁
- 公平锁:排队执行线程,先到先得
- 非公平锁:抢占式执行线程,谁抢到谁执行
- 示例代码:在原代码基础上设置为公平锁
static class MyRunnable implements Runnable { private int count = 10; //设置为公平锁 Lock lock = new ReentrantLock(true); @Override public void run() { while (true) { //给下面的代码加锁 lock.lock(); if (count > 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩:" + count); } else { lock.unlock(); break; } //解锁代码 lock.unlock(); } } }
- 公平锁和非公平锁
控制台输出:
![image]()
从控制台输出我们可以看到,每个线程都是轮流运行的,不会出现一个线程一直都能抢占都 cpu 资源的情况- lock方法:给代码上锁标记
- unlock方法:解锁代码
线程死锁出现的原因
- 在两个同步代码直接或间接互相调用的情况下,就极有有可能产生死锁
- 示例代码
public class Test {
public static void main(String[] args) {
A a = new A();
B b = new B();
//启动一个新线程
new MyThread(a,b).start();
//调用 b 的 say 方法
b.say(a);
}
static class MyThread extends Thread{
private A a;
private B b;
public MyThread(A a, B b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
//调用 a 的 say 方法
a.say(b);
}
}
static class A {
//说话方法
public synchronized void say(B b){
System.out.println("A对B说话");
//调用 b 的回复方法
b.reply();
}
//回复方法
public synchronized void reply(){
System.out.println("A回复");
}
}
static class B {
//说话方法
public synchronized void say(A a){
System.out.println("B对A说话");
//调用 a 的回复方法
a.reply();
}
//回复方法
public synchronized void reply() {
System.out.println("B回复A");
}
}
}
控制台输出:

从图中我们可以看看,A 和 B 都在调用对方的回复方法,即在A对象说完话,准备调用 B 对象的回复方法时,B对象中主线程中也刚说完话,也在调用 A 的回复方法,此时,就出现了双方都在得到对方释放锁的情况,因此造成了死锁
- 解决方法:在同步代码执行时应该避免执行其他同步代码
线程的等待和唤醒
- 在多个线程协作完成任务时,可能会出现线程B依赖于线程A ,线程A执行完毕后,线程B才可以执行。例如生产者和消费者,消费者需要等待生产者生产完毕才能消费
- 代码示例:在下面代码,我们需要生产者和消费者这两个线程交替运行
public class Test {
public static void main(String[] args) {
//货物
Goods goods = new Goods();
//生产者运行
new Producer(goods).start();
//消费者运行
new Consumer(goods).start();
}
//生产者
static class Producer extends Thread {
private Goods goods;
public Producer(Goods goods) {
this.goods = goods;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
if (i % 2 == 0) {
System.out.println("生产者生产货物咖啡");
goods.setName("咖啡");
} else {
System.out.println("生产者生产货物奶茶");
goods.setName("奶茶");
}
}
}
}
//消费者
static class Consumer extends Thread {
private Goods goods;
public Consumer(Goods goods) {
this.goods = goods;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
//消费者等待,线程休眠0.1秒
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
goods.getName();
}
}
}
static class Goods {
private String name;
public void getName() {
System.out.println("消费者消费货物" + name);
}
public void setName(String name) {
//生产者生产 0.1 秒
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.name = name;
}
}
}
控制台输出:

在控制台输出中,我们可以看出两个线程的执行是不符合预期的,出现了生产者连续生产或消费者连续消费的情况,如果我们需要让代码的结果符合预期,我们需要对代码做以下修改:
public class Test {
public static void main(String[] args) {
//货物
Goods goods = new Goods();
//生产者运行
new Producer(goods).start();
//消费者运行
new Consumer(goods).start();
}
//生产者
static class Producer extends Thread {
private Goods goods;
public Producer(Goods goods) {
this.goods = goods;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
if (i % 2 == 0) {
System.out.println("生产者生产货物咖啡");
goods.setName("咖啡");
} else {
System.out.println("生产者生产货物奶茶");
goods.setName("奶茶");
}
}
}
}
//消费者
static class Consumer extends Thread {
private Goods goods;
public Consumer(Goods goods) {
this.goods = goods;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
//消费者等待,线程休眠0.1秒
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
goods.getName();
}
}
}
static class Goods {
private String name;
//为货物加上标记,true 表示生产完毕,false表示还未产生
private boolean flag = false;
public synchronized void getName() {
if (flag) {
System.out.println("消费者消费货物" + name);
flag = false;
//唤醒所有在等待的线程
this.notifyAll();
try {
//消费完毕,进入等待
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void setName(String name) {
//生产者生产 0.1 秒
if (!flag) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.name = name;
flag = true;
//唤醒所有在等待的线程
this.notifyAll();
try {
//生产完毕,线程进入等待
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
控制台输出:

可以看到消费者和生产者都是交错执行的,不会出现了生产者连续生产或消费者连续消费的情况了
- 相关方法
- wait 方法:线程进入等待状态,需要执行 notify 方法唤醒,同时该方法会释放锁,sleep方法不会释放锁
- notify 方法:随机唤醒该对象中的一个等待线程
- notifyAll 方法:唤醒该对象中所有的等待线程
线程的六种状态
- new:刚被创建还没有启动的线程处于该状态
- Runnable:正在运行的线程处于该状态
- Blocked:被锁定阻塞的线程采用该状态,即还在等待其他线程释放
Synchronize锁以进入同步代码中的线程 - Waiting:无期限等待唤醒的线程处于该状态
- TimedWaiting:计时等待的线程处于该状态
- Terminated:已退出的线程处于该状态
线程池
-
需要线程数量较多并且每个线程执行的时间较短时,频繁的创建线程会导致系统效率的降低,出现了线程池,即在一个容器中事先存储多个线程,在需要时取出,用完时放回,这样就避免了频繁创建线程的操作,节省了大量时间和资源
-
线程池一般由一个线程列表和任务列表构成,线程列表的长度根据线程池类型不同而分为定长和不定长,不定长的线程池一般会根据任务数量新创建线程,有的线程池会因为线程长时间处于空闲而释放线程,任务即实现
Runnable接口的类,通过接口加入任务,空闲线程会自动执行,线程的执行为顺序为 排队执行 或 抢占执行(即谁先抢到谁先执行) -
ExecutorService线程池类,此类对象会在执行完任务会继续等待传入任务,因此程序不会结束,但在一定时间后该类对象会自动关闭 -
执行任务:
schedule方法传入任务对象了
缓存线程池
- 通过
Executors类的静态方法 newCachedThreadPool 获取 缓存线程池 对象,该线程池的线程个数没有限制 - 示例代码
/**
* 缓存线程池.
* (长度无限制)
* 执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在,则创建线程 并放入线程池, 然后使用
*/
ExecutorService service = Executors.newCachedThreadPool();
//向线程池中 加入 新的任务
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
定长线程池
- 通过
Executors类的静态方法 newFixedThreadPool 获取,参数 nThreads 为线程个数 - 示例代码
/**
* 定长线程池.
* (长度是指定的数值)
* 执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
* 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
*/
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
单线线程池
- 通过
Executors类的静态方法 newSingleThreadPool 获取,线程个数为1 - 效果与定长线程池 创建时传入数值1 效果一致.
- 示例代码
/**
* 单线程线程池.
* 执行流程:
* 1. 判断线程池 的那个线程 是否空闲
* 2. 空闲则使用
* 4. 不空闲,则等待 池中的单个线程空闲后 使用
*/
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
周期性任务定长线程池
- 通过
Executors类的静态方法 newScheduledThreadPool 获取,参数 corePoolSize 为线程数量 - 示例代码
public static void main(String[] args) {
/**
* 周期任务 定长线程池.
* 执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
* 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
*
* 周期性任务执行时:
* 定时执行, 当某个时机触发时, 自动执行某任务 .*/
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
/**
* 定时执行
* 参数1. runnable类型的任务
* 参数2. 时长数字
* 参数3. 时长数字的单位
*/
/*service.schedule(new Runnable() {
@Override
public void run() {
System.out.println("俩人相视一笑~ 嘿嘿嘿");
}
},5,TimeUnit.SECONDS);
*/
/**
* 周期执行
* 参数1. runnable类型的任务
* 参数2. 时长数字(延迟执行的时长)
* 参数3. 周期时长(每次执行的间隔时间)
* 参数4. 时长数字的单位
*/
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("俩人相视一笑~ 嘿嘿嘿");
}
},5,2,TimeUnit.SECONDS);
}









浙公网安备 33010602011771号