一.线程基础

1. 基础概念

1.1 进程和线程

概念:

进程:运行中的程序。指操作系统分配资源(比如内存等)的最小单位。

线程:程序执行的最小单位。或者叫CPU调度的最小单位。

区别:

  • 进程就是线程的容器,线程需要利用进程中的一些资源,处理代码,指令,最终实现进程锁预期的结果。
  • 同一个进程下的线程会共享进程中的资源。同时线程也拥有自身的独立存储空间(CPU)。而进程之间都是相互独立的。
  • 一个进程中至少有一个线程。进程就是一个进程。
  • 开销方面:线程的创建和终止的时间比较短。线程之间的切换速度比进程之间的切换速度快很多。
  • 进程之间通讯麻烦很多,需要借助操作系统的内核才可以实现。而线程之间通讯则方便很多,比如借助exchange等方法。

1.2 多线程

多线程是指一个进程中同时运行多个线程。

多线程目的:是为了提高CPU的利用率。可以通过避免一些网络IO或者磁盘IO的等待时间,让CPU去调用其他线程。可以大幅度提高程序的效率。

多线程的局限:

  • 如果线程特别多,那么CPU切换线程上下文时,会额外造成很大的消耗。
  • 任务的拆分需要依赖业务的场景。有一些异构化的任务,很难对任务拆分(比如对库存进行扣除操作)。
  • 线程安全问题:虽然多线程带来了一定的性能提升,但是做一些操作时,比如线程操作临界资源(共享资源),可能会发生一些数据不一致的安全问题,如果涉及到锁操作,甚至可能发生死锁问题。

1.3 串行、并行、并发

串行:类似队列,以串行化的方式进行操作,只有前一个处理完,后一个才能开始处理。

并行:并行就是同时处理。多核CPU同时调度多个线程,是真正多个线程同时运行。单核CPU是并发,无法做到并行。

并发:(这里的并发并不是三高中的高并发,而是多线程中的并发概念,也就是CPU调度线程的概念)指的是CPU在极短的时间内,反复切换执行不同的线程。看似是并行,实际上却是CPU极速切换,速度太快。

1.4 同步异步、阻塞非阻塞

同步异步:(指的是消息)执行某个功能后,被调用者是否会主动反馈信息。主动通知就是异步,否则就是同步。

  • 同步:一次调用,一次返回,整个过程就是同步调用的过程。
  • 异步:一次调用,没有返回或者不关心返回,而是通过后面的回调或者状态消息通知获取结果。

阻塞和非阻塞:(指的是进程/任务)执行某个功能后,调用者是否需要一直等待结果的反馈。

  • 阻塞:调用方线程在等待结果返回过程中,线程被挂起,(调用方不能处理其它事情)等结果返回后,唤醒线程;
  • 非阻塞:调用方线程在等待结果的过程中,线程没有被挂起,(可以处理其它事情)。

两个概念的侧重点不同。

同步阻塞、同步非阻塞、异步阻塞、异步非阻塞。

同步阻塞:烧水时,水不会通知你,你需要时不时去看查看(同步)。烧水过程中,你需要一直在旁边,不能干其他事(阻塞)。

同步非阻塞:烧水时,水不会通知你,你需要时不时去看查看(同步)。烧水过程中,你不需要一直在旁边,可以去做其他事(非阻塞)。

异步阻塞:水烧开时,水会通知你,你不需要去看查看(异步)。烧水过程中,你需要一直在旁边,不能干其他事(阻塞)。这种情况很少。

异步非阻塞:水烧开时,水会通知你,你不需要去看查看(异步)。烧水过程中,你不需要一直在旁边,可以去做其他事(非阻塞)。

异步非阻塞的效率是最高的。

2. 线程的创建

2.1 继承Thread类

package com.xqm.juc.threadBasic;

public class Test01 {
    public static void main(String[] args) {
        MyJob myJob=new MyJob();
        myJob.start();
    }
}

class MyJob extends Thread{
    @Override
    public void run() {
        System.out.println("继承Thread");
    }
}

由于一个类只能继承一个类,因此这种方式很少用。

2.2 实现Runnable方法

package com.xqm.juc.threadBasic;

public class Test02 {
    public static void main(String[] args) {
        MyRunnable myRunnable=new MyRunnable();
        // 作为Thread的有参构造传进去
        Thread thread=new Thread(myRunnable);
        thread.start();
    }
}

class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("实现Runnable");
    }
}

实现了Runnable方法后,需要new一个Thread,然后将Runnable作为参数传进去。

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

在工作中,最常用的方式:

  • 匿名内部类
  • lambda表达式

匿名内部类:

package com.xqm.juc.threadBasic;

public class Test03 {
    public static void main(String[] args) {
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("这是匿名内部类");
            }
        });
        thread.start();
    }
}

lambda:

package com.xqm.juc.threadBasic;

public class Test04 {
    public static void main(String[] args) {
        Thread thread=new Thread(()->{
            System.out.println("这是lambda表达式");
        });
        thread.start();
    }
}

2.3 实现Callable方法,重写call方法,配合FutureTask

由于Thread和Runnable线程方式是没有返回值的,因此如果需要返回值,就需要实现Callable方法。

实现Callable方法是同步非阻塞的,需要自己去查询结果。

package com.xqm.juc.threadBasic;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Test05 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable=new MyCallable();
        // 创建FutureTask,传入Callable
        FutureTask futureTask=new FutureTask(myCallable);
        Thread thread=new Thread(futureTask);
        thread.start();
        // 获取结果
        Object o = futureTask.get();
        System.out.println(o);
    }
}

class MyCallable implements Callable{

    @Override
    public Object call() throws Exception {
        System.out.println("实现callable方法");
        int count=0;
        for (int i = 0; i < 100; i++) {
            count+=i;
        }
        return count;
    }
}

2.4 直接通过线程池创建线程

JDK自带线程池、druid线程池等等。

3. 线程的使用

3.1 线程的状态

线程状态有5种、6种、7种,看如何划分。

3.1.1 五种状态

5种状态一般是针对传统的线程状态来说

image.png

3.1.2 六种状态

6种状态是为java中线程准备的,在Thead类中有state枚举。

public enum State {
	NEW,
	RUNNABLE,
	BLOCKED,
	WAITING,
	TIMED_WAITING,
	TERMINATED;
}

image.png

  • NEW:Thread线程被创建出来,但是还没执行start方法。
  • RUNNABLE:Thread线程调用start方法,线程状态就转变成RUNNABLE,不管CPU是否调度。
  • BLOCKED:synchronized没有拿到同步锁,被阻塞状态。
  • WAITING:调用wait方法就会处于这个状态,需要被手动唤醒。
  • TIMED_WAITING:调用sleep或者有时间的等待,线程到时间会自动唤醒。
  • TERMINATED:线程执行完毕或者被异常终止,线程结束状态。

java代码验证:

NEW:

package com.xqm.juc.threadBasic;

public class Test06 {
    public static void main(String[] args) {
        Thread thread=new Thread();
        // NEW
        System.out.println(thread.getState());
    }
}

RUNNABLE:

package com.xqm.juc.threadBasic;

public class Test07 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
          while (true){

          }
        });
        thread.start();
        Thread.sleep(500);
        // RUNNABLE
        System.out.println(thread.getState());
    }
}

BLOCKED:

package com.xqm.juc.threadBasic;

public class Test08 {
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        Thread thread = new Thread(() -> {
            //   线程拿不到锁,处于Blocked状态
            synchronized (o) {

            }
        });
        synchronized (o) {
            thread.start();
            Thread.sleep(500);
            // BLOCKED
            System.out.println(thread.getState());
        }
    }
}

WAITING:

package com.xqm.juc.threadBasic;

public class Test09 {
    public static void main(String[] args) throws InterruptedException {
        Object o=new Object();
        Thread thread = new Thread(() -> {
            synchronized (o){
                try {
                    //需要被手动唤醒
                    o.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        Thread.sleep(500);
        // WAITING
        System.out.println(thread.getState());
    }
}

TIMED_WAITING:

package com.xqm.juc.threadBasic;

import java.util.concurrent.TimeUnit;

public class Test10 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread.start();
        Thread.sleep(100);
        // TIMED_WAITING
        System.out.println(thread.getState());
    }
}

TERMINATED:

package com.xqm.juc.threadBasic;

public class Test11 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("线程结束");
        });
        thread.start();
        Thread.sleep(100);
        // TERMINATED
        System.out.println(thread.getState());
    }
}

3.1.3 七种状态

七种状态:

image.png

3.2 线程常用方法

3.2.1 获取当前线程

有一个currentThread()的静态方法。

package com.xqm.juc.threadBasic;

public class Test12 {
    public static void main(String[] args)  {
        // 获取到当前main线程的对象
        Thread thread = Thread.currentThread();
        // Thread[main,5,main] - 名字-优先级-组名字
        System.out.println(thread);
        // 获取线程名字 main
        System.out.println(thread.getName());
    }
}

3.2.2 线程的名字

通过setName或者在创建参数中设置线程的名字。如果没有设置,就会自动生成Thread0,Thread1…

在线程运行时设置有意义的名称,方便后期排查错误。

package com.xqm.juc.threadBasic;

public class Test13 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            // Thread-0  如果没有指定名称
            System.out.println(Thread.currentThread().getName());
        });
        thread.start();

        Thread thread1 = new Thread(() -> {
            // 指定名称 Thread-模块功能
            System.out.println(Thread.currentThread().getName());
        },"Thread-模块功能");
        // 或者通过setName
        // thread1.setName("指定名称");
        thread1.start();

    }
}

3.2.3 线程优先级

其实就是CPU调度的优先级,默认都是5。最大是10,最小是1。

设置优先级有时可能不是那么有效的,取决于电脑的配置。如果电脑配置很好,实际用处不是很大,即便一个设置了10,一个设置了1,也可能同时会运行完,或者设置了1的线程会先运行完。电脑配置越差,优先级作用体现越明显。

package com.xqm.juc.threadBasic;

public class Test14 {
    public static void main(String[] args) {
        Thread thread=new Thread(()->{
            System.out.println("我是Thread");
        });
        Thread thread1=new Thread(()->{
            System.out.println("我是Thread1");
        });
        thread.setPriority(10);
        thread1.setPriority(1);
        thread1.start();
        thread.start();
    }
}

3.2.4 线程的让步

可以通过Thread的静态方法yeild,将线程从运行态转变为就绪态,让出CPU的时间片。但是可能在让出之后,还是会立即获得CPU的时间片,重新变为运行态。

package com.xqm.juc.threadBasic;

public class Test15 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                if (i == 50) {
                    Thread.yield();
                }
                System.out.println("thread=" + i);
            }
        });
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println("thread1=" + i);
            }
        });
        thread.start();
        thread1.start();
    }
}

3.2.5 线程的休眠

sleep有两个方法重载。

  • 第一个是native修饰的,让线程转为等待状态的方法
  • 第二个是可以传毫秒和纳秒的方法。如果纳秒大于等于0.5毫秒,或者纳秒不为0同时毫秒为0,那就给毫秒+1
public static native void sleep(long millis) throws InterruptedException;
public static void sleep(long millis, int nanos)
    throws InterruptedException {
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }

        sleep(millis);
    }

sleep会抛出InterruptedException异常。调用sleep不会释放自身的锁。

3.2.6 线程的强占

join()方法。是Thread对象调用的方法。在某一个线程下去调用join方法。

package com.xqm.juc.threadBasic;

public class Test16 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("t1=" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("t2=" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        t2.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("main=" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (i == 1) {
                // 这里会等到t1执行完之后才会继续执行
                t1.join();
                // 这里会等待t1执行2s之后,main会继续执行
                // t1.join(2000);
            }
        }
    }
}

由于t1.join()在主线程中调用,实际就是t1线程插队到main线程,main线程就会被挂起,等待t1执行完才会继续执行。

注意:如果在等待期间,比如join(10000),10s,而两秒后t1结束了,这时候main会立即被唤醒,而不是等待10s后才会被唤醒。

3.2.7 守护线程

守护线程的含义就是在后台运行,直到所有的线程结束之后,守护线程会自动结束。

主线程默认是非守护线程。直到所有的非守护线程结束,JVM才会结束。

设置守护线程:

t1.setDaemon(true);

3.2.8 线程的等待和唤醒

wait和motify、notifyAll。每个对象都有自己的锁池和等待池。

可以让获取到锁的线程,通过wait()方法进入到锁的等待池,并且会释放锁资源。

可以让在等待池中的线程,通过notify或者notifyAll方法,将线程唤醒,添加到锁池中,去争夺锁资源。

notify是随机唤醒等待池中一个线程,notifyAll是将等待池中的所有线程唤醒,添加到锁池中。

wait和notify和notifyAll必须放在锁中,也就是synchronized中,否则会抛出异常。因为只有拿到锁资源后才能进行wait,没有锁资源才能进行notify。

package com.xqm.juc.threadBasic;

public class Thread17 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            sync();

        });
        Thread thread1 = new Thread(() -> {
            sync();

        });
        thread.start();
        thread1.start();
        try {
            Thread.sleep(12000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (Thread17.class) {
            // 只会唤醒一个
            Thread17.class.notify();
        }
    }

    public static synchronized void sync() {
        for (int i = 0; i < 10; i++) {
            if (i == 5) {
                try {
                    // 线程进入等待池
                    Thread17.class.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "  " + i);
        }
    }
}

结果:
thread和thread1会先执行5次,然后随机唤醒一个线程,执行完剩下的,另一个线程会一直阻塞下去。

3.3 线程结束方法

线程结束的方式很多,自然执行完毕结束,或者人为结束,或者抛出异常结束。

3.3.1 stop方法(不用)

强制让线程结束,无论线程在干嘛。

package com.xqm.juc.threadBasic;

public class Test18 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            try {
                Thread.sleep(15000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread.start();
        Thread.sleep(1500);
        thread.stop();
        System.out.println(thread.getState());
    }
}

结果:

TERMINATED

3.3.2 破坏死循环-使用共享变量

使用共享变量的时候,需要加上volatile关键字,不然会有线程可见性的问题。

这种方式用的也不多,有的线程会通过死循环来保证线程一直运行,通过共享变量来破坏死循环,达到退出线程的目的。

package com.xqm.juc.threadBasic;

public class Test19 {

    static volatile boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (flag) {

            }
            System.out.println("任务结束");
        });
        thread.start();
        // 等待thread启动
        Thread.sleep(500);
        flag=false;
        // 等待flag生效
        Thread.sleep(500);
        System.out.println(thread.getState());
    }
}

结果:

任务结束
TERMINATED

3.3.3 interrupt

通过 Thread.currentThread().isInterrupted()查看打断标记位是否为false。

package com.xqm.juc.threadBasic;

public class Test20 {
    public static void main(String[] args) {
        // 查看线程是否被打断,实际就是查看标记位是否为false
        System.out.println(Thread.currentThread().isInterrupted());
        // 再次查看
        Thread.currentThread().interrupt();
        // 变成了true,实际上只是改变了标记位,并不是直接打断了线程
        System.out.println(Thread.currentThread().isInterrupted());
        // 返回当前线程的interrupted标记位,并进行归位为false  true
        System.out.println(Thread.interrupted());
        // 这里输出为false,因为标记位归位了
        System.out.println(Thread.interrupted());

    }
}

结果:

false
true
true
false

进行线程的退出:

package com.xqm.juc.threadBasic;

public class Test21 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            while (!Thread.currentThread().isInterrupted()){
                // 执行业务
            }
            System.out.println("退出循环");
        });
        thread.start();
        Thread.sleep(500);
        thread.interrupt();
        Thread.sleep(500);
        System.out.println(thread.getState());
    }
}

结果:

退出循环
TERMINATED

实际上打断还是共享变量,标记位。

通过打断WAITING和TIMED_WAITING状态的线程,从而抛出异常自行处理。

package com.xqm.juc.threadBasic;

public class Test22 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            while (true){
                // 获取任务
                // 拿到任务并执行任务
                // 如果没有任务,让线程休眠
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // 这里可以做出自己的处理
                    System.out.println("基于打断标记位进行退出");
                    e.printStackTrace();
                    return;
                }
            }
        });
        thread.start();
        Thread.sleep(500);
        thread.interrupt();
    }
}

结果:

基于打断标记位进行退出
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.xqm.juc.threadBasic.Test22.lambda$main$0(Test22.java:11)
	at com.xqm.juc.threadBasic.Test22$$Lambda$1/565760380.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:745)

这种是阻塞进行中断

package com.xqm.juc.threadBasic;

public class Test23 {
    static Object object = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            synchronized (object) {
                // 获取任务
                // 拿到任务并执行任务
            }
            System.out.println("线程结束");
        });
        synchronized (object) {
            // 主线程拿到锁,开启thread线程
            thread.start();
            // 留时间让thread线程开启
            Thread.sleep(500);
            // thread线程获取不到锁,会进入阻塞状态,这时候打断
            thread.interrupt();
            // 线程休眠
            Thread.sleep(500);
            // 大于线程状态
            System.out.println(thread.getState());
            Thread.sleep(5000);
        }
        // 结果:
        // BLOCKED
        // 线程结束
    }
}

结果:

BLOCKED
线程结束