1.共享带来的问题
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}
字节码的可读性非常低。字节码指令通常是由编译器或解释器生成的,它们被用于在虚拟机上执行程序。
如果是单线程,以上 8 行代码是顺序执行(不会交错)没有问题:
1.1 临界区
1.一个程序运行多个线程本身是没有问题的
2.问题出在多个线程访问共享资源
多个线程读共享资源其实也没有问题
在多个线程对共享资源读写操作时发生指令交错,就会出现问题
3.一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
1.2 竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
2.synchronized
在 Java 早期版本中,
synchronized
属于 重量级锁,效率低下。这是因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock
来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。不过,在 Java 6 之后,
synchronized
引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让synchronized
锁的效率提升了很多。因此,synchronized
还是可以在实际项目中使用的,像 JDK 源码、很多开源框架都大量使用了synchronized
。
2.1 synchronized加在对象上
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
阻塞式的解决方案:synchronized,Lock
非阻塞式的解决方案:原子变量(Java中的原子变量包括AtomicInteger、AtomicLong等。)
synchronized,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}
14:31:38.844 [main] DEBUG tset.ChanellTest - 0
- synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门),房间只能一次进入一人进行计算,线程 t1,t2 想象成两个人
- 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行 count++ 代码
- 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了(monitor有owner字段可以知道对象被占有了),只能在门外等待,发生了上下文切换,阻塞住了
- 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦), 这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入
- 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥 匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断
2.2 synchronized加在方法上
synchronized只能锁对象,加在成员方法上,锁的是this对象。静态方法上,锁的就是类对象
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 (Test.class) {
}
}
}
3.变量线程安全分析
成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
如果只有读操作,则线程安全
如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
如果该对象没有逃离方法的作用访问,它是线程安全的
如果该对象逃离方法的作用范围,需要考虑线程安全
3.1 局部变量线程安全分析
public static void test1() {
int i = 10;
i++;
}
每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
3.2 常见线程安全类
String
Integer
StringBuffer
Random
Vector
Hashtable
java.util.concurrent 包下的类
说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的
它们的每个方法是原子的
但注意它们多个方法的组合不是原子的
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}
如上代码,线程安全类方法的组合不是线程安全的,只能保证get或put方法内的代码为原子性。
如上代码,可能会出现两个线程都进行了put方法,导致代码运行错误
4.Monitor(对象头)
synchronized
同步语句块的实现使用的是 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。
上面的字节码中包含一个 monitorenter
指令以及两个 monitorexit
指令,这是为了保证锁在同步代码块代码正常执行以及出现异常的这两种情况下都能被正确释放。
当执行 monitorenter
指令时,线程试图获取锁也就是获取 对象监视器 monitor
的持有权。
在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitoropen in new window实现的。每个对象中都内置了一个 ObjectMonitor
对象。
另外,wait/notify
等方法也依赖于monitor
对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify
等方法,否则会抛出java.lang.IllegalMonitorStateException
的异常的原因。
synchronized
修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的确实是 ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED
访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
monitor属于重量级锁
monitor被翻译为监视器或管程
monitor由操作系统提供:monitor是操作系统提供的一种机制。在操作系统中,monitor是一种同步工具,用于控制并发访问共享资源。通过使用monitor,操作系统可以确保多个线程或进程在访问共享资源时的顺序和互斥性,从而避免数据竞争和冲突。
Java中的monitor是一种线程同步机制,用于保护共享资源的并发访问。每个Java对象都关联了一个monitor,它用于控制线程对该对象的访问。
在Java中,一个对象的monitor可以通过synchronized关键字来进行访问控制。当一个线程要访问一个被synchronized关键字修饰的方法或代码块时,它必须先获取该对象的monitor。如果其他线程已经持有该对象的monitor,则当前线程将被阻塞,直到获取到monitor才能继续执行。
通过monitor,Java能够实现线程的互斥访问,保证同一时间只有一个线程能够访问某个对象的synchronized方法或代码块。这样可以避免多个线程同时修改共享资源导致的数据竞争和不一致性。
同时,monitor还提供了wait()和notify()方法,用于实现线程之间的协作。通过调用wait()方法,线程可以将自己放入等待队列中,进入等待状态。而其他线程可以通过调用notify()方法来唤醒正在等待的线程