这次记录的是多线程,我们能一遍听歌一遍聊QQ等等,都是多线程的操作。
我们常常都听到进程与线程,而它们却是有差异的。
当我们打开一个应用程序,CPU会给它分配一定的资源,这就是进程。最明显的就是我们打开任务管理器,那些CPU给的内存资源。
而线程呢,我们打开这些应用程序,程序会给我们不停的画窗口,这就是一个进程,我们每打开一个功能,就是打开一个进程。这些进程同时执行,就是多线程的概念。
一个进程至少有一个线程。
并发与并行:
并发是多个处理器,在同一时间,一起执行不同的线程。
并行是一个处理器,在不同时间处理不同的线程。
而我们的处理器大部分都是并行处理,因为有很多线程一起执行。我们往往感受不到卡顿,是因为它执行的速度太快了。这段好啰嗦
多线程的状态
线程的生命周期:新建状态(New)、可运行状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)和死亡状态(Dead)。
多线程的声明和状态情况
下面代码虽长,但我们实际使用的是:
1 | MyThread mt = new MyThread(); |
1 | public class Threading { |
1 | 线程的状态-新建:NEW |
上面则是截取的一段结果,我们可以看到它们执行顺序的不同,这是CPU决定的,我们不敢说也不敢动。
也能看到线程的四个状态。Running是获取不到的,就像我们看得到水,却看不见水流
这是从继承的角度去调用,我们都知道,Java是单继承,所以不能继承其它类。所以我们可以继承多线程的接口——Runnable。它里面只要一个抽象方法run()。所以在实际使用中,还是要传给Thread类里。
1 | public class Threading { |
这里我删除了多余代码,当然运行结果是不一样的,这里相当于是单线程操作,只运行了run()里的方法。
所以多线程的实现方法一般有两种。直接继承Thread类。或者实现Runnable接口,在传值给它的子类Thread类里,这种为了能实现其它类。
多线程的阻塞
多线程的阻塞有四个方法。第一个是刚才写的sleep():它能使指定的线程休眠一段时间。第二个是yield():是指定的线程回到可运行状态等待CPU的下次调用。第三个是join():它是停止之前的线程,让CPU执行现在的线程。第四个是wait()和notify():wait使线程阻塞,notify使可运行。
sleep()
1 | public class Threading { |
1 | t1:0 |
在执行的前8行运行结果都是无顺序的,而在t1=5时,它休眠了100毫秒,之后就是t2开始执行100毫秒,当然这100毫秒t2已经执行完毕了。100毫秒后就是t1从5开始执行。
yield()
1 | public class Threading { |
结果和sleep()类似。当然,因为这里的数据太小,不过它们的思想是不同的,sleep是一直在线程等待,而yield是中断现在的线程,回到可运行状态,等待CPU的再次调用继续执行。
join()
1 | public class Threading { |
1 | mt:0 |
可以看到,join()的使用,使得mt阻塞,开始执行mt2,等到mt2执行完毕后,才执行mt。join就跟我们平时排队时类似,总有那么几个人插队。
wait()和notify()
1 | public class Threading { |
1 | 线程一>>现在等待 |
如何MyThread2不加Thread.sleep(100),可能会出现下面这样的错误。在MyThread一直等待。所以一定要先让MyThread进入线程锁里。
1 | 线程二>>休眠状态 |
运行结果以第一个为例。先打印第一行,接着过了大约3秒,打印最后的几行。
为啥要加入synchronized使线程同步呢好像JVM要求使用wait()时,要使用线程同步。而且我们要等待和唤醒的是同一个对象——Object。而wait()不会造成死锁状态,它会把synchronized解锁。
线程的结束
Thread里有两个方法可以结束线程,但都不安全、不可靠。所以,一般都是让它自己运行结束,或使用boolean标记。
1 | public class Threading { |
线程安全
线程为啥不安全
为什么要线程安全呢?我们从下面的例子展开。
1 | public class Threading { |
上面是两个线程启动,有”延迟”的情况下,求sum的和,sum最后的和应该为200。那我们看看实际情况可能为多少。这里只运行了两次,因为要等10多秒。
1 | 193 |
每次得到的答案都不相同,这是为啥?这是两个线程同时运行,在同一时间都拿到了某个数,假设为100,它们都给100赋值,最后sum只加了一次。而取得相同数有很大概率。
下面模拟简单取火车票。
1 | public class Threading { |
1 | Thread-0>>20 |
我们可以看到,取了重复的,最后还取了一个负一。重复的如上面所讲,同时取了一个数。而负数呢?还剩最后一个数为1,它们三个都取到了,都进入while循环里,一个取了把sum变成了0,又一个取了把0变为-1 。
synchronized的使用与注意
线程不安全往往就会出现多取或重复取的情况,所以才有线程安全或线程锁的概念。
1 | public class Threading { |
最后也没有出现取重或取多的情况,可synchronized不是那么简单的使用?我们把新建线程都改改。
1 | public class Threading { |
1 | Thread-2>>5 |
明明锁了,可还是错了,那是因为我们锁的是对象,三个线程三个对象,对象不同,锁的也不同。那怎么锁?这是我们考虑的。
下面看看,这次锁的是MyThread这个类。
1 | public class Threading { |
虽然看了三个新的线程,但是结果符合我们的要求,没有重复,没有0和负一。
锁的时候一定要注意锁的是对象还是类,如果是对象是否为同一个对象。
当然,还可以锁run()这个方法,但我不太喜欢用,run()锁的也不是方法,而是这个对象,和我们用锁this没啥区别。
而且,锁方法范围太大,我们可以锁自己感觉需要锁的块。这样可以提高运行效率。
注意:锁块,一定要锁对位置,要不然可能锁不住,不能锁的太大,效率降低很多。
死锁
我现在做的是你下一步想做的事,你现在做的是我下一步想做的事,但我们都不想让出现在资源。这就出现了死锁。
1 | public class Threading { |
1 | Thread-0>>我要看电视 |
最后结果变为看电视的想去睡觉,但床被占了;睡觉的想去看电视,但电视被占了。谁也不想让谁,造成了死锁。
而死锁一般都是锁上加锁造成的,所以要想避免死锁,就不要多重加锁。
1 | class MyThread implements Runnable{ |
线程安全还有Lock()和unlock()方法,一个是上锁,一个是解锁。这里就不弄了,写的太长了….