从线程到并发编程

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/xufei_0320/article/details/86235277

什么是线程

说起线程,还是得从进程说起。那么进程是什么呢?现代操作系统在运行一个程序时,会为其创建一个进程。比如你电脑上打开个QQ或者是启动一个Java程序,操作系统都会为其创建一个进程。而线程是操作系统的最小调度单元,一个进程中可以有多个线程。OS调度会让多个线程之间高速切换,让我们以为是多个线程在同时执行。

线程的创建与销毁

线程的创建

那么怎么去创建一个线程呢。在Java中我们可以有以下三种方式来创建线程:

  1. 继承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();
        }
    }
    
  2. 实现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();
        }
    }
    
  3. 实现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();
        }
    }
    

终止线程

  1. interrupt中断标志

    前面看完了如何创建一个线程,那么又怎么去终止一个线程呢。以前的Thread类中有个stop方法可以用来终止线程,而现在已经被标记过期了,其实也不建议使用stop方法来终止线程,为什么呢!因为我想用过Linux系统的都知道kill -9吧,stop方法与其类似,stop方法会强制杀死线程,而不管线程中的任务是否执行完毕。那么我们如何更加优雅的去终止一个线程呢。

    这里Thread类为我们提供了一个interrupt方法。

    当我们需要终止一个线程,可以调用它的interrupt方法,相当于告诉这个线程你可以终止了,而不是暴力的杀死该线程,线程会自行中断,我们可以使用isInterrupted方法来判断线程是否已经终止了:这段代码可以测试到,如果interrupt方法无法终止线程,那么这个线程将会是死循环,而无法结束。这里使用interrupt以一种更加安全中断线程。

    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();
        }
    }
    
  2. 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的计算能力,提升性能。

顶级计算机科学家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命令来查看一下当前JavaPID

    $ jps
    1483 DeadLockDemo
    

    可以看到当前的程序PID1483(每个人的都不一样,得自己执行哦),接下来我们使用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并发编程的艺术》

猜你喜欢

转载自blog.csdn.net/xufei_0320/article/details/86235277