for循环多线程执行_【多线程与锁】多线程的锁机制

谢尔顿的左耳朵​www.zhangxiaoshuai.fun
10726af5ef0a6a8316829458327c7bf2.png

这篇文章主要分享一下多线程和锁的基础使用;

1.为什么要使用多线程?

假如你刚刚下班回家,你想自己煮点粥喝,在煮粥的时候,盲猜你也不会待在电饭煲旁边就等着吧?干等的请回……在等待的这段时间,完全可以做一些别的事情,例如:打打游戏?洗个衣服?炒个菜?然后等粥煮好了之后,还可以一边喝粥一边看电影,这在某种程度也可以看做是多线程。

虽然一个CPU同一时刻只能执行一个程序,但是为什么我们电脑上的电影、微信中的聊天,还有播放的音乐等等似乎都是同步进行的?

这是因为CPU的执行速度非常快,它在这几个程序之间来回切换执行,让我们看起来就好像就是在同时进行一样;使用多线程,我们可以“同时”去做不一样的事情,将CPU的高性能充分利用起来;

2.什么是锁?

假如我现在想要上厕所,而当我进去之后,不希望别人再进来(毕竟这种事情是比较私密的嘛),那我拿一把锁将门锁住,后面来的人就无法进入,只能在外面等着,只有等到我出来之后,然后将锁交给下一个人,这个人再上;这就是锁的作用;

3.为什么使用锁?

来看一段代码:

private 

启动100个线程对变量a进行累加操作,每个线程对a执行10000次累加操作,程序结束之后,正常情况下,我们得到a的值是1000000,但是实际呢?

我运行了五次代码,得到了五次不同的结果:969393、990229、981436、985627、987398

为什么会产生这种结果呢?

变量是如何被修改的:线程1拿到变量a的值,修改完成之后,将新值存入a中。这是一次完整的值的修改过程。

但是呢,我们启动了一百个线程,可能存在这种情况:线程1在修改完成,往a中存过程中,线程2又去拿到了a的值,也就是0,进行累加操作,等到线程2操作完成时,线程1已经将a的值改为了1,那么线程2继续把1存了进去,两次线程对a进行累加,但是值却累加了一次。这就可以解释为什么出现最终结果不足1000000的原因了。

而出现这种问题的本质就是没有使用

如果要保证结果的正确性,那么我们需要在线程对变量a进行修改的过程中将a保护起来,直到该线程对a完成了累加操作,这个时候别线程才可以对a进行操作;

而保护的操作就是加锁

上面的代码可以修改为:

private 

这时程序运行的结果就可以保证为1000000

4.如何使用锁?

先来看一道面试题吧:

使用两个线程交替打印出 1a2b3c4d5e……


下面是多种实现方法:

version 1.0 LockSupport

static 

上面的代码中,先是定义两个线程t1和t2,然后给出了两个数组,分别为数字数组和字符数组,通过控制线程对两个数组中的内容交替进行输出;

程序一开始,如果最先调度的是t1,那么它先打印第一个数字,然后t1将“许可证”交给t2,再禁止t1被调度;
如果先调度的是t2,那么t2会被禁止调度,然后cpu就会去调度t1,重复上面的步骤,然后t2打印,然后再将“许可证”交给t1,等到两个线程循环结束之后,我们就可以拿到最终打印的结果了。

version 2.0 自旋

enum 

上面的version2.0用到了自旋锁的概念,什么是自旋锁?简单说,一个等着上厕所的人在厕所门口打转;我们定义了一个枚举类和两个枚举变量T1和T2,我们新建一个枚举类型的变量r取值T1,程序启动之后,如果最先调度的是t2线程,可以看到这里是写了一个while循环的,循环体为空,如果其中的条件符合,那么它会形成死循环,这就是自旋,然后等到条件不满足之后,才会跳出循环执行下面的代码;当t2阻塞时,t1就有机会被调度了,很明显t1的while是不符合条件的,那么它直接往下走,打印出一个数字之后,然后将变量r取值为T2,这个时候,t1线程又回到了自旋的状态,那么调度器又去调度t2线程,这个时候t2中的while循环终于不符合条件了,代码向下走,然后打印出了一个字符,再将r取值为T1,重复上述步骤,最终,我们将得到打印的结果;

version 2.1

static 

2.1版本的代码和2.0版本的核心思想是相同的,都是通过变量判断形成自旋来阻塞线程,然后迫使调度器去调度另一个线程,在另一个线程执行完成后再次修改变量值,之后形成自旋,然后调度器再调用另一个线程;

version 3.0 synchronized

public 

version 3.0使用的是synchronized关键字、notify()wait()方法进行线程的之间的通信;这个概念也就就是文章开始第三个问题回答的代码展示;程序开始,调度器先进行线程t1的调度,t1拿到了对象o这把锁,然后打印了一个数字,接下来,o对等待的线程进行唤醒,也就是t2,因为现在只有两个我们定义的线程和守护线程;唤醒t2之后,然后t1释放锁进入等待状态,锁一旦被释放,那么t2这个时候又拿到了这把锁,执行代码,打印一个字符之后,然后它再唤醒t1线程,并释放锁进入等待,t1拿到锁……重复上述步骤,要注意的是:打印完成之后,必然是有一个线程处于等待状态的,我们必须将这个等待的线程进行唤醒,如果不进行这步操作,该程序将会进入死循环,由于我们不知道是哪个线程最后处于等待状态,我们对两个线程中的循环结束之后都进行唤醒操作就可以了。这里还有一个点:我们并不能控制CPU去调度哪一个线程,也存在CPU一上来就去调度t2线程的可能性(一般,一般情况下是调用在前面启动的线程);如果是这样的话,那么打印出来的结果就和我们需要的结果的先后顺序是相反的。
如果我们调换线程启动的顺序,那么最终得到的结果又会改成a1b2c3……(一般情况),针对这种情况,我们使用一个类CountDownLatch

version 3.1 CountDownLatch

private 

Java的concurrent包里面的CountDownLatch其实可以把它看作一个计数器,只不过这个计数器的操作是原子操作,同时只能有一个线程去操作这个计数器,也就是同时只能有一个线程去减这个计数器里面的值。

你可以向CountDownLatch对象设置一个初始的数字作为计数值,任何调用这个对象上的await()方法都会阻塞,直到这个计数器的计数值被其他的线程减为0为止。

无论CPU最开始调度的是t1还是t2,都可以保证线程t1总是在t2之前被调度;

如果t2先被调度,它会被阻塞住,因为计数器的值现在为1,然后CPU又去调度线程t1,在一次循环结束之后,“计数器”的值被调整为0,这个时候t2就可以被调度了。

version 4.0

public 

version 4.0中我们使用到了一个类ReentrantLock,也叫可重入锁

使用ReentrantLock的格式和synchronized是非常相似的,但是两者又有区别

ReetrantLocksynchronized
是否为独占锁
加/解锁手动自动
响应中断可以响应中断不可响应中断
是否可重入可重入可重入
锁是否公平可实现公平锁机制(谁等的时间久谁拿)非公平锁机制

上面在代码在实现上使用了lock对象获取了两个condition对象,还是一个执行完毕之后通过“唤醒”另一个线程,并让当前线程进入等待的执行逻辑,和version 3.0基本是一样的。但依然存在线程执行顺序的问题,因为一开始并不知道哪个线程会先被执行,所以推荐使用的version 1.0 / 2.0 / 2.1 / 3.1

无锁、偏向锁、轻量级锁和重量级锁的升级过程

d5ed89b313ae97a8959859c5c134fcda.png

偏向锁:严格来讲的,偏向锁其实并不能算是锁,它更像是一个“标识”,还是以上厕所为例,上厕所的时候,我在门上写了一个纸条“有人”,这个纸条就相当于偏向锁。

而一旦另一个人来上厕所,看见门上贴着“有人”的纸条,这个时候我就需要将纸条换成锁了(别问我是怎么提上裤子出来换的),因为如果不换的话,那个人就可能会直接打开门进来,这样就会造成不好的结果了,哈哈哈哈。

那么我加锁的过程中,也就是偏向锁升级为轻量级锁的过程。而它升级的过程也可以简单总结为:有竞争就升级。

如果来的人特别多,这个时候我就需要质量更好的锁(为什么需要质量好的锁,因为我怕他们破门而入),也就是将偏向锁升级为重量级锁的过程

那什么时候轻量级锁会升级为重量级锁呢?

轻量级锁升级的为重量级锁也是因为线程之间的竞争,自旋时间过长,也就是当竞争大了之后,就不得不将锁升级,这样才能保证数据的安全;

通俗的说,门口没人加偏向锁;门口有四五个人等待,加轻量级锁;门口几百个人等着上厕所就需要加上重量级锁了,而也不会一直让他们(线程)在门口等着,如果都在门口等着,是极其消耗CPU资源的。所以会将他们(线程)安排到队列中等待,这样就不用占用CPU资源了。

偏向锁轻量级锁重量级锁的比较

优点缺点适用场景
偏向锁加锁和解锁都不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距如果线程间存在锁竞争,会带来额外的锁撤销的消耗只有一个线程访问同步块
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度如果始终得不到锁竞争的线程,使用自旋会消耗CPU追求响应时间,同步块执行速度非常块
重量级锁线程竞争不使用自旋,不会消耗CPU线程阻塞,响应时间慢追求吞吐量,同步块执行时间较长

微信公众号:aligaduo1024

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值