多线程(5)

1.接着介绍内存可见性问题引起的线程不安全问题,首先观察下面的代码:

package Demo5;

import java.util.Scanner;

public class Demo1{
    public static int count=0;
   public static void main(String[] args){
       Thread t1=new Thread(()->{
          while(count==0){
              ;
          }
           System.out.println("线程t1执行结束");
       });
       Thread t2=new Thread(()->{
          Scanner scanner=new Scanner(System.in);
           System.out.println("请输入一个整数");
           count=scanner.nextInt();
       });
    t1.start();
    t2.start();
   }
}

看起来开始执行后,当从键盘上输入一个数字之后,t1,t2线程就会结束.但是实际运行起来却不是这样的如下:


 输入整数之后程序依然在执行,并没有按照预期线程执行完毕,原因是因为"内存可见性的问题".上述的线程t1是从内存中读取数据,t2是写入数据.t1的执行可以大体分为两步,从内存中读取count的值,然后比较,条件成立顺序执行,条件不成立,就跳转到另一个地址执行.当前循环旋转速度很快,短时间内会出现大量的读取和比较操作,由于读取操作是再内存中进行,而比较操作是在寄存器中进行,因此比较操作是比读取操作快几个数量级的.此外,在执行过程中,JVM还发现count的值在未输入之前是不变的.于是JVM就把读取count的操作给优化了,只是第一次真正执行读取操作,后续再执行到相应代码的时候,就不是真正的读count操作了,而是读刚才已经读取过在寄存器中的值了.可以尝试这样修改代码:

package Demo5;

import java.util.Scanner;

public class Demo1{
    public static int count=0;
   public static void main(String[] args){
       Thread t1=new Thread(()->{
          while(count==0){
              System.out.println("t1");
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  throw new RuntimeException(e);
              }
          }
           System.out.println("线程t1执行结束");
       });
       Thread t2=new Thread(()->{
          Scanner scanner=new Scanner(System.in);
           System.out.println("请输入一个整数");
           count=scanner.nextInt();
           System.out.println("线程t2执行结束");
       });
       t2.start();
    t1.start();
   // t2.start();
   }
}

在while()循环中加上一些操作,是循环的旋转速度大幅度降低,这样一来读取count的操作相比比较之后循环体中的代码执行时间就快很多了,也就没有优化的必要了.另外,由于IO操作是不能被优化的,IO操作反复执行的结果是不同的.

总结:上述问题的本质上是编译器的优化引起的,优化调读取count操作之后,使线程t2对count的修改没有被t1线程感知到,即内存可见性问题.那么如何解决上述问题呢?可以通过volatile关键字来使count不被编译器优化.代码如下:

package Demo5;

import java.util.Scanner;

public class Demo2 {
    public volatile static int count=0;
    public static void main(String[] args){
        Thread t1=new Thread(()->{
           while(count==0){
               ;
           }
            System.out.println("线程t1执行结束");
        });
        Thread t2=new Thread(()->{
           Scanner scanner=new Scanner(System.in);
            System.out.println("请输入整数");
            count=scanner.nextInt();
            System.out.println("线程t2执行结束");
        });
        t1.start();
        t2.start();
    }
}

2.线程的等待通知机制

前面介绍的join是线程的等待结束,这里的等待通知机制是等待代码中给我们进行显示的通知后重新等待调度执行.系统内部,线程是抢占式执行的,随即调度.但是可以通过"等待的方式",让线程一定程度上按照预期的顺序执行.无法主动让某个线程被调度,但是可以主动让某个线程等待这样就给别的线程机会去在CPU上执行了.由于线程在CPU上的调度是随机的,有可能出现某个线程频繁的获取和释放锁,由于获取释放的太快导致其他的CPU无法在CPU上执行.这种状况称为"线程饿死",为了防止这种状况的出现,可以利用线程的等待通知机制,通过条件,判定看当前逻辑是否能够执行,如果不能执行就主动的wait,就把执行的机会让给别线程了,避免该线程一直进行一些无意义的重复.等到后面可以执行了,让其他线程通知,使阻塞的线程被唤醒. 代码如下:

package Demo5;

public class Demo3 {
    public static void main(String[] args){
        Object object=new Object();
        Thread t1=new Thread(()->{
            synchronized(object){
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        //wait内部做的事情不仅仅是阻塞等待,还要解锁.
        //因此wait必须要和synchronized搭配使用.
        t1.start();
    }
}

首先wait是Object提供的方法,任何一个对象都有这个方法.调用wait方法时,首先会接解除调用该方法的线程的锁,然后阻塞等待.因此得先加上锁才能释放,所以wait必须要在synchronized() 的内部使用.当wait解除锁之后,其他线程是能够获取到object这个锁的.

 可以看到t1线程处于等待状态.可以通过另一个线程调用 notify来唤醒阻塞的线程.

package Demo5;

import java.util.Scanner;

public class Demo6 {
    public static void main(String[] args){
        Object object=new Object();
        Thread t1=new Thread(()->{
            System.out.println("t1将要开始等待");
            synchronized (object){
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t1等待结束");
        });
        Thread t2=new Thread(()->{
            System.out.println("t2将要开始通知");
            synchronized(object){
                Scanner scanner=new Scanner(System.in);
                int x=scanner.nextInt();
                object.notify();
                System.out.println("t2通知之后");
            }
        });
        t1.start();
        t2.start();
    }
}

 上述代码,线程t1先启动打印"t1将要开始等待",然后t2开始启动,由于锁竞争的原因,t2会阻塞等待一会.接着t1中的object.wait()会释放object锁,然后object锁在t2加上.在输入数字之后,object.notify()会唤醒线程t1的wait操作.从而使t1能够回到runnable状态,从而被调度执行.但是由于t1和t2的执行顺序是不固定的,有可能是t2先执行,先t2中的notify(),由于此时t1还没有执行wait操作.notify()不会有任何效果(也不会抛出异常).但是后续t1进入wait之后,就没有人能够唤醒了.

 


 

 


 


 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值