JUC教程

volatile关键字

大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。举个简单的例子,比如下面的这段代码:

i=i+1;

当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。本文我们以多核CPU为例。

比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?

可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。

最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。

也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。

为了解决缓存不一致性问题,通常来说有以下2种解决方法:

1)通过在总线加LOCK#锁的方式

2)通过缓存一致性协议

这2种方式都是硬件层面上提供的方式。

在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。

但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。

所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

原子变量与CAS算法

原子变量

java的自增操作并不是原子操作,i++的实际操作分为“读-改-写”
int i=10;
i=i++;
实际在计算机中是
int temp=i;//维护一个变量
i=i+1;
i=temp;
既然上面的操作不是原子,那么在多线程中是极其危险的(出现数据安全问题),如何保证数据原子性?
volatile修饰符只能解决内存可见性问题(主存修改变量时,会让线程的缓冲区的维护的变量失效,这时候线程会从主存中取值),
并不能解决原子性(多线程访问数据时,A线程获取变量计算后,并没有赋值给变量,被B线程抢到执行权,这时候B获取的变量还是原先的,并且计算完成后赋值给变量,而A线程获得执行权,直接赋值给变量,导致数据出现重复)问题,要解决这个问题,JUC(java.util.concurrent.atomic)为我们提供了一些原子变量(对应的就是基本类型的操作)。

 private AtomicInteger serialNumber = new AtomicInteger();//创建具有初始值 0 的新 AtomicInteger。
getAndIncrement()
           以原子方式将当前值加 1。相当于i++,并且保证数据安全性
compareAndSet(int expect, int update) 
          如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。
decrementAndGet() 
          以原子方式将当前值减 1。

CAS算法

java.util.concurrent.atomic原理就是使用了CAS算法,CAS算法是硬件对于并发操作的支持,其中包含了三个操作数:内存值,预估值和更新值。
每当要执行更新操作时,会先在同步方法中比较内存值和预估值是否相等,如果相等才会用更新值替换内存值,否则什么也不做。 

模拟CAS算法:

public class TestCompareAndSwap {
	private int value;
	//获取内存值
	public synchronized int get(){
		return value;
	}
	//对照,expecteValue:预估值和内存值比较,如果一样,就更改,不一样就什么也不做。
	//无论更新或失败,都返回旧值
	public synchronized int compareAndSwap(int expecteValue,int newValue){
		//内存值
		int odlValue=value;
		if(odlValue==expecteValue){
			this.value=newValue;
		}
		return odlValue;
	}
	//设置
	public synchronized boolean compareAndSet(int expecteValue,int newValue){
		//调用compareAndSwap()返回true说明一样,修改成功,false说明修改不成功。
		return expecteValue==compareAndSwap(expecteValue,newValue);
	}
}



synchronized对比cas算法


这是一种无锁的写法,很明显,这东西不安全,出现重复数值

public class youhua {
	public static void main(String[] args) {
		MyRunnable my = new MyRunnable();
		for (int i = 0; i < 10; i++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					for (int i = 0; i <50; i++) {
						System.out.println(Thread.currentThread().getName() + "值为:"+my.increment());
					}
				}
			}).start();
			;
		}
	}
}
class MyRunnable {
	private long value = 0;

	public long getValue() {
		return value;
	}
	public long increment() {
		value = value + 1;
		return value;
	}
}

《JUC教程》

很明显我们需要来吧锁
synchronized 

public class syn {
	public static void main(String[] args) {
		MyRunnable2 my = new MyRunnable2();
		long time1 = System.currentTimeMillis();
		List<Thread> threads = new ArrayList<Thread>();
		for (int i = 0; i < 10; i++) {
			Thread t = new Thread(new Runnable() {
				@Override
				public void run() {
					for (int i = 0; i < 11150; i++) {
					System.out.println(Thread.currentThread() + "值为:" + my.increment());
					}
				}
			});
			t.start();
			threads.add(t);
		}
		for (Thread t : threads) {
			try {
				t.join();//等待该线程终结
			} catch (InterruptedException e) {
				e.printStackTrace();
			} // 用join()等待所有的线程。先后顺序无所谓,当这段执行完,肯定所有线程都结束了。
		}
		long time2 = System.currentTimeMillis();

		System.out.println(time2 - time1);
	}
}

class MyRunnable2 {
	private long value = 0;

	public synchronized long getValue() {
		return value;
	}

	public synchronized long increment() {
		value = value + 1;
		return value;
	}
}

cas

public class youhua {
	public static void main(String[] args) {
		MyRunnable my = new MyRunnable();
		long time1 = System.currentTimeMillis();
		List<Thread> threads = new ArrayList<Thread>();
		for (int i = 0; i <1; i++) {
			Thread t = new Thread(new Runnable() {
				@Override
				public void run() {
					for (int i = 0; i < 10000; i++) {
					System.out.println(Thread.currentThread() + "值为:" + my.increment());
					}
				}
			});
			t.start();
			threads.add(t);
		}
		for (Thread t : threads) {
			try {
				t.join();//等待该线程终结
			} catch (InterruptedException e) {
				e.printStackTrace();
			} // 用join()等待所有的线程。先后顺序无所谓,当这段执行完,肯定所有线程都结束了。
		}
		long time2 = System.currentTimeMillis();

		System.out.println(time2 - time1);
	}
}
class MyRunnable {
	 private static AtomicLong value=new AtomicLong(0);
	public long increment() {
		return value.incrementAndGet();
	}
}

本人测试了下,感觉两种方式没啥区别,但是网上一些文章测试说cas算法比
synchronized 要快许多,但是上面代码我运行后,实际没啥区别,反而cas有时候还没synchronized 快。如果有这方面懂的人,请告诉我一下。


同步类:ConcurrentHashMap(遍历时用的多,写入多慎用)

传统的HashTable,多线程安全,因为,线程在访问HashTable从并行变为串行。一个线程只能进入后,其他线程不可进入,串行的效率极其底下,多个线程排队等待着操作一个变量是不能忍的。

而ConcurrentHashMap采用“锁分段”机制(默认把数据分为16段,每个段都有独立的锁,这就实现了并行的效果,不仅线程安全了,而且效率也高了),jdk1.8对ConcurrentHashMap又进行了升级,采用CAS机制。

HashTable:线程安全(相当于锁表)

ConcurrentHashMap:线程安全,可以实现并行(相当于锁行)


CopyOnWriterArrayList优于同步的ArrayList()

  CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

CountDownLatch(闭锁,在完成某些运算时,只有其他所有线程运算全部完成,当前运算才继续执行)

引用api的话

一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。

用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。  

应用场景:
计算5个线程总运行时间,我们知道main方法的代码和线程不是同步的,所以计算不出耗费时间。但是通过一下方式可以
countDownLatch conut=new countDownLatch(5);//初始化5个数
count.countDown();//每个线程中都加上这个代码,把初始化5减1(只有允许完后,才会返回1)
…..(这时候count为0,表示5个线程都占用了)
注意count.countDown()写在finaly(){
}代码块中最好
count.await();//等待,5个线程都运行完了,再执行下面代码(相当于阻塞)

Callable接口(实现线程方式一共有4种)

Callable 接口类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。
但是 Runnable 不会返回结果,并且无法抛出经过检查的异常。 

public class TestCallable {
public static void main(String[] args) throws InterruptedException, ExecutionException {
	ThreadDemo3 td=new ThreadDemo3();
	//执行callable方式,需要futureTask实现类的支持,用于接受运算结果。
	//FutureTask是Future接口的实现类
	FutureTask<Integer> result=new FutureTask<>(td);
	new Thread(result).start();
	//接受线程运算结果
	Integer i = result.get();//注意这个方法当上面线程执行完了,才继续走下去(闭锁)。
	System.out.println(i);
}
}
class ThreadDemo3 implements Callable<Integer>{

	@Override
	public Integer call() throws Exception {
		int sum=0;
		sum+=10;
		return sum;
	}
}

使用场景,main线程和Callable线程并行做运算,两处结果再进行计算,以前是串行计算,现在变为并行计算,并且线程可以返回值。哦了

接口 Lock

public interface Lock

Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。

锁是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问。一次只能有一个线程获得锁,对共享资源的所有访问都需要首先获得锁。不过,某些锁可能允许对共享资源并发访问,如 ReadWriteLock 的读取锁。

synchronized 方法或语句的使用提供了对与每个对象相关的隐式监视器锁的访问,但却强制所有锁获取和释放均要出现在一个块结构中:当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的词法范围内释放所有锁。

虽然 synchronized 方法和语句的范围机制使得使用监视器锁编程方便了很多,而且还帮助避免了很多涉及到锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。例如,某些遍历并发访问的数据结果的算法要求使用 “hand-over-hand” 或 “chain locking”:获取节点 A 的锁,然后再获取节点 B 的锁,然后释放 A 并获取 C,然后释放 B 并获取 D,依此类推。Lock 接口的实现允许锁在不同的作用范围内获取和释放,并允许以任何顺序获取和释放多个锁,从而支持使用这种技术。

随着灵活性的增加,也带来了更多的责任。不使用块结构锁就失去了使用 synchronized 方法和语句时会出现的锁自动释放功能。在大多数情况下,应该使用以下语句:

     Lock l = ...; 
     l.lock();
     try {
         // access the resource protected by this lock
     } finally {
         l.unlock();
     }
 

锁定和取消锁定出现在不同作用范围中时,必须谨慎地确保保持锁定时所执行的所有代码用 try-finally 或 try-catch 加以保护,以确保在必要时释放锁。 

以后是官方话语可以不看
Lock是一个显示锁,通过lock()方法上锁,必须通过unlock()方法进行释放锁
虽然Lock有好处,但是存在一定的风险,因为unlock()如果没有得到释放(所以放在finaly代码块中),就会有很大危险(一直锁,不释放),所以说不能完全替代synchronized:隐式锁



    原文作者:JUC
    原文地址: https://blog.csdn.net/qq_33811090/article/details/78719655
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞