多线程基础(4):几种常见的关键字(synchronized,volatile,notify和wait)-解决线程安全问题

一.synchronized

1.cynchronized的特性

1)互斥(保证原子性)

synchronized 会起到互斥的效果

  • 当一个线程进入 synchronized 修饰的代码块,相当于 加锁
  • 同一个线程退出 synchronized 修饰的代码块,相当于 解锁

在这里插入图片描述

如果线程一,首先执行到这个方法,首先会得到锁(synchronized),进行加锁.当其他线程执行到这个方法,若是线程一还未执行完毕,也就意味着这个方法还处于加锁状态,此时其他线程就只能阻塞等待.直到线程一完成被 synchronized 修饰的方法进行释放锁.然后其他线程就会去争这个锁,拿到锁才能进行执行,否则就一直阻塞等待.

也就意味着,被 synchronized 修饰的代码块,无法被并发执行,变成了串行.也就保证了原子性,进而能够保证线程安全

    static class Counter{
        private int i;
//        public synchronized void increase(){
//            i++;
//        }
        public void increase(){
            //也可以这样包裹代码块,其中 this 表示修饰的对象(代码块)
            synchronized (this) {
                i++;
            }
        }
    }

关于加锁的对象: Java中任意的对象,都可以作为"锁对象"

在这里插入图片描述

如上代码块也可以是一个对象.

2)刷新内存(保证内存的可见性)

例如之前所说的例子,频繁快速进行读取的时候,可能出现内存可见性问题.
经过编译器优化(进行指令重排序)之后,原本是在内存中读取的,被优化成在 CPU(寄存器) 中读取了.

int i = 0;
while(true){
    if(i != 0){ //读取 i 进行判断
        break;
    }
}

我们可以在里面增加打印操作,或者 使用Thread.sleep()使得该线程执行会缓慢些,也能够制止内存可见性问题.

但是,这样子并不完全保证说可以不出现内存可见性问题.

可以使用 synchronized 关键字,保证 i 每次读取都是在内存中读取的

又如下例子的代码:

public void increase(){
    i++;
}        
for (int i = 0; i < 50_000; i++) {
    counter.increase();
}

原本的流程是:①从内存中拿到 i ,放到 CPU 寄存器中 ②进行++操作 ③将 i 放回到内存中

每次流程都应该如此,但是,经过编译器优化(指令重排序)后,就变成了 ①②②②②②②②…③ 了.

编译器优化,能够极大的提高程序效率.虽然我们使用 cynchronized 关键字可以保证每次都在内存中进行读取,但也意味着我们将损耗一定的效率

3)可重入

synchronized 允许一个线程针对一把锁,进行再次加锁

synchronized public void increase(){
    synchronized (this) { 
        i++;
    }
}

按道理如果第一次加锁的代码阻塞了,就无法释放这个锁,就可能出现死锁

但是 synchronized 不会出现死锁的情况,他是一个"可重入锁"

在可重入锁的内部,包含了"线程持有者"和"计数器"两个信息:

  • 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取
    到锁, 并让计数器自增
  • 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到 )

二.volatile

1.保证内存的可见性

除了 synchronized 之外,volatile 也能够保证内存的可见性,但无法保证原子性,这是二者之间的区别

    static class Counter{
        volatile static int flg = 1;
    }
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                System.out.println("线程开始");
                while(true){
//                    System.out.println(Counter.flg);
                    if(Counter.flg != 1) {
                        break;
                    }
                }
                System.out.println("线程终止");
            }
        };
        t.start();

        try {
            Thread.sleep(3000);
            System.out.println("即将终止线程");
            Counter.flg = 0;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

具体使用情况具体分析,如果只需要保证内存可见性,那么就只需要使用 volatile 即可

2.JMM(java memory model) 内存模型:

在这里插入图片描述

当读取一个变量的时候,不一定真的是在内存中读取,也有可能这个数据已经在 CPU 或者 cache 中缓存着了,这个时候,编译器就可以进行优化(指令重排序),绕过内存,直接从 CPU 或者 cache 中读取这个数据.

**Java虚拟机规范中定义了Java内存模型.
目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果 **

在这里插入图片描述

JMM 就把 CPU 的寄存器,L1,L2,L3 cache 统称为"工作内存"
JMM 也把真正的内存成为"主内存"

  • 线程之间的共享变量存在 主内存 (Main Memory).
  • 每一个线程都有自己的 “工作内存” (Working Memory) .
  • 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
  • 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.

由于每个线程有自己的工作内存, 这些工作内存中的内容相当于同一个共享变量的 “副本”. 此时修改线程 1 的工作内存中的值, 线程2 的工作内存不一定会及时变化

1)初始情况下, 两个线程的工作内存内容一致

在这里插入图片描述

2) 一旦线程1 修改了 a 的值, 此时主内存不一定能及时同步. 对应的线程2 的工作内存的 a 的值也不一定能及时同步

在这里插入图片描述

这个时候代码就容易出现问题

也就是说 CPU 在和内存交互的时候,经常会把主内存的内容,拷贝到工作内存,然后进行操作,再写回到主内存.

这个过程就非常容易出现数据不一致的情况,这种情况在编译器开启优化的时候会特别严重.


volatile 或者 synchronized 就能够强制保证接下来的操作是操作内存:禁止指令重排序

使用 volatile 修饰变量时,根据 volatile 重排序规则表,Java 编译器在生成字节码时,会在指令序列中插入内存屏障指令来禁止特定类型的处理器重排序。

内存屏障是一组处理器指令,它的作用是禁止指令重排序和解决内存可见性的问题。

三.wait和notify

线程之间是抢占式执行,也就意味着,充满了不确定性.我们无法确定线程的执行顺序.而通过wait和notify机制就可以来控制线程之间的执行顺序.让多个线程之间更好的相互配合

1.wait()

wait 作用:

  • 使当前执行代码的线程进行等待.(把线程从就绪队列放到等待队列中)
  • 释放当前的锁
  • 满足一定条件时被唤醒,重新尝试获取这个锁

wait 结束等待的情况:

  • 其他线程调用该对象的 notify 方法
  • wait 等待时候超时(wait可以指定一个最长等待时间)
  • 其他线程调用该线程的 interrupted 方法.导致 wait 抛出 InterruptedException 异常

wait 以及 notify 必须要搭配 synchronized 进行使用

2.notify()

作用: 唤醒等待的线程

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)
  • 在执行 notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁

wait 和 notify 使用示例:

    static class MyThread extends Thread{
        private Object locker = null;

        public MyThread(Object locker) {
            this.locker = locker;
        }


        @Override
        public void run() {
            synchronized (this.locker) {
                    System.out.println(Thread.currentThread().getName()+": 开始等待");
                    try {
                        this.locker.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        }
                    while(true){
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()+": 运行中");
                    }
            }
        }
    }
    static class MyRunnable implements Runnable{
        private Object locker = null;

        public MyRunnable(Object locker) {
            this.locker = locker;
        }

        @Override
        public void run() {
            synchronized (this.locker) {
                System.out.println("3s后准备解锁!");
                try {
                    Thread.sleep(3000);
                    System.out.println("3s 之期已至! 解除封印!");
                    this.locker.notify();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 首先使用 wait 方法让线程 t1 进入等待队列中,然后经过 3s 后,
     * 执行到 线程2 的 notify 方法释放锁.当然,必须得对同一把锁进行操作.
     * 这里用到了一个锁对象
     * @param args
     */
    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new MyThread(locker);
        Thread t2 = new Thread(new MyRunnable(locker));
        t1.start();
        t2.start();
    }

3.notifyAll()

notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程

public void run() {
    synchronized (locker) {
   		 System.out.println("notify 开始");
   		 locker.notifyAll();
   		 System.out.println("notify 结束");
    }
}

通过这个方法,就可以将所有的被 wait 方法作用的线程给唤醒,放回到就绪队列中.不管是一个线程还是多个线程.

当然,如果单独的 notify() 就只能唤醒一个,若是多个线程被 wait() 作用的线程,则就随机唤醒一个线程.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值