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

JavaEE传送门

JavaEE

JavaEE——Java线程的几种状态

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


目录

  • 线程安全问题
    • 1. 内存可见性问题
    • 2. volatile 关键字
      • JMM (Java Memory Model)
    • 3. wait 和 notify
      • wait()
      • notify()


线程安全问题

1. 内存可见性问题

public class Test {
    static class Counter {
        public int count = 0;
    }
    
    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            while (counter.count == 0) {
                
            }
        });
    }
}

上述代码, 在第10行上, while() 中的语句, 在读取内存 (LOAD) , 进行比较(CMP).

while循环会转的非常快, 会频繁的进行多次 LOAD 和 CMP.

LOAD 消耗的时间长, 比 CMP 慢 3-4 个数量级 (1w倍).

JavaEE——No.2 线程安全问题_第1张图片
# 这时编译器开始优化, 既然需要频繁的执行 LOAD, 并且 LOAD 的结果还一样, 干脆就只执行一次 LOAD 就可以了, 后续进行 CMP就不再重新读内存了

20200811130123_5074f

这时如果再来一个 t2 , 就会出现一些问题

public class Test {
    static class Counter {
        public int count = 0;

    }
    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            while (counter.count == 0) {

            }
            System.out.println("t1 执行结束");
        });

        Thread t2 = new Thread(() -> {
            System.out.println("请输入一个整数(int) :");
            Scanner scanner = new Scanner(System.in);
            counter.count = scanner.nextInt();
        });
        
        t1.start();
        t2.start();
    }
}

我们这时运行代码, 并输入一个整数, 我们发现程序还是没有停止.
JavaEE——No.2 线程安全问题_第2张图片
我们刚刚输入的1 已经赋值给 counter.count , 内存已经被修改, 但是对于刚才的修改, 对于 t1 的读内存不会有影响, 因为 t1 已经被优化成不再循环读内存了(读一次就不读了).

这样就会出现, 存可见性问题, 编译器优化惹下的祸事 (编译器优化, 在多线程环境下可能存在误判).


2. volatile 关键字

volatile 修饰的变量, 能够保证 “内存可见性”.

此时被修饰的变量, 编译器就不会做出 “不读内存, 只读寄存器” 这样的优化

static class Counter {
    volatile public int count = 0;
}

我们这时再次运行代码, 输入一个整数, 我们发现程序停止了.
JavaEE——No.2 线程安全问题_第3张图片

代码在写入 volatile 修饰的变量的时候,

  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主内存

代码在读取 volatile 修饰的变量的时候,

  • 从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本

20200811130123_5074f

# 注意 # 编译器的优化, 并不是一直存在的, 会根据代码的实际情况.

public class Demo12 {
    static class Counter {
        public int count = 0;

    }
    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            while (counter.count == 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t1 执行结束");
        });

        Thread t2 = new Thread(() -> {
            System.out.println("请输入一个整数(int) :");
            Scanner scanner = new Scanner(System.in);
            counter.count = scanner.nextInt();
        });

        t1.start();
        t2.start();
    }
}

这时是不会出现内存可见性问题的

volatile 不保证原子性

volatilesynchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.


JMM (Java Memory Model)

volatile 禁止了编译器优化, 避免了直接读取 CPU 寄存器 (工作内存 work memory) 中缓存的数据, 而是每次都读内存 (主内存 main memory)

正常程序执行的过程中, 会把主内存的数据, 先加载到工作内存中, 再进行计算处理

编译器优化可能会导致不是每次都真正的读取主内存, 而是直接取工作内存中的缓存数据. (就可能导致内存可见性问题)

volatile 起到的效果, 就是保证每次读取内存都是真的从主内存重新读取.

# 注意 #

上述的工作内存并不是真正的内存, 主内存才是真正的内存. (“工作内存” 是 CPU 寄存器 + 缓存的 抽象的表述)


3. wait 和 notify

由于线程, 调度过程是随机的. 很多时候, 我们希望多个线程按照一个预期的顺序来执行.

wait 和 notify 就可以用来调配线程执行顺序

wait()

  • wait 是 Object 的方法, 如果我们想调用 wait 一定要有一个对象(任意类的实例),

  • wait 这里会有一个异常, 可能会被 interrupt 方法唤醒.

JavaEE——No.2 线程安全问题_第4张图片

我们来看这样一个代码

public class Test {
    public static void main(String[] args) {
        Object object = new Object();
        System.out.println("wait 之前");
        try {
            object.wait();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("wait 之后");
    }
}

当线程执行到 wait, 就会发生阻塞. 直到另一个线程, 调用 notify 把这个 wait 唤醒, 才会继续执行.

此时我们运行代码, 会出现异常, 非法的锁状态异常
JavaEE——No.2 线程安全问题_第5张图片

wait 本质上做了三件事

  1. 释放当前锁
  2. 等待 notify 唤醒
  3. 被唤醒后, 尝试重新获取锁

此时我们的代码还没有加锁, wait 无法释放锁. 所以我们需要添加一行代码.

public class Test {
    public static void main(String[] args) {
        Object object = new Object();
        System.out.println("wait 之前");
        synchronized (object) {//加锁
            try {
                object.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("wait 之后");
    }
}

那么 wait 为什们要先释放锁呢?

wait 释放锁, 保证其他线程能够正常往下进行. 给其他线程机会, 让其他线程可以拿到锁.

wait 结束等待的条件:

  • 其他线程调用该对象的 notify 方法.

  • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).

  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.


notify()

notify 方法是唤醒等待的线程.

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。

  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)

  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

我们来画图理解一下, 加入 t1 执行了 wait , t2 调用了 notify.
JavaEE——No.2 线程安全问题_第6张图片
# 注意事项 #

  • 我们要保证加锁调用的对象和调用 wait 的对象是同一个对象
  • 还要保证, 调用 wait 和调用 notify 的对象是同一个对象

我们再写一个代码

public class Test {
    public static void main(String[] args) {
        Object object = new Object();

        Thread t1 = new Thread(() -> {
            while(true) {
                synchronized (object) {
                    System.out.println("wait 之前");
                    try {
                        object.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("wait 之后");
                }
            }
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            while(true) {
                synchronized (object) {
                    System.out.println("notify 之前");
                    object.notify();
                    System.out.println("notify 之后");
                }

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t2.start();
    }
}

我们运行一下, 我们可以看到 通过 waitnotify 我们干预了线程的执行顺序.
JavaEE——No.2 线程安全问题_第7张图片

# 注意 #

  • 如果先执行了 notify , 后执行了 wait , 可能会造成 wait 无法被唤醒. (如果没有 wait 执行 notify, 并不会有副作用)
  • Java 除了 notify 还有 notifyAll . notify 是随机唤醒一个, notifyAll 则是全部唤醒(即使唤醒了所有的 wait ,这些 wait 又需要重新竞争锁, 这个过程仍然是串行的)

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

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

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

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