java中同步的访问共享的可变数据

首先同步是什么呢?
同步即在多线程环境下保证对象的一致性,同时当一个线程进入到同步的代码块或者方法中的时候,都能够看到由同一个锁保护的前一个线程所有的修改效果。

java语言保证了读或者写一个变量是原子的,除了读写long、double类型的数据(为什么给读写long,double类型的数据时非原子的使用AtomicLong/Double读写long/double可以保证原子性,同时保证内存的可见性),因此当我们读取或者写入变量的时候,我们的操作无需进行同步,就可以保证操作的原子性,但是由于JMM(java memory model)的限制,下图为JMM的示意图:
在这里插入图片描述
图片来自:https://juejin.im/post/5d5df4d7518825661a3c1dfe
每一个线程在工作的时候都会从主存中拷贝一份变量存储在自己的工作内存中,因此一个线程对一个变量的更改对另一个线程来说不能保证是可见的,因此,即使是读写都为原子的情况下,为了在多线程环境下内存的可见性,我们也应该进行同步

  • 不进行同步多线程下访问共享变量的例子:

public class StopThread {
    
    
    private static boolean stopRequested;

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread backgroundThread = new Thread(() -> {
    
    
            int i = 0;
            while (!stopRequested) {
    
    
                i++;
            }
        });
        backgroundThread.start();
        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}

上面的代码在运行之后并没有停止,原因在于没有同步,一个线程对该变量的修改,另一个线程没有获取到。

  • 使用synchronized进行同步访问共享变量的例子
 public class StopThread {
    
    
    private static boolean stopRequested;

    private static synchronized void requestStop(){
    
    
        stopRequested=true;
    }

    private static synchronized boolean stopRequested(){
    
    
        return stopRequested;
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread backgroundThread=new Thread(() -> {
    
    
            int i=0;
            while (!stopRequested()){
    
    
                i++;
            }
        });
        backgroundThread.start();
        TimeUnit.SECONDS.sleep(1);
        requestStop();
    }
}

上面的代码在运行之后大约在一秒之左右停止了,这个符合我们的预期
但是使用synchronized进行同步,我们为什么要在两个方法上都加上synchronized呢,原因在于synchronized下面的两条保证内存可见的通信规则,其实在这里我们也仅仅利用了synchronized的通信规则,因为方法中的读取和写入操作java本身就保证了他是原子的。

Synchronized的两条规定保证了内存的可见性:

1、线程解锁前,必须把共享变量的最新值刷新到主内存中;
2、线程加锁时,讲清空工作内存中共享变量的值,从而使用共享变量是需要从主内存中重新读取最新的值(加锁与解锁需要统一把锁)

线程执行互斥锁代码的过程:
1.获得互斥锁
2.清空工作内存
3.从主内存拷贝最新变量副本到工作内存
4.执行代码块
5.将更改后的共享变量的值刷新到主内存中
6.释放互斥锁

http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#incorrectlySync

因此,如果我们只给一个加上synchronized进行同步的话,
只给stopRequested()加synchronized,那就没有办法保证requesttStop()将最新的值写入到主存中,如果没有写到主存中,那么stopRequested()没有办法获取到最新的值。
只给requestStop()加synchronized的话,没有办法保证stopRequested()得到的值是最新的。

如果仅需要实现线程间通信效果的话,我们可以使用volatile关键字保证内存的可见性,这样一个线程读取该变量的时候得到的就是该变量最新的值。

  • 使用volatile进行同步,多线程环境下访问共享变量
public class StopThread {
    
    
    private static volatile boolean stopRequested;

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread backgroundThread = new Thread(() -> {
    
    
            int i = 0;
            while (!stopRequested) {
    
    
                i++;
            }
        });
        backgroundThread.start();
        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}

上面的代码在运行1秒左右会自动停止,达到了我们预期的效果

但是在使用volatile进行同步的时候我们需要注意,volatile修饰的变量的++操作是非原子的因为++分为两步,首先读取旧值,然后进行加一,如果第二个线程在第一个线程读取旧值和写回新值之间读取变量,那么两个线程最后会得到相同的值。

那么如何避免上述的问题呢?
使用Atomic包下面的工具。

例子:使用AtomicLong在多线程环境下生成序列号:

    private static AtomicInteger nextSerialNumber = new AtomicInteger();

    public static long generateSerialNumber() {
    
    
        return nextSerialNumber.getAndIncrement();
    }

上面的代码可以在不加锁的情况下保证我们得到唯一的序列号。
我们使用java.util.concurrent.atomic包下面的AtomicLong类,该类在保证了在不加锁的情况下,可以在单个变量上进行线程安全的编程,也就是该类既保证了通信效果,又保证了操作的原子性(使用AtomicLong/Double读写long/double可以保证原子性,同时保证内存的可见性)

总之非常重要的是在多个线程共享可变数据的时候,应保证多线程同步。

上面是自己对Java同步访问共享可变数据的学习和理解。如果有错误请指正,欢迎大家多多交流。

猜你喜欢

转载自blog.csdn.net/liu_12345_liu/article/details/103102888
今日推荐