JAVAEE初阶相关内容第六弹--多线程(初阶)

目录

Volatile关键字

内存可见性

从汇编来理解内存可见性问题:

概念扩展:

局部变量:

全局变量:

从JVM的角度表述内存可见性问题

volatile不保证原子性

wait和notify

wait( )方法

wait与synchronized

notify()方法

notifyAll()方法


Volatile关键字

volatile关键字只能修饰变量。

内存可见性

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

以下是一个没有加volatile关键字的时候执行的代码:

//首先创造一个类命名为NCount
    class  NCount{
        //创建一个变量
        public int flag = 0;
}
public class ThreadD15 {
    public static void main(String[] args) {
        NCount nCount = new NCount();
        //创建两个线程
        Thread t1 = new Thread(() ->{
            //第一个线程之进行条件判断,设置一个循环,如果满足循环条件的时候一直执行循环体
            while (nCount.flag == 0){
                //这里不添加任何语句
            }
            //如果不满足上面的while循环条件(flag的值被修改,则跳出循环)
            System.out.println("t1循环结束");
        });

        //在第二个线程里,我们来修改一下flag的值,这样就可以使t1线程不满足循环的条件。
        Thread t2 = new Thread(() -> {
            //选择从控制台进行输入
            Scanner scan = new Scanner(System.in);
            System.out.println("请手动输入一个非0的数");
            //将NCounter中的flag值修改为控制台输入进去的数字
            nCount.flag = scan.nextInt();
        });
        //运行t1和t2线程
        t1.start();
        t2.start();
    }
}

当我们进行代码运行的时候可以在控制台上看到以下的结果:

可以看到的是,关于我们刚才进行的代码编写,我们想得到的效果是线程t2将flag的值修改为不是0的数字后,被线程t1在下一次循环时,对while中的条件进行判定。发现不满足flag=0,跳出循环后打印出

可是并没有和我们所想的一样,程序就出现了bug。我们运行程序,打开jconsole:

可以看到的是t2线程已经没了,只剩下了t1线程继续执行循环。

以上这种情况就是内存可见性问题。

从汇编来理解内存可见性问题:

大概就是两步操作:

1.load,把内存中的flag值,读取到寄存器中。

2.cmp,把寄存器中的值和0进行比较,根据比较的结果,决定下一步需要向哪个方向执行。

这个循环的执行速度是非常快的,一秒执行百万次以上。

循环执行了很多很多次,在t2进行真正修改之前,load得到的结果都是相同的,另一方面,load操作与cmp操作相比是要慢很多很多的。由于load的执行速度“太慢“,再加上反复得到的结果都是相同的,JVM就自主的进行决定:判定load不会进行更改,不再重复的load了,干脆只读一次。

内存可见性问题:

一个线程针对一个变量进行读取操作,同时另一个线程对这个变量进行修改,此时独到的值,不一定是修改后的值。(归根到底是编译器/JVM在多线程环境下优化时产生了误判)

此时就需要我们手动的进行修改,可以在flag变量前加上关键字volatile意思就是告诉编译器这个变量是”易变的“,要确保每一次都会重新读取这个变量。

此时我们再次运行程序:

需要注意的小问题:

volatile只能修饰变量,但是不能修饰方法里的变量。

方法里的变量都是局部变量。

概念扩展:
局部变量:

局部变量(Local Variable)定义在函数体内部的变量,作用域仅限于函数体内部。离开函数体就会无效。再调用就是出错。

全局变量:

全局变量(Global Variable)定义在所有的函数外部定义的变量,它的作用域是整个程序,也就是所有的源文件,包括.c和.h文件。

全局变量既可以是某对象函数创建,也可以是在本程序任何地方创建。全局变量是可以被本程序所有对象或函数引用。

方法里的局部变量只能在当前的线程里面引用,不能多线程之间同时读取/修改,天然规避了线程安全的问题。局部变量:只在当前的方法里使用,出了方法变量就没了,方法内部的变量在”栈“这样的内存空间上,每个线程都有自己的栈空间,即使是同一种方法,在多个线程中被调用,这里的局部变量也会出现在不同的栈空间中,本质上也是不同的变量。

上面说的内存可见性,编译器优化的问题也不是始终会出现的,编译器可能会误判,但也不是会百分之百进行误判。例如我们将代码进行调整,在循环体里面加上100ms的休眠。

输出结果:

可以看出,此时我们即使不加volatile 代码也正确了,刚才的错误优化也没有了。

从JVM的角度表述内存可见性问题

Java程序里,主内存,每个线程还有自己的工作内存(t1与t2的工作内存不是一回事)

t1线程进行读取的时候,只读取了工作内存的值。

t2线程进行修改的时候,先修改的工作内存的值,然后再把工作内存的内容同步到主内存中。

但是由于编译器的优化,导致t1没有重新的从主内存同步数据到工作内存,读到的结果就是”修改之前“的结果。

这里可以按照以下的对应关系进行记忆

为什么在Java这里不直接叫”CPU寄存器“,而是专门搞了一个”工作内存“的说法?

这里的工作内存不一定只是CPU的寄存器,也有可能是CPU的缓存cache。

CPU读取寄存器,速度比读取内存快太多,因此就会在CPU内部引入cache。

寄存器、cache、内存的比较
寄存器存储空间小读写速度快
cache存储空间居中读写速度居中成本居中
内存存储空间大读写速度慢相对寄存器来说便宜

当CPU进行读取一个内存数据的时候可能是直接读内存的,可能是读cache的,也可能是读寄存器中的。

当引入cache之后,硬件结构就变的复杂了。工作内存(工作存储区):CPU寄存器+CPU的cache,为了表述简单且避免涉及到一些硬件细节的差异,所以在JAVA中就使用”工作内存“了。

volatile不保证原子性

volatile不能保证,原子性是靠synchronized来保证的。

二者都可以保证线程的安全,但是不能使用volatile来处理两个线程并发++这样的问题。

运行修改之后的代码:

对比synchronized的程序与结果:

wait和notify

线程最大的问题:抢占式执行,随即调度。因此线程之间的执行顺序是不确定的。

但是在实际的开发中,有些时候我们希望能够合理的协调多个线程之间的关系。所以程序员就发明了一些办法,来控制线程之间的执行顺序,虽然在内核中的调度是随机的,但是可以通过API让线程进行主动的阻塞,主动放弃CPU,给别的线程让路。

【例子】在篮球比赛中,队员们通过传球操作,将球投入到篮框中,这几个相互配合的队员就可以看做是不同的线程。

这里我们需要思考一个问题,在前面的博客中也记录了一些可以让线程等待的方式,例如join()和sleep()操作。那么使用这些可以么?

join如果被使用,则必须要t1彻底的执行完,t2才可以运行,那么如果希望t1执行百分之五十的进度,然后让t2开始执行这种情况下则是不可实现的。

至于sleep,sleep是指定了一个休眠时间,但是t1完成这些任务究竟花费多久的时间是没有办法估计的。

使用wait与notify可以解决以上提出的问题。

完成这个协调操作,主要涉及到三个方法:

wait( )/wait(long timeout)让当前线程进入等待线程

notify( )/notifyAll( ) :唤醒当前对象上的等待线程

注意!:wait,notify,notifyAll 都是Object的类方法,换句话说也就是JAVA中的随机一个类都可以有这三种方法

wait( )方法

wait与synchronized

某个线程调用wait方法,就会进入阻塞(无论是通过哪个对象wait)。此时就处于WAITING状态。

下面我们来看一个简单的代码案例;

public class ThreadD16 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        System.out.println("wait之前");
        object.wait();//这里wait不加任何参数,就是死等
        System.out.println("wait之后");

    }
}

我们来执行一下这个代码:

错误提示:非法锁状态异常

为什么会出现这个异常?

首先我们需要研究一下wait操作具体都做了什么?

(1)释放锁

(2)进行阻塞等待

(3)收到通知后,重新尝试获取锁,并在获取锁之后,继续向下执行。

这里的第一步释放锁,而在代码中,还没有加锁,所以就会出现异常。

因此,wait操作与synchronized操作需要搭配使用!!

对代码进行更改:

执行结果:

可以看出这里的wait进行了阻塞等待,阻塞在了synchronized代码块中,实际上这里的阻塞是已经释放了锁,此时的其他线程是可以获取到object这个对象的。

这里的阻塞就处于是WAITING状态。

notify()方法

notify方法是唤醒等待线程

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

如果多个线程等待,则会随机调度一个呈现wait状态的线程(无顺序)

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

代码示例:

public class ThreadD17 {
    //创建两个线程来阐述wait和notify的操作
    public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();
        Thread t1 = new Thread(() -> {
            System.out.println("wait之前");
            try {
                synchronized (obj) {
                    obj.wait();
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("wait之后");
        });

        Thread t2 = new Thread(() -> {
            System.out.println("notify之前");
            synchronized (obj) {
                obj.notify();
            }
            System.out.println("notify之后");
        });
        t1.start();
        Thread.sleep(1000);
        t2.start();

    }
}

运行结果:

此处的notify得和wait配对,如果二者使用的对象不同,则notify不会有·任何效果。notify只能唤醒在同一个对象上等待的线程。

notifyAll()方法

notifyAll与notify非常相似,多个线程wait的时候,notify随机唤醒一个,notifyAll将所有的线程都唤醒,这些线程继续进行锁竞争。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

西西¥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值