多线程

程序和进程

Process:指令和数据的有序结合,本身没有运行的含义,是一个静态的概念

Thread:程序的一次执行过程,是动态的概念,是系统分配资源的单位

通常一个进程中可以包含多个线程(至少包含一个),线程是CPU调度和执行的单位

很多多线程是模拟出来的,真正的多线程是指有多个CPU,即多核,如服务器。如果是模拟多线程,即在一个CPU的情况下,同一个时间点,CPU只能执行一个代码,因为切换的很快,就有同时执行的错觉

线程:是独立的执行路径

在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程

main()称之为主线程,是系统的入口,用于执行整个程序

在一个进程中如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序不能人为干预

每个线程在自己的工作内存交互,内存控制不当会导致数据不一致


进程

程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)

线程

一个进程之内可以分为一到多个线程。
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器

二者对比

进程基本上相互独立的,而线程存在于进程内,是进程的一个子集

进程拥有共享的资源,如内存空间等,供其内部的线程共享

进程间通信较为复杂

同一台计算机的进程通信称为 IPC(Inter-process communication)

不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP

线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量

线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

线程创建

Thread类

继承Thread类,重写run()方法,编写线程执行体,创建线程对象,调用start()方法启动线程

Thread类继承了Runnable接口

package com.it.thread;

//继承Thread类
public class TestThread extends Thread{

    public static void main(String[] args) {

        //创建线程对象
        TestThread testThread = new TestThread();
        //调用start()方法
        testThread.start();

        for (int i = 0; i < 2000; i++) {
            System.out.println("study thread" + i);
        }
    }

    //重写run()方法
    @Override
    public void run() {
        for (int i = 0; i < 2000; i++) {
            System.out.println("run thread" + i);
        }
    }

}
...
run thread1243
run thread1244
study thread1862
study thread1863
study thread1864
study thread1865
study thread1866
run thread1245
...

线程开启不一定立即执行,由CPU调度执行

认识多线程中的 start() 和 run()

1、start():

Java API中的介绍:

使该线程开始执行;Java 虚拟机调用该线程的 run 方法。

结果是两个线程并发地运行;当前线程(从调用返回给 start 方法)和另一个线程(执行其 run 方法)。

多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。

用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体中的代码执行完毕而直接继续执行后续的代码。通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里的run()方法 称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。

2、run():

Java API中的介绍:

如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。

Thread 的子类应该重写该方法。

run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。

3、总结:

调用start方法方可启动线程,而run方法只是thread类中的一个普通方法调用,还是在主线程里执行。

多线程同步下载图片

package com.it.thread;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;

//多线程同步下载图片
public class TestThread2 extends Thread{

    private String url;
    private String name;

    public TestThread2(String url, String name){
        this.url = url;
        this.name = name;
    }

    public static void main(String[] args) {
        TestThread2 t1 = new TestThread2("https://upload-bbs.mihoyo.com/upload/2021/07/27/264469153/fdfe02f60ce8078379dbd09e38350f2a_9119338217925927699.jpg?x-oss-process=image//resize,s_500/quality,q_80/auto-orient,0/interlace,1/format,jpg", "原神1.jpg");
        TestThread2 t2 = new TestThread2("https://upload-bbs.mihoyo.com/upload/2021/07/27/218492859/0066d5baf610b5a89ed44dafcbc0fc26_904584898153905617.jpg?x-oss-process=image//resize,s_500/quality,q_80/auto-orient,0/interlace,1/format,jpg","原神2.jpg");
        TestThread2 t3 = new TestThread2("https://upload-bbs.mihoyo.com/upload/2021/07/27/218492859/b69337893471973ae52ca13f4197b1c4_7012300301442561739.jpg?x-oss-process=image//resize,s_500/quality,q_80/auto-orient,0/interlace,1/format,jpg", "原神3.jpg");
        t1.start();
        t2.start();
        t3.start();
    }

    @Override
    public void run() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.download(url, name);
        System.out.println("下载了:" + name);
    }

    class WebDownloader{
        //下载方法
        public void download(String url, String name) {
            try {
                FileUtils.copyURLToFile(new URL(url), new File(name));
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("IO异常,download方法出现问题");
            }
        }
    }

}
下载了:原神3.jpg
下载了:原神1.jpg
下载了:原神2.jpg

Runnable接口

实现Runnable接口,重写run()方法,执行线程需要丢入runnable接口实现类,调用start方法

package com.it.thread;

//实现Runnable接口
public class TestThread3 implements Runnable{

    public void run() {

        for (int i = 0; i < 2000; i++) {
            System.out.println("run thread" + i);
        }

    }

    public static void main(String[] args) {

        //创建runnable接口实现类对象
        TestThread3 testThread3 = new TestThread3();
        //创建线程对象,通过线程对象来开启我们的线程,代理
        new Thread(testThread3).start();
        
        for (int i = 0; i < 2000; i++) {
            System.out.println("study thread" + i);
        }
    }
}

相比较继承Thread类,没有单继承局限性,推荐使用

购票模拟

package com.it.thread;

//多个对象同时操作同一个对象
//买票
public class TestThread4 extends Thread{

    private int ticketNums = 10;

    @Override
    public void run() {
        while(ticketNums > 0){
            //模拟延时200毫秒
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "拿到了票,还剩余" + ticketNums-- + "张票");
        }
    }

    public static void main(String[] args) {
        TestThread4 testThread4 = new TestThread4();

        new Thread(testThread4, "Kobe").start();
        new Thread(testThread4, "Lebron").start();
        new Thread(testThread4, "Chris").start();
        new Thread(testThread4, "Dwight").start();

    }
}
Dwight拿到了票,还剩余8张票
Lebron拿到了票,还剩余9张票
Chris拿到了票,还剩余10张票
Kobe拿到了票,还剩余7张票
Chris拿到了票,还剩余6张票
Kobe拿到了票,还剩余5张票
Dwight拿到了票,还剩余6张票
Lebron拿到了票,还剩余4张票
Kobe拿到了票,还剩余3张票
Chris拿到了票,还剩余0张票
Dwight拿到了票,还剩余1张票
Lebron拿到了票,还剩余2张票

问题:多个线程操作同一个资源的情况,线程不安全,数据紊乱,并发问题

龟兔赛跑

package com.it.thread;

public class Race implements Runnable{

    private static String winner;

    public void run() {
        for (int i = 0; i < 11; i++) {
            if("兔子".equals(Thread.currentThread().getName()) && i == 8){
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if(gameOver(i))break;
            System.out.println(Thread.currentThread().getName() + "跑了" + i + "米");
        }
    }

    private boolean gameOver(int steps){
        if(winner != null)return true;//产生胜利者比赛结束
        if(steps >= 10){
            winner = Thread.currentThread().getName();
            System.out.println("winner is " + winner);
            return true;
        }
        return false;
    }

    public static void main(String[] args) {

        Race race = new Race();
        new Thread(race, "乌龟").start();
        new Thread(race, "兔子").start();

    }
}
乌龟跑了0米
兔子跑了0米
乌龟跑了1米
兔子跑了1米
乌龟跑了2米
兔子跑了2米
乌龟跑了3米
兔子跑了3米
乌龟跑了4米
兔子跑了4米
乌龟跑了5米
兔子跑了5米
乌龟跑了6米
兔子跑了6米
乌龟跑了7米
兔子跑了7米
乌龟跑了8米
乌龟跑了9米
winner is 乌龟

Callable接口

  1. 实现Callable接口,需要返回值类型,

  2. 重写call()方法,需要抛出异常

  3. 创建目标对象

  4. 创建执行服务

  5. 提交执行

  6. 获取结果

  7. 关闭服务

package com.it.thread;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;

//继承Callable接口
public class TestCallable implements Callable<Boolean> {

    private String url;
    private String name;

    public TestCallable(String url, String name){
        this.url = url;
        this.name = name;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        TestCallable t1 = new TestCallable("https://upload-bbs.mihoyo.com/upload/2021/07/27/264469153/fdfe02f60ce8078379dbd09e38350f2a_9119338217925927699.jpg?x-oss-process=image//resize,s_500/quality,q_80/auto-orient,0/interlace,1/format,jpg", "原神1.jpg");
        TestCallable t2 = new TestCallable("https://upload-bbs.mihoyo.com/upload/2021/07/27/218492859/0066d5baf610b5a89ed44dafcbc0fc26_904584898153905617.jpg?x-oss-process=image//resize,s_500/quality,q_80/auto-orient,0/interlace,1/format,jpg","原神2.jpg");
        TestCallable t3 = new TestCallable("https://upload-bbs.mihoyo.com/upload/2021/07/27/218492859/b69337893471973ae52ca13f4197b1c4_7012300301442561739.jpg?x-oss-process=image//resize,s_500/quality,q_80/auto-orient,0/interlace,1/format,jpg", "原神3.jpg");

        //创建执行服务,创建线程池
        ExecutorService ser = Executors.newFixedThreadPool(3);

        //提交执行,详情见线程池
        Future<Boolean> result1 = ser.submit(t1);
        Future<Boolean> result2 = ser.submit(t2);
        Future<Boolean> result3 = ser.submit(t3);

        //获取结果
        System.out.println(result1);
        boolean r1 = result1.get();
        boolean r2 = result2.get();
        boolean r3 = result3.get();
        System.out.println(r1);
        System.out.println(r2);
        System.out.println(r3);

        if(r1&&r2&&r3){
            System.out.println("下载成功");
            //关闭服务
            ser.shutdown();
        }

    }

    public Boolean call(){
        TestCallable.WebDownloader webDownloader = new TestCallable.WebDownloader();
        webDownloader.download(url, name);
        System.out.println("下载了:" + name);
        return true;
    }

    class WebDownloader{
        //下载方法
        public void download(String url, String name) {
            try {
                FileUtils.copyURLToFile(new URL(url), new File(name));
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("IO异常,download方法出现问题");
            }
        }
    }
}
下载了:原神1.jpg
下载了:原神2.jpg
下载了:原神3.jpg
true
true
true
下载成功

优点:

  1. 可以定义返回值

  2. 可以抛出异常

静态代理模式

package com.it.proxy;

//模拟结婚代理
public class StaticProxy {

    public static void main(String[] args) {
        Marry marry = new WeddingCompany(new You());
        marry.HappyMarry();
    }
}

interface Marry{
    void HappyMarry();
}

class You implements Marry{
    public void HappyMarry() {
        System.out.println("你结婚了");
    }
}

class WeddingCompany implements Marry{

    private Marry target;

    public WeddingCompany(Marry target){
        this.target = target;
    }

    public void HappyMarry() {
        before();
        this.target.HappyMarry();
        after();
    }

    private void after() {
        System.out.println("收尾款");
    }

    private void before() {
        System.out.println("布置现场");
    }
}
布置现场
你结婚了
收尾款

总结

  1. 目标对象和代理对象都要实现同一个接口
  2. 代理对象要代理目标对象
  3. 好处:代理对象可以做一些目标对象做不了的事情,目标对象可以专注做自己的事情

Thread类和目标类都实现了Runnable接口,运用了静态代理模式

new Thread( ()-> System.out.println("I love U")).start();//Lambda 表达式
new WeddingCompany(new You()).HappyMarry();

Java Lambda 表达式

Lambda:希腊字母表的第11个字母λ

  1. 语法格式:

    (parameters) -> expression 或 (parameters) ->

    参数类型可以不写

  2. 重要特征:

  • **可选类型声明:**不需要声明参数类型,编译器可以统一识别参数值。
  • **可选的参数圆括号:**一个参数无需定义圆括号,但多个参数需要定义圆括号。
  • **可选的大括号:**如果主体包含了一个语句,就不需要使用大括号。
  • **可选的返回关键字:**如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。

避免匿名内部类定义过多,其实质属于函数式编程的概念

函数式接口(Functional Interface)

任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口(注解:@FunctionalInterface)

对于函数式接口,我们可以通过lambda表达式来创建该接口的对象

package com.it.proxy;

//推导lambda表达式
public class TestLambda {

    //3.静态内部类
    static class Like2 implements ILike{

        @Override
        public void lambda() {
            System.out.println("I like Lambda too");
        }
    }

    public static void main(String[] args) {

        ILike iLike = new Like();
        iLike.lambda();

        iLike = new Like2();
        iLike.lambda();

        //4.局部内部类
        class Like3 implements ILike{
            @Override
            public void lambda() {
                System.out.println("I also like Lambda");
            }
        }

        new Like3().lambda();

        //5.匿名内部类,没有类的名称,必须借助接口或者父类
        iLike = new ILike() {
            @Override
            public void lambda() {
                System.out.println("I like lambda more");
            }
        };
        iLike.lambda();

        //6.Lambda表达式
        iLike = ()-> System.out.println("I am lambda");
        iLike.lambda();
    }
}

//1.定义一个函数式接口
interface ILike{
    void lambda();//abstract
}

//2.实现类
class Like implements ILike{

    @Override
    public void lambda() {
        System.out.println("I like Lambda");
    }
}

线程状态

  1. 创建状态
  2. 就绪状态
  3. 运行状态
  4. 阻塞状态
  5. 死亡状态

5Q5LF{ZCV6Q)AJ{7WRUYO

停止线程

package com.it.thread;

//测试停止线程
//1、建设线程正常停止--->利用次数,不建议死循环
//2、建议使用标志位
//3、不要使用stop或destroy等过时的方法
public class TestStop implements Runnable{
    private boolean flag = true;


    @Override
    public void run() {
        int i = 0;
        while (flag) System.out.println("run...Thread " + i++);
    }

    //设置公开方法停止线程,转换标志位
    public void stop(){
        this.flag = false;
    }

    public static void main(String[] args) {
        TestStop testStop = new TestStop();
        new Thread(testStop).start();

        for (int i = 0; i < 20; i++) {

            System.out.println("main + " + i);
            if(i == 9){
                testStop.stop();
                System.out.println("线程停止");
            }

        }
    }
}
main + 0
main + 1
run...Thread 0
main + 2
run...Thread 1
run...Thread 2
run...Thread 3
run...Thread 4
run...Thread 5
run...Thread 6
run...Thread 7
run...Thread 8
run...Thread 9
run...Thread 10
run...Thread 11
run...Thread 12
main + 3
run...Thread 13
main + 4
run...Thread 14
main + 5
run...Thread 15
main + 6
run...Thread 16
main + 7
run...Thread 17
run...Thread 18
run...Thread 19
run...Thread 20
run...Thread 21
run...Thread 22
main + 8
main + 9
run...Thread 23
线程停止
main + 10
main + 11
main + 12
main + 13
main + 14
main + 15
main + 16
main + 17
main + 18
main + 19

阻塞线程

sleep(时间)制定线程阻塞的毫秒数

存在异常InterruptedException

sleep时间到达后线程进入就绪状态

可以用来模拟网络延时和倒计时等

每一个对象都有一个锁,sleep不会释放锁

package com.it.thread;

import java.text.SimpleDateFormat;
import java.util.Date;

//模拟倒计时
public class TestSleep {

    public static void main(String[] args) {
        Date startTime = new Date(System.currentTimeMillis());
        while(true){
            try {
                Thread.sleep(1000);
                System.out.println(new SimpleDateFormat(("yyyy/MM/dd HH:mm:ss")).format(startTime));
                startTime = new Date(System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}
2021/07/29 20:41:05
2021/07/29 20:41:06
2021/07/29 20:41:07
2021/07/29 20:41:08
2021/07/29 20:41:09
...

线程礼让

yield()让当前正在执行的线程暂停,但不阻塞

线程从运行状态变为就绪状态

让CPU重新调度,礼让不一定成功,由CPU决定

package com.it.thread;

public class TestYield {

    public static void main(String[] args) {
        new Thread(new MyYield(), "a").start();
        new Thread(new MyYield(), "b").start();
    }

}

class MyYield implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " start");
        Thread.yield();
        System.out.println(Thread.currentThread().getName() + " end");
    }
}
a start
b start
a end
b end

强制执行线程

join()合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞,不建议使用

package com.it.thread;

public class TestJoin implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println("vip: " + i);
        }
    }

    public static void main(String[] args) {
        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);
        thread.start();

        for (int i = 0; i < 20; i++) {
            System.out.println("main: " + i);
            if(i == 5){
                try {
                    thread.join();
                    System.out.println("-------------");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
main: 0
vip: 0
main: 1
vip: 1
main: 2
vip: 2
main: 3
main: 4
main: 5
vip: 3
vip: 4
vip: 5
vip: 6
vip: 7
vip: 8
vip: 9
vip: 10
vip: 11
vip: 12
vip: 13
vip: 14
vip: 15
vip: 16
vip: 17
vip: 18
vip: 19
vip: 20
vip: 21
vip: 22
vip: 23
vip: 24
vip: 25
vip: 26
vip: 27
vip: 28
vip: 29
vip: 30
vip: 31
vip: 32
vip: 33
vip: 34
vip: 35
vip: 36
vip: 37
vip: 38
vip: 39
vip: 40
vip: 41
vip: 42
vip: 43
vip: 44
vip: 45
vip: 46
vip: 47
vip: 48
vip: 49
-------------
main: 6
main: 7
main: 8
main: 9
main: 10
main: 11
main: 12
main: 13
main: 14
main: 15
main: 16
main: 17
main: 18
main: 19

观测线程状态

  • public static enum Thread.State
    extends Enum<Thread.State>
    

    线程状态。线程可以处于以下状态之一:

    • NEW
      尚未启动的线程处于此状态。

    • RUNNABLE
      在Java虚拟机中执行的线程处于此状态。

    • BLOCKED
      被阻塞等待监视器锁定的线程处于此状态。

    • WAITING
      正在等待另一个线程执行特定动作的线程处于此状态。

    • TIMED_WAITING

      正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。

    • TERMINATED

      已退出的线程处于此状态。

    一个线程可以在给定时间点处于一个状态。 这些状态是不反映任何操作系统线程状态的虚拟机状态。

package com.it.thread;

public class TestThreadState {

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

        //观测状态
        Thread.State state = thread.getState();
        System.out.println("state1: " + state);

        //启动后状态
        thread.start();
        state = thread.getState();
        System.out.println("state2: " + state);

        while(state != Thread.State.TERMINATED){
            Thread.sleep(500);
            state = thread.getState();
            System.out.println("state3: " + state);
        }
    }

}
state1: NEW
state2: RUNNABLE
state3: TIMED_WAITING
state3: TIMED_WAITING
state3: TIMED_WAITING
state3: TIMED_WAITING
state3: TIMED_WAITING
------------------
state3: TERMINATED

线程的优先级

Priority由数字表示,范围1~10

package com.it.thread;

public class TestPriority{

    public static void main(String[] args) {
        System.out.println("main--->" + Thread.currentThread().getPriority());
        MyPriority myPriority = new MyPriority();

        Thread t1 = new Thread(myPriority);
        Thread t2 = new Thread(myPriority);
        Thread t3 = new Thread(myPriority);
        Thread t4 = new Thread(myPriority);
        Thread t5 = new Thread(myPriority);

        t1.setPriority(1);
        t2.setPriority(2);
        t3.setPriority(Thread.MAX_PRIORITY);
        t4.setPriority(Thread.MIN_PRIORITY);
        t5.setPriority(Thread.NORM_PRIORITY);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }

}

class MyPriority implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "--->" + Thread.currentThread().getPriority());
    }
}
main--->5
Thread-0--->1
Thread-1--->2
Thread-2--->10
Thread-3--->1
Thread-4--->5

优先级高的不一定先跑,只是增加权重,调度的概率高,决定权还是在CPU的调度

先设置优先级,后启动

守护线程

线程分为用户线程和守护(daemon)线程

虚拟机必须确保用户线程执行完毕

虚拟机不必等待守护线程执行完毕,如后台记录操作日志,监控内存,垃圾回收等待…

package com.it.thread;

public class TestDaemon {

    public static void main(String[] args) {
        God god = new God();
        You you = new You();
        Thread thread = new Thread(god);
        thread.setDaemon(true); //设置为守护线程
        thread.start();

        new Thread(you).start();
    }
}

class God implements Runnable{

    @Override
    public void run() {
        while(true){
            System.out.println("god bless you");
        }
    }
}

class You implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 80; i++) {
            System.out.println("you are " + i + "years old");
        }
        System.out.println("goodbye, world");
    }
}
god bless you
god bless you
...
god bless you
you are 0years old
god bless you
...
you are 79years old
god bless you
...
goodbye, world
god bless you
god bless you
god bless you
god bless you
god bless you
god bless you
...

线程同步机制

并发:同一个对象被多个线程同时操作

处理多线程问题时,某些线程还想修改这个对象,这时我们就需要线程同步,线程同步其实就是一种等待机制,多个需要同时访问这个对象的线程进入这个线程的等待池形成队列,等待前面的线程使用完毕,下一个线程再使用

为了保证数据在方法中被访问时的正确性,在访问时加入锁机制 synchronized,每个对象都有一把锁,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可

  • 一个线程持有锁会导致其他需要此锁的线程挂起
  • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换 和 调度延时,引起性能问题
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁 会导致优先级倒置,引起性能问题
package com.it.thread;

//安全问题
public class TestBank {

    public static void main(String[] args) {
        new Drawing("Me", 50, 0).start();
        new Drawing("You", 100, 0).start();
    }


    static class Drawing extends Thread{
        private String name;
        private int drawMoney;
        private int nowMoney;

        public Drawing(String name, int drawMoney, int nowMoney) {
            this.name = name;
            this.drawMoney = drawMoney;
            this.nowMoney = nowMoney;
        }

        @Override
        public void run() {
            //sleep放大问题的发生性
            try {
                sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Account.totalMoney -= drawMoney;
            nowMoney += drawMoney;
            System.out.println(name + "取走了" + drawMoney + "元,现在有" + nowMoney + "元,账户余额:" + Account.totalMoney + "元");
        }
    }

    static class Account{
        private static int totalMoney = 100;

        public Account(int totalMoney) {
            this.totalMoney = totalMoney;
        }
    }
}
You取走了100元,现在有100元,账户余额:-50元
Me取走了50元,现在有50元,账户余额:-50元

List的不安全性

package com.it.thread;

import java.util.ArrayList;
import java.util.List;

public class UnsafeList {

    public static void main(String[] args) {

        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->list.add(Thread.currentThread().getName())).start();
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }

}
9998

同一时间,两个元素被添加到了数组的同一位置,就导致了添加到List的数据变少

同步方法和同步块

由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,包括两种用法:synchronized方法和synchronized块

synchronized方法控制对对象的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁后面被阻塞的线程才能获得这个锁

缺陷:若将一个大方法设置为synchronized将会影响效率

方法里面需要修改的内容才需要锁

package com.it.thread;

public class TestBank {

    public static void main(String[] args) {
        Account account = new Account(100);
        new Drawing(account, "Me", 50, 0).start();
        new Drawing(account, "You", 100, 0).start();
    }


    static class Drawing extends Thread{
        private String name;
        private int drawMoney;
        private int nowMoney;
        private Account account;

        public Drawing(Account account, String name, int drawMoney, int nowMoney) {
            this.account = account;
            this.name = name;
            this.drawMoney = drawMoney;
            this.nowMoney = nowMoney;
        }

        @Override
        public void run() {
            synchronized (account){
                //sleep放大问题的发生性
                try {
                    sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                account.totalMoney -= drawMoney;
                nowMoney += drawMoney;
                System.out.println(name + "取走了" + drawMoney + "元,现在有" + nowMoney + "元,账户余额:" + account.totalMoney + "元");
            }

        }
    }

    static class Account{
        private int totalMoney;

        public Account(int totalMoney) {
            this.totalMoney = totalMoney;
        }
    }
}
Me取走了50元,现在有50元,账户余额:50元
You取走了100元,现在有100元,账户余额:-50元
package com.it.thread;

//多个对象同时操作同一个对象
//买票
public class TestThread4 extends Thread{

    private int ticketNums = 10;

    @Override
    public synchronized void run() {
        while(ticketNums > 0){
            //模拟延时200毫秒
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "拿到了票,还剩余" + ticketNums-- + "张票");
        }
    }

    public static void main(String[] args) {
        TestThread4 testThread4 = new TestThread4();

        new Thread(testThread4, "Kobe").start();
        new Thread(testThread4, "Lebron").start();
        new Thread(testThread4, "Chris").start();
        new Thread(testThread4, "Dwight").start();

    }
}
Kobe拿到了票,还剩余10张票
Kobe拿到了票,还剩余9张票
Kobe拿到了票,还剩余8张票
Kobe拿到了票,还剩余7张票
Kobe拿到了票,还剩余6张票
Kobe拿到了票,还剩余5张票
Kobe拿到了票,还剩余4张票
Kobe拿到了票,还剩余3张票
Kobe拿到了票,还剩余2张票
Kobe拿到了票,还剩余1张票
package com.it.thread;

import java.util.ArrayList;
import java.util.List;

public class UnsafeList {

    public static void main(String[] args) {

        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                synchronized (list){
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }

}
10000
package com.it.thread;

import java.util.concurrent.CopyOnWriteArrayList;

//测试JUC(java.util.concurrent:并发包)安全类型的集合
public class TestJUC {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->list.add(Thread.currentThread().getName())).start();
        }
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}
10000

同步块:synchronized (Obj)

  • Obj称之为同步监视器
  • 同步方法中无需指定同步监视器,因为同步方法的监视器就是this,即对象本身,或者是class

死锁

多个资源各自占用一些共享资源,并且相互等待其他线程占有的资源才能运行,而导致都停止执行的行为

某一个同步块同时拥有两个以上对象的锁时,就可能发生死锁的问题

package com.it.thread;

//死锁
public class DeadLock {
    public static void main(String[] args) {
        MakeUp girl1 = new MakeUp(1, "A");
        MakeUp girl2 = new MakeUp(0, "B");

        girl1.start();
        girl2.start();
    }
}

//口红
class Lipstick{}

//镜子
class Mirror{}

class MakeUp extends Thread{

    //static保证资源只有一份
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    int choice;//选择
    String user;//用化妆品的人

    public MakeUp(int choice, String user) {
        this.choice = choice;
        this.user = user;
    }

    private void makeup() throws InterruptedException {
        if(choice == 0){
            synchronized (lipstick){//获得口红的锁
                System.out.println(this.user + "获得口红");
                Thread.sleep(1000);

                synchronized (mirror){
                    System.out.println(this.user + "获得镜子");
                }
            }
        }else{
            synchronized (mirror){//获得口红的锁
                System.out.println(this.user + "获得镜子");
                Thread.sleep(2000);

                synchronized (lipstick){
                    System.out.println(this.user + "获得口红");
                }
            }
        }
    }

    @Override
    public void run() {
        try {
            makeup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
B获得口红
A获得镜子

产生死锁的必要条件:

  1. 互斥条件:一个资源每次只能被一个进程使用
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
  4. 循环等待条件:若干进程之间形成一种头尾相连的循环等待资源关系

Lock类

从jdk5.0开始,java提供了通过显式定义同步锁对象来实现同步,同步锁使用Lock对象充当

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具,锁提供了对资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问资源之前应先获得Lock对象

ReentrantLock类(可重入锁)实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁和释放锁

与synchronized的区别

  1. Lock是显式锁,需手动开启和关闭,synchronized是隐式锁,出了作用域自动释放
  2. Lock只有代码块锁,synchronized有代码块和代码方法锁
  3. 使用Lock,JVM将花费较少的时间来调度线程,性能较好,并且具有更好的扩展性(提供更多的子类)
  4. 优先使用顺序:Lock > 同步代码块 > 同步方法

线程协作

并发协作模型:生产者/消费者模式

wait():表示线程一直等待,直至其他线程通知,与sleep不同,会释放锁

wait(long timeout):指定等待的毫秒数

notify():唤醒一个处于等待状态的线程

notifyAll():唤醒一个对象上所有调用wait()方法的线程,优先级高的线程优先调度

以上方法均是Object类的方法,只能在同步方法或同步块中使用,否则会抛出异常IIIegalMonitorStateException

package com.it.thread;

//生产者/消费者模式-->利用缓冲区解决:管程法
public class TestPC {

    public static void main(String[] args) {
        SynContainer synContainer = new SynContainer();
        new Productor(synContainer).start();
        new Consumer(synContainer).start();
    }
}

//生产者
class Productor extends Thread{
    SynContainer container;

    public Productor(SynContainer container) {
        this.container = container;
    }

    //生产
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            container.push(new Chicken(i));
            System.out.println("生产了编号为" + i + "的鸡");
        }
    }
}

//消费者
class Consumer extends Thread{
    SynContainer container;

    public Consumer(SynContainer container) {
        this.container = container;
    }

    //消费
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消费了编号为" + container.pop().getId() + "的鸡");
        }
    }
}

//产品
class Chicken{
    private int id;

    public Chicken(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}

//缓冲区
class SynContainer{
    //容器
    Chicken[] chickens = new Chicken[10];
    //计数器
    int count = 0;

    //生产者放入产品
    public synchronized void push(Chicken chicken){
        //如果容器满了,就等待消费者消费
        if(count == chickens.length){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //如果没有满,就丢入商品
        chickens[count] = chicken;
        count++;
        //通知消费者消费
        this.notify();
    }

    //消费者消费产品
    public synchronized Chicken pop(){
        //判断能否消费
        if(count == 0){
            //等待生产者生产
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //如果可以消费
        count--;
        Chicken chicken = chickens[count];
        //吃完,通知生产者生产
        this.notify();
        return chicken;
    }
}
生产了编号为0的鸡
生产了编号为1的鸡
生产了编号为2的鸡
生产了编号为3的鸡
生产了编号为4的鸡
生产了编号为5的鸡
生产了编号为6的鸡
生产了编号为7的鸡
生产了编号为8的鸡
生产了编号为9的鸡
生产了编号为10的鸡
消费了编号为9的鸡
消费了编号为10的鸡
生产了编号为11的鸡
消费了编号为11的鸡
... ...
生产了编号为97的鸡
生产了编号为98的鸡
生产了编号为99的鸡
消费了编号为96的鸡
消费了编号为99的鸡
消费了编号为98的鸡
消费了编号为97的鸡
消费了编号为94的鸡
消费了编号为93的鸡
package com.it.thread;

////生产者/消费者模式-->利用标志位解决:信号灯法
public class TestPC2 {
    public static void main(String[] args) {
        TV tv = new TV();
        new Player(tv).start();
        new Watcher(tv).start();
    }
}

//生成者-->
class Player extends Thread{
    TV tv;

    public Player(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if(i%2 == 0){
                this.tv.play("电影");
            }else{
                this.tv.play("动物世界");
            }
        }
    }
}

//消费者-->观众
class Watcher extends Thread{
    TV tv;

    public Watcher(TV tv) {
        this.tv = tv;
    }
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            this.tv.watch();
        }
    }

}

//产品-->节目
class TV extends Thread{
    //演员表演,观众等待,观众观看,演员等待
    String movie;
    boolean flag = true;

    //表演
    public synchronized void play(String movie){
        if(!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("表演" + movie);
        //通知观众观看
        this.notifyAll();
        this.movie = movie;
        this.flag = !this.flag;
    }

    //观看
    public synchronized void watch(){
        if(flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观看" + this.movie);
        //通知观众观看
        this.notifyAll();
        this.flag = !this.flag;
    }
}
表演电影
观看电影
表演动物世界
观看动物世界
表演电影
观看电影
表演动物世界
观看动物世界
表演电影
观看电影
表演动物世界
观看动物世界
表演电影
观看电影
表演动物世界
观看动物世界
表演电影
观看电影
表演动物世界
观看动物世界
表演电影
观看电影
表演动物世界
观看动物世界
表演电影
观看电影
表演动物世界
观看动物世界
表演电影
观看电影
表演动物世界
观看动物世界
表演电影
观看电影
表演动物世界
观看动物世界
表演电影
观看电影
表演动物世界
观看动物世界

线程池

背景:经常创建和销毁使用量特别大的资源,比如并发情况下的线程,对性能影响很大

思路:提前创建多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现重复利用

好处:提高响应速度(减少了创建的时间)、降低资源消耗(重复利用了池中线程,不需要每次都创建)、便于线程管理(corePoolSize,maximumPoolSize、keepAliveTime…)…

JDK5.0提供了线程池相关的API: ExecutorServiceExecutors

ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor

  • void execute(Runnable command):执行任务/命令。一般用来执行Runnable,无返回值
  • Future submit(Callable task):执行任务,一般用来执行Callable,有返回值
  • void shutdown():关闭连接池

Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

package com.it.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

//测试线程池
public class TestPool {
    public static void main(String[] args) {
        //1、创建服务,创建线程池
        //newFixedThreadPool 参数为线程池大小
        ExecutorService service = Executors.newFixedThreadPool(10);

        //执行
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());

        //2、关闭连接
        service.shutdown();
    }
}

class MyThread implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
pool-1-thread-1
pool-1-thread-4
pool-1-thread-3
pool-1-thread-2
pool-1-thread-5
package com.it.thread;

import java.util.concurrent.*;

//测试线程池
public class TestPool {
    public static void main(String[] args) {
        
        //FutureTask类实现了RunnableFuture接口,RunnableFuture继承了Runnable, Future接口
		FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyThread());
        new Thread(futureTask).start();

        Integer integer = null;
        try {
            integer = futureTask.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(integer);
    }
}

class MyThread implements Callable<Integer> {


    @Override
    public Integer call() throws Exception {
        System.out.println("MyThread");
        return 100;
    }
}
MyThread
100

问题

linux查看进程线程的方法

ps -fe 查看所有进程
ps -fT -p 查看某个进程(PID)的所有线程
kill 杀死进程
top 按大写 H 切换是否显示线程
top -H -p查看某个进程(PID)的所有线程

指令并行原理

线程上下文切换(Thread Context Switch)

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

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

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

状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等

Context Switch 频繁发生会影响性能

了解:上下文切换会带来直接和间接两种因素影响程序性能的消耗. 直接消耗包括: CPU寄存器需要保存和加载, 系统调度器的代码需要执行, TLB实例需要重新加载, CPU 的pipeline需要刷掉; 间接消耗指的是多核的cache之间得共享数据, 间接消耗对于程序的影响要看线程工作区操作数据的大小).

Thread的常见方法

start与run

直接调用 run 是在主线程中执行了 run,没有启动新的线程

使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码

sleep 与 yield

sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)

  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出InterruptedException

  3. 睡眠结束后的线程未必会立刻得到执行

  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

yield

  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器

interrupt

  1. 打断 sleep,wait,join(join的底层还是调用wait) 的线程(打断后都会抛InterruptedException,并且都会清除标记状态)

  2. 打断sleep会清空打断状态

  3. 打断正常运行的线程, 不会清空打断状态

join、join(time)

需要等待结果返回,才能继续运行(可以实现同步)

不推荐的方法stop,suspend,resume

stop会直接终止线程,直接释放所有占用的资源,会破坏锁结构

suspend和resume对应,前者挂起线程,后者恢复线程(使用会很容易造成死锁)

两阶段终止模式

业务

一个线程不断的去记录系统的信息,当用户按下停止按钮时,启动另一个线程去停止记录线程。

旧思路

如何让一个线程 T1 去停止另一个线程 T2 呢?很简单,Thread 为我们提供了一个 API,就是 stop() 方法。

使用 stop 方法确实能达到效果,但是却有个致命的问题,当 stop 一个线程时,该线程在哪里被 stop 我们是无法感知的! stop 方法并不稳定,现在已经过时。

新思路

可应采取两阶段暂停模式来优雅的停止线程。而两阶段终止模式的关键就在于 interrupt() 方法的使用。
先介绍下 interrupt() 方法。
当调用一个线程的 interrupt 方法时,并不会直接让该线程停止,而是给该线程设置一个打断标记,也就是打断标记为 true。

但是注意,如果被打断线程正处于 sleepwaitjoin这三种状态,则会导致被打断的线程抛出 InterruptedException,并清除打断标记,也就是置为 false

那么我们就可以通过判断打断标记是否为真,来停止线程。

package com.hu;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
@Slf4j
class ThreadStudyApplicationTests {

    @Test
    void contextLoads() {

        Thread t1 = new Thread(() -> {
            Thread current = Thread.currentThread();
            while (true) {
                if (current.isInterrupted()) {
                    log.info("收尾操作");
                    break;
                }
                try {
                    // 如果睡眠时被打断,需要重新打断,将打断标记设为true
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    // 重新打断
                    current.interrupt();
                    log.info("是否被打断:{}", current.isInterrupted());
                    e.printStackTrace();
                }
                // 正常运行时打断,打断标记为true
                log.info("记录系统信息");
            }
        }, "t1");
        t1.start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Thread t2 = new Thread(()->{
            log.info("停止监控记录");
            t1.interrupt();
        }, "t2");
        t2.start();
    }

}

同步模式之Balking

Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回,比如单例模式