Java 线程安全(一)-- volatile

volatile 的存在,解决了不同内存间拷贝的同步问题,在每一次使用或者修改时候,都去原持有内存中去拿最新的状态,或者说可以这样理解,volatile 的修饰让线程放弃了使用各自内存的做法转而使用共享内存,从而保证了可靠性,但是降低了效率,所以说我们只在需要不同线程访问同一个值的时候才去打开这个限制

第一种情况

首先我们来写一个测试代码

public class Synchronized1Test {
    
    
    private boolean running = true;
    
    public void runTest() {
    
    
        new Thread() {
    
    
            @Override
            public void run() {
    
    
                while (running) {
    
    
                    System.out.println("线程开始执行,,,,");
                }
            }
        }.start();

        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        stop();
    }
    
    private void stop() {
    
    
        running = false;
    }
}

代码大致要表达的是,主线程在一秒之后要调用 stop 方法将 running = false,这样,子线程的死循环就会被打破,这样子线程停止执行,主线程也执行完毕,整个程序结束。

但是实际的执行结果是,子线程的死循环并不会结束,会一直不停的执行下去

这是因为线程直接做变量的值的修改并不是直接改的,而是先拷贝一份到自己线程的内存区域,更改完之后再适时同步给原持有此值的线程,这样做的目的是提高程序的运行效率

而在这里,主线程持有此变量,子线程在执行时候,将值拷贝了一份到自己的内存中,然后子线程并没有做值的修改,并不存在将值同步给主线程的过程,而此值的持有线程是主线程,主线程在更改完此值之后只会同步给自己,并不会同步给子线程,所以说子线程的值永远为最初拷贝走的值,永远为true,就永远不会停下来了。

有一个关键字 volatile 能解决这个问题

public class Synchronized1Test {
    
    
    private volatile boolean running = true;
    
	.....
}

volatile 的存在,解决了不同内存间拷贝的同步问题,在每一次使用或者修改时候,都去原持有内存中去拿最新的状态,或者说可以这样理解,volatile 的修饰让线程放弃了使用各自内存的做法转而使用共享内存,从而保证了可靠性,但是降低了效率,所以说我们只在需要不同线程访问同一个值的时候才去打开这个限制

再次运行,果然一秒结束

第二种情况

public class Synchronized2Test {
    
    
    
    private int x = 0;

    private void count() {
    
    
        x++;
    }

    public void runTest() {
    
    
        new Thread() {
    
    
            @Override
            public void run() {
    
    
                for (int i = 0; i < 1_000_000; i++) {
    
    
                    count();
                }
                System.out.println("x 在线程1的最终值" + x);
            }
        }.start();

        new Thread() {
    
    
            @Override
            public void run() {
    
    
                for (int i = 0; i < 1_000_000; i++) {
    
    
                    count();
                }
                System.out.println("x 在线程2的最终值" + x);
            }
        }.start();
    }
}

从代码来看,两个线程分别对同一个数进行一百万次从自增操作,无论谁最后执行完成,理论上来讲,总有一个线程打印出来的结果是两百万,但实际的运行效果,无论执行多少次都没有两百万的结果出来。

都是因为上文说的,值的同步性问题,在各自内存区域修改完后适时同步回去,导致的这个问题

然后我们加上 volatile 关键字

public class Synchronized2Test {
    
    
    private volatile int x = 0;
    
	.....
}

运行,还是不行,为啥啊,都加上了怎么还是不行呢?

这里主要的原因是因为 x++; 这个操作

在 Java 中, x++; 是两步操作,不是原子操作,是可拆的操作

他的运算过程大致相当于是


	int temp = x + 1;
	
	x = temp;
	

这样,在代码分为两行执行的时候,抢线程的操作就发生了,就出现了值异常的情况

到目前为止,线程安全除了加 volatile 关键字外,还需要保证会互相影响的操作合成一个原子操作

这里解释一下原子操作,简单来说,把多个步骤看成一个整体,要么你别执行我,要么就完全执行,不要在执行一半时候发生线程切换

这里引入我们保证一块代码块执行原子操作的关键字:synchronized

这时候我们改一下我们的代码,给 count 方法增加 synchronized 关键字,并且可以将 volatile 关键字去除了

public class Synchronized2Test {
    
    

    private int x = 0;

    private synchronized void count() {
    
    
        x++;
    }
    
    .....
}

运行,果然顺利的打印出来了两百万,多次运行也是这个结果

受保护的类型

到此,我们讲一下官方提供的原子型的类型,这里拿之前出现过的 AtomicInteger 举例

AtomicInteger 对标的就是 int,他是对 int 的包装,可以保证 int 具有同步性原子性

这里的同步性可以理解为上文提到的被 volatile 修饰,原子性可以理解为被 synchronized 修饰

例:


// 可以理解为默认值为 0 的 int
AtomicInteger count  = new AtomicInteger(0);

// 相当于是 ++count
count.incrementAndGet();

// 相当于是 count++
count.getAndIncrement();

代码中的 count.incrementAndGet() 相当于是 ++count

count.getAndIncrement() 相当于是 count++

除此之外还有

常规类型 保护类型
int AtomicInteger
boolean AtomicBoolean
int[] AtomicIntegerArray
T AtomicReference< T>

不一一列举

其中要说的就是 AtomicReference,他把传入的范型,自动帮我们做成了保护类型,使得在赋值和取值时候不出错

猜你喜欢

转载自blog.csdn.net/qq_35178391/article/details/126020180