volatile的用法

来源: java-synchronization-and-thread-safety-tutorial-with-examples

Java通过Thread类支持多线程访问。我们知道,从同一对象创建的多个线程共享该对象的成员变量,当多个线程读写共享的成员变量时可能导致 data inconsistency (数据不一致)。

数据不一致的原因是更新任何一个成员变量都不是原子操作,它需要三步:首先从成员变量读取当前值,然后进行必要的操作得到新的值,最后将新的值赋给成员变量。

看下面这个简单的例子,多线程更新共享的数据:

java
package com.journaldev.threads;

public class ThreadSafety {

    public static void main(String[] args) throws InterruptedException {

        ProcessingThread pt = new ProcessingThread();
        Thread t1 = new Thread(pt, "t1");
        t1.start();
        Thread t2 = new Thread(pt, "t2");
        t2.start();
        //wait for threads to finish processing
        t1.join();
        t2.join();
        System.out.println("Processing count="+pt.getCount());
    }

}

class ProcessingThread implements Runnable{
    private int count;

    @Override
    public void run() {
        for(int i=1; i< 5; i++){
            processSomething(i);
            count++;
        }
    }

    public int getCount() {
        return this.count;
    }

    private void processSomething(int i) {
        // processing some job
        try {
            Thread.sleep(i*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

上面程序中,count 在 for 循环中一共被执行4次自增操作,由于有两个线程,所以 count 在两个线程结束后应该为 8。但你运行几次后会发现,count 值并不总是8,而是在6、7和8之间变化。原因是,虽然 count++ 看起来像是原子操作,但实际上并不是,所以导致数据不一致。

Java线程安全

线程安全是指让程序安全地在多线程环境中执行,有不同的方式来实现线程安全。

  • 同步(synchronization)是Java中最简单的,也是使用最广泛的线程安全方法
  • 使用 java.util.concurrent.atomic 包中的原子包装类,比如 AtomicInteger
  • 使用 java.util.concurrent.locks 包中的锁
  • 使用线程安全的集合类,可以参考关于 ConcurrentHashMap 另一篇文章
  • 使用 volatile 关键字强制线程从主存中读数据,而不是从线程缓存中读数据

Java同步

同步(synchronization)用于保证线程安全,JVM保证同一时刻只有一个线程能执行同步的代码。Java中的 synchronized 关键字用于创建同步代码。synchronized 在内部给对象(Object)或类(Class)加锁来保证同一时刻只有一个线程执行同步代码。

  • Java同步通过对资源加锁和解锁来工作,在任何线程进入同步代码前,它会要求锁定对象。当线程执行完代码后,解除对象上的锁定,允许其它线程锁定对象。而在对象已经被某个线程锁定期间,其他线程会处于wait状态等待锁定这个对象
  • synchronized 关键字有两种使用方法,一是让整个方法成为同步方法,二是仅创建同块代码块
  • 同步方法会锁定 对象,静态的同步方法会锁定 ,所以最佳实践是使用同步代码块锁定方法中的确需要同步的那部分代码
  • 创建同步代码块时,需要提供需要被锁定的资源,它可以是 XYZ.class 或类中的任何成员对象
  • synchronized(this) 将在进入同步代码块中锁定当前对象
  • 应当使用最少的锁定(lowest level of locking),比如一个类中有多个同步块,其中一个同步块锁定对象,那么别的线程不能执行其他的同步块。锁定对象时,会锁定对象的所有成员变量
  • Java的同步通过损失性能的方式来保证数据完整性,所以应当仅在绝对必要时使用同步
  • Java的同步仅在同一JVM内起作用,所以如果要锁定跨多个JVM的资源,Java同步不起作用,你需要使用其它的全局锁定机制
  • Java的同步可能引起死锁,请参考这篇文章 deadlock in java and how to avoid them
  • Java的 synchronized 关键字不能用于构造方法或局部变量
  • 更好的做法是创建私有的Object对象专门用于同步代码块,这个对象引用不会被修改。比如,你将一个有 setter、可能被修改的成员变量用于同步,这个成员可能被修改,结果导致同步代码块可以同时被不同线程执行(错误!)
  • 不应使用常量池中的对象进行同步,比如千万不要使用String对象来进行同步,因为其他线程可能也锁定了同一个String对象,由于String对象可能来自常量池,结果两个完全不相关的线程却相互影响

通过如下修改可以保证前面的代码线程安全:

java
//dummy object variable for synchronization
private Object mutex=new Object();
...
//using synchronized block to read, increment and update count value synchronously
synchronized (mutex) {
        count++;
}

来看一个同步的例子。

java
public class MyObject {

  // Locks on the object's monitor
  public synchronized void doSomething() { 
    // ...
  }
}

// Hackers code
MyObject myObject = new MyObject();
synchronized (myObject) {
  while (true) {
    // Indefinitely delay myObject
    Thread.sleep(Integer.MAX_VALUE); 
  }
}

Hackers code尝试锁定 myObject 实例,并且一旦锁定后就不同释放锁,导致 doSomething() 方法会等待锁并一直阻塞,结果系统进入死锁,引起拒绝服务(DoS)。

java
public class MyObject {
  public Object lock = new Object();

  public void doSomething() {
    synchronized (lock) {
      // ...
    }
  }
}

//untrusted code

MyObject myObject = new MyObject();
//change the lock Object reference
myObject.lock = new Object();

注意,上面的代码中 lock 对象是 public 的,通过修改 lock,可以多线程执行同步代码。就算 lockprivate 的,但如果可通过 setter 修改,仍然会有同样问题。

java
public class MyObject {
  //locks on the class object's monitor
  public static synchronized void doSomething() { 
    // ...
  }
}

// hackers code
synchronized (MyObject.class) {
  while (true) {
    Thread.sleep(Integer.MAX_VALUE); // Indefinitely delay MyObject
  }
}

Hackers code会得到MyObject类的锁,且不释放,同样会导致死锁和拒绝服务(DoS)。

下面是另一个例子,多个线程遍历字符串数组,每处理完一个字符串,将线程名添加到该字符串后面。

java
package com.journaldev.threads;

import java.util.Arrays;

public class SyncronizedMethod {

    public static void main(String[] args) throws InterruptedException {
        String[] arr = {"1","2","3","4","5","6"};
        HashMapProcessor hmp = new HashMapProcessor(arr);
        Thread t1=new Thread(hmp, "t1");
        Thread t2=new Thread(hmp, "t2");
        Thread t3=new Thread(hmp, "t3");
        long start = System.currentTimeMillis();
        //start all the threads
        t1.start();t2.start();t3.start();
        //wait for threads to finish
        t1.join();t2.join();t3.join();
        System.out.println("Time taken= "+(System.currentTimeMillis()-start));
        //check the shared variable value now
        System.out.println(Arrays.asList(hmp.getMap()));
    }

}

class HashMapProcessor implements Runnable{

    private String[] strArr = null;

    public HashMapProcessor(String[] m){
        this.strArr=m;
    }

    public String[] getMap() {
        return strArr;
    }

    @Override
    public void run() {
        processArr(Thread.currentThread().getName());
    }

    private void processArr(String name) {
        for(int i=0; i<strArr.length; i++){
            //process data and append thread name
            processSomething(i);
            addThreadName(i, name);
        }
    }

    private void addThreadName(int i, String name) {
        strArr[i] = strArr[i] +":"+name;
    }

    private void processSomething(int index) {
        // processing some job
        try {
            Thread.sleep(index*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

上面程序的输出是

Time taken= 15005
[1:t2:t3, 2:t1, 3:t3, 4:t1:t3, 5:t2:t1, 6:t3]

字符串数组中的值不一致,原因是共享的数据未同步。修改 addThreadName() 方法保证线程安全,代码如下:

private Object lock = new Object();
private void addThreadName(int i, String name) {
    synchronized(lock){
    strArr[i] = strArr[i] +":"+name;
    }
}

修改后,程序输出正常:

Time taken= 15004
[1:t1:t2:t3, 2:t2:t1:t3, 3:t2:t3:t1, 4:t3:t2:t1, 5:t2:t1:t3, 6:t2:t1:t3]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要使用volatile关键字,只需要在声明变量时在其前面加上volatile修饰符即可。volatile关键字用于修饰变量,以确保每次读取该变量时都是从主内存中获取最新的值,而不是从本地线程缓存中获取。这样可以保证多个线程能够正确地看到该变量的最新值。 需要注意的是,volatile关键字只能保证变量的可见性,无法保证操作的原子性。如果需要保证操作的原子性,可以使用synchronized关键字或其他原子性操作类,如AtomicInteger。 以下是一个使用volatile关键字的示例代码: public class MyThread implements Runnable { private volatile boolean flag = false; public void run() { while (!flag) { // 执行某些操作 } } public void stop() { flag = true; } } 在上述代码中,flag变量被声明为volatile类型。在MyThread线程中,通过不断地检查flag的值来决定是否继续执行。当stop方法被调用时,将flag设置为true,从而使得MyThread线程能够感知到这个变化并退出循环。 总结起来,使用volatile关键字可以保证变量的可见性,但不能保证操作的原子性。对于需要保证操作的原子性的情况,可以考虑使用synchronized关键字或其他原子性操作类。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [volatile用法](https://blog.csdn.net/qq_31452291/article/details/119239182)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [volatile详解](https://blog.csdn.net/weixin_43899792/article/details/124492448)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值