Java并发编程(一)

前言

今天进入Java多线程系列的内容,首先我们看这样一个例子。

public class Demo {
  private int num=0;
  public void increase(){
    num++;
  }
  public int getNum(){
    return this.num;
  }
  public static void main( String[] args ){
  	Demo demo = new Demo();
  	for( int i=0; i<10000; i++ ){
  	  demo.increase();
  	}
  	System.out.println(demo.getNum());	
  }
}
// 输出结果是 10000

看起来没有问题,然后现在我们因为效率太低,需要多线程来提高效率,这个时候我们的代码如下:

public class Demo {
    private int num=0;
    public void increase( ){
      this.num = num+1;
    }
    public int getNum(){
      return this.num;
    }
    public static void main( String[] args ) throws Exception {
        final Demo demo = new Demo();
        for( int i=0; i<1000; i++ ){
          Thread t = new Thread(){
          public void run(){
            for( int j=0; j<1000; j++ ){
              demo.increase();
            }
          }
          }; 
          t.start();
        }
        while(Thread.activeCount()>1){
            Thread.yield();
        }
        System.out.println(demo.getNum());	
    } 
  }
输出结果:968681

可以看到用1000个线程执行,每个线程执行1000次,结果却不是1000000,那么问题出在哪里呢。那么就引出了今天的主题——并发编程的特性。

问题分析

我们首先要了解一下JVM的基础知识,内存结构。
在这里插入图片描述
这里可以看到,cpu为了提高速度,都有自己的高速缓存,这就导致,每个线程执行操作的都是变量副本,这就出现了不同线程之前“变量不可见”。如果副本跟主存之间同步不及时,就会出现覆盖的问题,最终导致结果比预期低。

volatile关键字

针对这个问题,为了支持多线程JVM提供了 volatile 关键字。这个关键字可以保证缓存中的变量及时刷新到主存中,并且其他使用这边变量的缓存失效。保证了内存可见性。
所以我们在num前面加上volatile关键字看看结果。

public class Demo {
    private volatile int num=0;
    public void increase( ){
      this.num = num+1;
    }
    public int getNum(){
      return this.num;
    }
    public static void main( String[] args ) throws Exception {
        final Demo demo = new Demo();
        for( int i=0; i<1000; i++ ){
          Thread t = new Thread(){
          public void run(){
            for( int j=0; j<1000; j++ ){
              demo.increase();
            }
          }
          }; 
          t.start();
        }
        while(Thread.activeCount()>1){
            Thread.yield();
        }
        System.out.println(demo.getNum());	
    } 
  }
  // 输出结果:982232

原子操作类——java.concurrent.Atomic.*

我们发现结果有上升,但是还是错误的输出结果,那这是怎么回事呢。我们继续分析,既然已经满足了内存可见性,那么问题只能出在计算的时候。联想到 this.num = num+1; 这个操作需要三步,
1、读取num的值;2、计算num+1的值;3、将结果覆盖num的值。如果程序在这个过程中间失去时间片,而其他线程就修改了num的值,这就导致结果覆盖问题了。这就引出了并发编程的另外一个特性——原子性。
为了应对原子性的问题,java提供了专门的包来保证原子性。代码如下:

public class Demo {
    private volatile AtomicInteger num=new AtomicInteger(0);
    public void increase( ){
      num.incrementAndGet();
    }
    public int getNum(){
      return this.num.intValue();
    }
    public static void main( String[] args ) throws Exception {
        final Demo demo = new Demo();
        for( int i=0; i<1000; i++ ){
          Thread t = new Thread(){
          public void run(){
            for( int j=0; j<1000; j++ ){
              demo.increase();
            }
          }
          }; 
          t.start();
        }
        while(Thread.activeCount()>1){
            Thread.yield();
        }
        System.out.println(demo.getNum());	
    } 
  }

同步锁——synchronized

到此似乎这个问题得到了解决,那么还有没有其他方式解决原子性的问题呢,其实上面出问题主要是因为执行 this.num = num+1的过程中,其他线程访问了increase方法,我们还可以通过java提供的synchronized 关键字来保证这段代码不能同时有线程访问。代码如下

public class Demo {
    private volatile int num=0;
    public synchronized void increase( ){
      this.num = num+1;
    }
    public int getNum(){
      return this.num;
    }
    public static void main( String[] args ) throws Exception {
        final Demo demo = new Demo();
        for( int i=0; i<1000; i++ ){
          Thread t = new Thread(){
          public void run(){
            for( int j=0; j<1000; j++ ){
              demo.increase();
            }
          }
          }; 
          t.start();
        }
        while(Thread.activeCount()>1){
            Thread.yield();
        }
        System.out.println(demo.getNum());	
    } 
  }
  //输出结果:1000000

JVM重排序

上面解决了可见性和原子性的问题,但是在运行的过程中又出现这样一个问题。

class Singleton {
    private static Singleton instance;

    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

上面代码运行发现有时候返回的instance类不正确,不能正常使用。我们分析上面代码,synchronized 保证了同时只有一个线程执行这里的创建操作。所以是满足原子性的,同时synchronized 包裹的变量也会实时刷新到主存中,在这里也满足可见性,(注意只是在这个例子里面,后面我会讲解)。那么为什么会出现问题呢。这里就涉及到JVM的指令重排了。

这个例子中,getInstance() 方法使用了双重检查锁定模式(DCL)来实现单例。这种方式的目的是为了减少同步开销,只有当 instance 为 null 时,才会进入同步代码块。

然而,这个例子在多线程环境下可能导致问题,原因在于 instance = new Singleton() 这一行。这里的操作实际上可以分为以下三个步骤:

分配内存空间。
初始化 Singleton 对象。
将 instance 指向分配的内存空间。
由于JVM允许指令重排,步骤2和步骤3可能会发生重排。这将导致以下情况:

线程A执行到 instance = new Singleton(),并完成了内存分配和指令重排,但还没有完成对象初始化。
线程B执行到 if (instance == null),发现instance已经指向了一个内存地址,因此不为null。此时线程B返回了一个未完成初始化的 Singleton 对象。

为了解决这个问题,JVM在volatile 关键字也添加了禁止指令重排的能力,具体的实现原理后面会讲。

总结

通过以上几个例子,我们发现并发编程有三大特性,分别为,
原子性,可见性,顺序性, 以及解决他们的简单方法,后面还会详细他们解决的原理和作用边界。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值