Java基础篇 - 多线程详解

目录

1.线程相关概念

1.1 程序

1.2 进程

1.3 线程

1.4 并发与并行

2.线程的基本使用

2.1 创建线程的两种方式

2.2 线程应用案例1.继承Thread类

2.3 线程应用案例2-实现Runnable接口

2.4 线程使用应用案例-多线程执行

 2.5 继承Thread类 和 实现Runnable接口的区别

3. 线程常用方法

3.1 常用方法第一组

3.2 常用方法第二组

4. 用户线程和守护线程

4.1 基本介绍:

 4.2 如何将一个线程设置成守护线程(setDaemon)

5. 线程的生命周期 

5.1 线程的几种状态

 5.2 编写程序查看线程的几种状态

6. 线程的同步

6.1 线程同步机制

6.2 同步具体方法(Synchronized )

6.3 同步分析

​编辑

7. 互斥锁

7.1 互斥锁基本介绍

7.2 实现的步骤

7.3 多线程模拟售票问题

 8. 线程的死锁

 9. 释放锁


1.线程相关概念

1.1 程序

是为了完成特定任务,用某种语言编写的一组指令的集合。

简单的说,就是我们写的代码

1.2 进程

  • 进程是指运行中的程序,比如我们使用了QQ,就启动了一个进程,操作系统就会为该进程分配内存空间。当我们玩游戏时,又启动了一个进程,操作系统会为该游戏分配新的内存空间。
  • 进程是程序的一次执行过程,或者是正在运行的一个程序,是动态过程:有他自身的产生,存在和消亡过程。

1.3 线程

  • 线程是由进程创建的,是进程的一个实体 

  • 一个进程可以拥有多个线程,如下:

  • 48756b702d434586ae3bdc0bd9dba8fb.png

  • 单线程:同一个时刻,只允许执行一个线程

  • 多线程:同一个时刻,可以执行多个线程,比如:一个qq进程,可以同时打开多个聊天窗口,一个迅雷进程,可以下载多个文件

1.4 并发与并行

并发:同一个时刻,多个任务交替执行,造成一种“貌似同时”的错觉,简单地说,单核cpu实现的多任务就是并发。(比如当一个人在开车时,他不可以做其他事情,比如打电话)

34f718431a7c40b6b12db89b72c944a9.png

并行:同一个时刻,多个任务同时执行,多核cpu可以实现并行,并发和并行

320a2d4c86c74c1ead71cd18e0ab5a41.png

2.线程的基本使用

2.1 创建线程的两种方式

在Java中线程常用的有两种方法

  • 继承Thread类,重写run方法
  • 实现Runnable接口,重写run方法

860f837faa514de5b7fdcacbe529247f.png

2.2 线程应用案例1.继承Thread类

要求:编写一个线程,该线程每隔一秒,在控制台输出“喵喵,我是小猫咪”。当输出10次时,结束该线程。可以使用JConsole监控线程执行情况。

public class Thread01 {
    public static void main(String[] args) throws InterruptedException {
        //创建 Cat 对象,可以当做线程使用
        Cat1 cat = new Cat1();
        cat.start();
        //启动线程-> 最终会执行 cat 的 run 方法
        //run 方法就是一个普通的方法, 没有真正的启动一个线程,就会把 run 方法执行完毕,才向下执行
        
        // 说明: 当 main 线程启动一个子线程 Thread-0, 主线程不会阻塞, 会继续执行
        //这时 主线程和子线程是交替执行..
        System.out.println("主线程继续执行" + Thread.currentThread().getName());
        for (int i = 0; i < 10; i++) {
            System.out.println("主线程 i=" + i);
            //让主线程休眠
            Thread.sleep(1000);
        }
        
    }
//1. 当一个类继承了 Thread 类, 该类就可以当做线程使用
//2. 我们会重写 run 方法,写上自己的业务代码
//3. Thread 类 实现了 Runnable 接口的 run 方法
}

class Cat1 extends Thread {
    int times = 0;
    @Override
    public void run() {//重写 run 方法,写上自己的业务逻辑
        while (true) {
            //该线程每隔 1 秒。在控制台输出 “喵喵, 我是小猫咪”
            System.out.println("喵喵, 我是小猫咪" + (++times) + " 线程名=" + Thread.currentThread().getName());
            //让该线程休眠 1 秒
            try {//此时有异常,需要处理或抛出
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (times == 10) {
                break;//当 times 到 10, 退出 while, 这时线程也就退出..
            }
        }
    }
}

运行结果:

主线程继续执行main
喵喵, 我是小猫咪1 线程名=Thread-0
主线程 i=0
主线程 i=1
喵喵, 我是小猫咪2 线程名=Thread-0
喵喵, 我是小猫咪3 线程名=Thread-0
主线程 i=2
主线程 i=3
喵喵, 我是小猫咪4 线程名=Thread-0
主线程 i=4
喵喵, 我是小猫咪5 线程名=Thread-0
主线程 i=5
喵喵, 我是小猫咪6 线程名=Thread-0
喵喵, 我是小猫咪7 线程名=Thread-0
主线程 i=6
喵喵, 我是小猫咪8 线程名=Thread-0
主线程 i=7
主线程 i=8
喵喵, 我是小猫咪9 线程名=Thread-0
主线程 i=9
喵喵, 我是小猫咪10 线程名=Thread-0

可以看出,主线程和cat线程是交替执行的。 

->start()方法的底层调用了start0方法。

922dbcd5d016452191ac68bdf45b2200.png

ffaaead4646344bcabb5d4c418e2112a.png

(暂时不懂没关系) 

2.3 线程应用案例2-实现Runnable接口

说明:

  • Java是单继承的,在某些情况下一个类可能已经继承了某个父类,这时在用继承Thread类方法来创建线程显然不可能了
  • Java设计者提供了另一种方式来创建线程,就是实现Runnable接口来创建线程

应用案例:编写程序,该程序每隔一秒,在控制台输出“hi”,当输出10次后,自动退出。

public class Thread2 {
    public static void main(String[] args) {
        Hi hi = new Hi();
        //hi.start; 不能这样做
        Thread thread = new Thread(hi);
        thread.start();
    }
}

class Hi implements Runnable {
    int count = 1;
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("hi" + count++);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果:(每隔一秒输出一次)

hi1
hi2
hi3
hi4
hi5
hi6
hi7
hi8
hi9
hi10

2.4 线程使用应用案例-多线程执行

要求:请编写一个程序,创建两个线程,一个线程每隔1秒输出“hello,world”,输出10次,退出,另一个线程每隔一秒输出“hi”,输出5次退出

public class Thread03 {
    public static void main(String[] args) {
        T1 t1 = new T1();
        T2 t2 = new T2();
        Thread thread1 = new Thread(t1);
        Thread thread2 = new Thread(t2);
        thread1.start();//启动第 1 个线程
        thread2.start();//启动第 2 个线程
    }
}

class T1 implements Runnable {
    int count = 0;
    @Override
    public void run() {
        while (true) {
        //每隔 1 秒输出 “hello,world”,输出 10 次
            System.out.println("hello,world " + (++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() {
    //每隔 1 秒输出 “hi”,输出 5 次
        while (true) {
            System.out.println("hi " + (++count));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count == 5) {
                break;
            }
        }
    }
}

 运行结果:

hello,world 1
hi 1
hello,world 2
hi 2
hi 3
hello,world 3
hello,world 4
hi 4
hi 5
hello,world 5
hello,world 6
hello,world 7
hello,world 8
hello,world 9
hello,world 10

-> 可以看出两个线程都存在时,是交替执行的

0a47943666ec439da6cdd56db2ba1b3a.png

 2.5 继承Thread类 和 实现Runnable接口的区别

  • 从Java的设计来看,通过继承Thread或者实现Runnable接口来创建线程本质上没有区别,Thread类本身就实现了Runnable接口
  • 实现Runnable接口方式更加适用多个线程共享一个资源的情况,并且避免了单继承的限制,建议使用Runnable

3. 线程常用方法

3.1 常用方法第一组

  • setName 设置线程名称

  • getName  返回该线程的名称

  • start  使线程开始执行

  • run   调用线程对象run方法

  • setPriority  更改线程的优先级

  • getPriority  获取线程的优先级

  • sleep  让当前正在执行的线程休眠指定毫秒数

  • interrupt  中断线程

3.2 常用方法第二组

  • yield: 线程的礼让,让出cpu,但礼让的时间不确定,所以也不一定礼让成功
  • join:线程的插队,插队的线程一但插队成功,则肯定先执行完插入的线程的所有的任务。

80e567004b174c64973568c30a20ec8f.png

-> 测试join方法:

public class ThreadMethodExercise {
    public static void main(String[] args) throws InterruptedException {
        T2 t2 = new T2();
        Thread thread = new Thread(t2);
        for (int i = 1; i <= 5; i++) {
            Thread.sleep(1000);
            System.out.println("hi"+ i );
            if(i==3){
                thread.start();
                thread.join();//线程插队
            }
        }
    }
}

class T2 implements Runnable {

    private int count = 0;

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

运行结果:

hi1
hi2
hi3
hello1
hello2
hello3
hello4
hello5
hi4
hi5
 

4. 用户线程和守护线程

4.1 基本介绍:

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

 4.2 如何将一个线程设置成守护线程(setDaemon)

/**
 * 把一个线程设置为守护线程
 * 守护线程:当所有的用户线程结束时,守护线程自动结束。
 * 这里MyDaemonThread为守护线程
 * main线程为用户线程44
 */
public class ThreadMethod02 {
    public static void main(String[] args) throws InterruptedException {
        MyDaemonThread myDaemonThread = new MyDaemonThread();
        //如果我们希望当main线程结束后,子线程自动结束
        //将子线程设置为守护线程即可
        myDaemonThread.setDaemon(true); //daemon 守护线程
        myDaemonThread.start();
        for (int i = 1; i <= 3 ; i++) {
            System.out.println("主线程......");
            Thread.sleep(1000);
        }
    }
}
class MyDaemonThread extends Thread{//守护线程
    @Override
    public void run() {
        while(true){
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("守护线程....");
        }
    }
}

运行结果:

b5f0e8bf194b4fe9a25e9a9f43fa8c7a.png

5. 线程的生命周期 

5.1 线程的几种状态

7cd0d9154a6644a2b74e2a333972e378.png

e1a457d5bfd647c098fc66aa661f4f1d.png

 5.2 编写程序查看线程的几种状态

public class ThreadState_ {
    public static void main(String[] args) throws InterruptedException {
        TT t = new TT();
        System.out.println(t.getName() + " 状态 " + t.getState());
        t.start();
        while (Thread.State.TERMINATED != t.getState()) { //当t的状态不是TERMINATED时
            System.out.println(t.getName() + " 状态 " + t.getState());
            Thread.sleep(500);
        }

        //线程结束的状态
        System.out.println(t.getName() + " 状态 " + t.getState());
    }
}
class TT extends Thread {
    @Override
    public void run() {
        while (true) {
            for (int i = 0; i < 3; i++) {
                System.out.println("hi " + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            break;
        }
    }
}

运行结果:

Thread-0 状态 NEW
Thread-0 状态 RUNNABLE
hi 0
Thread-0 状态 TIMED_WAITING
hi 1
Thread-0 状态 TIMED_WAITING
Thread-0 状态 TIMED_WAITING
hi 2
Thread-0 状态 TIMED_WAITING
Thread-0 状态 TIMED_WAITING
Thread-0 状态 TERMINATED

6. 线程的同步

6.1 线程同步机制

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

  • 也可以这样理解:当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才可对该内存地址进行操作

6.2 同步具体方法(Synchronized )

1.同步代码块

Synchronized(对象){ //得到对象的锁,才能操作同步代码)

        //需要同步代码

}

2. 同步方法

public Synchronized void m(String name){

        //需要被同步的代码

}

6.3 同步分析

e8052cde2bb041d4bd9c8c454f17db04.png

->多个线程会去争夺一把锁,每次只能有一个线程进入 

7. 互斥锁

7.1 互斥锁基本介绍

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

7.2 实现的步骤

  • 先分析上锁的代码
  • 选择同步代码块或同步方法
  • 要求多个线程的锁对象为同一个即可

7.3 多线程模拟售票问题

/**
 * 使用多线程,模拟三个接口同时售票
 * 若不使用线程同步机制,会导致票数超卖,小伙伴们可以自己去尝试
 */
public class SellTicket {
    public static void main(String[] args) {

        //多个线程的锁对象必须为同一个
        SellTicked01 sellTicked01 = new SellTicked01();
        new Thread(sellTicked01).start();
        new Thread(sellTicked01).start();
        new Thread(sellTicked01).start();

//        错误的写法
//        SellTicked01 sellTicked01 = new SellTicked01();
//        SellTicked01 sellTicked02 = new SellTicked01();
//        SellTicked01 sellTicked03 = new SellTicked01();
//
//        sellTicked01.start();
//        sellTicked02.start();
//        sellTicked03.start();
//        new了三个对象 所以锁不住
    }
}

//使用synchronized实现线程同步 在方法上加锁
class SellTicked01 implements Runnable {
    private /*static*/ int tickNum = 10; //让多个线程共享tickNum
    private /*static*/ boolean loop = true;

    // 同步方法,在同一时刻,只能有一个线程来执行sell 方法
    // 这时锁在this对象
    // 也可以在代码块上写synchronized -> 同步代码块
    public /*static*/ synchronized void sell() { //在静态方法中只能访问静态属性或静态方法
        if (tickNum <= 0) {
            System.out.println("售票结束");
            loop = false;
            return;
        }
        //休眠50毫秒,模拟
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("窗口" + Thread.currentThread().getName() + "售出一张票 " + "剩余票数=" + (--tickNum));
    }

    @Override
    public void run() {
        while (loop) {
            sell(); //sell方法是一个同步方法
        }
    }
}

//使用synchronized实现线程同步 同步代码块实现
class SellTicked04 implements Runnable {
    private int tickNum = 10; //让多个线程共享tickNum
    private boolean loop = true;

    // 同步方法,在同一时刻,只能有一个线程来执行sell 方法
    // 这时锁在this对象
    // 也可以在代码块上写synchronized -> 同步代码块 互斥锁还是加在this对象
    public /*synchronized */  void sell() {
        synchronized (this) {
            if (tickNum <= 0) {
                System.out.println("售票结束");
                loop = false;
                return;
            }
            //休眠50毫秒,模拟
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("窗口" + Thread.currentThread().getName() + "售出一张票 " + "剩余票数=" + (--tickNum));
        }
    }

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

运行结果:

窗口Thread-0售出一张票 剩余票数=9
窗口Thread-0售出一张票 剩余票数=8
窗口Thread-0售出一张票 剩余票数=7
窗口Thread-2售出一张票 剩余票数=6
窗口Thread-2售出一张票 剩余票数=5
窗口Thread-2售出一张票 剩余票数=4
窗口Thread-2售出一张票 剩余票数=3
窗口Thread-2售出一张票 剩余票数=2
窗口Thread-2售出一张票 剩余票数=1
窗口Thread-2售出一张票 剩余票数=0
售票结束
售票结束
售票结束

 8. 线程的死锁

->基本介绍:死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。

->如下图:线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

9dcd99e461094647a2d09e4d4d0eeabc.png

模拟线程死锁:

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();
        B.start();
    }
}

//线程
class DeadLockDemo extends Thread {
    static Object o1 = new Object();// 保证多线程,共享一个对象,这里使用 static
    static Object o2 = new Object();
    boolean flag;

    public DeadLockDemo(boolean flag) {//构造器
        this.flag = flag;
    }

    @Override
    public void run() {
//下面业务逻辑的分析
//1. 如果 flag 为 T, 线程 A 就会先得到/持有 o1 对象锁, 然后尝试去获取 o2 对象锁//2. 如果线程 A 得不到 o2 对象锁,就会 Blocked(阻塞)
//3. 如果 flag 为 F, 线程 B 就会先得到/持有 o2 对象锁, 然后尝试去获取 o1 对象锁//4. 如果线程 B 得不到 o1 对象锁,就会 Blocked(阻塞)
        if (flag) {
            synchronized (o1) {//对象互斥锁, 下面就是同步代码
                System.out.println(Thread.currentThread().getName() + " 进入 1");
                synchronized (o2) { // 这里获得 li 对象的监视权
                    System.out.println(Thread.currentThread().getName() + " 进入 2");
                }
            }
        } else {
            synchronized (o2) {
                System.out.println(Thread.currentThread().getName() + " 进入 3");
                synchronized (o1) { // 这里获得 li 对象的监视权
                    System.out.println(Thread.currentThread().getName() + " 进入 4");
                }
            }
        }
    }
}

运行结果:(我们会发现程序会堵塞,进入了死锁状态,无法继续运行) 

52561a3bcdb24267ad644f02acdd4ecc.png

 9. 释放锁

会释放锁的操作:

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

不会释放锁的操作:

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

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值