一、共享带来的问题
1. 临界区
(1)一个程序运行多个线程本身是没有问题的
(2)问题出在多个线程访问共享资源
1️⃣多个线程读共享资源其实也没有问题
2️⃣在多个线程对共享资源读写操作时发送指令交错,就会出现问题
(3)一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
2. 竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
二、Synchronized 解决方案(P54)
1. 应用之互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
(1)阻塞式的解决方案:synchronized、Lock
(2)非阻塞式的解决方案:原子变量
synchronized,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程再想获取这个对象锁时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
注意:
虽然 Java 中互斥和同步都可以采用 synchronized 关键字来完成,但有区别:
(1)互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码。
(2)同步是由于线程执行的先后顺序不同,需要一个线程等待其他线程运行到某个点。
2. synchronized
synchronized(对象) // 线程1, 线程2(blocked) { 临界区 }
@Slf4j(topic = "c.Test17") public class Test17 { static int counter = 0; static Object lock = new Object(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 5000; i++) { synchronized (lock) { counter++; } } },"t1"); Thread t2 = new Thread(() -> { for (int i = 0; i < 5000; i++) { synchronized (lock) { counter--; } } },"t2"); t1.start(); t2.start(); t1.join(); t2.join(); log.debug("{}",counter); } }
思考:
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
3. 面向对象改进
@Slf4j(topic = "c.Test17")
public class Test17 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", room.getCounter());
}
}
class Room {
private int counter = 0;
public synchronized void increment() {
counter++;
}
public synchronized void decrement() {
counter--;
}
public synchronized int getCounter() {
return counter;
}
}
三、方法上的 synchronized(P59)
(1)普通方法
(2) 静态方法
(3)不加 synchronized 的方法
不加 synchronzied 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)
四、变量的线程安全分析(P63)
1. 成员变量和静态变量是否线程安全?
(1)如果它们没有共享,则线程安全
(2)如果它们被共享了,根据它们的状态是否能否改变,又分两种期刊
1️⃣如果只有读操作,则线程安全
2️⃣如果有读写操作,则这段代码是临界区,需要考虑线程安全
2. 局部变量是否线程安全?
(1)局部变量是线程安全的
(2)但局部变量引用的对象则未必
1️⃣如果该对象没有逃离方法的作用范围,它是线程安全的
2️⃣如果该对象逃离方法的作用范围,需要考虑线程安全
3. 常见线程安全类
String
包装类(Integer 等)
StringBuffer
Random
Vector
Hashtable
java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的方法时,是线程安全的。也可以理解为:
(1)它们的每个方法是原子的
(2)但注意它们多个方法的组合不是原子的
4.1 不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。