1、atomic包
atomic下运用了CAS的AtomicBoolean、AtomicInteger、AtomicReference等原子变量类
AtomicInteger count = new AtomicInteger();
//获取值
count.get();
//自增
count.incrementAndGet();
CAS
CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:
-
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
- 比较 A 与 V 是否相等。(比较)
- 如果比较相等,将 B 写入 V。(交换)
- 返回操作是否成功。
总结一下 JAVA 的 cas 是怎么实现的:
- java 的 cas 利用的的是 unsafe 这个类提供的 cas 操作。
- unsafe 的cas 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg
- Atomic::cmpxchg 的实现使用了汇编的 cas 操作,并使用 cpu 硬件提供的 lock信号保证其原子性
CAS中的ABA问题
如果一开始位置V得到的旧值是A,当进行赋值操作时再次读取发现仍然是A,并不能说明变量没有被其它线程改变过。有可能是其它线程将变量改为了B,后来又改回了A。
解决ABA问题
-
在变量前面追加版本号:每次变量更新就把版本号加1,则A-B-A就变成1A-2B-3A。
-
atomic包下的AtomicStampedReference类
- 第一个参数expectedReference:表示预期值。
- 第二个参数newReference:表示要更新的值。
- 第三个参数expectedStamp:表示预期的时间戳。
- 第四个参数newStamp:表示要更新的时间戳。
public class Main {
public static void main(String[] args) {
AtomicStampedReference<Integer> reference = new AtomicStampedReference<Integer>(10,1);
reference.compareAndSet(10,11,reference.getStamp(),reference.getStamp()+1);
System.out.println(reference.getReference());//输出11
reference.compareAndSet(11,12,1,reference.getStamp()+1);
System.out.println(reference.getReference());//修改失败,输出11
}
}
作者:keyuan0214 链接:https://www.jianshu.com/p/6d0f135a89cd 来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2、locks包
AbstractQueuedSynchronizer(AQS)
AQS就是一个并发包的基础组件,用来实现各种锁,各种同步组件的。它包含了state变量、加锁线程、等待队列等并发中的核心组件。
看一下ReentrantLock和AQS之间的关系。
我们看上图,说白了,ReentrantLock内部包含了一个AQS对象,也就是AbstractQueuedSynchronizer类型的对象。
这个AQS对象就是ReentrantLock可以实现加锁和释放锁的关键性的核心组件。
ReentrantLock加锁和释放锁的底层原理
好了,现在如果有一个线程过来尝试用ReentrantLock的lock()方法进行加锁,会发生什么事情?
很简单,这个AQS对象内部有一个核心的变量叫做state,是int类型的,代表了加锁的状态。
初始状态下,这个state的值是0。
另外,这个AQS内部还有一个关键变量,用来记录当前加锁的是哪个线程,初始化状态下,这个变量是null。
接着线程跑过来调用ReentrantLock的lock()方法尝试进行加锁,这个加锁的过程,直接就是用CAS操作将state值从0变为1。
(关于CAS,之前专门有文章做过详细阐述,大家可以自行阅读了解)
如果之前没人加过锁,那么state的值肯定是0,此时线程1就可以加锁成功。
一旦线程1加锁成功了之后,就可以设置当前加锁线程是自己。所以大家看下面的图,就是线程1跑过来加锁的一个过程。
其实看到这儿,大家应该对所谓的AQS有感觉了。说白了,就是并发包里的一个核心组件,里面有state变量、加锁线程变量等核心的东西,维护了加锁状态。
你会发现,ReentrantLock这种东西只是一个外层的API,内核中的锁机制实现都是依赖AQS组件的。
这个ReentrantLock之所以用Reentrant打头,意思就是他是一个可重入锁。
可重入锁的意思,就是你可以对一个ReentrantLock对象多次执行lock()加锁和unlock()释放锁,也就是可以对一个锁加多次,叫做可重入加锁。
大家看明白了那个state变量之后,就知道了如何进行可重入加锁!
其实每次线程1可重入加锁一次,会判断一下当前加锁线程就是自己,那么他自己就可以可重入多次加锁,每次加锁就是把state的值给累加1,别的没啥变化。
接着,如果线程1加锁了之后,线程2跑过来加锁会怎么样呢?
我们来看看锁的互斥是如何实现的?
线程2跑过来一下看到,哎呀!state的值不是0啊?所以CAS操作将state从0变为1的过程会失败,因为state的值当前为1,说明已经有人加锁了!
接着线程2会看一下,是不是自己之前加的锁啊?当然不是了,“加锁线程”这个变量明确记录了是线程1占用了这个锁,所以线程2此时就是加锁失败。
给大家来一张图,一起来感受一下这个过程:
接着,线程2会将自己放入AQS中的一个等待队列,因为自己尝试加锁失败了,此时就要将自己放入队列中来等待,等待线程1释放锁之后,自己就可以重新尝试加锁了
所以大家可以看到,AQS是如此的核心!AQS内部还有一个等待队列,专门放那些加锁失败的线程!
同样,给大家来一张图,一起感受一下:
接着,线程1在执行完自己的业务逻辑代码之后,就会释放锁!他释放锁的过程非常的简单,就是将AQS内的state变量的值递减1,如果state值为0,则彻底释放锁,会将“加锁线程”变量也设置为null!
整个过程,参见下图:
接下来,会从等待队列的队头唤醒线程2重新尝试加锁。
好!线程2现在就重新尝试加锁,这时还是用CAS操作将state从0变为1,此时就会成功,成功之后代表加锁成功,就会将state设置为1。
此外,还要把“加锁线程”设置为线程2自己,同时线程2自己就从等待队列中出队了。
最后再来一张图,大家来看看这个过程。
作者:石杉的架构笔记
来源:慕课网
本文原创发布于慕课网 ,转载请注明出处,谢谢合作
链接:http://www.imooc.com/article/293135
ReentantLock
public class TestReentrantLock {
// true为公平锁,false为非公平锁,默认为false
private static ReentrantLock lock = new ReentrantLock(true);
void run() {
for (int i = 0; i < 100; i++) {
lock.lock();
System.out.println(Thread.currentThread().getName() + "---");
lock.unlock();
}
}
public static void main(String[] args) {
TestReentrantLock t = new TestReentrantLock();
Thread t1 = new Thread(t::run);
Thread t2 = new Thread(t::run);
t1.start();
t2.start();
}
}
Synchronized 和 lock区别如下:
-
来源:
lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现; -
异常是否释放锁:
synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。) -
是否响应中断
lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断; -
是否知道获取锁
Lock可以通过trylock来知道有没有获取锁,而synchronized不能; -
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。 所以说,在具体使用时要根据适当情况选择。
-
用法
synchronized可以加在方法上,也可以加在特定代码块中;lock一般加在代码块上 -
锁类型
Lock是通过CAS实现的乐观锁,而我synchronized则是使用操作系统互斥量实现的悲观锁
ReentrantReadWriteLock
-
ReetrantReadWriteLock读写锁的实现中,读锁使用共享模式;写锁使用独占模式,换句话说,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的
-
ReetrantReadWriteLock读写锁的实现中,需要注意的,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁
-
读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容,读写锁之间为互斥
-
ReentrantReadWriteLock支持锁降级,不支持锁升级(同一个线程中,在没有释放读锁的情况下,就去申请写锁)
3、CountDownLatch
这个类是一个同步计数器,主要用于线程间的控制,当CountDownLatch的count计数>0时,await()会造成阻塞,直到count变为0,await()结束阻塞,使用countDown()会让count减1。CountDownLatch的构造函数可以设置count值,当count=1时,它的作用类似于wait()和notify()的作用。如果我想让其他线程执行完指定程序,其他所有程序都执行结束后我再执行,这时可以用CountDownLatch,但计数无法被重置,如果需要重置计数,请考虑使用 CyclicBarrier 。
public class TestCountdownLatch {
public static void main(String[] args) {
usingCountDownLatch();
}
private static void usingCountDownLatch() {
Thread[] threads = new Thread[100];
CountDownLatch latch = new CountDownLatch(threads.length);
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
int result = 0;
for (int j = 0; j < 10000; j++) result += j;
latch.countDown();
});
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end latch");
}
}
4、CyclicBarrier
该类从字面理解为循环屏障,它可以协同多个线程,让多个线程在这个屏障前等到,直到所有线程都到达了这个屏障时,再一起执行后面的操作。假如每个线程各有一个await,任何一个线程运行到await方法时就阻塞,直到最后一个线程运行到await时才同时返回。和之前的CountDownLatch相比,它只有await方法,而CountDownLatch是使用countDown()方法将计数器减到0,它创建的参数就是countDown的数量;CyclicBarrier创建时的int参数是await的数量。 barrie在释放等待线程后可以重用,所以称它为循环 的barrier
public class TestCyclicBarrier {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(20, () -> {
System.out.println(Thread.currentThread().getName() + ":完成最后任务");
});
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "到达");
Thread.sleep(100);
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
5、Semaphore
该类用于控制信号量的个数,构造时传入个数。总数就是控制并发的数量。假如是5,程序执行前用acquire()方法获得信号,则可用信号变为4,程序执行完通过release()方法归还信号量,可用信号又变为5.如果可用信号为0,acquire就会造成阻塞,等待release释放信号。acquire和release方法可以不在同一个线程使用。Semaphore实现的功能就类似厕所有5个坑,假如有10个人要上厕所,那么同时只能有多少个人去上厕所呢?同时只能有5个人能够占用,当5个人中 的任何一个人让开后,其中等待的另外5个人中又有一个人可以占用了。另外等待的5个人中可以是随机获得优先机会,也可以是按照先来后到的顺序获得机会,这取决于构造Semaphore对象时传入的参数选项。单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合。
public class TestSemaphore {
public static void main(String[] args) {
Semaphore s = new Semaphore(2, true);
new Thread(() -> {
try {
s.acquire();
System.out.println("t1.start");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
s.release();
}
}).start();
new Thread(() -> {
try {
s.acquire();
System.out.println("t2.start");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
s.release();
}
}).start();
}
}
6、Future
Future是一个接口。Future模式可以这样来描述:我有一个任务,提交给了Future,Future替我完成这个任务。期间我自己可以去做任何想做的事情。一段时间之后,我就便可以从Future那儿取出结果。就相当于下了一张订货单,一段时间后可以拿着提订单来提货,这期间可以干别的任何事情。其中Future 接口就是订货单,真正处理订单的是Executor类,它根据Future接口的要求来生产产品。它经常配合线程池来一起工作,将任务交给线程池去处理。
FutureTask是一个实现类,它实现了Runnable接口,配合Callable接口创建线程。Callable接口的call()方法作为线程执行体,call可以有返回值,也可以抛出异常。Callable对象不能直接作为Thread的目标,但用Future可以完成。
7、Exchanger
这个类用于交换数据,只能用于两个线程。当一个线程运行到exchange()方法时会阻塞,另一个线程运行到exchange()时,二者交换数据,然后执行后面的程序。
import java.util.concurrent.Exchanger;
public class JUCTest implements Runnable{
private Exchanger<String> exchange ;
private String name;
private String str;
public JUCTest(Exchanger<String> exchange,String name,String str){
this.exchange=exchange;
this.name=name;
this.str=str;
}
@Override
public void run(){ //线程同步用了synchronized否则无法保证s的正确性
try{
System.out.println(name+"线程自己的数据时:"+str);
String s=exchange.exchange(str); //交换数据
System.out.println(name+"获取另一个线程的数据:"+s);
}
catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
Exchanger<String> ex=new Exchanger<String>();
new Thread(new JUCTest(ex,"zhou","Hello")).start();
new Thread(new JUCTest(ex,"yu","World")).start();
}
}
8、Phaser
JAVA 1.7引入了一个新的并发API:Phaser,一个可重用的同步barrier。用来解决控制多个线程分阶段共同完成任务的情景问题。
比如:5个学生一起参加考试,一共有三道题,要求所有学生到齐才能开始考试,全部同学都做完第一题,学生才能继续做第二题,全部学生做完了第二题,才能做第三题,所有学生都做完的第三题,考试才结束。分析这个题目:这是一个多线程(5个学生)分阶段问题(考试考试、第一题做完、第二题做完、第三题做完),所以很适合用Phaser解决这个问题。