|
JavaEE
JavaEE——Thread类
JavaEE——Java线程的几种状态
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
class Counter {
public int count = 0;
public Object locker = new Object();
public void increase() {
count++;
}
}
public class Test {
private static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
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();
//让main线程的等待 t1, t2 线程结束
System.out.println(counter.count);
}
}
我们写了两个线程, 每个线程都针对 counter 进行5w次自增. 我们的预期结果是10w, 我们来运行一下这个程序, 看一下 counter.count
的结果
我们发现, 结果并不是10w, 且多次运行, 结果是不一样的
这说明, 我们上述代码是存在bug 的!!
那么上述问题是怎么出现的呢?
上述进行的 count++ 操作, 在底层, 是三条指令, 在CPU 上完成的
- load 把内存的数据读取到 CPU 中
- add 把 CPU 的寄存器中的值, 进行 +1
- save 把寄存器中的值, 写回到内存中
由于, 我们当前是两个线程修改同一个变量, 且每次修改分为三步 (不是原子的
) , 以及线程之间的调度顺序是不确定的
.
因此, 两个线程在真正执行这些操作的时候, 就可能由多种执行的排列顺序.
在上述的排列组合情况中, 有些排列组合是没有问题的, 但是还有些排列组合是有问题的 (两次累加得到)
#
如果是这两种情况, 我们所得到的结果是没有问题的
#
除了上述两种之外, 剩下的排列方式, 都是有问题的, 比如:
假设两个线程在两个CPU
核心上
形如这样的排列顺序下, 此时多线程自增就会存在 “线程安全” 问题.
在整个线程调度过程中, 执行的顺序都是随机的.
由于在调度过程中, 每种情况的次数, 不确定. 由此得到的结果就是不确定的值.
#
synchronized 修饰 increase 方法. 进行加锁
class Counter {
public int count = 0;
public synchronized void increase() {
count++;
}
}
当进入方法的时候, 就会加锁, 方法执行完毕, 自然解锁
加锁的背后是如何实现的呢?
锁, 具有独占特性, 如果当前锁没人来加, 加锁操作可以成功, 如果当前锁已经加上了, 加锁操作就会阻塞等待
本来线程调度是随机的过程, 现在使用锁, 使两组 load
, add
, save
能够串行执行了.
# 注意 #
加锁不是说, CPU一鼓作气执行完, 中间也可能会有调度切换. 即使 t1 走了, t2 仍是 BLOCKED
状态, 无法再 CPU 上运行的.
#
synchronized 修饰代码块. 进行加锁
锁当前对象
class Counter {
public int count = 0;
public void increase() {
synchronized (this) {//谁调用increase谁就是锁对象
count++;
}
}
}
# 注意 #
在( )中, 需要填需要加锁的对象. 被用来加锁的对象, 就简称为 "锁对象"
) 在Java中, 任意对象都可以作为锁对象
synchronized ( ) {
//....
}
- 如果两个线程针对同一对象加锁(即锁对象相同), 就会产生锁竞争, 线程一加锁成功, 线程二就需要阻塞等待.
- 如果两个线程针对不同对象加锁, 就无竞争.
- 如果 synchronized 直接修饰方法, 相当于锁对象是 this ! !
- 无论是正常执行完代码块, 还是异常执行完代码块, 都会触发解锁操作.
锁类对象
类对象
Java 中提供了一组反射 API . 这些 API 可以让我们理解到对象/类详细信息.
例如:
Counter 类, 里面有几个属性, 每个属性叫什么名字, 是什么类型, 里面有几个方法, 每个方法的名字, 有几个参数, 是什么类型, 返回值的类型…
(上述这些信息本来就保存在 类对象里面(Counter.class
), 类对象来自于 .class
文件, .class文件来自于.java
源代码文件)
class Counter {
public int count = 0;
public void increase() {
synchronized (Counter.class) {//类对象, 在 JVM 进程中只有一个
count++;
}
}
}
1) 互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁
2) 可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
把自己锁死
一个线程加锁后未释放, 再次加锁无法加锁成功, 就会阻塞等待. 如果第一个锁一直不释放, 这时就会死锁.(称为不可重入锁)
我们看这样一个代码
class Counter {
public int count = 0;
public synchronized void increase() {
synchronized (this) {//谁调用increase谁就是锁对象
count++;
}
}
}
上述代码的两个 synchronized
都是针对 this 加锁的, 理论上, 刚调用方法时, 已经给 this 对象加锁, 执行下面代码块时, 并未解锁, 再次加锁, 可能造成死锁. 但是synchronized
同步块对同一条线程来说是可重入的.
可重入锁的底层是如何实现的呢?
1) 让锁里持有线程对象, 记录是哪个线程持有这把锁
例如: t 线程尝试对 this 加锁, 这个锁中就记录了, 是 t 线程持有这把锁. 第二次进行加锁的时候, 锁发现, 还是 t 线程, 就直接通过了, 没有任何负面影响, 不会阻塞等待
2) 维护一个计数器, 用来衡量啥时候是真加锁, 啥时候是真解锁, 啥时候是直接放行.
Java 标准库中很多都是线程不安全的
. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.
但是还有一些是线程安全的
. 使用了一些锁机制来控制.
还有的虽然没有加锁, 但是不涉及 “修改”, 仍然是线程安全的
|
以上就是今天要讲的内容了,希望对大家有所帮助,如果有问题欢迎评论指出,会积极改正!!