JavaEE——No.1 线程安全问题

JavaEE传送门

JavaEE

JavaEE——Thread类

JavaEE——Java线程的几种状态


目录

  • 线程安全问题
    • 1. 线程安全的概念
    • 2. 一个线程不安全示例
    • 3. synchronized 关键字
      • 3.1 synchronized 的使用
      • 3.2 synchronized 的特性
      • 3.3 Java 标准库中的线程安全类


线程安全问题

1. 线程安全的概念

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。


2. 一个线程不安全示例

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 的结果
JavaEE——No.1 线程安全问题_第1张图片
我们发现, 结果并不是10w, 且多次运行, 结果是不一样的
JavaEE——No.1 线程安全问题_第2张图片
这说明, 我们上述代码是存在bug 的!!

20200811130123_5074f

那么上述问题是怎么出现的呢?

上述进行的 count++ 操作, 在底层, 是三条指令, 在CPU 上完成的

  1. load 把内存的数据读取到 CPU 中
  2. add 把 CPU 的寄存器中的值, 进行 +1
  3. save 把寄存器中的值, 写回到内存中

由于, 我们当前是两个线程修改同一个变量, 且每次修改分为三步 (不是原子的) , 以及线程之间的调度顺序是不确定的.

因此, 两个线程在真正执行这些操作的时候, 就可能由多种执行的排列顺序.
JavaEE——No.1 线程安全问题_第3张图片
在上述的排列组合情况中, 有些排列组合是没有问题的, 但是还有些排列组合是有问题的 (两次累加得到)

#如果是这两种情况, 我们所得到的结果是没有问题的
JavaEE——No.1 线程安全问题_第4张图片
#除了上述两种之外, 剩下的排列方式, 都是有问题的, 比如:
JavaEE——No.1 线程安全问题_第5张图片
假设两个线程在两个CPU核心上
JavaEE——No.1 线程安全问题_第6张图片
JavaEE——No.1 线程安全问题_第7张图片
JavaEE——No.1 线程安全问题_第8张图片

形如这样的排列顺序下, 此时多线程自增就会存在 “线程安全” 问题.

在整个线程调度过程中, 执行的顺序都是随机的.

由于在调度过程中, 每种情况的次数, 不确定. 由此得到的结果就是不确定的值.


3. synchronized 关键字

3.1 synchronized 的使用

# synchronized 修饰 increase 方法. 进行加锁

class Counter {
    public int count = 0;

    public synchronized void increase() {
        count++;
    }
}

当进入方法的时候, 就会加锁, 方法执行完毕, 自然解锁

这时我们再运行程序, 我们可以发现运行结果变成了10w
JavaEE——No.1 线程安全问题_第9张图片

加锁的背后是如何实现的呢?

锁, 具有独占特性, 如果当前锁没人来加, 加锁操作可以成功, 如果当前锁已经加上了, 加锁操作就会阻塞等待

JavaEE——No.1 线程安全问题_第10张图片
本来线程调度是随机的过程, 现在使用锁, 使两组 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++;
        }
    }
}

3.2 synchronized 的特性

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) 维护一个计数器, 用来衡量啥时候是真加锁, 啥时候是真解锁, 啥时候是直接放行.


3.3 Java 标准库中的线程安全类

Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder

但是还有一些是线程安全的. 使用了一些锁机制来控制.

  • Vector (不推荐使用)
  • HashTable (不推荐使用)
  • ConcurrentHashMap
  • StringBuffer

还有的虽然没有加锁, 但是不涉及 “修改”, 仍然是线程安全的

  • String

(( ◞•̀д•́)◞⚔◟(•̀д•́◟ ))

以上就是今天要讲的内容了,希望对大家有所帮助,如果有问题欢迎评论指出,会积极改正!!
在这里插入图片描述
加粗样式

这里是Gujiu吖!!感谢你看到这里
祝今天的你也
开心满怀,笑容常在。

你可能感兴趣的:(JavaEE,java-ee,java)