JUC总结系列篇 (二) : 对线程的理解和使用总结

文章内容:

一.为什么需要多线程
二.线程的创建
三.线程的方法sleep(),run(),wait(),yeid(),join(),interrupt()等方法归纳总结
四.线程的状态及其转换
五.线程的交替执行案例
六.多个线程依次执行案例
七.多线程并发带来的线程安全问题

一.为什么需要多线程?

  • 相比与以前的单核时代,目前的计算机都是多CPU的,多线程可以更好的利用CPU,提高CPU的利用率
  • 在IO密集时,多线程可以提高在线程IO阻塞时CPU的利用率(一个线程被阻塞不占用CPU的时候,另一个线程就可以利用空闲的CPU)
  • 多线程的并发可以提高程序的运行速度(一个任务一个任务依次执行肯定是比多个任务同时进行慢的)
  • 当然多线程的并发操作也会伴随着线程安全问题(另写文章总结

二.线程的创建

从本质上说创建线程只有一种方式,就是构造一个 Thread 类,这是创建线程的唯一方式。
而要想实现线程执行的内容,却有三种方式:

  • 通过实现 Runnable 接口的方式重写 run() 方法
  • 继承 Thread 类重写 run() 方法的方式
  • 实现Callable并重写call()方法,利用Callable构造一个FutureTask<>,最后把FutureTask当作参数来构造Thread对象,call()方法的内容就是线程执行的内容 (call方法有返回值,而run方法无返回值)

实现 Runnable 接口:

    public static class MyRunable implements Runnable{

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "    通过实现Runable的方式");
        }
    }
        new Thread(new MyRunable()).start();
        new Thread(new MyRunable()).run();

在这里插入图片描述
从这里也可以看出run方式和start方式的执行过程是不一致的,直接run是在当前run的线程中执行,而start是重新开辟一条新线程执行(后面会总结)

继承 Thread 类重写 run() 方法的方式:

    public static class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "    通过继承Thread的方式");
        }
    }
        new MyThread().start();
        new MyThread().run();

在这里插入图片描述

Tips:

实现 Runnable 接口比继承 Thread 类实现线程要好在哪里:

  • 从代码的架构考虑,实际上,Runnable 里只有一个 run() 方法,它定义了需要执行的内容,在这种情况下,实现了 Runnable 与 Thread 类的解耦,Thread 类负责线程启动和属性设置等内容,权责分明,Runnable传入给Thread执行就好
  • 在某些情况下可以提高性能,使用继承 Thread 类方式,每次执行一次任务,都需要新建一个独立的线程。如果使用实现 Runnable 接口的方式,就可以把任务直接传入线程池,使用一些固定的线程来完成任务,不需要每次新建销毁线程,大大降低了性能开销。
    对于线程池的使用总结,可以点击JUC总结系列篇 (一):对Java线程池的理解和使用总结
  • Java 语言不支持双继承,如果我们的类一旦继承了 Thread 类,那么它后续就没有办法再继承其他的类,限制了代码未来的可拓展性。

实现Callable并重写call()方法

    public static class MyCallable implements Callable<String>{

        @Override
        public String call() throws Exception {
            System.out.println(Thread.currentThread().getName() + "    通过实现Callable的方式");
            return "Call方法执行完毕返回值";
        }
    }

第一种使用:无需获取返回值

          new Thread(new FutureTask<>(new MyCallable())).start();
          new Thread(new FutureTask<>(new MyCallable())).run();

在这里插入图片描述
第二种使用:获取线程的返回值,通过Future的get方法

        FutureTask<String> task = new FutureTask<>(new MyCallable());
        Thread t1 = new Thread(task);
        t1.start();
        try {
            t1.join();
            System.out.println(task.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

在这里插入图片描述
注意上面必须使用join方法,否则main函数可能先执行完了,就获取不到返回值了。
比如:

        FutureTask<String> task = new FutureTask<>(new MyCallable());
        Thread t1 = new Thread(task);
        t1.start();
        if(!t1.isAlive()){
            try {
                System.out.println(task.get());
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } catch (ExecutionException e) {
                throw new RuntimeException(e);
            }
        }else{
            System.out.println("t1线程还在执行中,拿不到返回值");
        }

在这里插入图片描述
关于join方法,后面会讲解
#

三. 线程的sleep(),run(),wait(),yeid(),join(),interrupt()等方法归纳总结

sleep():

    public static native void sleep(long millis) throws InterruptedException;
  • 静态方法,参数是毫秒,需要指定等待的时间,可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态(注意是在哪个线程里面调用,就使哪个线程进入休眠阻塞)
  • 相当于让线程睡眠,交出CPU,让CPU去执行其他的任务,但是要注意,sleep方法是不会释放锁的(does not lose owner ship of any monitors),抱着锁睡觉,也就是说如果有synchronized同步块,线程获取到锁调用sleep后,其他线程仍然不能访问共享数据(因为拿不到锁)
  • 执行该方法和线程优先级无关,可以让其他同优先级或者高/低优先级的线程得到执行的机会
    使用:
    public static class MyRunable implements Runnable{

        @Override
        public void run() {

            System.out.println(Thread.currentThread().getName() + "    线程进入休眠" + System.currentTimeMillis());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "    线程结束休眠" + System.currentTimeMillis());
        }
    }

new Thread(new MyRunable()).start();

在这里插入图片描述

  • Tips:
    操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU资源比较多,也就是说CPU优先执行优先级高的线程的概率高,但是不能保证优先级高就一定会先被执行。
    优先级一共分为1~10个等级,数字越大优先级越高,默认5,超出范围则抛出java.lang.IllegalArgumentException异常。

    可以使用 getPriority获取线程的优先级setPriority设置线程的优先级
    举个栗子:
    public static class MyHighPriorityRunable implements Runnable{

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "    高优先级输出");
        }
    }
    public static class MyLowPriorityRunable implements Runnable{

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "    低优先级输出");
        }
    }
    public static void main(String[] args) {
        Thread hPThread = new Thread(new MyHighPriorityRunable());
        hPThread.setPriority(10);
        Thread lPThread = new Thread(new MyLowPriorityRunable());
        lPThread.setPriority(1);
        lPThread.start();
        hPThread.start();
    }

在这里插入图片描述
改变优先级依旧随机输出:

        Thread hPThread = new Thread(new MyHighPriorityRunable());
        hPThread.setPriority(1);
        Thread lPThread = new Thread(new MyLowPriorityRunable());
        lPThread.setPriority(10);
        lPThread.start();
        hPThread.start();

在这里插入图片描述

根据例子可以看出,设置优先级也不能保证优先级高的先输出

执行线程时调用run()和start()辨析:

        new Thread(new MyRunable()).start();
        new Thread(new MyRunable()).run();

为什么不能直接调用 run() 方法,而需要调用 start() 方法?

  • 调用 start() 方法可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行,还是在原来的线程里执行run方法
  • 直接执行 run() 方法,会把 run() 方法当成一个 当前线程下的普通方法去执行,并不会开启新线程去执行它,所以这并不是多线程工作。
  • start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。
    public static class MyRunable implements Runnable{

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "  run方法里开启的线程");
                }
            }).run();
        }
    }

new Thread(new MyRunable()).start();

在这里插入图片描述
若执行线程的方法改成

new Thread(new MyRunable()).run();

在这里插入图片描述
以上就是t.run()和t.start()的区别

yield():

public static native void yield();
  • 静态方法
  • 调用yield方法会让当前线程交出CPU权限`,让CPU去执行其他的线程,它跟sleep方法类似,同样不会释放锁,但是yield不能控制具体的交出CPU的时间,
  • 注意,调用yield方法并不会让线程进入到阻塞状态,而是让线程重新回到就绪状态,等待重新获得CPU执行时间的机会,这一点和sleep的不一样的。
  • yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。

很少使用,因此不写案例。

wait():

  • 这个其实是Object的方法
  • 一般需要和notify()以及notifyAll()一起使用,用于协调多个线程对共享数据的存取,必须synchronized语句块内使用,持有拥有对象的锁
  • 该方法会释放对象的“锁标志”。当调用后会使当前线程暂停执行(进入等待阻塞状态),并将当前线程放入对象等待池中,直到调用了 notify() 方法后,将从对象等待池中移出任意一个线程并放入锁标志等待池中,只有锁标志等待池中的线程可以获取锁标志,它们随时准备争夺锁的拥有权。

notify()/notifyall():

  • notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。
  • notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。

案例:利用synchronized + wait + notify使线程交替进行

先从字母开始,然后字母,数字线程交替执行

        Object o = new Object();
        char[] chs1 = "12345".toCharArray();
        char[] chs2 = "ABCDEF".toCharArray();
        int len1 = chs1.length;
        int len2 = chs2.length;
        CountDownLatch latch = new CountDownLatch(1);
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (o) {
                    int i = 0;
                    for (char num : chs1) {
                        i++;
                        System.out.print(num);
                        try {
                            if(i < len2){
                                //数字线程后执行,需要i<len2,
                                // 因为当i == len2时,字母线程已经执行完了,不能再唤醒数字线程,会造成线程阻塞
                                o.notify();
                                o.wait();
                            }else{
                                o.notify();
                            }
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }

                    }
                }
            }
        });
        Thread t2 = new Thread(()-> {
                synchronized (o) {
                    int i = 0;
                    for (char num : chs2) {
                        try {
                            i++;
                            System.out.print(num);
                            latch.countDown();
                            if(i <= len1){
                                //字母线程先执行,所以需要 i<= len1,
                                // 因为i==len1的时候,数字线程还没有打印完,还可以唤醒字母线程
                                o.notify();
                                o.wait();
                            }else{
                                o.notify();
                            }
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }

        });
        t1.start();
        t2.start();

在这里插入图片描述
该案例用到了synchronized和CountDownLatch保证同步和按顺序执行,属于并发编程的同步解决方案,后面会另写文章总结
读者看不懂没关系,主要记住多线程交替工作的思路抢锁–>执行–>让锁就可以了,synchronized就是一把 “锁”

join()方法:

  • 方法会使当前线程等待调用 join() 方法的线程结束后才能继续执行。
  • 如在main主线程当中,调用了thread.join()方法,则main方法会等待thread线程执行完毕或者等待一定的时间,如果调用的是无参join方法,则等待执行完毕,如果调用的是指定了时间参数的join方法,则等待一定的时间。
  • join(mills): 传入参数>0则等待线程多少毫秒后并发执行,传入参数若是 <0 则报错,0则等待无限长,直到线程执行完毕,(在A线程里调用B.join(10),那A线程就要等B线程10毫秒后俩者再并发执行)

经典案例:实现十个线程依次计算,最后打印出总和,第1个线程计算1+2+…+10,第2个线程计算11+12+…+20,以此类推

public class CalculateThread extends Thread {

    private int stratNum;
    public static int sum;//10个线程和

    public CalculateThread(int startNum) {
        this.stratNum = startNum;
    }
    public static synchronized void add(int value) {
        sum = sum + value;
    }

    public void run() {
        int sum = 0;

        for (int i = 0; i < 10; i++) {
            sum = sum + stratNum + i;
        }
        System.out.println(Thread.currentThread().getName()+" 计算后和为"+sum);

        add(sum);
    }

    public static void main(String[] args) throws Exception {
        Thread[] threadList = new Thread[10];//线程数组
        for (int i = 0; i < 10; i++) {
            threadList[i] = new CalculateThread(10 * i + 1);
            threadList[i].start();
            threadList[i].join();//每个线程1开始,就排好join 执行完后下一个线程才能执行
        }
        System.out.println("10个线程计算结果相加后和为: " + sum);

    }

}

在这里插入图片描述

Runable和Thread中的run()方法(区别去上面提到的执行时调用的run方法):

  • run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,线程等待获得CPU执行时间,一旦获得了CPU执行时间,便进入run方法区执行具体的任务

interrupt():

  • 顾名思义,就是中断的意思,单独调用interrupt方法可以使得处于阻塞状态的线程抛出一个异常,也就是说它可以用来中断一个正处于阻塞状态的线程。
  • 根据描述,线程在调用wait和sleep后也是处于阻塞状态的,因此是可以响应中断
  • 注意,调用run方法线程不会处于阻塞状态,所以在run方法中写while(true) 也不会响应中断,不过可以搭配isInterrupted()中断线程
  • Thread.interrupted()是一个静态方法,返回中断值,默认是false,被中断后是true
    它会返回调用线程(而不是被调用线程)的中断标志位,返回后重置中断标志位(又置为false)。
public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

案例

  • Thread.interrupted()
             while (true){
                 if(isInterrupted()){
                     System.out.println(Thread.interrupted());
                     System.out.println(Thread.interrupted());
                     return;
                 }
             }
        MyThread t1 = new MyThread();
        t1.start();
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        t1.interrupt();

在这里插入图片描述
可知在调用Thread.interrupted()后,中断标志位又变为false了

  • 利用isInterrupted() + interrupt()来中断 run方法中的while(true)
    public static class MyThread extends Thread{
        @Override
        public void run() {
            while(true){
                if(this.isInterrupted()) {
                    System.out.println(Thread.currentThread().getName() + "  " + Thread.interrupted());
                    return;
                }
                System.out.println(Thread.currentThread().getName() + "    通过继承Thread的方式    " +  Thread.interrupted());
            }

        }
    }

        MyThread t1 = new MyThread();
        t1.start();
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        t1.interrupt();

在这里插入图片描述

  • 中断sleep()
    public static class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "    通过继承Thread的方式    " +  Thread.interrupted());
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "    通过继承Thread的方式    " +  Thread.interrupted());
        }
    }
        MyThread t1 = new MyThread();
        t1.start();
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        t1.interrupt();

在这里插入图片描述

  • 中断wait()
    public static class MyThread extends Thread{
        @Override
        public void run() {
            Object o = new Object();
            System.out.println(Thread.currentThread().getName() + "    通过继承Thread的方式    " +  Thread.interrupted());
            try {
                synchronized (o){
                    o.wait();
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "    通过继承Thread的方式    " +  Thread.interrupted());
        }
    }

        MyThread t1 = new MyThread();
        t1.start();
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        t1.interrupt();

在这里插入图片描述

wait和sleep的区别和联系

  • sleep() 不会释放锁,wait() 会释放锁。
  • 都可以暂停线程的执行
  • wait()通常用来进行线程间的交互/通信,通过Notify() 或 NotifyAll()唤醒,而sleep()用来暂停执行
  • wait() 方法被调用后,不会自动苏醒。需要别的线程来调用同一个锁对象上的 notify/notifyAll 方法。sleep() 方法调用后,经过设置后的时间后会苏醒,也可以通过调用 wait(long timeout),超时后也能自动苏醒。
    上述中断wait案例中:若把wait设置个时间,比sleep少,那么线程就赶在被中断前苏醒,就不会被中断了
o.wait(500);
Thread.sleep(2000);

四.线程的状态以及转换

  • NEW:
    初始/新建状态,线程被创建了,但还没有执行start()方法;
    Thread state for a thread which has not yet started.
  • RUNNABLE(就绪状态和运行状态统称):
    运行状态,这里对应操作系统中的就绪和运行状态,统称为运行中
    Thread state for a runnable thread. A thread in the runnable state is executing in the Java virtual machine but it may be waiting for other resources from the operating system such as processor.(在JVM是运行状态,但在操作系统的其他程序可能是等待中(就绪))
  • BLOCKED:
    阻塞状态,表示线程因为锁被阻塞了
    Thread state for a thread blocked waiting for a monitor lock. A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method or reenter(重入) a synchronized block/method after calling Object.wait.
    在等待锁去进入一个synchronized修饰的代码块或方法时,以及调用了wait方法(把锁给让出去)后再等待锁去进入一个synchronized修饰的代码块或方法时会处于这个状态
  • WAITING
    等待状态,线程进入等待状态,需要等待其他线程通过 通知 中断 来唤醒
    Thread state for a waiting thread. A thread is in the waiting state due to calling one of the following methods:
    Object.wait with no timeout
    Thread.join with no timeout
    LockSupport.park
    A thread in the waiting state is waiting for another thread to perform a particular action. For example, a thread that has called Object.wait() on an object is waiting for another thread to call Object.notify() or Object.notifyAll() on that object. A thread that has called Thread.join() is waiting for a specified thread to terminate.
    以下时候线程会处于这个状态:
    1.Object.wait(),不传入参数
    2.Thread.join(),不传入参数
    注意上面是对象调用,因为wait和join都不是静态方法
    3.LockSupport.park()
    4.举例子,调用wait后等待notify/notifyall唤醒时,以及等待join进来的线程执行完后等
  • TIME_WAITING:
    超时等待状态,不同于WAITING,可以在指定时间内自动返回
    Thread state for a waiting thread with a specified waiting time. A thread is in the timed waiting state due to calling one of the following methods with a specified positive waiting time:
    Thread.sleep
    Object.wait with timeout
    Thread.join with timeout
    LockSupport.parkNanos
    LockSupport.parkUntil
    以下时候线程会处于这个状态:
    1.Object.wait(mills),传入参数
    2.Thread.join(mills),传入参数
    注意上面是对象调用,因为wait和join都不是静态方法
    3.LockSupport.parkNanos
    4.LockSupport.parkUntil
    5.Thread.sleep()
  • TERMINATED:
    终止状态,表示线程已经执行完毕
    Thread state for a terminated thread. The thread has completed execution.
    由于每个人的翻译不一样,看的教材也不一样,所以对线程状态的描述也不太一致,其他博客可能也有不一样的称呼。
  • 状态转移图:
    在这里插入图片描述

五.线程交替执行案例----翻上去

六.多线程依次执行案例 ----翻上去

七.多线程并发带来的安全性问题

并发安全性问题的根源:

  • 原子性 : 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。(经典转账问题,一个扣钱一个加钱,必须保证原子性) synchronized可以保证代码片段的原子性
  • 可见性 : 当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile和synchronized关键字都可以保证共享变量的可见性
  • 有序性 : 代码在执行的过程中的先后顺序,JAVA在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序,volatile关键字可以禁止指令进行重排序优化。

在计算机内部,CPU,内存,I/O设备的速度有着极大的差异,CPU巨快,为了合理充分利用CPU的高性能,平衡这三者的速度差异,计算机体系结构,操作系统,编译程序 都做出了贡献

  • 通过设置缓存(计算机体系结构):将一些关键的,常用的数据放置在CPU缓存中,以提高数据的获取速度,均衡了CPU的快速而磁盘读取缓慢的速度差异。但设置缓存也导致了 可见性 的问题。

  • 操作系统增加了进程,线程,以分时复用CPU,进而充分利用CPU,均衡CPU和I/O设备的速度差异,在进行线程切换的时候容易导致了原子性问题

  • 编译程序优化指令执行次序,使得缓存能够得到更加合理的利用(局部性原理),但这也带来有序性的问题,代码的实际执行顺序与代码书写顺序并不一致

解决多线程并发带来的安全性问题的本质其实就是解决原子性,可见性,有序性 带来的问题
由于这是属于多线程并发的内容,因此会另起文章总结。
如volatile,以及锁机制Synchronized和Lock等的原理和使用届时都会归纳总结。

参考与感谢

由于这是很久之前学习的知识做的本地笔记,现今我重新实践并且做归纳总结,因此不知道具体参考了哪些教程和博客。所以我在此感谢所有参考到的教材作者以及博客作者,创造不易。也感谢在这个计数开源的时代,让我能找到很多优秀教程进行学习,并且输出。

真心推荐这篇文章可以让初学者进行归纳学习,毕竟是站在巨人的肩膀上总结的精华。喜欢的可以点赞加关注

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值