Java_SE(四)多线程

        我们在之前,学习的程序在没有跳转语句的前提下,都是由上至下依次执行,那现在想要设计一个程序,边打游戏边听歌...要解决这个问题,就需要用到多进程或者多线程的知识来解决。接下来我们先来了解Java中的多线程。


目录

一. 线程概述

1.1 并发与并行

1.2 线程与进程

1.2.1 概述

1.2.2 线程调度

二. 线程创建(重要)

2.1 继承Thread类

2.2 实现Runnable接口

2.3 两种方式的区别

2.4 匿名内部类方式实现线程的创建

2.5 线程Thread类的常用方法

二. 线程安全

2.1 线程安全

2.2 线程同步

2.2.1 同步代码块

2.2.2 同步方法

2.2.3 Lock锁

三. 线程状态

3.1 线程状态概述


一. 线程概述

1.1 并发与并行

  • 并发:指两个或多个事件在同一个时间段内发生。(同一时刻将时间切碎,交替做多件事情)
  • 并行:指两个或多个事件在同一时刻发生(同时发生,同一时刻做多件事情)

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核 越多,并行处理的程序越多,能大大的提高电脑运行的效率。

1.2 线程与进程

1.2.1 概述

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。 

简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程。

1.2.2 线程调度

  • 分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

二. 线程创建(重要)

常见的线程创建有两种方式:

2.1 继承Thread类

继承Thread并重写run方法:

  • 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  • 创建Thread子类的实例,即创建了线程对象。
  • 调用线程对象的start()方法来启动该线程。
public class ThreadDemo1 {
    public static void main(String[] args) {
        //创建两个线程
        Thread t1 = new MyThread1();
        Thread t2 = new MyThread2();
        
        t1.start();
        t2.start();

    }
}

class MyThread1 extends Thread{
    public void run(){
        for (int i=0;i<1000;i++){
            System.out.println("hello姐~");
        }
    }
}
class MyThread2 extends Thread{
    public void run(){
        for (int i=0;i<1000;i++){
            System.out.println("来了~老弟!");
        }
    }
}

2.2 实现Runnable接口

实现Runnable接口单独定义线程任务:

  • 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  • 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  • 调用线程对象的start()方法来启动线程。
public class ThreadDemo2 {
    public static void main(String[] args) {
        //实例化任务
        Runnable r1 = new MyRunnable1();
        Runnable r2 = new MyRunnable2();
        //创建线程并指派任务
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);

        t1.start();
        t2.start();
    }
}

class MyRunnable1 implements Runnable{
    public void run() {
        for (int i=0;i<1000;i++){
            System.out.println("你是谁啊?");
        }
    }
}

class MyRunnable2 implements Runnable{
    public void run() {
        for (int i=0;i<1000;i++){
            System.out.println("开门!查水表的!");
        }
    }
}

2.3 两种方式的区别

实现Runnable接口比继承Thread类所具有的优势:

  • 适合多个相同的程序代码的线程去共享同一个资源。
  • 可以避免java中的单继承的局限性。
  • 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
  • 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

2.4 匿名内部类方式实现线程的创建

public class ThreadDemo3 {
    public static void main(String[] args) {
        Thread t1 = new Thread(){
            public void run(){
                for(int i=0;i<1000;i++){
                    System.out.println("你是谁啊?");
                }
            }
        };
//        Runnable r2 = new Runnable() {
//            public void run() {
//                for(int i=0;i<1000;i++){
//                    System.out.println("我是查水表的!");
//                }
//            }
//        };
        //Runnable可以使用lambda表达式创建
        Runnable r2 = ()->{
                for(int i=0;i<1000;i++){
                    System.out.println("我是查水表的!");
                }
        };


        Thread t2 = new Thread(r2);

        t1.start();
        t2.start();
    }
}

        t1.start();
        t2.start();
    }
}

2.5 线程Thread类的常用方法

  • void run():线程本身有run方法,可以在第一种创建线程时重写该方法来定义线程任务
  • void start():启动线程的方法。调用后线程被纳入到线程调度器中统一管理,并处于RUNNABLE状态,等待分配时间片开始并发运行。启动线程一定是调用start方法,而不能调用run方法。
  • String getName():获取线程名字。
  • long getId():获取线程唯一标识。
  • int getPriority():获取线程优先级,对应的是整数1-10。
  • boolean isAlive():线程是否还活着。
  • boolean isDaemon():是否为守护线程。
  • boolean isInterrupted():是否被中断了。
  • void setPriority(int priority):设置线程优先级,参数可以传入整数1-10。1为最低优先级,5为默认优先级,10为最高优先级。优先级越高的线程获取时间片的次数越多。可以使用Thread的常量MIN_PRIORITY、NORM_PRIORITY、MAX_PRIORITY,他们分别表示最低,默认,最高优先级。
  • static void sleep(long ms):静态方法sleep可以让运行该方法的线程阻塞参数ms指定的毫秒。
  • static Thread currentThread():获取运行该方法的线程。 

二. 线程安全

2.1 线程安全

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
我们通过电影院卖票的案例,演示线程的安全问题:模拟售票窗口,实现多个窗口同时卖100张票。代码演示:

public class Ticket implements Runnable {

    private int ticket = 100;

    /*
     * 执行卖票操作
     */
    @Override
    public void run() {
        //每个窗口卖票的操作
        //窗口 永远开启
        while (true) {
            if (ticket > 0) {//有票 可以卖
                //出票操作
                //使用sleep模拟一下出票时间
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //获取当前线程对象的名字
                String name = Thread.currentThread().getName();
                System.out.println(name + "正在卖:" + ticket--);
            }
        }
    }

    public static void main(String[] args) {
        //创建线程任务对象
        Ticket ticket = new Ticket();
        //创建三个窗口对象
        Thread t1 = new Thread(ticket, "窗口1");
        Thread t2 = new Thread(ticket, "窗口2");
        Thread t3 = new Thread(ticket, "窗口3");

        //同时卖票
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果:

窗口3正在卖:65
窗口2正在卖:63
窗口3正在卖:63
窗口1正在卖:63
窗口3正在卖:62
窗口2正在卖:61

可以看到程序出现的问题:相同的票数,比如63这张票被卖了3次。这种问题一旦出现,几个窗口(线程)票数不同步了,这种情况就称为线程不安全。

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

2.2 线程同步

当多个线程并发操作同一临界资源,由于线程切换时机不确定,导致操作临界资源的顺序出现混乱,严重时可能导致系统瘫痪。

临界资源:操作该资源的全过程同时只能被单个线程完成。

Java中提供了同步机制(synchronized)来解决线程同步问题。有三种方式完成同步操作:

  • 同步代码块
  • 同步方法
  • 锁机制

2.2.1 同步代码块

同步代码块:synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

格式:

synchronized(同步锁){
     需要同步操作的代码
}

同步锁:对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁。

  • 锁对象可以是任意类型。
  • 多个线程对象要使用同一把锁。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

public class Ticket implements Runnable {
    private int ticket = 100;

    Object lock = new Object();

    @Override
    public void run() {
        //每个窗口卖票的操作
        //窗口 永远开启
        while (true) {
            synchronized (lock) {
                if (ticket > 0) {//有票 可以卖
                    //出票操作
                    //使用sleep模拟一下出票时间
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    //获取当前线程对象的名字
                    String name = Thread.currentThread().getName();
                    System.out.println(name + "正在卖:" + ticket--);
                }
            }
        }
    }
}

2.2.2 同步方法

同步方法:当一个方法使用synchronized修饰后,这个方法称为"同步方法",即:多个线程不能同时在方法内部执行,只能有先后顺序的一个一个进行,将并发操作同一临界资源的过程改为同步执行,就可以有效的解决并发安全问题。

格式:

public synchronized void method(){
        可能会产生线程安全问题的代码 
}

public class Ticket implements Runnable{
    private int ticket = 100;
    /*
     * 执行卖票操作
     */
    @Override
    public void run() {
        //每个窗口卖票的操作         
        //窗口 永远开启         
        while(true){
            sellTicket();
        }
    }

    /*
     * 锁对象 是 谁调用这个方法 就是谁
     *   隐含 锁对象 就是  this
     *
     */
    public synchronized void sellTicket(){
        if(ticket>0){//有票 可以卖  
            //出票操作
            //使用sleep模拟一下出票时间
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
                e.printStackTrace();
            }
            //获取当前线程对象的名字
            String name = Thread.currentThread().getName();
            System.out.println(name+"正在卖:"+ticket--);
        }
    }
}

2.2.3 Lock锁

java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

Lock锁也称同步锁,加锁与释放锁方法化了,如下:

  • public void lock():加同步锁。

  • public void unlock():释放同步锁。

public class Ticket implements Runnable {
    private int ticket = 100;

    Lock lock = new ReentrantLock();

    /*
     * 执行卖票操作
     */
    @Override
    public void run() {
        
        while (true) {
            lock.lock();
            if (ticket > 0) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                String name = Thread.currentThread().getName();
                System.out.println(name + "正在卖:" + ticket--);
            }
            lock.unlock();
        }
    }
}

三. 线程状态

3.1 线程状态概述

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在API中 java.lang.Thread.State 这个枚举中给出了六种线程状态:

线程状态导致状态发生条件
NEW(新建)线程刚被创建,但是并未启动。还没调用start方法。
Runnable(可运行)线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。
Blocked(锁阻塞)当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待)一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
TimedWaiting(计时等待)同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
Teminated(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

今天大脑已宕机,图有机会的话就上,没机会的话,大家自行百度hahaha~


本文完!

写在结尾:

2022 年 9 月 28 日  一个java菜鸟,发布于北京海淀。

好记性不如烂笔头,持续学习,坚持输出~今天是持续写作的第9天。可以点赞、评论、收藏啦。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码云说

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值