多线程之旅:锁策略(2)
上一篇文章,小编介绍到了一些关于锁策略的内容。
那么这篇文章,小编继续分享,由锁策略延伸出的一些内容。
比如,上篇讲过,synchronized中
加锁过程中,是有无锁==》偏向锁==》轻量级锁==》重量级锁==》
那么这个轻量级锁呢,会使用CAS,对操作进行原子化,保证数据安全,从而避免阻塞状态。
那么这个CAS,到底是什么呢?
CAS
全称:compare and swap
字面意思就是比较交换。
它的核心操作就是:
假设内存中有元数据A,寄存器1:旧的预期值V,寄存器2:需要修改的值B
1.比较A与V的值
2.如若相等,此时就把B的值写进A中
3.然后返回操作是否成功信息
交换过程:

那么这里提供了一个伪代码:

这里的CAS,是将操作变成原子化执行,原理呢就是通过底层“一条CPU指令“执行。
既然是一条cpu指令执行操作,就是很大程度上避免了多线程编程,也多线程编程提供这样的一个思路,就是无锁化编程,毕竟加锁操作,是要增加资源开销的。
对于一些判定再赋值、++、--操作,CAS是非常适用这里的,毕竟那些操作,在多线程环境下,存在多线程安全问题。
那么这个CAS会有哪些应用呢?
1.原子类
原子类是java标准库中,提供了一种以线程安全的方式执行单一变量操作的类。
位于java.until.concurrent.atomic包下
常见的一些基本类型的原子类:、
AtomicInteger:对整数类型的原子操作。
AtomicLong:提供对长整型的原子操作。
AtomicBoolean:提供对布尔值的原子操作。
AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray:分别提供对整数数组、长整型数组和对象引用数组的原子操作。
这里提供一个如何使用的例子:
代码例子:
public class Demo31 {
private static AtomicInteger count=new AtomicInteger();
public static void main1(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for(int i=0;i<5000;i++){
count.getAndIncrement();
}
});
Thread t2=new Thread(()->{
for(int i=0;i<5000;i++){
count.getAndIncrement();
}
});
t1.start();
t2.start();
Thread.sleep(1000);
System.out.println("count:"+count);
}
}例子,实现的是count++操作到10000次,由两个线程执行。
那么用原子类实现的话,cout++ ==> count.getAndIncrement();
同理,其他的一些操作
count-- ==>count.getAndDecrement();
++count ==> count.incrementAndGet();
--count ==> count.decrementAndGet();
那么要是给到我们模拟实现通过CAS实现原子类呢?
那么此时,这里也是给到一个伪代码实现
class AtomicInteger{
//这个是内存中的值public int value;
public int getIntIncrement(){
//oldValue指的是寄存器1中int oldValue=value;
while (CAS(value,oldValue,oldValue+1)!=true){
//oldValue+1指的是寄存器2的值
oldValue=value;
}
return oldValue;
}
}那么对于CAS呢,还一个应用的地方。
实现自旋锁
这个自旋锁就前面一篇文章分享过了,就是当这个线程尝试去获取锁的时候,如若此时的锁获取失败了,它任然是不释放资源,继续去尝试获取锁。
这里也是提供一个伪代码实现
class SpinLock{
//表示该线程没有持有该锁public Thread owner=null;
public void lock(){
while(!CAS(this.owner,null,Thread.currentThread())){
//如若当前的锁,任然是null,即没有被其他线程获取到,设置当前线程为持有该锁状态//如若被获取到了,就不断自旋
}
}
public void unlock(){
this.owner=null;
}
}ok,一套下来,诶发现这个CAS是不是还挺好用的,但是呢,一个事物有好的一面,也有不好的一面。
CAS是会存在一个关键性问题。
ABA问题
CAS之所以可以保证线程安全,是因为,可以感知到当前的内存值有没有被别的线程修改。
但是此时此刻,如若是一个线程修改了当前内存值,但是,这个线程又修改回去了。
那么此时,CAS它是感觉不到的。
举个极端例子
银行转账
假设我们的转账功能代码如下:
public void TransferCount(int n){
int oldvalue=value;
if(!CAS(value,oldvalue,oldvalue-n)){
System.out.println("转账失败");
}else{
System.out.println("转账成功");
}
}转账过程如下,由两个线程控制:

那么此时,就是一个bug的出现
CAS感知不到,value值是不是被修改了。
那么对于这个情况,也不是没有办法去解决。
我们可以引入版本号去解决
代码实例:
public void TransferCount(int n){
int oldVersion=version;
if(!CAS(version,oldVersion,oldVersion+1)){
System.out.println("转账失败");
}else{
value-=n;
}
}因为版本号,是一直递增/递减,所以,即使是中间修改value值,
但是版本号变了,和原来的不一致,所以CAS是可以感知到的。
其实,对于CAS的ABA问题呢,其实一般是bug不太大。
就像上面举的例子,也是在极端情况下进行的。
那么接下来分享的是java.until.concurrent一些类
这个java.until.concurrent可以简称为JUC
JUC
首先第一个是Callable
Callable
它是一个接口,类似于Runnable接口
不过它呢,是带有返回值的。
它的一个call()方法是可以返回当前的值,这个call()方法还可以抛出受检异常。
那么举个例子来示范下它是怎么用的吧
比如我们计算从0-5000的值
代码范例:
public static void main2(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable=new Callable<Integer>() {
@Overridepublic Integer call() throws Exception {
int sum=0;
for(int i=0;i<5000;i++){
sum+=i;
}
return sum;
}
};
FutureTask<Integer> futureTask=new FutureTask<>(callable);
Thread t=new Thread(futureTask);
t.start();
System.out.println(futureTask.get());
}值得注意的是,这个Callable是要带一个泛型参数的,并且还是里面的call()方法返回类型
也是和泛型参数是一致的。
但为什么多出了一个FutureTask来了呢?
主要是Thread不能接收callable作为参数,
所以引入了这个FutureTask
这个FutrueTask是什么呢?
它实现了Runnable和Futrue接口,可以看作为一个任务存储的东西。
它把存储到的任务交给线程执行。
同时自身带了一个get()方法用来返回任务类型的中的值
ReentrantLock
可重入互斥锁,与synchronized类似,实现互斥效果,也是为了保证线程安全。
它呢,常用的有三个用法
lock():加锁,长时间获取不到锁就死等
try lock(超时时间):加锁,如果获取不到锁,等待一段时间后,就会放弃加锁
unlock():解锁,通常是放在了finall{}中的。
但既然有了synchronized后,为什么还需要这个Reentrantlock呢?
这是因为和synchronized是有些区别的
1.synchronized是在jvm内部,大部分是通过c++代码实现的
而Reentrantlock则是在jvm外,通过java代码实现
2.synchronized使用时,不是手动释放锁,而Reentrantlock则是可以通过unlock手动去释放锁,
使用起来,较为灵活。
3.synchronized 在获取不到锁的时候,是死等,Reentrantlock则是通过try lock操作等待一段时间释放锁
4.synchronized是非公平锁,Reentrantlock通过传入true给构造方法,开启公平锁模式。
5.同时Reentrantlock还提供了更强的”等待通知机制“
基于Condition 类,可以实现通知到某一个线程。
Semaphore
信号量
用来表示 可用资源个数,本质上是一个计数器来的。
增加操作就是+1,称为P操作
减少操作就是-1,称为V操作
有几个常用操作
acquire():从信号量获取一个许可,如果没有可用的许可,则当前线程等待直到有许可可用或者当前线程被中断。
release():释放一个许可,将其返回给信号量
tryAcquire():尝试获取一个许可,如果有立即返回true,否则立即返回false,不会阻塞当前线程。
availabePermits():返回此信号量中当前可用的许可数
drainPermits():获取并返回立即可用的所有许可,即消耗所有许可,即原本有三个信号量,那就调用这个方法后,所有许可没有了,执行acquire就会阻塞,直到释放一个许可。
值得注意的是,当信号设置为1的时候,也是可以当一个”锁“来使用的
那么这里给到一个例子,也是计算count++一万次,通过两个线程进行操作
public static int count1=0;
public static void main3(String[] args) throws InterruptedException {
Semaphore semaphore=new Semaphore(1);
Thread t1=new Thread(()->{
for(int i=0;i<5000;i++){
try {
semaphore.acquire();
count1++;
semaphore.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread t2=new Thread(()->{
for(int i=0;i<5000;i++){
try {
semaphore.acquire();
count1++;
semaphore.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count1);
}CountDownLatch
这个类,作用就是允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。
举个代码例子:
public static void main(String[] args) throws InterruptedException {
ExecutorService pool=Executors.newFixedThreadPool(4);
CountDownLatch countDownLatch=new CountDownLatch(20);
for(int i=0;i<20;i++){
int id=i;
pool.submit(()->{
System.out.println("任务执行:"+id);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("任务结束:"+id);
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("所有任务执行完毕!");
}
}这个代码就是说,让一共有20个任务,然后线程池中存储4个任务,submit执行完任务后,
countDownLatch-1,然后在主线程中,countDownLatch.await()阻塞主线程执行
直到countDownLatch减到0。
这个JUC相关类就介绍到这
接下来来分享下
线程安全中的一些集合类
我们知道
这个ArrayLsit是线程不安全的。
有一些类,比如Vetor,stack,Hashtable是线程安全
它们内置了synchronized
但是里面呢,有点像是”无脑“加锁的感觉,所以对于一些日常开发呢,
并不是很推荐使用。
话说回来,既然这个ArrayList是线程不安全的,那么如若想要它变得安全
我们确实是可以使用这个加锁操作。
但是呢,线程的标准库中,也是提供了一个带锁的List
比如:

那么和vector有什么区别呢?
最大的区别就是,synchronizedList提供了一种方式,使得可以在需要的时候进行加锁操作
而vector呢,则是所有方法都带锁,在有些时候不需要锁的情况下,也是会引入的。
所以对资源的利用率不是很高。
除了这一个SynchronizedList之外,还有一些方法:

当前,除了这样的类型除外,还有另一个类可以使用,这个也是可以保证线程安全的。
比如CopyOnWriteArrayList。
这个是位于java.util.concurrent包中
CopyOnWriteArrayList这个集合类,并没锁,它是通过”写时拷贝“来实现线程安全。
通过这个”写时拷贝“,可以避免两个线程同时修改一个变量的情况。
那这个”写时拷贝“到底是怎么样的呢?

值得注意的是,赋值操作是原子性,就是将新的数组赋值到ArrayList引用过程中
写时拷贝也是有缺点的。
1.无法应对多个线程修改的情况
2.数组内容数据量过大,拷贝非常慢
ok,这个东西呢,就暂且分享到这。
如若是多线程使用队列的话,那么这里推荐使用BlockingQueue
多线程使用哈希表的话,那么推荐使用这个ConcurrentHashMap
即使是HashMap也是一个可选项,但是这个ConcurrentHashMap是改进过后的。
HashMap是对put、get等方法也是加上锁了,整个hash对象就是一把”锁“,对其一些操作,会触发锁竞争
ConcurrentHashMap改进的核心就是:
1.优化了锁的粒度
具体来说呢,
就是对hash表中的”链表“进行加锁,从而变成一个个锁桶。
只有进行两次对同一个元素修改的时候,才会触发到锁竞争,所以这时候是降低了锁冲突的概率。
其次,还有一些改进了的地方。
2.ConcurrentHashMap引入了CAS原子化操作
针对size++/--操作,借助CAS原子化操作,而不是真正的加锁。
3.针对这个读操作进行一些特殊处理。
就是引入了volatile和关键字和其他一些精巧涉及,使其读操作,不会出现”读一半“情况。
4.针对其Hash扩容,也进行了一定的优化
以前的Hash表,进行扩容的时候
是把整个数据都扩容过去了,虽然是一次性完毕,但是数据量过大,造成耗时过长。
所以,ConcurrentHashMap操作呢,就是每次操作的时候,会触发一部分key进行搬运
直至所有的key搬运完成。
值得注意的是,
修改/删除/查询操作在新的扩容表和旧的扩容表中操作
而插入操作是在,新的扩容表中操作。
ok,那么小编就暂且分享到这里。
完!