多线程这个章节还是有点东西的,但是你放心,我都会举一些好玩的例子,让你在不知不觉中就看懂了。
什么是进程
定义,我知道你们不喜欢看。所以来看看我的例子。
假如你是一个牛马(不要玻璃心哦),你在一个公司上班,你的老板就会发任务给你做,而你肯定还有同事,你干你的事,你同事干他的事,你们互不搭理(不熟啊)。而我们就可以看为,你就是一个进程,你完成你自己的事(进程执行自己的代码),你同事完成他的事(多个进程之间“同时”执行“多个代码),你们互不搭理(多个进程之间不会干扰,也就是假如你不干了,但是你同事还是一样干(一个进程崩了,别的进程还是可以工作的)),这个是好事,你总不会认为公司少了你的干不了吧(个别原因除外),而老板发任务给你们就是(操作系统下发任务给进程)。
线程
还是上面的故事,我们接着往下说,假如你变为了老板,你的主要任务是什么,赚钱(对,你不要说俗,老师都是为了钱)那你想赚钱,你可以想什么办法呢,两方面,一个是找更多业务(这个要考虑的东西就多了,所以不是首选)二就是让一个员工做更多的事(资本是这样的),我们业务数就这么多,一个人做多了,就可以裁员,也就省下了钱。所以在计算机中也是这样,一个进程,他做事做少了。我们就想要提高他的效率(我们就想他做更多的事),所以我们就发明了线程,一个进程可以有多个线程,但是最少一个线程。(就是你可以同时做几个项目,但是你最少要一个,因为你一个都不做,那裁员不裁你裁谁)
总结一下,进程就是相当于一个员工完成全部的工作,而线程就是员工完成具体的一件事。
进程和线程的区别
因为线程就是员工完成具体的一件事,而进程是员工完成全部的工作,所以进程的体量要比线程大的多,所以下面就好理解了。
创建线程⽐创建进程更快
销毁线程⽐销毁进程更快
调度线程⽐调度进程更快。
还要一个最主要的区别就是,不同的进程是有不同的内存空间的,而在同一个进程下的多个线程他们是公用一个内存空间的。这个非常好理解,因为进程就是相当于一个员工要完成的工作,而我们每一个员工都有自己的工作岗位吧(应该不会有没有岗位的吧),而线程是一个员工要完成具体的一件事,所以多线程就是一个员工要干好多事,那还不是这个员工干啊,这个员工也还是这个岗位啊。
后面一些线程的使用,就是自己看看文档,看看资料就会的,没有什么要理解的,我就不写了。,我们后面写的基本就是两个一个start(线程开始)join(等待线程结束)所以我们接下来聊一聊线程的重量级(线程安全)
线程安全
线程随机调度问题
我们想要一个线程安全,就要知道他为什么不安全,而不安全的原因就是线程调度的随机的,给大家讲一个最痛苦的经历,就是你小时候打游戏,突然你妈不让你打了,让你去吃饭,这个就是线程调度,所以说线程调度是很痛苦的,我们从一个问题出发。
public static int count = 0;
public static void main(String[] args){
Thread thread = new Thread(()->{
for(int i = 0;i<5000;i++){
count++;
}
});
Thread thread1 = new Thread(()->{
for(int j = 0;j<5000;j++){
count++;
}
});
thread.start();
thread1.start();
try {
thread.join();
thread1.join();
}catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(+count);
}
这个就是典型的我们创建两个线程,同时加一个数,大家觉得这个打印出来的数是多少。10000?如果你说10000

嘻嘻,我还真的就搞出来了10000,但是还是错了,(™的,我第一次调试的时候,还真的是10000,搞的我还以为写错了,后面就正常了)



是一个随机的数,那为什么是一个随机的数呢,这个就是因为线程调度了。看下面

计算机在做count++的操作的时候,分为了三步,1.拿到count的值。2.把这个值++,3.把新的值放回原来的位子。而计算机为什么要分为三步呢(我也想一步搞好,条件不允许啊)。而这个又是线程,而线程调度就把这个东西打乱了,看图

因为在t1调度的时候,我们只拿到了count = 900,就被调度去执行t2,t2还是拿count = 900,这样我们要执行两次的++,就只执行了1次。那我问你,

这个值是不是一直都是5000以上呢,你说对啊,tall me why

你说这个不就是两个可能一个是两个没有穿插,就是都成功了,还有就是两个穿插,只成功一个。那我们要最小的不就是,全部都穿插,都只成功一个,不就是5000吗,一点聪明,但是我的评价是一般(虽然一开始我也是这样想的)那你想,假如

兄弟们,虽然这个情况特别极端,那我问你,会不会发生,所以这个数是可以小于5000的,只不过特别极端。但是一个服务器这么多人,使用这么多次,就可能会触发很多次,不过这个是后话,因为就上面一个随机数,可能老板就要骂你了。所以下面我们就说说解决方法,
线程随机调度的解决方法
其实看了我上一篇MySQL的索引和事务(事务的隔离一看就懂)-优快云博客的小伙伴已经知道了,在聊事务的时候,事务有一个特性叫原子性,就是做一个事,要不全部成功,要不全部失败。所以我们就需要让count++这样的操作有原子性,所以这个时候我们就需要使用到synchronized这样的关键字,所以我们就来看看synchronized是怎么使用的

locket这个是锁的类型,等下聊,后面的大括号就是进去加锁,出来就解锁,而我们为什么要这样设计呢,就是为了怕我们会忘记解锁,你说这个不是基本操作吗,写出这个bug不是程序员的问题吗,确实,在我们这个情况下确实不会忘记,但是假如有锁的情况下报错了,出异常了,就很容易忘记,虽然这个确实是我们失误,但是我们还是可以通过别的方法来避免这样的失误,
现在聊聊locket这个锁的类型,这个就是我们随便拿一个类型创建的一个变量,我们也可以创建几个变量为不同的锁,我想到一个特别的例子,就是上厕所,locket就是厕所门(有锁的),我在里面的时候(上了锁),你是不是就进不来了,就有人说,为什么我一定要去你那个厕所,我不能换一个吗。你当然可以换一个,而且你还可以成功的进去(别的厕所没有上锁),但是我写的代码命令你一定要上locket的厕所,所以你只能在外面等我,你想想外面的count++,加了锁,两个线程就不可以穿插了,就不会发生count的数是随机的了,(我们把这个叫为线程安全)。
可见性问题和解决方法
我们还是先看一个问题

我先讲讲,我这个题的思路啊,我们先创建一个变量,我们在thread里面让他一直循环,一直到我们的flag不为0的时候,就打印循环结束。而我们只有在thread1中,我们随便输入一个数,flag才不会为0,所以我们看执行结果。
我大概等了1分钟还是没有出来,这个就是thread还在循环,他没有停下来,但是我们明明有一步把他变了啊,这个就是典型的内存可见性问题了。首先,我们看看官方解释吧


讲简单就是这个程序有一个内存空间(这个就是那个硬件的内存),而我们单独为线程开辟了内存空间(这个实际上就是cpu的寄存器)而计算机为什么要单独开辟空间啊,直接用内存不就好了啊,这个其实就是寄存器和内存的区别了(能不能想起来),主要的就三点,一。内存的存储空间大,寄存器小。二。寄存器访问速度快,内存慢。三。内存断电内容可以保存。但是和这个相关的就是第二点寄存器访问速度快,可以提高效率,那我问你
寄存器效率搞高,为什么要内存

孩子们,寄存器贵啊,就像谁不想天天吃好的,喝好的啊,没钱啊。
我们有点说远了,但是还是和这个题有关的。来我们上面聊到了,分为主内存和工作内存,程序的叫主内存,单一线程的叫工作内存,我们在执行线程的时候,我们会把主内存的变量拷贝到工作内存上面,但是计算机拷贝了几次发现都是这个数,后面就懒的拷贝了(jvm的效率优化功能),所以才会出现,我在thread1里面修改了数,但是thread里面就没有改。这个解决方法很简单,就是给这个变量加volatile(这个就是告诉计算机,这个数后面要改,你不要偷奸耍滑),就可以正常实现了。
可重入问题和解决方法
可重入问题,其实就是字面意思,就是一个锁对自己再锁一次,有没有成为死锁。

就是相对于这样,我和大家解释一下这个代码啊,首先我们进入第一个synchronized,就从写加锁的地方开始,但是我在下一个的代码中我第二个synchronized要加锁,,而我要拿到这个锁是不是就要释放第一个synchronized,是不是就要走到第一个synchronized结锁的地方,但是我都卡在了第二个加锁的地方,我怎么走到第一个锁结锁的地方呢,这个就是死锁,但是这个的解决方法,很简单,因为synchronized就是可重入锁,这种就可以编译过
死锁还有一种情况,是这样一个场景,我和你两个人吃牛排,吃牛排不是一般要刀和叉子吗(假设少一个都吃不了),我手上有一把刀,你手上有一把叉子,我们两个都想先吃,所以我找你要叉子,你又找我要刀,就卡在这里了,这个也是一种死锁,就有两个线程,一个线程各有一把锁,但是完成任务要两把锁才可以做,所以就卡了。这个是解决方法也很简单,就比如我们两吃牛排,我们分一个主次就好了,这个线程也分一个谁先谁后就好了。
这个线程安全还是有点意思的,希望对大家有帮助,这个是我写的下一篇,使用今天的多线程来写一个可以使用的程序,来对今天的多线程学习做一个反馈。 多线程2(定时器的实现)-优快云博客
7275



被折叠的 条评论
为什么被折叠?



