多线程与并发(一):线程的基本概念和用法

1. 基础知识点回顾

1.1 并发和并行

并发(Concurrent):系统只有一个CPU,多个线程在操作时,在一段时间内只有一个线程在运行,其他线程处于挂起状态。
并发编程的本质:充分利用CPU的资源

并行(Parallel):系统有一个以上CPU时,多个线程可以同时执行

* 应用之异步调用

从方法调用的角度:

  • 需要等待结果返回,才能继续运动就是同步
  • 不需要等待结果返回,就能继续运行就是异步

注意:同步在多线程中,还有另外一层意思,让多个线程步调一致

1) 设计
多线程可以让方法执行变为异步(即不要干巴巴等着),比如读取磁盘文件时,假设读取操作花费了5秒,如果没有线程调度机制,这五秒调用者什么都做不了,其他代码都得暂停

2) 结论

  • 比如在项目中视频文件需要转换格式等操作比较费时时,这时开一个新线程处理视频格式转换,避免阻塞主线程
  • UI程序中,开线程进行其他操作,避免阻塞UI线程

方法三:FutureTask 配合 Thread
FutureTask 可以接受 Callable类型参数,用来处理有返回结果的情况

//创建任务对象
FutureTask  task3 = new FutureTask<>(()->{
    log.debug("hello");
    return 100;
});

new Thread(task3, "task3").start();

//主线程阻塞,等待task3执行完毕
Integer result = task3.get();

3.4 线程运行原理

栈与栈帧

  • 每个栈由多个栈帧组成,对应着每次方法调用所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

线程上下文切换
因为以下一些原因导致CPU不再执行当前线程,转而执行另一个线程的代码

  • 线程的CPU时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了sleep ,yield, wait, join, park, synchronized, lock等方法

当发生上下文切换时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器,它的作用就是记住下一条jvm指令的执行地址,是线程私有的

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量表,操作数栈,返回地址等
  • 上下文切换频繁发生会影响性能

3.5 常见方法

方法名 功能说明 注意
start() 启动一个新的线程 start()方法只是让线程进入就绪状态,里面的代码不一定立刻运行(CPU时间片还没分给它)。每个线程对象的start 方法只能调用一次,调用多次会出现IllegalThreadStateException
run() 新线程启动后调用的方法
join() 等待线程运行结束 用在两个线程通信时
join(long n) 等待线程运行结束,最多等待n毫秒
getId() 获取线程id id唯一
getName() 获取线程名
setName(String) 修改线程名
getPriority() 获取线程优先级
setPriority() 修改线程优先级 java中规定线程优先级是1~10的整数,较大的优先级能提高该线程被CPU调度的几率
getState() 获取线程状态 java中线程状态是由6个enum表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING
isInterrupted() 判断是否打断 不会清除打断标记
isAlive() 线程是否存活(还没有运行完)
interrupt() 打断线程 如果被打断线程正在sleep, wait, join 会导致被打断的线程抛出InterruptedException,并清除 打断标记;如果打断正在运行的程序,则会设置打断标记;park线程被打断,也会设置打断标记
interrupted() 静态方法,判断当前线程是否被打断 会清除打断标记
sleep(long n) 静态方法,让当前执行线程休眠n毫秒,休眠时让出CPU时间片给其他线程
yield() 静态方法,提示线程调度器让出当前线程对CPU的使用 主要是为了测试和调试

3.7 sleep 与yield

sleep

1.调用sleep 会让当前线程从RUNNING 进入 Timed_Waiting状态

  1. 其他线程可以使用interrupt 方法打断正在睡眠的线程,这时sleep 方法会抛出InterruptedException.
  2. 睡眠结束后的线程未必会立刻得到执行
  3. 建议用TimeUnit 的sleep 代替Thread 的sleep 来获得更好的可读性

yield

  1. 调用yield 会让当前线程从RUNNING 进入 RUNNABLE 状态,然后调度执行其他同优先级的线程。如果这段时间没有同优先级的线程,那么不能保证让当前线程暂停的效果
  2. 具体的实现依赖于操作系统的任务调度

2.2 线程优先级

  • 线程优先级会提示调度器优先调度该线程,但仅仅是一个提示,调度器可以忽略
  • 如果CPU比较忙,那么优先级高的线程就会获得更多的时间片,但CPU闲下来,优先级几乎没用

3.8 join 方法详解

为什么需要join

下面的代码执行,打印的r是什么?

public class JoinTest {

    static int r =0;

    public static void main(String[] args) {
        test();
    }

    private static void test() {

        Thread t = new Thread(()->{
            System.out.println(Thread.currentThread().getName() + " start ...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r = 10;
            System.out.println(Thread.currentThread().getName() + " end ...");
        });

        t.start();
        System.out.println(Thread.currentThread().getName() + " result is " + r);
    }
}

执行结果:
main result is 0
Thread-0 start ...
Thread-0 end ...


分析:

  • 因为主线程和线程t是并行执行的,t线程需要1秒之后才能计算出r=10;
  • 而主线程一开始就要打印结果,所以只能打印r = 0;

解决方法

  • 让主线程睡一会,用sleep 行不行?为什么?
  • 用join,加在 t.start之后即可
public class JoinTest {

    static int r =0;

    public static void main(String[] args) {
        test();
    }

    private static void test() {

        Thread t = new Thread(()->{
            System.out.println(Thread.currentThread().getName() + " start ...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r = 10;
            System.out.println(Thread.currentThread().getName() + " end ...");
        });

        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " result is " + r);
    }
}

执行结果:
Thread-0 start ...
Thread-0 end ...
main result is 10


join 方法源码

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

* 应用之同步(案例1)

调用方角度来讲,如果

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能继续运行就是异步
public class JoinTest {
    static int r1 = 0;
    static int r2 = 0;

    public static void main(String[] args) {
        test2();
    }

    private static void test2() {
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " start ...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r1 = 10;
            System.out.println(Thread.currentThread().getName() + " end ...");
        });


        Thread t2 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " start ...");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r2 = 20;
            System.out.println(Thread.currentThread().getName() + " end ...");
        });

        t1.start();
        t2.start();
        long start = System.currentTimeMillis();
        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();

        System.out.println("r1 is "+ r1 + " ; r2 is " + r2 + " ; time is " + (end - start)); //注意这个时间差值
    }
}

注意end - start 的时间差值

执行结果:
Thread-0 start ...
Thread-1 start ...
Thread-0 end ...
Thread-1 end ...
r1 is 10 ; r2 is 20 ; time is 2002


分析如下:

  • 第一个join:等待t1 时,t2并没有停止,而在运行
  • 第二个join: 1s后,执行到此,t2也运行了 1s,因此只需再等待1s。
    如果颠倒两个join 呢?
    最终输出都是一样的。

注意 :如果线程提前结束,join 也不必继续等待。

有时效的join(long n)
public class JoinTest {

    static int r = 0;

    public static void main(String[] args) {
        test3();
    }

    private static void test3() {
        Thread t = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " start ...");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r = 10;
            System.out.println(Thread.currentThread().getName() + " end ...");
        });

        t.start();
        try {
            t.join(1500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " result is " + r);
    }
}

执行结果:
Thread-0 start ...
main result is 0
Thread-0 end ...


2.4 interrupt 方法详解

打断 sleep, wait , join 的线程

打断sleep 的线程,会清空打断状态,以sleep为例

public class SleepTest {

    public static void main(String[] args) {

        Thread t = new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        t.start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.interrupt();
        System.out.println("interrupt is " + t.isInterrupted());
    }
}

执行结果:
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.lily.base.SleepTest.lambda0(SleepTest.java:9)
at java.lang.Thread.run(Thread.java:748)
interrupt is false


public class SleepTest {

    public static void main(String[] args) {

        Thread t = new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        t.start();
        t.interrupt();
        System.out.println("interrupt is " + t.isInterrupted()); //还没有睡眠
    }
}

执行结果:
interrupt is true(还没有睡眠打断的是正常的线程)
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.lily.base.SleepTest.lambda0(SleepTest.java:9)
at java.lang.Thread.run(Thread.java:748)


打断正常的线程

打断正常运行的线程,不会清空打断标记

public class InterruptTest {

    public static void main(String[] args) {

        Thread t = new Thread(()->{
            while (true) {

            }
        });

        t.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        t.interrupt();
    }
}

调用interrupt()方法打断仍然继续运行,但是此时 打断标记为真,可根据该字段结束线程

public class InterruptTest {

    public static void main(String[] args) {

        Thread t = new Thread(()->{
            while (true) {
                if (Thread.currentThread().isInterrupted()){
                    break;
                }
            }
        });

        t.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        t.interrupt();
    }
}

由此发现打断线程可以实现停止线程。

打断park线程

打断park 线程,不会清除打断标记

public class ParkInterruptTest {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            System.out.println("park...");
            LockSupport.park();
            System.out.println("unpark...");
            System.out.println("打断状态 " + Thread.currentThread().isInterrupted());
        });

        t.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.interrupt();
    }
}

执行结果:
park...
unpark...
打断状态 true


如果打断标记已经是true ,则park 会失效。

public class ParkInterruptTest {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            System.out.println("park...");
            LockSupport.park();
            System.out.println("unpark...");
            System.out.println("打断状态 " + Thread.currentThread().isInterrupted());

            LockSupport.park();
            System.out.println("unpark 1...");
        });

        t.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.interrupt();
    }
}

执行结果:
park...
unpark...
打断状态 true
unpark 1...


2.5 不推荐方法

有一些不推荐使用的方法,方法已过时,容易破坏同步代码块,造成死锁

方法名 static 功能说明
stop() 停止线程运行
suspend() 挂起(暂停)线程运行
resume 恢复线程运行

3. 主线程和守护线程

默认情况下,java进程需要等待所有 线程都运行结束,才会结束。有一种特殊的线程叫守护线程。只要其他非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束

public class DeamonTest {

    public static void main(String[] args) {

        Thread t = new Thread(()->{
            System.out.println(Thread.currentThread().getName() + " start");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " end");
        });

        t.setDaemon(true);
        t.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " end");
    }
}

执行结果:
Thread-0 start
main end


注意:垃圾回收器就是一种守护线程

4. 线程五种状态

从操作系统层面描述


线程五种状态.jpg
  • 初始状态:仅是在语言层面创建了线程对象,还未与操作系统线程关联
  • 可运行状态:指该线程已经被创建(与操作系统线程关联),可以由cpu调度执行
  • 运行状态:指获取了CPU时间片运行中的状态
    • 当CPU时间片用完,会从运行状态转换至可运行状态,会导致线程上下文切换
  • 阻塞状态
    • 如果调用了阻塞API,如BIO读写文件,这时该线程实际不会用到CPU,会导致线程上下文切换,进入阻塞状态
    • 等BIO操作完,会由操作系统唤醒阻塞的线程,转换至可运行状态
  • 终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其他状态

5. 线程六种状态

这时从JAVA api层面描述的
根据Thread.State 枚举,分为六种状态


线程状态转换.jpg

源码:

public enum State {
    NEW,
   
   /**
    * 可运行状态。线程对象调用start()方法
    * 如果处于Runnable的线程调用yield()让出JVM资源,那么就会进入New状态和其他New状态 
    * 线程进行竞争重新进入Runnable
    */
    RUNNABLE,

   /**
    * 阻塞状态。阻塞线程不释放当前占有的系统资源,
    * 当sleep()结束或者join()等待其他线程来到,当前线程进入Runnable状态等待JVM分配资 
    * 源。
    * 线程对象调用sleep()或者join()方法
    */
    BLOCKED,

   /**
    * 等待状态。
    *
    *  {@link Object#wait() Object.wait} with no timeout
    *  {@link #join() Thread.join} with no timeout
    *  {@link LockSupport#park() LockSupport.park}
    * 
    *一个线程已经调用了Object.wait(),会等待另一个线程调用Object.notify()或Object.notify()
    *一个已经调用Thread.join()的线程将等待这个指定线程终止
    */
    WAITING,

   /**
    *  定时等待状态:
    *  当线程进入Runnable状态,但还没有开始运行的时候,此时发现需要的资源处于同步状态 
    *  synchronized,这个时候线程将会进入Timed_waiting,JVM会使用队列对这些线程进行控 
    *  制,是这样状态的线程会先得到JVM资源进行执行进入Waiting
    *   {@link #sleep Thread.sleep}
    *   {@link Object#wait(long) Object.wait} with timeout
    *   {@link #join(long) Thread.join} with timeout
    *   {@link LockSupport#parkNanos LockSupport.parkNanos}
    *   {@link LockSupport#parkUntil LockSupport.parkUntil}
    * 
    */
    TIMED_WAITING,

    TERMINATED;
    }
  • NEW : 线程刚被创建,但是还没有调用start()方法
  • RUNNABLE: 当调用了start() 方法之后,注意,java API层面的RUNNABLE 状态涵盖了操作系统层面的 可运行状态运行状态阻塞状态(由于BIO导致的线程阻塞,在Java中无法区分,仍然认为是可运行的)
  • BLOCKED : WAITING TIMED_WAITING 都是java API层面对阻塞状态的细分
  • TERMINATED : 当线程代码运行结束

六种状态演示

public class StateTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            System.out.println("running");
        },"t1");
        Thread t2 = new Thread(()->{
            while(true){

            }
        },"t2");
        t2.start();

        Thread t3 = new Thread(()->{
            System.out.println("running");
        },"t3");
        t3.start();

        Thread t4 = new Thread(()->{
            synchronized (StateTest.class){
                try{
                    Thread.sleep(100000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        },"t4");
        t4.start();

        Thread t5 = new Thread(()->{
            try{
                t2.join();
            }catch (InterruptedException e){
                e.printStackTrace();
            }

        },"t5");

        t5.start();

        Thread t6 = new Thread(()->{
            synchronized (StateTest.class){
                try{
                    Thread.sleep(100000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }

        },"t6");

        t6.start();

        try{
            Thread.sleep(500);
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        System.out.println("t1 state is " + t1.getState());
        System.out.println("t2 state is " + t2.getState());
        System.out.println("t3 state is " + t3.getState());
        System.out.println("t4 state is " + t4.getState());
        System.out.println("t5 state is " + t5.getState());
        System.out.println("t6 state is " + t6.getState());
    }
}

执行结果:
running
t1 state is NEW
t2 state is RUNNABLE
t3 state is TERMINATED
t4 state is TIMED_WAITING
t5 state is WAITING
t6 state is BLOCKED


你可能感兴趣的:(多线程与并发(一):线程的基本概念和用法)