何为线程
说起线程,还是得从进程说起。那么进程是什么呢?现代操作系统在运行一个程序时,会为其创建一个进程。比如你电脑上打开个QQ
或者是启动一个Java
程序,操作系统都会为其创建一个进程。而线程是操作系统的最小调度单元,一个进程中可以有多个线程。OS
调度会让多个线程之间高速切换,让我们以为是多个线程在同时执行。
线程的创建与销毁
线程的创建
那么怎么去创建一个线程呢。在Java
中我们有且仅有一种(至于为啥说只有一种,而网上说有三种,那是因为其他两种并非创建线程,而是创建了线程的执行单元,最后还是交由Thread
去执行)方式来创建线程,那就是new Thread()
,但是我们却可以有以下三种方式来使用:
-
继承
Thread
类,重写run
方法。public class ThreadDemo1 extends Thread { @Override public void run() { System.out.println("extends thread run"); } public static void main(String[] args) { ThreadDemo1 thread1 = new ThreadDemo1(); ThreadDemo1 thread2 = new ThreadDemo1(); thread1.start(); thread2.start(); } }
-
实现
Runnable
接口,重写run
方法。public class ThreadDemo2 implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName() + " implements runnable run"); } public static void main(String[] args) { new Thread(new ThreadDemo2(), "thread1").start(); new Thread(new ThreadDemo2(), "thread2").start(); } }
-
实现
Callable
接口,重写call
方法,实现带返回值的线程。public class ThreadDemo3 implements Callable<String> { public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService executorService = newFixedThreadPool(1); ThreadDemo3 thread = new ThreadDemo3(); Future<String> future = executorService.submit(thread); System.out.println(future.get()); executorService.shutdown(); } @Override public String call() throws Exception { System.out.println(Thread.currentThread().getName() + " implements callable"); return Thread.currentThread().getName(); } }
终止线程
-
interrupt
中断标志前面看完了如何创建一个线程,那么又怎么去终止一个线程呢。以前的
Thread
类中有个stop
方法可以用来终止线程,而现在已经被标记过期了,其实也不建议使用stop
方法来终止线程,为什么呢!因为我想用过Linux
系统的都知道kill -9
吧,stop
方法与其类似,stop
方法会强制杀死线程,而不管线程中的任务是否执行完毕,那么我们如何更加优雅的去终止一个线程呢。这里
Thread
类为我们提供了一个interrupt
方法。当我们需要终止一个线程,可以调用它的
interrupt
方法,相当于告诉这个线程你可以终止了,而不是暴力的杀死该线程,线程会自行中断,我们可以使用isInterrupted
方法来判断线程是否已经终止了,我们可以用下面的代码来加以验证:public class InterruptDemo { private static int i; public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { while(!Thread.currentThread().isInterrupted()){ i++; } System.out.println("result: " + i); }, "interrupt-test"); thread.start(); TimeUnit.SECONDS.sleep(2); thread.interrupt(); } }
如果
interrupt
方法无法终止线程,那么这个线程将会是死循环,而无法结束。这里使用interrupt
以一种更加安全中断线程。 -
volatile
共享变量作为中断标志这里先不介绍
volatile
的内存语义以及原理,它可以解决共享变量的内存可见性问题,使其他线程可以及时看到被volatile
变量修饰的共享变量的变更,所以我们也可以使用volatile
来达到中断线程的目的。public class VolatileDemo { private volatile static boolean flag = false; public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { long i = 0L; while (!flag) { i++; } System.out.println(i); }, "volatile-demo"); thread.start(); System.out.println("volatile-demo is start"); Thread.sleep(1000); flag = true; } }
比如上面示例中的代码,我们可以控制在特定的地方,改变共享变量,来达到让线程退出。
线程复位
-
interrupted
前面说了使用
interrupt
可以告诉线程可以中断了,线程同时也提供了另外一个方法即Thread.interrupted()
可以将已经设置过中断标志的线程进行复位。public class InterruptDemo { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { while (true) { boolean isInterrupted = Thread.currentThread().isInterrupted(); if(isInterrupted){ System.out.println("before: " + isInterrupted); Thread.interrupted(); // 对线程进行复位,中断标识为false System.out.println("after: " + Thread.currentThread().isInterrupted()); } } }, "InterruptDemo"); thread.start(); TimeUnit.SECONDS.sleep(1); thread.interrupt(); // 设置中断标识为true } }
输出结果:
before: true after: false
通过
demo
可以看到线程确实是先被设置了中断标识,后又被复位。 -
异常复位
除了使用
interupted
来设置中断复位,还有一种情况,就是对抛出InterruptedException
异常的方法,在InterruptedExceptio
抛出之前,JVM
会先把线程的中断标识位清除,然后才会抛出InterruptedException
,这个时候如果调用isInterrupted
方法,将会返回false
,例如:public class InterruptDemo { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { while (true) { try { Thread.sleep(10000); } catch (InterruptedException e) { // 抛出InterruptedException会将复位标识设置为false e.printStackTrace(); } } }, "InterruptDemo"); thread.start(); TimeUnit.SECONDS.sleep(1); thread.interrupt(); // 设置中断标志为true TimeUnit.SECONDS.sleep(1); System.out.println(thread.isInterrupted()); } }
输出结果:
java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at top.felixu.chapter1.lifecycle.InterruptDemo.lambda$main$0(InterruptDemo.java:48) at java.lang.Thread.run(Thread.java:748) false
通过例子可以看到,在抛出异常之后,
isInterrupted
确实是又变成了false
。
为什么要并发编程
单线程有时候也可以解决问题啊,那么我们为什么还要并发编程呢,很大程度上是因为更好的利用CPU
资源,提升我们系统的性能。根据摩尔定律(当价格不变时,集成电路上可容纳的元器件的数目,约每隔18-24
个月便会增加一倍,性能也将提升一倍。换言之,每一美元所能买到的电脑性能,将每隔18-24
个月翻一倍以上。这一定律揭示了信息技术进步的速度。)推算,不久就会有超强的计算能力,然而,事情并未像预料的那样发展。2004
年,Intel
宣布4GHz
芯片的计划推迟到2005
年,然后在2004
年秋季,Intel
宣布彻底取消4GHz
的计划。现在虽然有4GHz
的芯片但频率极限已逼近,而且近10
年停留在4GHz
,也就是摩尔定律应该是失效了。既然单核CPU
的计算能力短期无法提升了,而我们伟大的硬件科学家们可不会承认自己的理论是错的,那既然一个CPU
搞不定了,那么就多个嘛,多核CPU
在此时应运而生。单线程毕竟只可能跑在一个核心上,浪费了CPU
的资源,从而催生了并发编程,并发编程是为了发挥出多核CPU
的计算能力,提升性能。
顶级计算机科学家
Donald Ervin Knuth
如此评价这种情况:在我看来,这种现象(并发)或多或少是由于硬件设计者无计可施了导致的,他们将摩尔定律的责任推给了软件开发者。
并发编程总结起来说大致有以下优点:
-
充分利用
CPU
,提高计算能力。 -
方便对业务的拆分。比如一个购物流程,我们可以拆分成下单,减库存等,利用多线程来加快响应。
-
对于需要阻塞的场景,可以异步处理,来减少阻塞。
-
对于执行性能,可以通过多线程并行计算。
并发编程有哪些问题
看起来好像多线程确实很好,那么我们就可以尽量多的去开线程了嘛。也并不是这样的,多线程的性能也受多方面因素所影响:
-
时间片的切换
时间片是
CPU
分配给线程执行的时间,即便是单核CPU
也是可以通过时间片的切换使多个线程切换执行,让我们觉得是多个线程在同时执行,因为时间片的切换是非常快的,我们感觉不到的。每次切换线程是需要时间的,而且切换的时候需要保存当前线程的状态,以便切换回来的时候可以继续执行。所以当线程较多的时候,切换时间片所带来的消耗也同样可观。那么有没有什么姿势可以解决这个问题呢,是有的:- 无锁并发编程:多线程在竞争锁时会引起上下文的切换,可以使用对数据
Hash
取模分段的思想来避免使用锁。 -
CAS
算法:可以使用Atomic
包中相关原子操作,来避免使用锁。 - 使用最少线程:根据业务需求创建线程数,过多的创建线程会造成线程闲置和资源浪费。
- 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
- 无锁并发编程:多线程在竞争锁时会引起上下文的切换,可以使用对数据
-
死锁
为了保证多线程的正确性,很多时候,我们都会使用锁,它是一个很好用的工具,然而在一些时候,不正确的姿势会造成死锁问题,进而引发系统不可用。下面我们就来看一个死锁案例:
public class DeadLockDemo { public static void main(String[] args) { new DeadLockDemo().deadLock(); } private void deadLock() { Object o1 = new Object(); Object o2 = new Object(); Thread one = new Thread(() -> { synchronized (o1) { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (o2) { System.out.println(Thread.currentThread().getName()); } } }, "thread-one"); Thread two = new Thread(() -> { synchronized (o2) { synchronized (o1) { System.out.println(Thread.currentThread().getName()); } } }, "thread-two"); one.start(); two.start(); } }
运行之后便会发现程序无法终止了,那么究竟发生了什么呢?我们通过
jps
命令来查看一下当前Java
的PID
。$ jps 1483 DeadLockDemo
可以看到当前的程序
PID
为1483
(每个人的都不一样,得自己执行哦),接下来我们使用jstack
命令dump
出当前程序的线程信息,看一下究竟发生了什么。jstack 1483 . . . . . .省略部分信息 "thread-two" #12 prio=5 os_prio=31 tid=0x00007fbba9956800 nid=0x5603 waiting for monitor entry [0x0000700011058000] java.lang.Thread.State: BLOCKED (on object monitor) at top.felixu.section1.deadlock.DeadLockDemo.lambda$deadLock$1(DeadLockDemo.java:32) - waiting to lock <0x000000076ada81b8> (a java.lang.Object) - locked <0x000000076ada81c8> (a java.lang.Object) at top.felixu.section1.deadlock.DeadLockDemo$$Lambda$2/381259350.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) "thread-one" #11 prio=5 os_prio=31 tid=0x00007fbba8033800 nid=0xa803 waiting for monitor entry [0x0000700010f55000] java.lang.Thread.State: BLOCKED (on object monitor) at top.felixu.section1.deadlock.DeadLockDemo.lambda$deadLock$0(DeadLockDemo.java:24) - waiting to lock <0x000000076ada81c8> (a java.lang.Object) - locked <0x000000076ada81b8> (a java.lang.Object) at top.felixu.section1.deadlock.DeadLockDemo$$Lambda$1/1607521710.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) . . . . . .省略部分信息 Found one Java-level deadlock: ============================= "thread-two": waiting to lock monitor 0x00007fbba9006eb8 (object 0x000000076ada81b8, a java.lang.Object), which is held by "thread-one" "thread-one": waiting to lock monitor 0x00007fbba90082a8 (object 0x000000076ada81c8, a java.lang.Object), which is held by "thread-two" Java stack information for the threads listed above: =================================================== "thread-two": at top.felixu.section1.deadlock.DeadLockDemo.lambda$deadLock$1(DeadLockDemo.java:32) - waiting to lock <0x000000076ada81b8> (a java.lang.Object) - locked <0x000000076ada81c8> (a java.lang.Object) at top.felixu.section1.deadlock.DeadLockDemo$$Lambda$2/381259350.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) "thread-one": at top.felixu.section1.deadlock.DeadLockDemo.lambda$deadLock$0(DeadLockDemo.java:24) - waiting to lock <0x000000076ada81c8> (a java.lang.Object) - locked <0x000000076ada81b8> (a java.lang.Object) at top.felixu.section1.deadlock.DeadLockDemo$$Lambda$1/1607521710.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) Found 1 deadlock.
从上面来看,两个线程都是阻塞状态,都在等待别的线程释放锁,但是永远都等不到,从而形成了死锁。那么平常开发过程中尽量按以下操作来避免不必要的死锁(当然有时候不注意还是会莫名死锁,得
dump
信息加以分析才能找出问题的):- 避免一个线程同时获取多个锁。
- 尽量避免一个线程在锁内同时获取多个资源,尽量保证每个锁内只占有一个资源。
- 尝试使用定时锁,使用
lock.tryLock(timeout)
来替代使用内部锁机制。 - 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
-
软件和硬件资源的限制
程序跑在服务器上,必然受到服务器等方面的限制。
- 硬件资源限制:一般指磁盘读写速度、带宽、
CPU
性能等方面 - 软件资源限制:一般指数据库连接数、
Socket
连接数等方面
- 硬件资源限制:一般指磁盘读写速度、带宽、
所以,如何合理的使用线程需要我们在实践中具体去分析。
结语
并发编程一直是个难点,也是在面试中不可避免被问到的知识点,后面会更多讨论其他方面的知识点,也需要更多的动手实践,才能体会其中的一些深层意义。
参考自《Java并发编程的艺术》
网友评论