共享带来的问题、synchronized 解决方案、变量的线程安全分析

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的值,它不会再原有的基础上修改, 而是会重新开辟一个空间来存储)

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值