Java并发编程(原子性、可见性、有序性、synchronized、CAS、volatile、ThreadLocal)笔记

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_37221991/article/details/95899417

自己网上总结了一些多线程并发的一些文章,如有错误请指教!

多线程的三大特性

一、原子性

原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。

线程切换带来的原子性问题

Java中的一条语句,在翻译为机器码之后,可能对应的是多个指令。

比如:**i++**这个操作至少需要3条指令;

  • 把 i 的值从内存=加载到寄存器;
  • 执行+1操作;
  • 把值写入内存;

假如 i=0,两个线程同时执行该操作,可能线程1执行完第一步,就切换到线程2执行,本来两个线程各执行一次后 i 的值应该为 2 ,此时就出现 两次递增操作后值为 1 的现象;

在 Java 中 synchronized 和在 lock、unlock 和一些concurrent包下提供了一些原子类(AtomicInteger、AtomicLong、AtomicReference等)中操作保证原子性。

二、可见性

JMM规定多线程之间的共享变量存储在主存中,每个线程单独拥有一个本地内存(逻辑概念),本地内存存储线程操作的共享变量副本;
在这里插入图片描述
可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。当线程1对共享变量A进行修改之后,线程2的工作内存中A可能还不是最新的值。这时候线程1的操作对线程2就不具有可见性。

缓存导致的可见性问题

Java内存模型规定所有的变量存储在主内存中。每个线程都有自己的工作内存,线程在工作内存中保存了使用到的主内存中变量的副本拷贝,线程对变量的操作必须在工作内存中进行,不能直接读写主内存中的变量。不同线程之间无法访问对方工作内存的变量。线程之间共享变量值的传递均需要通过主内存来完成。通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。

在 Java 中 volatile、synchronized 和 final 实现可见性。

三、有序性

Java程序中,如果在本线程中观察,所有的操作都是有序的;如果在另一个线程观察,所有的操作都是无序的。前半句指的是线程内表现为串行的语义,后半句指的是指令重排序和主内存和工作内存同步延迟的问题。

编译优化带来的有序性问题

为了充分利用处理器的性能,处理器会对输入的代码进行乱序执行。在计算之后将乱序执行的结果重组,并保证该结果和顺序执行的结果一致,但是并不保证程序中各个语句的计算顺序和输入代码的顺序一致。Java虚拟机也有类似的指令重排序优化。

比如:Object obj = new Object(),

这条语句对应的指令为:

  • 分配一块内存M;

  • 在M上初始化 Object 对象;

  • 将M的地址赋值给 obj;

计算机经过优化后可能先执行第三步,再第二步,如果执行完第三步后切换到别的线程,若此时访问该变量则会发生空指针异常;

Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性。

Java是如何保证三大特性的?

一、 如何保证操作的原子性

内置锁(同步关键字):synchronized;

显示锁:Lock;

阻塞的策略,性能不太好,但是由于操作上的优势,只需要简单的声明一下即可,而且被它声明的代码块也是具有操作的原子性。
最后需要注意的是synchronized是同步机制中最安全的一种方式,其他的任何方式都是有风险的,当然付出的代价也是最大的。

自旋锁:CAS(ABA问题);

Atomic

具体原理见 并发编程之ThreadLocal、Volatile、Synchronized、Atomic关键字

二、如何保证操作的可见性

1.volatile

class Example {
    private boolean stop = false;
    public void execute() {
        int i = 0;
        System.out.println("thread1 start loop.");
        while(!getStop()) {
            i++;
        }
        System.out.println("thread1 finish loop,i=" + i);
    }
    public boolean getStop() {
        return stop; // 对普通变量的读
    }
    public void setStop(boolean flag) {
        this.stop = flag; // 对普通变量的写
    }
}
public class VolatileExample {
    public static void main(String[] args) throws Exception {
        final Example example = new Example();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                example.execute();
            }
        });
        t1.start();

        Thread.sleep(1000);
        System.out.println("主线程即将置stop值为true...");
        example.setStop(true);
        System.out.println("主线程已将stop值为:" + example.getStop());
        System.out.println("主线程等待线程1执行完...");

        t1.join();
        System.out.println("线程1已执行完毕,整个流程结束...");
    }
}

上面程序的意思是:让线程1先执行然后主(main)线程修改标志看是否能让子线程跳出循环。执行程序后发现程序并没有执行完,而是在等待线程1执行完毕。这就说明主线程修改stop变量并不对线程1可见,所以普通变量是不保证可见性的。
在这里插入图片描述
当你把变量stop用volatile修饰时,主线程修改stop变量会立马对线程1可见并终止程序,这就证明volatile变量是具有可见性特性的。下面修改后的结果。
在这里插入图片描述
当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。

当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

以上面VolatileExample程序为例进行简单说明,当主线程对stop进行修改后且子线程尚未对stop进行读时,主线程已经把stop的值刷新到了主内存。其示意图如下:

在这里插入图片描述
当子线程进行读取时,会把本地内存置为无效直接去主内存中读取。(这里的主线程和子线程可以了解为两个普通线程没有父子关系)其示意图如下:
在这里插入图片描述

2.synchronized(加各种锁)实现可见性

JMM关于synchronized的两条规定:

1)线程解锁前,必须把共享变量的最新值刷新到主内存中

2)线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值

(注意:加锁与解锁需要是同一把锁)
通过以上两点,可以看到synchronized能够实现可见性。同时,由于synchronized具有同步锁,所以它也具有原子性

三、如何保证操作的有序性

1.volatile 是因为其本身包含“禁止指令重排序”的语义

2.synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。

四、ThreadLocal 实现了不可见性

ThreadLocal 与volatile相反,volatile是保证了线程之间内存的可见性,而ThreadLocal 是保证了内存之间的不可见性(可以理解为各个线程的成员变量都是自己new出来的)

ThreadLocal的设计,并不是解决资源共享的问题,而是用来提供线程内的局部变量,这样每个线程都自己管理自己的局部变量,别的线程操作的数据不会对我产生影响,互不影响,所以不存在解决资源共享这么一说,如果是解决资源共享,那么其它线程操作的结果必然我需要获取到,而ThreadLocal则是自己管理自己的,相当于封装在Thread内部了,供线程自己管理,这样做其实就是以空间换时间的方式(与synchronized相反),以耗费内存为代价,单大大减少了线程同步(如synchronized)所带来性能消耗以及减少了线程并发控制的复杂度。

ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程的上下文
一般使用ThreadLocal,官方建议我们定义为private static ,至于为什么要定义成静态的,这和内存泄露有关,以后再讨论。
它有三个暴露的方法,set、get、remove。

	public class TestThreadLocal {
	    private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
	        @Override
	        protected Integer initialValue() {
	            return 0;
	        }
	    };
	
	    public static void main(String[] args) {
	        for (int i = 0; i < 5; i++) {
	            new Thread(new MyThread(i)).start();
	        }
	    }
	
	    static class MyThread implements Runnable {
	        private int index;
	
	        public MyThread(int index) {
	            this.index = index;
	        }
	
	        public void run() {
	            System.out.println("线程" + index + "的初始value:" + value.get());
	            for (int i = 0; i < 10; i++) {
	                value.set(value.get() + i);
	            }
	            System.out.println("线程" + index + "的累加value:" + value.get());
	        }
	    }
	}

运行结果如下,这些ThreadLocal变量属于线程内部管理的,互不影响:

线程0的初始value:0
线程3的初始value:0
线程2的初始value:0
线程2的累加value:45
线程1的初始value:0
线程3的累加value:45
线程0的累加value:45
线程1的累加value:45
线程4的初始value:0
线程4的累加value:45

我在ThreadLocal上遇到的坑 ThreadLocal笔记

展开阅读全文

没有更多推荐了,返回首页