当前位置:   article > 正文

JAVA面向对象-----多线程 1_并发:一个单核cpu(采用时间片)同时执行多个任务,实际采用的是时间片转机制,也就是

并发:一个单核cpu(采用时间片)同时执行多个任务,实际采用的是时间片转机制,也就是

基本概念

程序

程序(program):为完成特定任务、用某种语言编写的一-组指令的集合。即指一段静态的代码,静态对象。在计算机操作系统中,程序就表现为一个可执行文件

进程

进程(process):程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程,有它自身的产生、存在和消亡的过程,即进程的生命周期。

➢ 程序是静态的,进程是动态的;
➢ 每一个进程,系统是分配一个唯一id标识(pid);
进程作为系统资源分配的最小单位,系统在运行时会为每个进程分配不同的内存区域。

线程

线程(thread): 进程可进一 步细化为线程,是-一个程序内部的一 条执行路径。若一个进程同一时间并行执行多个线程,就是支持多线程的。

➢ 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
➢ 一个进程中的多个线程共享相同的内存单元/内存地址空间,它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会帶来安全的隐患。

进程与线程关系图:
在这里插入图片描述

单核CPU / 多核CPU

单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。
例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员,如果有某个人不想交钱,那么收费人员可以把他“挂起”(晾着他, 等他想通了,准备好了钱,再去收费)。但是因为CPU时间单元特别短,因此感觉不出来。

如果是多核CPU的话, 才能更好的发挥多线程的效率。(现在的服务器都是多核的)。
例如:一个Java应用程序java.exe, 其实至少有三个线程: main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。

时间片

现代操作系统比如Mac OS X, UNIX, Linux, Windows等,都是支持多任务的操作系统。所谓的多任务,就是操作系统可以同时运行多个任务。

操作系统(如Windows、Linux) 的任务调度是采用时间片轮转的抢占式调度方式,也就是说一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行。任务执行的一小段时间叫做时间片,任务正在执行时的状态叫运行状态,任务执行一段时间后强制暂停去执行下一个任务,被暂停的任务就处于就绪状态等待下一个属于它的时间片的到来。

这样每个任务都能得到执行,由于CPU的执行效率非常高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任务在"同时进行”,这也就是我们所说的并发

内核态与用户态

一般的操作系统(如Windows、Linux)对执行权限进行分级:用户态内核态

  1. 操作系统内核作为直接控制硬件设备的底层软件,权限最高,称为内核态,或核心态。
  2. 用户程序的权限最低,称为用户态。

需要注意的是:当用户态转变为内核态时,是比较消耗资源的!!!

并行与并发

并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片轮转调度的方式)同时执行多个任务。比如:秒杀、多个人做同一件事。

进程的状态

在这里插入图片描述

  1. 就绪:进程处于可运行的状态,只是CPU时间片还没有轮转到该进程,则该进程处于就绪状态。
  2. 运行:进程处于可运行的状态,且CPU时间片轮转到该进程,该进程正在执行代码,则该进程处于运行状态。
  3. 阻塞:进程不具备运行条件,正在等待某个事件的完成。

多线程的优点

思考:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,那么为何仍需多线程呢?

多线程程序的优点:

  1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验;
  2. 提高计算机系统CPU的利用率;
  3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。

何时需要多线程?

  1. 程序需要同时执行两个或多个任务。
  2. 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
  3. 需要一些后台运行的程序时。

线程的创建和使用

Thread中的常用方法:

  1. start():启动当前线程: 调用当前线程的run();
  2. run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中;
  3. currentThread(): 静态方法,返回执行当前代码的线程;
  4. getName(): 获取当前线程的名字;
  5. setName(): 设置当前线程的名字;
  6. yield(): 释放当前cpu的执行权;
  7. join(): 在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态;
  8. stop():已过时。当执行此方法时,强制结束当前线程;
  9. sleep(long millitime):让当前线程“睡眠’指定的millitime毫秒。在指定的millitime毫秒时间内, 当前线程是阻塞状态;
  10. isAlive():判断当前线程是否存活。

参考代码:

package test_MultiThreading;

public class ThreadTest_3 {
    public static void main(String[] args) {
        //为该分线程起个名字~~
        //方式一:在下面构造一个带参数的构造函数,初始化的时候直接命名~~
        MyThread_3 myThread_3 = new MyThread_3("Thread-1");
        //方式二:调用setName()函数
        //myThread_3.setName("分线程1");
        myThread_3.start();
        //给主线程起个名字~~
        Thread.currentThread().setName("主线程");
        for (int i=1;i<100;i++){
            if (i % 2 != 0){
                //Thread.currentThread().getName()--->获取当前进程名字
                System.out.println(Thread.currentThread().getName()+":"+ i);
            }
            //在主线程中,当运行到i=21的时候,主线程断开,进入Thread-1线程中~~
            if (i == 21){
                try {
                    myThread_3.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

class MyThread_3 extends Thread{
    @Override
    public void run(){
        for (int i=1;i<100;i++){
            if (i % 2 == 0){
                //让进程睡眠一段时间,单位:ms
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //Thread.currentThread().getName()--->获取当前进程名字
                System.out.println(Thread.currentThread().getName()+":"+ i);
            }
            //当i能够整除20的时候,释放当前线程的使用权~~
            if (i%20 == 0){
                Thread.currentThread().yield();
            }
        }
    }

    //创建一个带参数的构造函数
    public MyThread_3(String name){
        super(name);
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

方式一:继承Thread类

步骤

1.创建一个继承于Thread类的子类;
2.重写Thread类的run()—>将此线程需要执行的操作声明在run()函数中;
3.创建Thread类的子类的对象;
4. 通过此对象调用start()—>启动当前线程,调用当前线程的run()函数。

例1:遍历100以内的所有偶数。

参考代码:

//1.创建一个继承于Thread类的子类
class MyThread extends Thread{
    //2.重写Thread类的run()
    @Override
    public void run(){
        for (int i=1;i<100;i++){
            if (i % 2 == 0){
                //Thread.currentThread().getName()--->获取当前进程名字
                System.out.println(Thread.currentThread().getName()+":"+ i);
            }
        }
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        //3.创建Thread类的子类的对象
        MyThread myThread_1 = new MyThread();
        //4. 通过此对象调用start()
        myThread_1.start();

        //注意事项:
        //myThread.run();
        //通过这种方式启动的线程不是多线程~~

        //如果此时,我们需要再次调用myThread线程,不可以直接再次调用start(),
        //而是需要重新创建对象后再调用~~
        MyThread myThread_2 = new MyThread();
        myThread_2.start();

        //测试一下多线程
        for (int i=0;i<50;i++){
            System.out.println(Thread.currentThread().getName()+":"+i*i);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

运行结果:
在这里插入图片描述

这个输出结果是可以看出,该程序运行是开辟了3个线程进行运行的!
注意:

  1. 我们也可以使用myThread.run()的方式直接启动线程,但是这就不再是多线程了,而是全部都在main这个主线程下运行程序~~
  2. 如果此时,我们需要再次调用myThread线程,不可以直接再次调用start(),而是需要重新创建对象后再调用~~否则会抛IllegalThreadStateException异常。

例2:创建两个分线程,分别遍历100以内的所有偶数和所有奇数。

参考代码:

public class ThreadTest_2 {
    public static void main(String[] args) {
        MyThread myThread_1 = new MyThread();
        MyThread myThread_2 = new MyThread();

        myThread_1.start();
        myThread_2.start();
    }
}

class MyThread_1 extends Thread{
    @Override
    public void run(){
        for (int i=1;i<100;i++){
            if (i % 2 == 0){
                //Thread.currentThread().getName()--->获取当前进程名字
                System.out.println(Thread.currentThread().getName()+":"+ i);
            }
        }
    }
}

class MyThread_2 extends Thread{
    @Override
    public void run(){
        for (int i=1;i<100;i++){
            if (i % 2 != 0){
                //Thread.currentThread().getName()--->获取当前进程名字
                System.out.println(Thread.currentThread().getName()+":"+ i);
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

运行结果:
在这里插入图片描述

Thread的匿名子类

在上面两个例子中,我们发现这样一个问题:我们所创建的分线程,只是使用了一次就不用了,每次使用需要创建新的对象,其实还是挺麻烦的~~那么我们就可以使用Thread的匿名子类。

        //创建Thread类的匿名子类的方式
        new Thread(){
            @Override
            public void run(){
                //具体重写内容
            }
        }.start();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

方式二:实现Runnable接口

步骤

1.创建一个实现了Runnable接口的实现类;
2.实现类去实现Runnable 中的抽象方法run();
3.创建实现类的对象;
4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象;
5.通过Thread类的对象 调用start()。

参考代码:

public class ThreadTest_5 {
    public static void main(String[] args) {
        //3.创建实现类的对象
        MyThread_5 myThread_5 = new MyThread_5();
        //4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象;
        Thread thread1 = new Thread(myThread_5);
        thread1.setName("线程1");
        //5.通过Thread类的对象 调用start()
        thread1.start();

        //再启动一个线程
        Thread thread2 = new Thread(myThread_5);
        thread2.setName("线程2");
        thread2.start();
    }
}

//1.创建一个实现Runnable接口的实现类
class MyThread_5 implements Runnable{
    //2.实现类去实现Runnable中的run()抽象方法
    @Override
    public void run() {
        for (int i=0;i<100;i++){
            if (i%2 == 0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

我们会有这样一个疑问:因为调用start()方法后,线程启动,随后会执行当前线程内的run()函数,但是我们当前线程是Thread,为什么执行下来是调用的下面Runnable中的抽象run()方法呢?

通过阅读源码可以发现,Thread类中run()函数中的target是一个Runnable类型的值~~故而可以执行到Runnable节课欧中的抽像方法!
在这里插入图片描述
在这里插入图片描述

例1:创建三个窗口卖票,总票数100张。(方式一实现)

参考代码:

public class ThreadTest_4 {
    public static void main(String[] args) {
        MyThread_4 window1 = new MyThread_4();
        MyThread_4 window2 = new MyThread_4();
        MyThread_4 window3 = new MyThread_4();
        window1.setName("窗口1");
        window2.setName("窗口2");
        window3.setName("窗口3");
        window1.start();
        window2.start();
        window3.start();
    }
}

class MyThread_4 extends Thread{
    private static int ticket = 100;  //注意这里必须要用static修饰
    @Override
    public void run(){

        while(true){
            if (ticket > 0){
                System.out.println(getName() + ":卖票,票号为:"+ ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

运行结果:
在这里插入图片描述
我们会发现,运行结果中有3个票号为100的票,这是因为涉及到线程安全问题~ ~后面会讲如何解决!

例2:创建三个窗口卖票,总票数100张。(方式二实现)

参考代码:

public class ThreadTest_6 {
    public static void main(String[] args) {
        MyThread_6 window = new MyThread_6();
        Thread window1 = new Thread(window);
        Thread window2 = new Thread(window);
        Thread window3 = new Thread(window);
        window1.setName("窗口1");
        window2.setName("窗口2");
        window3.setName("窗口3");
        window1.start();
        window2.start();
        window3.start();
    }
}

class MyThread_6 implements Runnable{
    private int ticket = 100; //注意这里没有用static修饰
    @Override
    public void run() {
        while(true){
            if (ticket > 0){
                System.out.println(Thread.currentThread().getName() + ":卖票,票号为:"+ ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

运行结果:
在这里插入图片描述
思考一下,这道例题用方式一和方式二实现的最大区别在于定义stick总数的时候有没有用static修饰?
方式一必须要用是因为方式一种创造出了三个对象,那么如果不用static修饰,每个创造对象,stick就会+100,最后就有300张stick。
方式二不需要用static修饰stick,是因为方式二中只创造了一个对象,这一个对象放到了三个构造器中,这三个线程只使用了一个对象。

Runnable的匿名子类

// 使用匿名类创建 Runnable 子类对象
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        //具体重写内容
   }
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

两种创建方式的比较

综合上面两个方式及其例题,我们发现,线程的创建和使用中,方式二更好一下~~

方式一的局限性在于java的单继承特性,拿购票窗口举例,如果使用方法一的方式实现多线程,现实中的购票窗口还有其他特性,也许还需要继承其他父类,那么方法一下面它就无法继承其他类了,所以还是使用方式二实现Runnable接口比较方便。

方式二能够更好的处理多个线程有共享数据的情况,例如购票窗口中的总票数和窗口之间的关系,方式二在创建中就不需要按每个窗口来创建对象,只需要创建一个对象,然后调用好对象的构造器就可实现多个对象的创建。

方式一和方式二之间的联系就在于两种方法都需要重写run()函数,将线程要执行的逻辑声明在run()函数中。

线程中断的两种方法

1. 通过共享的标记来进行沟通

public class FlagStopThread {
    //使用标志位isQuit标识是否继续执行线程中的任务,true中断,false继续执行
    private static class MyRunnable implements Runnable {
        public volatile boolean isQuit = false;
        @Override
        public void run() {
            while (!isQuit) {
                System.out.println(Thread.currentThread().getName()+ ": 别管我,我忙着转账呢!");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+ ":好!停止转账!");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        System.out.println(Thread.currentThread().getName()+ ": 让李四开始转账。");
        thread.start();
        Thread.sleep( 1000);
        System.out.println(Thread.currentThread().getName()+ ": 老板来电话了,得赶紧通知李四对方是个骗子!");
        target.isQuit = true;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

在这里插入图片描述

2. 调用 interrupt() 方法来通知

public class InterruptThread {
private static class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 两种方法均可以
        while (!Thread.interrupted()) {
            //while (!Thread.currentThread().isInterrupted()) {
            System.out.println(Thread.currentThread().getName() + ": 别管我,我忙着转账呢!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println(Thread.currentThread().getName()+ ": 有内鬼,终止交易!");
                break;
            }
        }
        System.out.println(Thread.currentThread().getName()+ ": 好!停止转账!");
    }
}

public static void main(String[] args) throws InterruptedException {
    MyRunnable target = new MyRunnable();
    Thread thread = new Thread(target, "李四");
    System.out.println(Thread.currentThread().getName()+ ": 让李四开始转账。");
    thread.start();
    Thread.sleep(1000);
    System.out.println(Thread.currentThread().getName()+ ": 老板来电话了,得赶紧通知李四对方是个骗子!");
    thread.interrupt();
   }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

在这里插入图片描述
重点说明下第二种方法:

通过 thread 对象调用 interrupt() 方法通知该线程停止运行。thread 收到通知的方式有两种:

  1. 如果线程调用了 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式
    通知,清除中断标志
  2. 否则,只是内部的一个中断标志被设置
    thread 可以通过,Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志,Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志

第二种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。

Thread的优先级

线程的调度

调度策略

1.时间片

在这里插入图片描述

2.抢占式,高优先级的线程抢占CPU

线程的优先级等级

优先级分类

  1. MAX_ PRIORITY:10
  2. MIN_ PRIORITY:1
  3. NORM_ PRIORITY: 5

涉及的方法

  1. getPriority():返回线程优先值;
  2. setPriority(int newPriority) :改变线程的优先级。

说明

  1. 线程创建时继承父线程的优先级;
  2. 高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。|

~~未完待续

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号