线程安全-Day1
文章目录
前言
一、什么是线程安全?
1、线程不安全现象
先用一段代码来演示一下线程不安全的现象吧
public class Main1 {
// 定义一个共享的数据 —— 静态属性的方式来体现
static int r = 0;
// 定义加减的次数
static final int COUNT = 10000;
// 定义两个线程,分别对 r 进行 加法 + 减法操作
static class Add extends Thread {
@Override
public void run() {
for (int i = 0; i < COUNT; i++) {
r++;
}
}
}
static class Sub extends Thread {
@Override
public void run() {
for (int i = 0; i < COUNT; i++) {
r--;
}
}
}
public static void main(String[] args) throws InterruptedException {
Add add = new Add();
add.start();
Sub sub = new Sub();
sub.start();
add.join();
sub.join();
// 理论上,r 被加了 COUNT 次,也被减了 COUNT 次
// 所以,结果应该是 0
System.out.println(r);
}
}
解释一下这一段代码:先定义一个共享的数据,然后再定义两个线程,线程一对共享数据做加法操作(10000次),线程二对共享数据做减法操作(10000次),按照我们正常的理解,只要++和–次数一样,那么最后共享数据的值一定是0,但是具体运行结果我也截图放出来,很明显,这个结果非常随机,有可能是0又有可能不是0,预期结果并不是100%会出现,这就是线程不安全的现象。
2、为什么会出现线程不安全现象
(1)站在开发者角度来看
有两个主要的原因会产生线程不安全现象:
-
不同线程之间共享数据;
-
至少有一个线程对共享的数据进行了修改;
那么在多线程代码中,哪些情况下不需要考虑线程安全问题?
- 几个不同线程之间没有共享数据,那么它们天生就是线程安全的;
- 即使多个线程之间有共享数据,但都是读操作、不涉及写操作,那么它们也是线程安全的
(2)从系统角度出发
- 在Java语言中,一条语句可能会对应多条指令;
- 线程调度是随时都有可能发生的,多条指令之间可能会发生线程调度,但不会切割指令(指令具有原子性)。
二、如何考虑线程安全
1、避免数据共享
尽量让几个线程之间不做数据共享,各干各的,就不需要考虑线程安全的问题了。
2、只读操作
如果无法避免多个线程之间共享数据的问题,那么就尽可能不让这些线程对数据进行写操作,只是读操作。
static final int COUNT=100;
此时,即使多个线程同时使用这个COUNT也无所谓了
3、线程安全问题发生
原因
- 原子性被破环
- 由于内存可见性,导致某些线程读取到“脏数据”
- 由于代码重排序问题导致线程之间关于数据的配合出现了问题
1、违反原子性的场景
①read-write场景
i++;
arrat[size]=num;
②check-up场景
if(a!=10){ a=10; }
2、内存可见性
一个线程对数据的操作,其它线程很可能是无法感知的。甚至某些情况下会被优化成完全看不见的结果。
3、代码重排序
编译原理中状态转换和代码优化的相关知识
所以接下来会学到一些机制,目的是和JVM进行“沟通”,避免上述问题的发生。
三、锁机制
synchronized——同步锁/监视器锁
(1)语法
- 修饰方法(普通、静态方法):
synchronized int add(…){…} - 修饰代码块
synchronized (引用){…}
1、 synchronized修饰普通方法,相当于对当前对象加锁。
synchronized (this){ 临界区——要执行的一些语句 }
2、synchronized修饰静态方法,相当于对当前类加锁。
synchronized (类.class){ 临界区——要执行的一些语句 }
(2) synchronized 的大致原理
锁的状态:锁上(locked) 和打开 (unlocked)
synchronized(ref){ 尝试向该引用指向的对象加锁
执行的一些语句
} 解锁
(3)尝试加锁的内部操作
尝试加锁的操作已经被系统保证了原子性
if( lock==false ){ 说明这个锁没有被锁上
lock=true; 当前线程把锁锁上
… 执行的一些语句
}
到这里说明加锁失败(locked=true)
Queue<线程> queue = …;queue代表该锁的阻塞队列
queue.add( Thread.currentThread() );将当前要加锁的线程放入这把锁的阻塞队列中
Thread.currentThread().state = 阻塞;无法加锁那就让出CPU,先把自己变成阻塞状态
Thread.yielad();接着再让出CPU
(4)释放锁的内部操作
释放锁的过程由系统保证原子性
释放锁 lock = false;
从等待锁的阻塞队列中选一个线程出来,恢复CPU
Thread t = queue.poll();
t.state = 就绪;等待被分配CPU
(5)互斥
加锁 {
代码(临界区)
} 解锁
当多个线程都有加锁操作时并且申请的是同一把锁,会造成临界区代码会互斥着进行,与临界区代码是不是同一份代码无关。
举个例子👇👇👇
public class Main {
//定义了一把锁
static Object lock=new Object();
static class MyThread1 extends Thread{
@Override
public void run() {
synchronized (lock){
for (int i = 0; i < 1_000; i++) {
System.out.println("我是巴啦啦老魔仙");
}
}
}
}
static class MyThread2 extends Thread{
@Override
public void run() {
synchronized (lock){
for (int i = 0; i < 1_000; i++) {
System.out.println("我来自魔仙堡");
}
}
}
}
public static void main(String[] args) {
MyThread1 t1=new MyThread1();
MyThread2 t2=new MyThread2();
t1.start();
t2.start();
}
}
当不加锁时,两个线程交替执行(两句话轮流出现);加锁之后,线程1和线程2中的任意一个线程就将 lock 锁锁上了,另一个线程尝试加锁时一定会失败,此时只能等先成功加锁的线程执行结束另一个线程才能重新尝试加锁。所以造成的现象就是两个线程执行顺序一定是先后关系(即线程1或2中的那句话打印1000次之后才轮到线程2或1中的那句话打印1000次,并按照此规律一直重复)
总结
明天继续!