文章目录
1.共享带来的问题
1.1 小故事
1.2 Java代码的体现
两个线程对初始值为0的静态变量一个做自增,一个做自减,各做5000次,结果是0吗?
@Slf4j
public class Test {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 1; i < 5000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 1; i < 5000; i++) {
count--;
}
});
t1.start();
t2.start();
t1.join(); // 主线程等待t1线程执行完
t2.join(); // 主线程等待t2线程执行完
// main线程只有等待t1, t2线程都执行完之后, 才能打印count, 否则main线程不会等待t1,t2 直接就打印count的值为0
log.debug("count的值是{}", count);
}
}
运行结果
1.3 问题分析
以上的结果可能是正数、负数、零。为什么呢?因为Java中对静态变量的自增,自减并不是原子操作,要彻 底理解,必须从字节码来进行分析
如果是单线程以上8行代码是顺序执行(不会交错)没有问题:
多线程下这8行代码可能出现交错运行
线程cpu时间片用完,线程挂起,发生线程上下文切换
出现负数的情况:
正数的情况:
1.4 临界区 Critical Section
1.5 竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
2.synchronized 解决方案
为了避免临界区的竞态条件发生,由多种手段可以达到
- 阻塞式解决方案: synchronized , Lock
- 非阻塞式解决方案: 原子变量
使用阻塞式的解决方案:synchronized, 来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让 同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能 保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
2.1 synchronized 语法
synchronized(对象) { // 线程1获得锁, 那么线程2的状态是(blocked)
临界区
}
上面的实例程序使用synchronized后如下,计算出的结果是正确!
@Slf4j
public class Test {
static int count = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 1; i < 5000; i++) {
synchronized (room) {
count++;
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 1; i < 5000; i++) {
synchronized (room) {
count--;
}
}
});
t1.start();
t2.start();
t1.join(); // 主线程等待t1线程执行完
t2.join(); // 主线程等待t2线程执行完
// main线程只有等待t1, t2线程都执行完之后, 才能打印count, 否则main线程不会等待t1,t2 直接就打印count的值为0
log.debug("count的值是{}", count);
}
}
运行结果:
2.2 上下文切换-synchronized理解
思考
如果把synchronized(obj)放在for循环的外面, 如何理解?
for循环也是一个原子操作, 表现出原子性
如果t1 synchronized(obj1) 而 t2 synchronized(obj2)会怎么运行?
因为t1, t2拿到不是同一把对象锁, 所以他们仍然会发现安全问题 – 必须要是同一把对象锁
如果t1 synchronized(obj) 而 t2 没有加会怎么样 ?
因为t2没有加锁, 所以t2, 不需要获取t1的锁, 直接就可以执行下面的代码, 仍然会出现安全问题
面向对象改进
class Room {
private int value = 0;
public void increment() {
synchronized (this) {
value++;
}
}
public void decrement() {
synchronized (this) {
value--;
}
}
public int get() {
synchronized (this) {
return value;
}
}
}
@Slf4j
public class Test1 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
room.increment()
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("count: {}", room.get());
}
}
2.3 synchronized 加在方法上
- 加在实例方法上, 锁对象就是对象实例
public class Demo {
//在方法上加上synchronized关键字
public synchronized void test() {
}
//等价于
public void test() {
synchronized(this) {
}
}
}
- 加在静态方法上, 锁对象就是当前类的Class实例
public class Demo {
//在静态方法上加上synchronized关键字
public synchronized static void test() {
}
//等价于
public void test() {
synchronized(Demo.class) {
}
}
}
3.变量的线程安全分析
3.1 局部变量线程安全分析
如图:
局部变量线程私有
3.2 成员变量线程安全分析
无论哪个线程中的 method2 和 method3 引用的都是同一个对象中的 list 成员变量
public class Test15 {
public static void main(String[] args) {
UnsafeTest unsafeTest = new UnsafeTest();
for (int i =0;i<100;i++){
new Thread(()->{
unsafeTest.method1();
},"线程" + i).start();
}
}
}
class UnsafeTest{
ArrayList<String> arrayList = new ArrayList<>();
public void method1(){
for (int i = 0; i < 100; i++) {
method2();
method3();
}
}
private void method2() {
arrayList.add("1");
}
private void method3() {
arrayList.remove(0);
}
}
Exception in thread "线程1" Exception in thread "线程2" java.lang.ArrayIndexOutOfBoundsException: -1
将list改为局部变量
class UnsafeTest {
public void method1() {
ArrayList<String> arrayList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
method2(arrayList);
method3(arrayList);
}
}
private void method2(List<String> arrayList) {
arrayList.add("1");
}
private void method3(List<String> arrayList) {
arrayList.remove(0);
}
}
3.3 局部变量暴露引用
方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public会不会导致线程安全问题?
- 情况1:有其它线程调用 method2 和 method3
- 只修改为public修饰,此时不会出现线程安全的问题, 即使线程2调用method2/3方法, 给2/3方法传过来的list对象也是线程2调用method1方法时,传递给method2/3的list对象, 不可能是线程1调用method1方法传的list对象。
- 情况2:在情况1 的基础上,为ThreadSafe 类添加子类,子类覆盖method2 或 method3方法,即如下所示:
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
public void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
- 如果改为public, 此时子类可以重写父类的方法, 在子类中开线程来操作list对象, 此时就会出现线程安全问题: 子类和父类共享了list对象
- 父类中的方法定义成private私有的,这样子类就不能看到父类方法了,也就无法重写了; 所以所private修饰符是可以避免线程安全问题.
- 父类方法定义成final,这样子类可以调用,但是不能重写。
- 从这个例子可以看出 private 或 final 提供【安全】的意义所在,请体会开闭原则中的【闭】。
3.4 常见线程安全类
Hashtable table = new Hashtable();
new Thread(()->{
// put方法增加了synchronized
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();
线程安全类方法的组合
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}
线程一和线程二 受线程上下文影响 都通过了 table.get(“key”) == null 的判断,因此后者会覆盖前者的值。
不可变类的线程安全
String、 Integer等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
String 有 replace,substring 等方法【可以】改变值啊,其实调用这些方法返回的已经是一个新创建的对象了! (在字符串常量池中当修改了String的值,它不会再原有的基础上修改, 而是会重新开辟一个空间来存储)