前言
多线程与锁
文章目录
一、多线程
1、应用场景
-
操作系统中每个程序都是一个进程,进程中有些操作比较慢,如果一个进程只有一个线程容易导致卡死;
-
场景:当打开一个网站的页面后,页面加载会有很多操作(包括加载、渲染等)
- 对于加载过程,加载图片和渲染其它地方是并行执行的,不会因为图片加载完再渲染其它地方;
-
一个进程可以有多个执行顺序(线程),当一个执行顺序(线程)在进行耗时操作时,
CPU
会自动执行其它线程,使CPU
不闲置,提高运行效率; -
每一个程序都是一个进程;同一时间有多个线程在执行,高效;
2、代码场景
- 正常代码执行是一行一行顺序执行的(没有多线程的情况下);
- 当存在多线程时,整个代码的执行,并不是只有一条线;在执行中,有多条执行线,当一条执行线卡住处于执行状态时,操作系统会执行另外一条线,不会浪费
cpu
的资源; - 如下图所示,当主线程执行到
start
函数时,执行就变成了两条线(一条线去执行子线程的run
函数,另一条线为主线程继续往后执行;主线程继续往后执行的代码是不会等子线程start
函数中代码执行完再执行的,是同时执行的);
2、多线程的实现
- 每个线程都是一个对象;
- 将实现多线程的逻辑写在
run
函数中即可; - 启动一个子线程
- 当主线程执行到
start
函数的时,整个程序的控制流分岔;
- 当主线程执行到
- 代码首先顺序执行主线程,当主线程遇到
start
,会新开一个线程,子线程自动执行run
函数,原来的主线程控制流继续执行; - 子线程在执行的时候顺序是随机的,每个线程自己独立看起来的话,是同时执行的;但是,在
CPU
执行中,虽然每个线程看起来是并行做的,但是CPU
是一个线程一个线程按顺序做的,同一时间只能执行一个线程,只是我们看起来多个线程是同时执行的;-
子线程在执行的时候顺序是随机的:
- 这意味着,当你有多个线程同时运行时,你不能预测哪个线程会首先执行完其任务。线程的调度是由操作系统或线程库来管理的,并且可能会受到许多因素的影响,如线程的优先级、系统的负载等。因此,从编程的角度来看,线程的执行顺序是随机的。
-
每个线程自己独立看起来的话,是同时执行的:
- 这句话描述了多线程编程的一个关键特性,即并发性。从程序员的角度来看,多个线程似乎是同时执行的。这允许程序同时处理多个任务,从而提高效率。然而,这种“同时执行”的感觉只是表象,实际上线程的执行是由操作系统管理的,并且可能会受到很多因素的影响。
-
但是,在 CPU 执行中,虽然每个线程看起来是并行做的:
- 这里再次强调了并发和并行的区别。并发是指多个任务在逻辑上同时执行,而并行是指多个任务在物理上同时执行。在 CPU 层面上,多个线程看似并行执行(即同时执行),但实际上在任一时刻,CPU 只能执行一个线程。
-
但是 CPU 是一个线程一个线程按顺序做的:
- 这句话揭示了 CPU 的实际工作原理。CPU 有一个或多个核心,每个核心在任何给定的时间点上只能执行一个线程。当 CPU 切换到另一个线程时,当前线程的执行会被暂停,而新线程的执行会开始。这种切换非常快,以至于从用户的角度来看,多个线程似乎是同时执行的。
-
同一时间只能执行一个线程:
- 这是对 CPU 工作原理的再次强调。无论有多少线程在系统中运行,CPU 在任何给定的时间点上都只能执行一个线程。
-
只是我们看起来多个线程是同时执行的:
- 这句话总结了多线程编程的表象和实际工作原理之间的区别。由于 CPU 的快速切换和操作系统的调度,多线程程序在宏观上看起来是同时执行的,但实际上每个线程的执行都是按顺序进行的。
-
1)继承 Thread 类
-
每创建一个线程的话,都需要
new
一个新的线程对象,不同线程的对象是不同的; -
遇到
start()
函数会新开一个线程,自动执行run()
; -
多个线程执行的顺序是随机的;
-
与实现
Runnable
接口的方式区别为不能共用同一个对象;class Worker extends Thread { @Override public void run() { for (int i = 0; i < 10; i ++ ) { System.out.println("Hello! " + this.getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } public class Main { public static void main(String[] args) { Worker worker1 = new Worker(); Worker worker2 = new Worker(); worker1.setName("thread-1"); worker2.setName("thread-2"); worker1.start(); worker2.start(); } }
2)实现 Runnable 接口
-
实现
Runnable
接口只用写一个类即可; -
不同线程可以共用一个对象
- 实现两个线程的话,可以只用一个实例,两个线程执行的是一个实例中的
run
函数;
- 实现两个线程的话,可以只用一个实例,两个线程执行的是一个实例中的
-
用一个对象和用多个对象有什么区别——》涉及到锁(同步的概念)
- 同步的话是需要同步同一个资源(对象)要用实现
Runable
的写法;
class Worker implements Runnable { @Override public void run() { for (int i = 0; i < 10; i ++ ) { System.out.println("Hello! " + "thread-1"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } public class Main { public static void main(String[] args) { Worker worker = new Worker(); new Thread(worker).start(); new Thread(worker).start(); } }
- 同步的话是需要同步同一个资源(对象)要用实现
3)常用 API 函数
start()
- 开启一个线程;
Thread.sleep()
- 表示让当前线程睡眠,在主线程中执行的话是让主线程睡眠,在子线程中执行的话是让子线程睡眠;
join()
-
多个线程执行顺序是随机的,
join
表示一个线程等待另一个线程执行完成结束; -
下面代码,主线程等两个子线程执行完成后再执行
worker1.join()
:当主线程执行到这句话时,主线程就会卡住,只有当worker1
线程执行完成后,才会继续执行worker1.join()
后面的代码;
class Worker implements Runnable { @Override public void run() { for (int i = 0; i < 10; i ++ ) { System.out.println("Hello! " + "thread-1"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } public class Main { public static void main(String[] args) { Worker worker1 = new Worker(); Worker worker2 = new Worker(); worker1.setName("thread-1"); worker2.setName("thread-2"); worker1.start(); worker2.start(); worker1.join(); worker2.join(); System.out.println("Main thread finisher"); } }
-
interrupt()
:给线程发送一个异常;setDaemon()
:将线程设置为守护线程。当只剩下守护线程时,程序自动退出;-
一个程序可能同时有很多线程,当只剩下守护线程时,守护线程就会自动结束(当其它线程都结束的时候,守护线程会自动结束)
-
作用:每个 Java 程序都有垃圾回收机制,守护线程在所有用户线程结束之后结束掉;
-
应用场景:需求:当其它有用线程都结束后,我们希望这个线程自动结束,就可以将这个线程设置为守护线程 ;
-
先设置为守护线程,然后在执行
start
;
-
四、锁
1、为什么会有锁的概念?
- 针对多线程,同一个进程的多个线程会共享一段内存空间,可能会出现读写冲突的问题;
2、背景举例
1)出现冲突
-
背景问题:统计某个网页页面的某门课的报名人数;
-
cnt
:存储报名人数; -
网站界面显示是存在多个线程执行的;
-
每次执行后得到
cnt
值的结果都是不一样的,产生这个问题的原因就是两个线程存在读写冲突的问题; -
每个线程对应 一个人报名成功,
cnt = cnt + 1
(多线程运行过程中,多个线程在执行这条语句时,并不是某个线程会完整地执行完这个语句再执行下一个线程),多个线程在执行时可能会同时执行这条语句; -
cnt = cnt + 1
:对应三个操作,先取cnt
的值,再将cnt + 1
,再给cnt
赋值; -
出现的问题
- 第一个线程取
cnt
的值,此时只取出来cnt
,且cnt
的值为 0,然后被操作系统挂载; - 第二个线程开始执行,执行完
3
个操作,第二个线程此时执行完毕,此时cnt = 1
; - 第一个线程继续执行,执行
cnt + 1(0 + 1 = 1)
,此时cnt = 1
; - 导致两个人购课成功后,
cnt
的值仍然为1
,出错;
- 第一个线程取
-
出现读写冲突的情况
- 多个线程在同时访问同一个资源时,并不一定有冲突;
- 两个线程只读取变量的信息时,不管多少个线程并行,都不会有问题,因为不管什么时候读,值是不会变的;
- 当两个线程同时写一个变量值时,会出现问题,第一个线程会写一个版本,第二个线程也写一个版本,最终值取哪个值是不确定(因为线程执行的顺序是随机的);
- 一个线程读,另一个线程写也会出现冲突,先读再写,读的是旧值,先写再读,读的是新值,也是不确定的(因为线程执行顺序是随机的);
- 总结:当两个线程至少有一个线程时写的情况下,如果同时访问同一个资源就会有冲突;
2)解决冲突
-
方法
1
-
等第一个线程执行完,再执行第二个线程,如下代码所示;
-
但是这样就不是多线程代码了, 同一时间只能有一个线程在执行,并不高效 ;
public class Main { public static void main(String[] args) throws InterruptedException { Worker worker1 = new Worker(); Worker worker2 = new Worker(); worker1.start(); worker1.join(); worker2.start(); worker2.join(); System.out.println(Worker.cnt); } }
-
-
方法
2
- 既保证高效又保证没有读写冲突,引入下面的锁;
2、锁
1)方式 1
- 访问公共资源(
cnt
),在访问之前,先去拿这把锁,同一个锁同一时间只能被一个线程锁定; - 一个线程拿到这把锁(
lock()
),其余线程就会被阻塞在这句话上(停止不动);当一个线程处理完后,会自动将锁释放掉(unlock()
);class Worker extends Thread { public static int cnt = 0; private static final ReentrantLock lock = new ReentrantLock(); // 定义一把锁,静态成员变量,每个对象共享一个 // 这样定义实现多个线程只有一把锁 @Override public void run() { for (int i = 0; i < 100000; i ++ ) { // 当在操作公共资源时,先锁住(多个线程获取锁时只有一个线程可以获取到锁,其余线程卡在此处,不往下执行) lock.lock(); try { cnt ++ ;// 实现了同一时间只会有一个线程完整地执行这句话(获取 cnt -> cnt + 1 -> 将cnt的值写会cnt) } finally { lock.unlock(); // 释放锁 // 当锁释放后,操作系统会自动通知被卡在锁的其它线程 } } } } public class Main { public static void main(String[] args) throws InterruptedException { Worker worker1 = new Worker(); Worker worker2 = new Worker(); worker1.start(); worker2.start(); worker1.join(); worker2.join(); System.out.println(Worker.cnt); } }
2)方式 2(Synchronized)
-
特点
- 上面的过程中,先定义一把锁,然后
lock
,再unlock
比较麻烦,可以采用简写方式(语法糖)——》 只需要Synchronized
就可以锁住一段代码; Synchronized
达到锁住一段代码的作用时,Synchronized
的参数是对象类型,且对象要求是不可变的,多个线程操作的是同一个对象才可以;
- 上面的过程中,先定义一把锁,然后
-
利用
Synchronized
锁住一段代码class Worker extends Thread { // 在第一种方法中,cnt 是一个 Integer 类型的对象, //而 Integer 对象是不可变的,所以不能直接使用 synchronized 关键字对它进行同步。 //因为每次对 cnt 进行操作时,都会创建一个新的 Integer 对象。因此,这种写法是错误的。 public static Integer cnt = 0; // 不能用下面的写法,因为 Integer 定义的对象是不可变的,cnt = cnt + 1 会生成新对象 // synchronized 锁住的对象要求是不可变的 @Override public void run() { synchronized(cnt) { for (int i = 0; i < 1000; i ++) { cnt = cnt + 1; } } // 采用下面这种写法 // 静态对象,不同实例共享一个静态对象,同一时间只能有一个线程执行后面的代码 private final static Object object = new Object(); @Override public void run() { // 使用 synchronized(object) 来锁住代码块,确保同一时间只有一个线程可以执行其中的代码。这样就保证了对 cnt 的操作是线程安全的 // 引入了一个静态的 Object 对象 object 作为同步锁。这个对象是不可变的,并且被所有 Worker 实例共享 synchronized(object) { // 保证了同一时间只有一个线程可以执行这段代码 for (int i = 0; i < 1000; i ++) { cnt = cnt + 1; } } } public class Main { public static void main(String[] args) throws InterruptedException { Worker worker1 = new Worker(); Worker worker2 = new Worker(); worker1.start(); worker2.start(); worker1.join(); worker2.join(); System.out.println(Worker.cnt); } }
-
利用
Synchronized
锁住方法class Worker implements Runable { public static Integer cnt = 0; private synchronized void work() { // 因为作用的是同一个对象(this),所以可以加 for (int i = 0; i < 1000; i ++) { cnt = cnt + 1; } } @Override public void run() { work(); } } public class Main { public static void main(String[] args) throws InterruptedException { Worker worker = new Worker(); // 两个线程传入的是同一个对象 worker // 实现了两个线程访问的是同一个对象 Thread worker1 = new Thread(worker); Thread worker2 = new Thread(worker); worker1.start(); // 第一个线程 worker2.start(); // 第二个线程 worker1.join(); worker2.join(); System.out.println(Worker.cnt); // 输出结果为 2000,结果正确 } }
3)wait 和 notify(Synchronized 的代码中)
notify()
:随便唤醒一个线程,notifyAll()
:唤醒所有线程;wait()
:使线程阻塞(等待被唤醒);notify()
:唤醒等待的线程;- 当一个线程执行到
wait
方法时,该线程被阻塞,会自动将锁释放掉,并且通知其它线程执行; - 当线程被唤醒后继续执行
wait
后面的代码;class Worker extends Thread { private final Object object; private final boolean needWait; public Worker(Object object, boolean needWait) { this.object = object; this.needWait = needWait; } @Override public void run() { synchronized (object) { try { if (needWait) { object.wait(); System.out.println(this.getName() + ": 被唤醒啦!"); } else { object.notifyAll(); } } catch (InterruptedException e) { throw new RuntimeException(e); } } } } public class Main { public static void main(String[] args) throws InterruptedException { Object object = new Object(); for (int i = 0; i < 5; i ++ ) { Worker worker = new Worker(object, true); worker.setName("thread-" + i); worker.start(); } Worker worker = new Worker(object, false); worker.setName("thread-" + 5); Thread.sleep(1000); worker.start(); } }
- 每个 worker 实例持有一个对象 object 和一个布尔值 needWait;
- 在 Worker 的构造函数中,传入一个对象和一个布尔值,分别表示该 Worker 实例将要操作的对象以及是否需要等待;
- 在 run() 方法中,使用 synchronized 关键字锁定了传入的对象 object。如果 needWait 为 true,则调用 object.wait() 使线程等待,直到其他线程调用 object.notifyAll() 或 object.notify() 唤醒它;如果 needWait 为 false,则调用 object.notifyAll() 唤醒所有等待该对象的线程;
- 在 Main 类的 main 方法中,首先创建了5个 Worker 实例,它们都需要等待(needWait 为 true),然后再创建一个 Worker 实例,它负责唤醒所有等待的线程(needWait 为 false);
- 主线程睡眠了一段时间后(1000毫秒),然后启动了这个唤醒线程;