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算法
原子变量
private AtomicInteger serialNumber = new AtomicInteger();//创建具有初始值 0
的新 AtomicInteger。
getAndIncrement()
以原子方式将当前值加 1。相当于i++,并且保证数据安全性
compareAndSet(int expect, int update)
如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。
decrementAndGet()
以原子方式将当前值减 1。
模拟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);
}
}
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;
}
}
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
。
Callable接口(实现线程方式一共有4种)
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 加以保护,以确保在必要时释放锁。