并发编程基础(下)
书接上文。上文主要讲了下线程的基本概念,三种创建线程的方式与区别,还介绍了线程的状态,线程通知和等待,join等,本篇继续介绍并发编程的基础知识。
sleep
当一个执行的线程调用了thread的sleep方法,调用线程会暂时让出指定时间的执行权,在这期间不参与cpu的调度,不占用cpu,但是不会释放该线程锁持有的监视器锁。指定的时间到了后,该线程会回到就绪的状态,再次等待分配cpu资源,然后再次执行。
我们有时会看到sleep(1),甚至还有sleep(0)这种写法,肯定会觉得非常奇怪,特别是sleep(0),睡0秒钟,有意义吗?其实是有的,sleep(1),sleep(0)的意义就在于告诉操作系统立刻触发一次cpu竞争。
让我们来看看正在sleep的进程被中断了,会发生什么事情:
class mysleeptask implements runnable{ @override public void run() { system.out.println("mytask1"); try { timeunit.seconds.sleep(5); } catch (interruptedexception e) { system.out.println("中断"); e.printstacktrace(); } system.out.println("mytask2"); } } public class sleep { public static void main(string[] args) { mysleeptask mysleeptask=new mysleeptask(); thread thread=new thread(mysleeptask); thread.start(); thread.interrupt(); } }
运行结果:
mytask1 中断 java.lang.interruptedexception: sleep interrupted at java.lang.thread.sleep(native method) at java.lang.thread.sleep(thread.java:340) at java.util.concurrent.timeunit.sleep(timeunit.java:386) at com.codebear.mysleeptask.run(sleep.java:10) at java.lang.thread.run(thread.java:748) mytask2
yield
我们知道线程是以时间片的机制来占用cpu资源并运行的,正常情况下,一个线程只有把分配给自己的时间片用完之后,线程调度器才会进行下一轮的线程调度,当执行了thread的yield后,就告诉操作系统“我不需要cpu了,你现在就可以进行下一轮的线程调度了 ”,但是操作系统可以忽略这个暗示,也有可能下一轮还是把时间片分配给了这个线程。
我们来写一个例子加深下印象:
class myyieldtask implements runnable { @override public void run() { for (int i = 10; i > 0; i--) { system.out.println("我是" + thread.currentthread().getname() + ",我分配到了时间片"); } } } public class myyield { public static void main(string[] args) { thread thread1 = new thread(new myyieldtask()); thread1.start(); thread thread2 = new thread(new myyieldtask()); thread2.start(); } }
运行结果:
当然由于线程的特性,所以每次运行结果可能都不太相同,但是当我们运行多次后,会发现绝大多数的时候,两个线程的打印都是比较平均的,我用完时间片了,你用,你用完了时间片了,我再用。
当我们调用yield后:
class myyieldtask implements runnable { @override public void run() { for (int i = 10; i > 0; i--) { system.out.println("我是" + thread.currentthread().getname() + ",我分配到了时间片"); thread.yield(); } } } public class myyield { public static void main(string[] args) { thread thread1 = new thread(new myyieldtask()); thread1.start(); thread thread2 = new thread(new myyieldtask()); thread2.start(); } }
运行结果:
当然在一般情况下,可能永远也不会用到yield,但是还是要对这个方法有一定的了解。
sleep 和 yield 区别
当线程调用sleep后,会阻塞当前线程指定的时间,在这段时间内,线程调度器不会调用此线程,当指定的时间结束后,该线程的状态为“就绪”,等待分配cpu资源。
当线程调用yield后,不会阻塞当前线程,只是让出时间片,回到“就绪”的状态,等待分配cpu资源。
死锁
死锁是指多个线程在执行的过程中,因为争夺资源而造成的相互等待的现象,而且无法打破这个“僵局”。
死锁的四个必要条件:
- 互斥:指线程对于已经获取到的资源进行排他性使用,即该资源只能被一个线程占有,如果还有其他线程也想占有,只能等待,直到占有资源的线程释放该资源。
- 请求并持有:指一个线程已经占有了一个资源,但是还想占有其他的资源,但是其他资源已经被其他线程占有了,所以当前线程只能等待,等待的同时并不释放自己已经拥有的资源。
- 不可剥夺:当一个线程获取资源后,不能被其他线程占有,只有在自己使用完毕后自己释放资源。
- 环路等待:即 t1线程正在等待t2占有的资源,t2线程正在等待t3线程占有的资源,t3线程又在等待t1线程占有的资源。
要想打破“死锁”僵局,只需要破坏以上四个条件中的任意一个,但是程序员可以干预的只有“请求并持有”,“环路等待”两个条件,其余两个条件是锁的特性,程序员是无法干预的。
聪明的你,一定看出来了,所谓“死锁”就是“悲观锁”造成的,相对于“死锁”,还有一个“活锁”,就是“乐观锁”造成的。
守护线程与用户线程
java中的线程分为两类,分别为 用户线程和守护线程。在jvm启动时,会调用main函数,这个就是用户线程,jvm内部还会启动一些守护线程,比如垃圾回收线程。那么守护线程和用户线程到底有什么区别呢?当最后一个用户线程结束后,jvm就自动退出了,而不管当前是否有守护线程还在运行。
如何创建一个守护线程呢?
public class daemon { public static void main(string[] args) { thread thread = new thread(() -> { }); thread.setdaemon(true); thread.start(); } }
只需要设置线程的daemon为true就可以。
下面来演示下用户线程与守护线程的区别:
public class daemon { public static void main(string[] args) { thread thread = new thread(() -> { while (true){} }); thread.start(); } }
当我们运行后,可以发现程序一直没有退出:
因为这是用户线程,只要有一个用户线程还没结束,程序就不会退出。
再来看看守护线程:
public class daemon { public static void main(string[] args) { thread thread = new thread(() -> { while (true){} }); thread.setdaemon(true); thread.start(); } }
当我们运行后,发现程序立刻就停止了:
因为这是守护线程,当用户线程结束后,不管有没有守护线程还在运行,程序都会退出。
线程中断
之所以把线程中断放在后面,是因为它是并发编程基础中最难以理解的一个,当然这也与不经常使用有关。现在就让我们好好看看线程中断。
thread提供了stop方法,用来停止当前线程,但是已经被标记为过期,应该用线程中断方法来代替stop方法。
interrupt
中断线程。当线程a运行(非阻塞)时,线程b可以调用线程a的interrupt方法来设置线程a的中断标记为true,这里要特别注意,调用interrupt方法并不会真的去中断线程,只是设置了中断标记为true,线程a还是活的好好的。如果线程a被阻塞了,比如调用了sleep、wait、join,线程a会在调用这些方法的地方抛出“interruptedexception”。
我们来做个试验,证明下interrupt方法不会中断正在运行的线程:
class interrupttask implements runnable { @override public void run() { copyonwritearraylist copyonwritearraylist = new copyonwritearraylist(); try { long start = system.currenttimemillis(); for (int i = 0; i < 150000; i++) { copyonwritearraylist.add(i); } system.out.println("结束了,时间是" + (system.currenttimemillis() - start)); system.out.println(thread.currentthread().isinterrupted()); } catch (exception ex) { ex.printstacktrace(); } } } public class interrupttest { public static void main(string[] args) throws interruptedexception { thread thread1 = new thread(new interrupttask()); thread1.start(); thread1.interrupt(); } }
运行结果:
结束了,时间是7643 true
在子线程中,我们通过一个循环往copyonwritearraylist里面添加数据来模拟一个耗时操作。这里要特别要注意,一般来说,我们模拟耗时操作都是用sleep方法,但是这里不能用sleep方法,因为调用sleep方法会让当前线程阻塞,而现在是要让线程处于运行的状态。我们可以很清楚的看到,虽然子线程刚运行,就被interrupt了,但是却没有抛出任何异常,也没有让子线程终止,子线程还是活的好好的,只是最后打印出的“中断标记”为true。
如果没有调用interrupt方法,中断标记为false:
class interrupttask implements runnable { @override public void run() { copyonwritearraylist copyonwritearraylist = new copyonwritearraylist(); try { long start = system.currenttimemillis(); for (int i = 0; i < 500; i++) { copyonwritearraylist.add(i); } system.out.println("结束了,时间是" + (system.currenttimemillis() - start)); system.out.println(thread.currentthread().isinterrupted()); } catch (exception ex) { ex.printstacktrace(); } } } public class interrupttest { public static void main(string[] args) throws interruptedexception { thread thread1 = new thread(new interrupttask()); thread1.start(); } }
运行结果:
结束了,时间是1 false
在介绍sleep,wait,join方法的时候,大家已经看到了,如果中断调用这些方法而被阻塞的线程会抛出异常,这里就不再演示了,但是还有一点需要注意,当我们catch住interruptedexception异常后,“中断标记”会被重置为false,我们继续做实验:
class interrupttask implements runnable { @override public void run() { copyonwritearraylist copyonwritearraylist = new copyonwritearraylist(); try { long start = system.currenttimemillis(); timeunit.seconds.sleep(3); system.out.println("结束了,时间是" + (system.currenttimemillis() - start)); } catch (exception ex) { system.out.println(thread.currentthread().isinterrupted()); ex.printstacktrace(); } } } public class interrupttest { public static void main(string[] args) throws interruptedexception { thread thread1 = new thread(new interrupttask()); thread1.start(); thread1.interrupt(); } }
运行结果:
false java.lang.interruptedexception: sleep interrupted at java.lang.thread.sleep(native method) at java.lang.thread.sleep(thread.java:340) at java.util.concurrent.timeunit.sleep(timeunit.java:386) at com.codebear.interrupttask.run(interrupttest.java:20) at java.lang.thread.run(thread.java:748)
可以很清楚的看到,“中断标记”被重置为false了。
还有一个问题,大家可以思考下,代码的本意是当前线程被中断后退出死循环,这段代码有问题吗?
thread th = thread.currentthread(); while(true) { if(th.isinterrupted()) { break; } try { thread.sleep(100); }catch (interruptedexception e){ e.printstacktrace(); } }
本题来自 极客时间 王宝令 老师的 《java并发编程实战》
代码是有问题的,因为catch住异常后,会把“中断标记”重置。如果正好在sleep的时候,线程被中断了,又重置了“中断标记”,那么下一次循环,检测中断标记为false,就无法退出死循环了。
isinterrupted
这个方法在上面已经出现过了,就是 获取对象线程的“中断标记”。
interrupted
获取当前线程的“中断标记”,如果发现当前线程被中断,会重置中断标记为false,该方法是static方法,通过thread类直接调用。
并发编程基础到这里就结束了,可以看到内容还是相当多的,虽说是基础,但是每一个知识点,如果要深究的话,都可以牵扯到“操作系统”,所以只有深入到了“操作系统”,才可以说真的懂了,现在还是仅仅停留在java的层面,唉。
上一篇: DSAPI 导出EXEDLL函数到字符串
下一篇: 你TM能先把裤子穿上吗