首页 > 技术文章 > 19、多线程基础

DarkSki 2022-03-08 23:54 原文

1 什么是线程?

1.1 几个概念

  • 程序:由程序员编写的代码
  • 进程:指运行中的程序,进程是程序的执行过程,或者是正在运行的程序。是动态过程:产生、存在和消亡的过程
  • 线程:线程由进程创建,是一个实体。一个进程可以由多个线程
    • 打开迅雷——一个进程产生了
    • 迅雷下载多个任务——一个进程产生了多个线程

1.2 线程

(1)单线程:同一时刻,只允许执行一个线程

(2)多线程:同一时刻,可以执行多个进程

(3)并发:同一时刻,多个任务交替执行,造成一种【貌似同时】的错觉,简单地说,单核CPU实现多任务就是并发

(4)并行:同一时刻,多个任务同时执行,多核CPU可以实现并行

2 线程的实现

2.1 继承Thread类,重写run方法

(1)入门代码

public class Thread01 {

    public static void main(String[] args) throws InterruptedException {

        //创建线程
        Cat cat = new Cat();
        //启动子线程:子线程会和main主线程并行
        cat.start();
        //如果使用cat.run()方法,则没有调用cat线程,仍为main线程内调用的一个方法,将会在执行完run方法后在执行下面的语句
        //cat.run();

        //当main线程启动子线程时,主线程不会阻塞,会继续执行
        //主线程运行结束,子线程会继续执行完毕
        for (int i = 0; i < 10; i++) {
            System.out.println("主线程继续执行" + Thread.currentThread().getName() + "\ti=" + i);
            Thread.sleep(1000);
        }
    }
}

/**
 * 方法一:继承Thread类,该类就可以当作线程使用
 * 重写父类Thread类的run方法,而Thread类实现了Runnable接口的抽象方法
 */
class Cat extends Thread {
    int times = 0;

    @Override
    public void run() {//按业务逻辑重写run方法

        while (true) {
            //每隔一秒输出一次
            System.out.println("我是一只小喵喵" + ++times + "线程名" + Thread.currentThread().getName());
            //让线程休眠1秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (times == 8) {
                break;
            }
        }
    }
}

(2)运行流程

graph LR Thread01进程 --启动--> main主线程 main主线程 --启动--> Thread-0子线程

(3)start方法的底层源码分析

  • start()方法调用start0()方法
  • start0()方法是本地方法,由JVM调用,底层由c/c++实现
  • start0()是真正实现多线程的方法,而不是run方法
  • start()方法调用start0()方法后,该线程并不会立刻执行,只是将线程编程可执行的状态,具体执行取决于CPU,由CPU同意调度

2.2 实现Runnable接口,重写run方法

(1)为什么需要另外一种方法,实现多线程

​ Java是单继承的,在某些情况下,一个类可能已经继承了某个父类,此时通过继承Thread类来创建线程显然是不可能的

(2)入门代码——【使用了静态代理模式】

public class Thread02 {

    public static void main(String[] args) {

        Dog dog = new Dog();
        //此时无法调用start()方法,直接调用run()方法没有创建线程,所以创建Thread对象,将dog对象传入Thread
        //【如何实现,见(3)】
        Thread thread = new Thread(dog);
        thread.start();
    }
}

class Dog implements Runnable {

    int count = 0;

    @Override
    public void run() {
        while (true) {
            System.out.println("小狗哇哇叫" + ++count + "\t线程" + Thread.currentThread().getName());

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

            if (count == 10) {
                break;
            }
        }
    }
}

(3)代码模拟实现Runnable接口开发线程的机制

public class ProxyMX {
    public static void main(String[] args) throws InterruptedException {

        //声明线程
        Tiger tiger = new Tiger();
        //声明线程代理类,利用构造器将实现了Runnable接口的对象传入线程代理类
        Proxy proxy = new Proxy(tiger);
        //调用代理类中的方法,最终在多线程的情况下启动子线程【注意:这里仅演示线程代理类的流程,并没有实现多线程】
        proxy.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("主线程在执行" + Thread.currentThread().getName() + ++i);
            Thread.sleep(1000);
        }

    }
}

class Animal {
}

class Tiger extends Animal implements Runnable {

    int count = 0;

    @Override
    public void run() {

        while (true) {
            System.out.println("老虎嗷嗷叫" + ++count);

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

            if (count == 10) {
                break;
            }
        }
    }
}

//线程代理类,模拟了一个极简的thread类
class Proxy implements Runnable {//proxy代理

    private Runnable target = null;

    public Proxy(Runnable target) {
        this.target = target;
    }

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

    public void start() {
        start0();
    }

    public void start0() {
        run();
    }

}

3 线程的使用

public class Thread03 {

    public static void main(String[] args) {

        T1 t1 = new T1();
        T2 t2 = new T2();
        Thread thread01 = new Thread(t1);
        Thread thread02 = new Thread(t2);
        //通过匿名内部类可以清楚地看到是实现了Runnable接口的实例化对象
        Thread thread03 = new Thread(new Runnable() {
            int count = 0;
            @Override
            public void run() {
                while (true) {
                    System.out.println("我试试匿名内部类");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    if (count == 20) {
                        break;
                    }
                }
            }
        });

        thread01.start();
        thread02.start();
        thread03.start();
    }
}

class T1 implements Runnable {
    int count = 0;

    @Override
    public void run() {

        while (true) {
            System.out.println("hello, world!\t" + ++count);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (count == 10) {
                break;
            }
        }
    }
}

class T2 implements Runnable {
    int count = 0;

    @Override
    public void run() {
        while (true) {
            System.out.println("hi\t" + ++count);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count == 5) {
                break;
            }
        }
    }
}

4 继承Thread和实现Runnable的区别

(1)从Java设计来看,通过继承Thread类和实现Runnable接口来创建线程,本质上没有任何区别

(2)实现Runnable接口的方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制

  • 【多个线程共享一个资源】:即多个线程代理类启动同一个实现了Runnable的类的实例化对象

5 线程常用方法

方法名 作用
setName 设置线程名称,使之与参数name相同
getName 返回该线程的名称
start 执行线程
run 调用线程对象run方法
setPriority 更改线程的优先级
getPriority 获取线程优先级
sleep 指定线程休眠时间
interrupt 中断线程,一般用于中断休眠

【注意事项和细节】

  • start底层会创建一个新的线程,调用run不会启动新的线程
  • 线程优先级的范围
  • interrupt中断线程,但并没有真正的结束线程,一般用于中断正在休眠的线程
  • sleep:线程的静态方法,使当前线程休眠
方法名 作用
yield 【线程的礼让】让出cpu,让其他线程执行,礼让事件不确定,因此也不一定礼让成功
join 【线程的插队】插队的线程一定插入成功,即优先执行完插入的线程所有任务

6 用户线程和守护线程

(1)几个概念

  • 用户线程:也叫工作线程,当线程的任务执行完或通知后结束
  • 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束
    • 常见的守护线程:垃圾回收机制

(2)守护线程的使用

public class ThreadMethod03 {

    public static void main(String[] args) throws InterruptedException {
        MyDaemonThread myDaemonThread = new MyDaemonThread();

        //将子线程设置为守护线程,当main线程结束后,子线程自动结束
        myDaemonThread.setDaemon(true);
        myDaemonThread.start();
        for (int i = 0; i <= 10; i++) {
            System.out.println("宝强在辛苦地工作。。。");
            Thread.sleep(1000);
        }
    }
}

class MyDaemonThread extends Thread {
    @Override
    public void run() {
        for (;;) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("马蓉和宋泽在快乐聊天。。。");
        }
    }
}

7 线程的七大状态

image-20220217160816783

状态
NEW 尚未启动的线程
Runnable 在JVM中执行的线程
【Ready】
【Running】
TimeWaiting 正在等待另一个线程执行动作达到指定等待时间的线程
Waiting 正在等待另一个线程执行特定动作的线程
Blocked 被阻塞等待监视器锁定的线程
Teminated 已退出的线程

8 线程的同步

8.1 线程的同步机制

​ 在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同意时刻,最多有一个线程访问,一保证数据的完整性

​ 所谓的【线程同步】,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,知道该线程完成操作,其他线程才能对该内存地址进行操作。

8.2 同步的实现——关键字Synchronized

​ 通常将共享资源的操作放置在synchronized定义的区域内,这样当其他线程要获取该锁时,必须等待锁被释放才能进入该区域

(1)修饰代码块——同步代码块

  • 作用范围:大括号内的代码
  • 作用对象:调用该代码块的对象
synchronized (Object) {
    //需要被同步的代码
}
  • Object为任意一个对象
  • 每个对象都存在一个标志位,并具有两个值,反别为0,1
  • 一个线程运行到同步块时首先检查该对象的标志位
    • 若为0,表明此同步块中存在其他线程运行,此时该线程处于就绪状态,直到同步块中代码执行完为止,此时该对象的标志位被设置为1
    • 若为1,该线程可以执行同步代码块中的代码,并将Object对象的标志位设置为0,防止其他线程执行代码块

(2)修饰方法——同步方法

​ 将每个能访问公共资源的方法修饰为synchronized

  • 作用范围:整个方法
  • 作用对象:调用该方法的对象
public synchronized void m() {
    //需要被同步的代码
}

(3)同步的使用

public class SellTicket {

    public static void main(String[] args) {
        SellTicket sellTicket = new SellTicket();
        Thread thread1 = new Thread(sellTicket);
        Thread thread2 = new Thread(sellTicket);
        Thread thread3 = new Thread(sellTicket);
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

//使用同步方法——synchronized实现同步
class SellTicket implements Runnable {
    private static int ticketNum = 100;
    private boolean loop = true;

    public synchronized void sell() {//同一时刻只能有一个线程执行sell方法
            //判断是否有余票
            if (ticketNum <= 0) {
                System.out.println("售票结束...");
                loop = false;
                return;
            }

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

            System.out.println("窗口:" + Thread.currentThread().getName() + "售票成功!\t"
                    + "剩余:" + (--ticketNum) + "张");
    }

    @Override
    public void run() {
        while (loop) {
            sell();
        }
    }
}

8.3 互斥锁

(1)基本介绍

  • Java中引用对象互斥锁的概念,来保证共享数据操作的完整性
  • 每个对象都对应一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只有一个线程可以访问该对象
  • 关键字synchronized来与对象的互斥锁联系,当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问
  • 同步的局限性:导致程序的执行效率降低
  • 同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是同一个对象)
  • 同步方法(静态的)的锁为当前类本身

9 线程的死锁

(1)什么是死锁?

​ 多个线程都占用了对方的锁资源,但互不相让导致死锁,需要避免

(2)模拟死锁

public class DeadLock_ {

    public static void main(String[] args) {
        DeadLockDemo A = new DeadLockDemo(true);
        A.setName("A线程");
        DeadLockDemo B = new DeadLockDemo(false);
        B.setName("B线程");

        A.start();
        /**
         * (1)A.start()调用run方法,flag为true
         *      持有o1锁,并试图获取o2锁
         * (2)B.start()调用run方法,flag为false
         *      持有o2锁,并试图获取o1锁
         * (3)二线程并发运行,所以对(1)来说o2未释放,对(2)来说o1未释放
         */
        B.start();
    }
}

class DeadLockDemo extends Thread {
    static Object o1 = new Object();
    static Object o2 = new Object();

    boolean flag;

    public DeadLockDemo(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        if (flag) {
            synchronized (o1) {
                System.out.println(Thread.currentThread().getName() + "进入1");
                synchronized (o2) {
                    System.out.println(Thread.currentThread().getName() + "进入2");
                }
            }
        } else {
            synchronized (o2) {
                System.out.println(Thread.currentThread().getName() + "进入3");
                synchronized (o1) {
                    System.out.println(Thread.currentThread().getName() + "进入4");
                }
            }
        }
    }
}

10 释放锁

(1)哪些操作会释放锁

  • 当前线程的同步方法、同步代码块执行结束
  • 当前线程在同步代码块,同步方法中遇到break、return
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
  • 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停了,并释放锁

(2)哪些操作不会释放锁

  • 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行,不会释放锁
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁

推荐阅读