首先要明白线程的工作原理,jvm有一个main memory,而每个线程有自己的工作内存,一个线程对一个variable进行操作时,都要在自己的工作内存里面建立一个copy,操作完之后再写入主内存。多个线程同时操作同一个variable,就可能会出现不可预知的结果。
如果结果不一样则成为线程不安全,线程安全体现在结果上就是每次的结果都相同。其定义是:
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
如下图所示
右图中工作内存指的就是每个thread的工作区,线程完成对应的操作之后将结果写入到主内存中。
该如何保证呢?即如何保证原子性呢?
1、Atomic 包可以确保。
public class AtomicExample1 { // 请求总数 public static int clientTotal = 5000; // 同时并发执行的线程数 public static int threadTotal = 200; public static AtomicInteger count = new AtomicInteger(0); public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for (int i = 0; i < clientTotal ; i++) { executorService.execute(() -> { try { semaphore.acquire(); add(); semaphore.release(); } catch (Exception e) { log.error("exception", e); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); log.info("count:{}", count.get()); } private static void add() { count.incrementAndGet(); // count.getAndIncrement(); } } //incrementAndGet的底层方法(来自于unsafe.class文件) public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
以AtomicInteger为例,其是如何保证线程安全的呢?
答:调用了底层的compareAndSwapLong方法,简称CAS。其工作原理就是在while循环里面比较工作区内存中的值和主内存中的值是否相同,如果相同则进行运算。其中compareAndSwapLong的第一个参数是传进来的AtomicInteger对象,第二个参数是当前值,即工作区内存中的值,第三个参数是从主内存中取出来的变量值,如果两者一样则可进行加1操作。
-----------------------------------------update Start 2020年5月13日22:02:52----------------
事实上,zookeeper中在对某个node更新数据的时候也使用到了CAS理论:
对于值V,每次更新之前都会比较其值是否是预期值A,只有符合预期,才会将V原子化地更新到新值B。Zookeeper的setData接口中的version参数可以对应预期值,表明是针对哪个数据版本进行更新,假如一个客户端试图进行更新操作,它会携带上次获取到的version值进行更新,而如果这段时间内,Zookeeper服务器上该节点的数据已经被其他客户端更新,那么其数据版本也会相应更新,而客户端携带的version将无法匹配,无法更新成功,因此可以有效地避免分布式更新的并发问题。
----------------------------------------------END---------------------------------------------
当线程竞争激烈的时候可以使用
LongAdder。
和Atomic有关的几个类分别是:
1. AtomicReference 2. 3. AtomicLongFieldUpdater
二、synchronized
而用synchronized的关键是建是建立一个monitor,这个monitor可以是要修改的variable也可以其他你认为合适的object比如method,然后通过给这个monitor加锁来实现线程安全,每个线程在获得这个锁之后,要执行完load到workingmemory -> use&assign -> store到mainmemory 的过程,才会释放它得到的锁。这样就实现了所谓的线程安全。其是依赖于JVM的。
用该关键字修饰 代码块 和方法时作用于调用的对象(也就是说能保证一个对象的调用结果是顺序的), 修饰静态方法 和 类作用于所有对象。这种方式通常是不推荐的,因为该方式是通过同一时间只有一个线程的方式来访问资源实现线程安全的。
public class SynchronizedExample1 { // 修饰一个代码块 public void test1(int j) { synchronized (this) { for (int i = 0; i < 10; i++) { log.info("test1 {} - {}", j, i); } } } // 修饰一个方法 public synchronized void test2(int j) { for (int i = 0; i < 10; i++) { log.info("test2 {} - {}", j, i); } } // 修饰一个类 public static void test1(int j) { synchronized (SynchronizedExample2.class) { for (int i = 0; i < 10; i++) { log.info("test1 {} - {}", j, i); } } } // 修饰一个静态方法 public static synchronized void test2(int j) { for (int i = 0; i < 10; i++) { log.info("test2 {} - {}", j, i); } } public static void main(String[] args) { SynchronizedExample1 example1 = new SynchronizedExample1(); SynchronizedExample1 example2 = new SynchronizedExample1(); ExecutorService executorService = Executors.newCachedThreadPool(); //引入线程池是为了验证在多线程下被synchronized 修饰时是否能保证原子性。 executorService.execute(() -> { example1.test2(1); }); executorService.execute(() -> { example2.test2(2); }); } }
三、Lock
依赖cpu指令,用代码实现
三者比较
sync是不可中断锁,适合竞争不激烈的时候,automic适合竞争常态,比lock性能好,只能同步一个值。Lock可中断,多样化同步。
线程安全之可见性
一个线程对主内存的修改可以及时的被其他线程观察到。导致共享变量在线程间不可见的原因有
线程交叉执行
重排序集合线程交叉执行
共享变量鞥新后的值没有在工作内存和主内存之间及时更新。
synchronized的可见性
有两点要求:
1、线程解锁前,必须把共享变量的最新值刷新到主内存。
2、线程加锁时,将清空共享内存中的共享变量的值,从而使用共享变量时从主存中重新读取最新的值。
volatile如何保证可见性 ?
通过加入内存屏障和禁止重排序优化来实现的。
volatile虽然可以使用内存屏障保证线程可见性,但是并不能保证线程是安全的。
线程安全之有序性
happens-before,共有八条规则,只要两次的操作顺序能由这八条规则推导出来,则线程执行是有序的,否则JVM是可以对指令进行重排序的。
-----------------------------------------------------
举例 比如一个 ArrayList 类,在添加一个元素的时候,它可能会有两步来完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。
在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1; 而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。 那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。