并发编程
一、线程和进程
进程:是一个应用程序
线程:是一个进程中的执行场景/执行单元
注:一个进程中可以有多个线程
二者对比:
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享
- 进程间通信较为复杂
- 同一台计算机的进程通信成为IPC
- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如HTTP
- 线程间的通信比较简单,因为他们共享进程的内存,一个例子是多个线程可以访问一个共享变量
- 线程更轻量,线程上下文切换成本一般比进程上下文切换低
Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。
这里定义和线程相关的另一个术语 - 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。
1.1并行和并发
1.1.1并发:
早期计算机的 CPU 都是单核的,一个 CPU 在同一时间只能执行一个进程/线程,当系统中有多个进程/线程等待执行时,CPU 只能执行完一个再执行下一个。
为了表述简单,下文我们将不再区分进程和线程,而将它们统一称为“任务”。在不同的场景下,一个任务可能是进程,也可能是线程。
计算机在运行过程中,有很多指令会涉及 I/O 操作,而 I/O 操作又是相当耗时的,速度远远低于 CPU,这导致 CPU 经常处于空闲状态,只能等待 I/O 操作完成后才能继续执行后面的指令。
为了提高 CPU 利用率,减少等待时间,人们提出了一种 CPU 并发工作的理论。
所谓并发,就是通过一种算法将 CPU 资源合理地分配给多个任务,当一个任务执行 I/O 操作时,CPU 可以转而执行其它的任务,等到 I/O 操作完成以后,或者新的任务遇到 I/O 操作时,CPU 再回到原来的任务继续执行。
下图展示了两个任务并发执行的过程:
虽然 CPU 在同一时刻只能执行一个任务,但是通过将 CPU 的使用权在恰当的时机分配给不同的任务,使得多个任务在视觉上看起来是一起执行的。CPU 的执行速度极快,多任务切换的时间也极短,用户根本感受不到,所以并发执行看起来才跟真的一样。
操作系统负责将有限的 CPU 资源分配给不同的任务,但是不同操作系统的分配方式不太一样,常见的有:
- 当检测到正在执行的任务进行 I/O 操作时,就将 CPU 资源分配给其它任务。
- 将 CPU 时间平均分配给各个任务,每个任务都可以获得 CPU 的使用权。在给定的时间内,即使任务没有执行完成,也要将 CPU 资源分配给其它任务,该任务需要等待下次分配 CPU 使用权后再继续执行。
将 CPU 资源合理地分配给多个任务共同使用,有效避免了 CPU 被某个任务长期霸占的问题,极大地提升了 CPU 资源利用率。
1.1.2并行:
并发是针对单核 CPU 提出的,而并行则是针对多核 CPU 提出的。和单核 CPU 不同,多核 CPU 真正实现了“同时执行多个任务”。
多核 CPU 内部集成了多个计算核心(Core),每个核心相当于一个简单的 CPU,如果不计较细节,你可以认为给计算机安装了多个独立的 CPU。
多核 CPU 的每个核心都可以独立地执行一个任务,而且多个核心之间不会相互干扰。在不同核心上执行的多个任务,是真正地同时运行,这种状态就叫做并行。
例如,同样是执行两个任务,双核 CPU 的工作状态如下图所示:
双核 CPU 执行两个任务时,每个核心各自执行一个任务,和单核 CPU 在两个任务之间不断切换相比,它的执行效率更高。
1.1.3并发+并行
在上图中,执行任务的数量恰好等于 CPU 核心的数量,是一种理想状态。但是在实际场景中,处于运行状态的任务是非常多的,尤其是电脑和手机,开机就几十个任务,而 CPU 往往只有 4 核、8 核或者 16 核,远低于任务的数量,这个时候就会同时存在并发和并行两种情况:所有核心都要并行工作,并且每个核心还要并发工作。
例如一个双核 CPU 要执行四个任务,它的工作状态如下图所示:
每个核心并发执行两个任务,两个核心并行的话就能执行四个任务。当然也可以一个核心执行一个任务,另一个核心并发执行三个任务,这跟操作系统的分配方式,以及每个任务的工作状态有关系。
1.1.4总结
并发针对单核 CPU 而言,它指的是 CPU 交替执行不同任务的能力;并行针对多核 CPU 而言,它指的是多个核心同时执行多个任务的能力。
单核 CPU 只能并发,无法并行;换句话说,并行只可能发生在多核 CPU 中。
在多核 CPU 中,并发和并行一般都会同时存在,它们都是提高 CPU 处理任务能力的重要手段。
1.2线程生命周期
-
新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
-
就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
-
运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
-
阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
- 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
- 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
- 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
-
死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
1.2.1线程的优先级
每一个 Java 线程都有一个优先级,这样有助于操作系统确定线 程的调度顺序。
Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
1.3应用
1.3.1异步调用
从方法调用的角度来讲,如果
- 需要等待返回结果,才能继续运行就是同步
- 不许要等待返回结果,就能继续运行叫做异步
注意:同步在多线程中还有另外一层意思,是指多个线程步调一致
1.3.1.1设计
多线程可以让方法执行变为异步的,比如磁盘读取文件时,假设读取操作花费了5秒钟,如果没有线程调度机制,这五秒调用者什么都做不了,代码都要暂停
1.2.1.2结论
- 在项目中,视频文件需要转化格式等操作比较费时,这是开一个新的线程处理视频转换,避免阻塞主线程
- tomcat的异步servlet也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞tomcat的工作线程
- UI程序中,开线程进行其他操作,避免阻塞UI线程
1.3.2提高效率
二、线程
2.1创建和运行线程
方法一,直接使用Thread
//创建线程对象
Thread t = new Thread(){
public void run(){
//需要执行的任务
}
}
//启动线程
t.start();
例如:
@Slf4j
public class Test_01 {
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run() {
log.info("running");
}
};
thread.setName("t1");
thread.start();
log.debug("running");
}
}
输出:
15:54:31.125 [main] DEBUG com.example.Test.Test_01 - running
15:54:31.125 [t1] INFO com.example.Test.Test_01 - running
方法二、使用Runnable配合Thread
把【线程】和【任务】(要执行的代码)分开
- Thread代表线程
- Runnable可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable(){
public void run(){
//要执行的任务
}
};
//创建线程对象
Thread t = new Thread(runnable);
//启动线程
t.start();
例如:
@Slf4j
public class Test01 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
log.debug("running");
}
};
Thread t= new Thread(runnable,"t2");
t.start();
}
}
输出:
16:02:56.798 [t2] DEBUG com.example.Test.Test01 - running
Java8之后的简化写法()
@Slf4j
public class Test_02 {
public static void main(String[] args) {
//第一次简化
Runnable runnable = () -> log.debug("running");
Thread t= new Thread(runnable,"t2");
t.start();
}
public void test(){
//第二次简化
Thread t= new Thread(() -> log.debug("running"),"t2");
}
}
输出:
16:02:56.798 [t2] DEBUG com.example.Test.Test01 - running
小结:
- 案例1是把线程和任务合并在了一起,案例2是把线程和任务分开了
- 用Runnable更容易与线程池的高级API配合
- 用Runnable让任务脱离了Thread集成体系,更加灵活
方法三、FutureTask配合Thread
FutureTask能接收Callable类型的参数,用来处理有返回结果的情况
FutureTask<Integer> task = new FutureTask<>(() ->{
log.debug("hello");
return 100;
});
new Thread(task,"t3").start();
Integer result = task.get();
log.debug("结果是{}",result);
例子:
@Slf4j
public class FutureTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("running");
Thread.sleep(2000);
return 100;
}
});
Thread t = new Thread(task,"t");
t.start();
log.debug("结果是{}",task.get());
}
}
输出:
16:43:06.196 [t] DEBUG com.example.Test.FutureTest - running
16:43:08.200 [main] DEBUG com.example.Test.FutureTest - 结果是100
2.2多个线程执行
主要理解
- 交替执行
- 谁先谁后,不由我们控制
2.3查看进程的方法
Windows
- 任务管理器
- tasklist查看进程
- taskkill杀死进程
2.4线程运行原理
栈与栈帧
我们都知道JVM中由堆、栈、方法所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存
- 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
- 每个栈帧只能有一个活动栈帧,对应着当前正在执行的那个方法
线程上下文切换(Thread Context Switch)
因为一些原因导致cpu不再执行当前的线程,转而执另一个线程的代码
- 线程的cpu时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了sleep、yield、wait、join、synchronized、lock等方法程序
当Context Switch发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器,他的作用是记住下一条JVM指令的执行地址,是线程私有的
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch 频繁会影响性能
2.5 常用方法
2.6 start和run
调用run
@Slf4j
public class StartAndRun {
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run() {
log.debug("running....");
User user = new User(123,"张三");
log.info("{}",user.toString());
}
};
thread.run();
log.debug("do other things.....");
}
}
运行结果
08:58:37.718 [main] DEBUG com.example.Test.StartAndRun - running....
08:58:37.722 [main] INFO com.example.Test.StartAndRun - User(userId=123, name=张三)
08:58:37.723 [main] DEBUG com.example.Test.StartAndRun - do other things.....
可以看到都是在主线程执行的
调用start
@Slf4j
public class StartAndRun {
public static void main(String[] args) {
Thread thread = new Thread("t1"){
@Override
public void run() {
log.debug("running....");
User user = new User(123,"张三");
log.info("{}",user.toString());
}
};
thread.start();
log.debug("do other things.....");
}
}
运行结果
09:04:42.188 [main] DEBUG com.example.Test.StartAndRun - do other things.....
09:04:42.188 [t1] DEBUG com.example.Test.StartAndRun - running....
09:04:42.191 [t1] INFO com.example.Test.StartAndRun - User(userId=123, name=张三)
可以看到主线程有运行的,另外一个线程(t1)也有运行的
多次调用start
@Slf4j
public class StartAndRun {
public static void main(String[] args) {
Thread thread = new Thread("t1"){
@Override
public void run() {
log.debug("running....");
User user = new User(123,"张三");
log.info("{}",user.toString());
}
};
System.out.println(thread.getState());
thread.start();
thread.start();
System.out.println(thread.getState());
log.debug("do other things.....");
}
}
运行结果
NEW
09:08:38.676 [t1] DEBUG com.example.Test.StartAndRun - running....
09:08:38.682 [t1] INFO com.example.Test.StartAndRun - User(userId=123, name=张三)
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:708)
at com.example.Test.StartAndRun.main(StartAndRun.java:23)
多次调用start()报异常
2.7sleep和yield
sleep
- 调用slee[线程休眠,会从***Running***进入***Timed Waiting***状态
- 其他线程可以使用interrupt方法打断正在睡眠的线程,这时的sleep方法会爆出InterruptedException
- 睡眠结束后后的线程未必会立刻得到执行
- 建议用TImeUnit的sleep代替Thread的sleep来获得更好的可读性
@Slf4j
public class SleepAndYield {
public static void main(String[] args) {
Thread thread = new Thread("t1") {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread.start();
log.debug("t1 state:{}",thread.getState());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 state:{}",thread.getState());
}
}
运行结果
09:17:15.567 [main] DEBUG com.example.Test.SleepAndYield - t1 state:RUNNABLE
09:17:16.086 [main] DEBUG com.example.Test.SleepAndYield - t1 state:TIMED_WAITING
t1线程由RUNNABLE
–>TIMED_WAITING
,运行到睡眠状态
yiled
- 调用yield会让当前线程从Running进入Runnable状态,然后调度执行其他同优先级的线程。如果这时没有同优先级的线程,那么不能保证当前线程暂停的效果
- 具体的实现依赖于操作系统的任务调度器
线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,但他仅仅是一个提示,调度器可以忽略它
- 如果CPU比较忙,那么优先级高的线程会获得更多的时间片,但CPU闲时,优先级几乎没作用
案例-防止CPU占用100%内存
- sleep实现
在没有利用CPU来计算时,不要让while(true) 空转浪费CPU,这时可以使用yield或sleep来让出CPU的使用权给其他程序
while (true){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 可以用wait或条件变量达到类似的效果
- 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
- sleep适用于无需锁同步的场景
2.8join方法详解
- join -----等待线程结束(谁调用就是哪个线程,相当于按行执行代码了,因为只有当该线程结束之后,才会调用后面行的代码)
以调用角度来讲,如果:
- 需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异步
例:
@Slf4j
public class JoinTest {
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException{
test2();
}
private static void test2() throws InterruptedException{
Thread t1 = new Thread(()->{
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});
Thread t2 = new Thread(()->{
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r2 = 20;
});
t1.start();
t2.start();
long start = System.currentTimeMillis();
log.debug("join begin");
t1.join();
log.debug("t1 join end");
t2.join();
log.debug("t2 join end");
long end = System.currentTimeMillis();
log.debug("r1:{} r2:{} cost:{}",r1,r2,end-start);
}
}
运行结果
08:20:30.149 [main] DEBUG com.example.Test.JoinTest - join begin
08:20:31.150 [main] DEBUG com.example.Test.JoinTest - t1 join end
08:20:32.155 [main] DEBUG com.example.Test.JoinTest - t2 join end
08:20:32.155 [main] DEBUG com.example.Test.JoinTest - r1:10 r2:20 cost:2010
可以看到,当t1线程调用join方法之后,才会开始执行后面的代码,为什么总耗时为2s呢,因为join调用之前,t1和t2同时开始,所以就算要等到t1结束t2早就已经等够了2s,所以总耗时为2s
[如图]
2.8.2有时效的Join
方法名 | static | 功能说明 | 注意 |
---|---|---|---|
join | 等待线程运行结束 | ||
join(long n) | 等待线程运行结束,最多等待n毫秒 |
例:
public static void test3() throws InterruptedException {
Thread t1 = new Thread(()->{
try {
sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});
long start = System.currentTimeMillis();
t1.start();
log.debug("join begin");
t1.join(1500);
long end = System.currentTimeMillis();
log.debug("r1: {} costs: {}",r1,end-start);
}
运行结果
08:36:43.223 [main] DEBUG com.example.Test.JoinTest - join begin
08:36:43.245 [main] DEBUG com.example.Test.JoinTest - r1: 10 costs: 25
2.9interrupt方法详解
阻塞
2.9.1打断sleep、wait、join状态的线程
以sleep为例:
@Slf4j
public class InterruptTest {
public static void main(String[] args) {
test1();
}
public static void test1(){
Thread t1 = new Thread(()->{
try {
//1s
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1");
t1.start();
try {
//0.5s
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.interrupt();
log.debug("打断状态{}",t1.isInterrupted());
}
}
运行结果
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.example.Test.InterruptTest.lambda$test1$0(InterruptTest.java:19)
at java.lang.Thread.run(Thread.java:748)
08:43:59.882 [main] DEBUG com.example.Test.InterruptTest - 打断状态false
正常运行的线程打断之后就是true,sleep之类的打断之后才是false
2.9.2两阶段终止模式
Two Phase Termination
在一个线程T1中如何优雅的终止线程T2?这里的“优雅”是指给T2一个料理后事的机会
1、错误思路
- 使用线程对象的stop()方法停止线程
- stop方法会真正杀死进程,如果这时线程锁住了共享资源,那么当它被杀死只会就再也没有机会释放锁,其他线程将会永远无法获取锁
- 使用System.exit(int) 方法停止线程
- 目的仅是停止一个线程,但这种方法会让整个程序都停止
示例:
@Slf4j
public class Test3 {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination twoPhaseTermination = new TwoPhaseTermination();
twoPhaseTermination.start();
//3.5s后打断,打断睡眠状态
Thread.sleep(3500);
twoPhaseTermination.stop();
}
}
@Slf4j
class TwoPhaseTermination{
private Thread monitor;
public void start(){
monitor = new Thread(()->{
Thread current = Thread.currentThread();
while (true){
//获取当前线程
//打断标记如果为true 那么跳出循环
if(current.isInterrupted()){
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000); //可能在这被打断
log.debug("执行监控记录"); //也可能在这被打断
} catch (InterruptedException e) {
e.printStackTrace();
//线程睡眠状态被打断后,为了使其能够正常跳出循环(监控跳出),需要重新设置打断标记,所以重新打断
current.interrupt();
}
}
});
monitor.start();
}
public void stop(){
//为了让他优雅的被打断,不能使用停止标记
monitor.interrupt();
}
}
运行结果
09:40:27.488 [Thread-0] DEBUG com.example.Test.TwoPhaseTermination - 执行监控记录
09:40:28.498 [Thread-0] DEBUG com.example.Test.TwoPhaseTermination - 执行监控记录
09:40:29.498 [Thread-0] DEBUG com.example.Test.TwoPhaseTermination - 执行监控记录
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.example.Test.TwoPhaseTermination.lambda$start$0(Test3.java:37)
at java.lang.Thread.run(Thread.java:748)
09:40:29.987 [Thread-0] DEBUG com.example.Test.TwoPhaseTermination - 料理后事
在3s后,sleep开始被打断,运行到跳出循环,说明设置的监控标志没问题
isInterrupted() 和 interrupter()
方法名 | static | 功能说明 | 注意 |
---|---|---|---|
isInterrupted() | 判断是否被打断 | 不会清除打断标记 | |
interrupter() | 打断线程 | 会清除打断标记 |
2.10不推荐使用的方法
方法名 | static | 功能说明 |
---|---|---|
stop() | 停止线程运行 | |
suspend() | 挂起(暂停)线程 | |
resume() | 恢复线程运行 |
2.11主线程和守护线程
默认情况下,Java进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其他非守护线程运行结束了,即使守护线程的代码没执行完,也会强制结束
例
@Slf4j
public class DaemonTest {
public static void main(String[] args) throws Exception{
Thread thread = new Thread(()->{
while (true){
if(Thread.currentThread().isInterrupted()){
break;
}
}
log.debug("结束");
});
thread.setDaemon(true);
thread.start();
Thread.sleep(1000);
log.debug("结束");
}
}
运行结果
22:24:28.681 [main] DEBUG com.example.Test.DaemonTest - 结束
由此可见,当main线程执行结束之后,thread线程也执行结束,并且直接结束了thread线程,不仅结束了while循环,而且下面的代码也没有执行,执行的结束是main方法内的结束代码
| 注意
- 垃圾回收器线程就是一种守护线程
- Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到shutdown命令后,不会等待他们处理完请求
2.12五种状态
2.13六种状态
从Java API层面来描述
根据Thread.State
枚举,分为六种状态
- NEW 线程刚被创建,但是没有调用star()方法
- RUNNABLE 当调用了start()方法之后,注意,Java API层面的RUNNABLE状态涵盖了操作系统层面的【可运行状态】、【运行装填】和【阻塞状态】(由于BIO导致的线程阻塞,在Java里无法区分,仍然认为是可运行)
- BLOCKED、WAITING、TIMED_WAITING都是Java API层面对【阻塞部分】的细分,后面会在状态转换这一节详述
- TERMINATED当线程代码运行结束
小结
应用:
- 异步调用:主线程执行期间,其他线程异步执行耗时操作
- 提高效率:并行计算,缩短运算时间
- 同步等待:join
- 统筹规划:合理使用线程,得到最优效果
原理:
- 线程运行流程:栈、栈帧、上下文切换、程序计数器
- Thread两种创建方式的源码
三、共享模型之管程
4.1临界区(Critical Section)
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问共享资源
- 多个线程读取共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
- 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
例如如下的临界区
static int counter = 0;
static void increment(){
counter++;
}
static void decrement(){
counter--;
}
4.2synchronized解决方案
互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的
- 阻塞式的解决方案:synchronized、Lock
- 非阻塞式的解决方案:原子变量
synchronized,俗称【对象锁】
本次课使用阻塞式的解决方案: synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
注意:虽然java中互斥和同步都可以采用synchronized关键字来完成,但它们还是有区别的
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
4.2.1synchronized
语法
synchronized(对象){ //线程1、线程2(blocked)
临界区
}
实例
@Slf4j
public class SynchronizedTest {
static int count = 0;
static Object object = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (object){
count++;
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (object){
count--;
}
}
});
t1.start();
t2.start();
log.debug("{}",count);
}
运行结果
08:18:53.134 [main] DEBUG com.example.Test.SynchronizedTest - 0
可以看到,运行结果为0,说明加锁之后可以确保在一个线程执行的时候,另一个线程会被阻塞,所以无论运行多少次,结果都是0,但是如果不加锁,那么运行结果会有很多种结果
思考
synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断
为了加深理解:
-
如果把synchronized(obj)放在for循环外面,如何理解
- 可以
-
如果把t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎么样运作?
- 不会,必须是同一个对象
-
如果t1 synchronize(obj) 而t2没有加会怎么样?如何理解?
- 不会,数据仍然不对应
4.3方法上的synchronized
class Test{
public synchronized void test(){
}
}
//等价于
class Test{
public void test(){
synchronized(this){
}
}
}
class Test{
public synchronized static void test(){
}
}
//等价于
class Test{
public static void test(){
synchronized(this){
}
}
}
上面的案例优化
@Slf4j
public class SynchronizedTest {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
log.debug("{}","线程1run");
for (int i = 0; i < 5000; i++) {
room.add();
}
});
Thread t2 = new Thread(() -> {
log.debug("线程2run");
for (int i = 0; i < 5000; i++) {
room.decrease();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",room.getCount());
}
}
class Room{
private int count = 0;
public synchronized void add(){
count++;
}
public synchronized void decrease(){
count--;
}
public synchronized int getCount(){
return count;
}
}
运行结果
08:46:26.954 [Thread-0] DEBUG com.example.Test.SynchronizedTest - 线程1run
08:46:26.954 [Thread-1] DEBUG com.example.Test.SynchronizedTest - 线程2run
08:46:26.958 [main] DEBUG com.example.Test.SynchronizedTest - 0
“线程八锁”
其实就是考察synchronized锁住的是哪个对象
案例1 互斥 12或21
@Slf4j
class Number{
public synchronized void a(){
log.debug("1");
}
public synchronized void b(){
log.debug("2");
}
}
public static void main (String[] args){
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.a(); }).start();
}
结果
07:46:46.556 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1
07:46:46.558 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2
锁住的是同一个对象(this) 所以会有互斥,可能是线程1先执行,也有可能是2先执行
案例2 互斥 12或21
@Slf4j
class Number{
public synchronized void a(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
public synchronized void b(){
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
结果
07:49:30.848 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1
07:49:30.852 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2
同案例一,看谁先调度
案例3
@Slf4j
class Number{
public synchronized void a(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
public synchronized void b(){
log.debug("2");
}
public void c(){
log.debug("3");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
new Thread(()->{ n1.c(); }).start();
}
结果
07:51:46.658 [Thread-2] DEBUG com.example.EightSynchroizedDemo.Number - 3
07:51:47.660 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1
07:51:47.660 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2
//3 1s 12
//23 1s 1
//32 1s 1
线程3和其他两个线程并行执行,但是线程1和线程2互斥
案例4
@Slf4j
class Number{
public synchronized void a(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
public synchronized void b(){
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
结果
07:56:06.328 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2
07:56:07.340 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1
锁住的对象不同,无互斥,所以总是线程2先执行,因为线程1睡眠1s
案例5
@Slf4j
class Number{
public static synchronized void a(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
public synchronized void b(){
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
结果
07:58:07.288 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2
07:58:08.282 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1
同案例5,但是a方法加了static,所以此时a方法实际上锁住的是类对象,但是此时都是n1对象调用的a方法和b方法。
案例6
@Slf4j
class Number{
public static synchronized void a(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
public static synchronized void b(){
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
// Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
结果
08:00:53.791 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1
08:00:53.797 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2
此时a方法和b方法都是对类对象加锁,但是类对象只有一份,所以此时两线程会互斥,看谁先被调度
案例7
@Slf4j
class Number{
public static synchronized void a(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
public synchronized void b(){
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
结果
08:03:00.603 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2
08:03:01.605 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1
不互斥,a方法是类对象,b方法是this对象,通过不同的对象实例调用,不是对同一个方法生效,所以不互斥
案例8
@Slf4j
class Number{
public static synchronized void a(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
public static synchronized void b(){
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
结果
08:06:45.100 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1
08:06:45.104 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2
a方法和b方法都是锁住类对象,但是类对象只有一个,此时在运行时,即使调用对象不同,但是也是互斥。
说明
用static修饰的加锁方法,为什么锁住的是类对象,可以这样理解,在调用静态方法时,可以通过对象名.方法名(Number.a())调用,静态方法的调用基本都是这样,所以没吃调用静态方法实际上都是对象名.方法名(Number.a()),长的是不是都一样,只是后面方法名不通过罢了,所以静态变量锁会互斥
4.4变量的线程安全分析
成员变量和静态变量是否线程安全
-
如果没共享,那么安全
-
如果被共享,根据他们状态是否能改变,有分两种情况
-
如果只有读操作,则线程安全
-
如果有读写操作,则这段代码是临界区,需要考虑线程安全
-
局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全