5. synchronized 关键字 – 监视器锁 monitor lock
1. 观察线程不安全
package thread;
public class ThreadDemo19 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
//创建两个线程,每个线程都针对上面的count变量循环自增5w次
Thread t1 = new Thread(()-> {
for(int i = 0; i<50000; i++) {
count++;
}
});
Thread t2 = new Thread(()-> {
for(int i = 0; i<50000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
执行上面的代码,我们发现结果并不是100000, 并且多次运行, 每次的结果都有所不同:
2. 线程安全的概念
3. 线程不安全的原因
上⾯的线程不安全的代码中, 涉及到多个线程针对 count 变量进⾏修改. 此时这个 count 是⼀个多个线程都能访问到的 “共享数据“
原⼦性
什么是原⼦性
我们把⼀段代码想象成⼀个房间,每个线程就是要进⼊这个房间的⼈。如果没有任何机制保证,A进⼊ 房间之后,还没有出来;B 是不是也可以进⼊房间,打断 A 在房间⾥的隐私。这个就是不具备原⼦性的。
那我们应该如何解决这个问题呢?是不是只要给房间加⼀把锁,A 进去就把⻔锁上,其他⼈是不是就进不来了。这样就保证了这段代码的原⼦性了。 有时也把这个现象叫做同步互斥,表⽰操作是互相排斥的。
⽐如刚才我们看到的 count++,其实是由三步操作组成的:
3. 把数据写回到 CPU
如果⼀个线程正在对⼀个变量操作,中途其他线程插⼊进来了,如果这个操作被打断了,结果就可能是错误的。
这点也和线程的抢占式调度密切相关. 如果线程不是 “抢占” 的, 就算没有原⼦性, 也问题不⼤.
可⻅性
可⻅性指, ⼀个线程对共享变量值的修改,能够及时地被其他线程看到
4. 解决之前的线程不安全问题
使用 synchronized 关键字将一条指令的多个操作, 打包成一个原子的操作.
下面是使用 synchronized 来解决上面代码的问题:
如果两个线程, 针对不同的对象加锁, 也会存在线程安全问题.
如果一个线程加锁, 一个线程不加锁, 是否会存在线程安全问题?
把count 放到一个Test t对象中, 通过类方法add 来进行修改, 加锁的时候锁对象写作 this
package thread;
class Test {
public int count = 0;
public void add() {
synchronized (this) {
count++;
}
}
}
public class ThreadDemo20 {
public static void main(String[] args) throws InterruptedException {
Test t = new Test();
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
t.add();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
t.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + t.count);
}
}
5. synchronized 关键字 – 监视器锁 monitor lock
5.1 synchronized 的特性
1) 互斥
synchronized 会起到互斥效果, 某个线程执⾏到某个对象的 synchronized 中时, 其他线程如果也执⾏ 到同⼀个对象 synchronized 就会阻塞等待
可以粗略理解成, 每个对象在内存中存储的时候, 都存有⼀块内存表⽰当前的 “锁定” 状态(类似于厕所 的 “有⼈/⽆⼈”).
针对每⼀把锁, 操作系统内部都维护了⼀个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试 进⾏加锁, 就加不上了, 就会阻塞等待, ⼀直等到之前的线程解锁之后, 由操作系统唤醒⼀个新的线程, 再来获取到这个锁.
注意:
2) 可重⼊
synchronized 同步块对同⼀条线程来说是可重⼊的,不会出现⾃⼰把⾃⼰锁死的问题;
理解 “把⾃⼰锁死”
// 第⼀次加锁, 加锁成功
// 第⼆次加锁, 锁已经被占⽤, 阻塞等待.
按照之前对于锁的设定, 第⼆次加锁的时候, 就会阻塞等待. 直到第⼀次的锁被释放, 才能获取到第⼆ 个锁. 但是释放第⼀个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想⼲了, 也就⽆法进 ⾏解锁操作. 这时候就会 死锁
这样的锁称为 不可重⼊锁.
Java 中的 synchronized 是可重⼊锁, 因此没有上⾯的问题.
5.2 synchronized 使⽤⽰例
synchronized 本质上要修改指定对象的 “对象头”. 从使⽤⻆度来看, synchronized 也势必要搭配⼀个 具体的对象来使⽤.
1) 修饰代码块: 明确指定锁哪个对象.
锁任意对象
public class SynchronizedDemo {
private Object locker = new Object();
public void method() {
synchronized (locker) {
}
}
}
锁当前对象
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
2) 直接修饰普通⽅法: 锁的 SynchronizedDemo 对象
public class SynchronizedDemo {
public synchronized void methond() {
}
}
3) 修饰静态⽅法: 锁的 SynchronizedDemo 类的对象
public class SynchronizedDemo {
public synchronized static void method() {
}
}
我们重点要理解,synchronized 锁的是什么. 两个线程竞争同⼀把锁, 才会产⽣阻塞等待.
6.由内存可见性引起的线程安全问题
这个代码中, 预期通过 t2 线程输入整数, 只要输入的整数不为 0 , 就可以使 t1 线程结束:
package thread;
import java.util.Scanner;
public class ThreadDemo23 {
private static int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(()->{
while(flag == 0) {
}
System.out.println("t1 线程结束");
});
Thread t2 = new Thread(()->{
System.out.println("请输入flag的值:");
Scanner scanner = new Scanner(System.in);
flag = scanner.nextInt();
});
t1.start();
t2.start();
}
}
但是, 当我启动线程, 输入一个非0的值, t1 并没有结束:
volatile 关键字
volatile 能保证内存可⻅性
• 将改变后的副本的值从⼯作内存刷新到主内存
• 从主内存中读取volatile变量的最新值到线程的⼯作内存中
• 从⼯作内存中读取volatile变量的副本
前⾯我们讨论内存可⻅性时说了, 直接访问⼯作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度⾮常快, 但是可能出现数据不⼀致的情况. 加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了.
volatile 不保证原⼦性
volatile 和 synchronized 有着本质的区别. synchronized 能够保证原⼦性, volatile 保证的是内存可⻅ 性.
代码⽰例 这个是最初的演⽰线程安全的代码.
• 给 increase ⽅法去掉 synchronized
• 给 count 加上 volatile 关键字.
static class Counter {
volatile public int count = 0;
void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
此时可以看到, 最终 count 的值仍然⽆法保证是 100000.
原文地址:https://blog.csdn.net/m0_73648729/article/details/134620909
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.7code.cn/show_19820.html
如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!