多线程

1、前言

我们以前写的Java程序都是单线程的,也就是说一次只做一件事,这个很简单,直接在main方法中写就行了,主线程可以去实现它。生活中,比如一个人,可以同时进行呼吸、血液循环、码字等多个活动。又比如一台电脑,它可以同时播放音乐、播放视频、下载资源等,也可以同时完成多项活动。Java中模拟了这种状态,引入了多线程的机制。当程序同时完成多项任务时,它就是一个多线程程序。实际开发中,多线程的应用是非常广泛的,因此有必要了解多线程机制,并且掌握多线程的使用。

2、进程、线程、多线程

2.1、进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

理解:进程可看成是系统资源和程序代码执行位置的集合。在操作系统中,每个应用程序的执行都在操作系统内核中登记一个进程标志,操作系统根据分配的标志对应用程序的执行进行调度和系统资源分配。每个进程都有自己的内存单元,进程之间是互相独立的,一个进程一般不允许访问其他进程的内存空间,即进程独享内存资源,彼此独立

电脑上打开任务管理器:
在这里插入图片描述
标记的每一个都是进程,可以查看详细信息:
在这里插入图片描述
可以看到,每一个运行的进程都会有一个PID,PID是进程的身份标识,每个进程运行都会占用系统的一个端口,可以根据进程的PID查询该进程所占用的端口号。每个进程也都有自己的内存,这个内存只分配给这个进程,一般情况下,其他进程是无法使用这个内存的,也就是说每个进程的内存都是独立专用的,其他进程无法访问。

我们常用的Windows操作系统是多任务操作系统,以进程为单位。一个进程是一个包含自身地址的程序,每个独立执行的程序都是一个进程,系统分配给每个进程一段有限的时间去使用CPU(CPU时间片),CPU在这个时间内只执行这个进程,然后下一个时间片跳到另一个进程中去执行,CPU时间片切换的非常快,在我们看来,每个进程就是同时进行的,其实并不是。

时间片:

时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。而不会造成CPU资源浪费。在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。

windows操作系统的执行模式如下图:
在这里插入图片描述

2.2、线程

线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。

线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。

同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。

一个进程可以有很多线程,每条线程并行执行不同的任务。

在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见,即提高了程序的执行吞吐率。在单CPU单核的计算机上,使用多线程技术,也可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,编写专门的workhorse线程执行密集计算,从而提高了程序的执行效率。

理解:线程是比进程更小的执行单位,一个线程不能独立存在,它必须存在于进程之中。如果将进程分开,则进程中的系统资源可看做是一个静态对象,而进程中的代码执行位置可看做是一个动态对象,动态部分就是线程。进程中的多个线程可以共享进程的内存资源,且线程之间可进行通信,多个线程可提高进程的运行效率,通常一个进程中包含多个线程。如很多的下载软件就是进行多线程下载资源,从而提高了下载软件数倍效率。

2.3、多线程

多线程就是多个线程的集合,就是一个程序中同时执行一个以上的线程,一个线程的执行并不需要等待另一个线程执行完毕后才开始执行,所有线程可发生在同一时刻,每个线程不是独立的存在,彼此之间有联系,并发就是这样实现的。将包含有多个线程的程序作为一个系统进程来处理,这样的Java程序即称为多线程应用,多线程程序中,多个线程共享内存,在某种情况下使用多线程可以提高程序的运行效率,达到充分利用 CPU 的目的。但是多线程程序比较复杂,因为共享一个JVM内存,所以会涉及到数据共享及操作的冲突问题,因此非必要情况下,尽量不使用多线程处理。

2.4、进程和线程的联系及区别

联系:

线程是不能独立运行的,线程必须存在于进程之中。如下图:
在这里插入图片描述
线程的运行必须依赖于进程。

区别:

  • 同一进程内的线程之间可以相互通信,而进程之间是独立的。
  • 进程拥有自己的内存空间。线程使用进程的内存空间,且要和该进程的其他线程共享这个空间,而不是在进程中单独的给每个线程划分一点空间。
  • 同一进程内的线程在共享内存中运行,而进程在不同的内存空间中独立运行。
  • 线程可以使用wait、notify、notifyAll等方法直接与同进程中的其他线程通信,而进程之间需要使用特别的通信技术来互相通信。

3、并行与并发

3.1、并行

并行(Parallel)指的是同时处理多个任务的能力,理解并行的概念重点是在同时这个词。比如人,同时呼吸、思考、敲字、体内的新陈代谢,这些都是在同一时刻进行的,因此可以称为并行。又比如我们电脑上的CPU,当CPU有多个内核时,一个CPU内核可以执行一个进程,另一个内核可以执行另一个进程,两个内核之间互不抢占CPU资源,可以同时进行,这种方式也称之为并行。所以,一个单核的CPU,是不可能做到并行的。以下用一张经典的图来说明并行:
在这里插入图片描述
两台咖啡机同时工作,那么这就是并行。

3.2、并发

并发(Concurrent)指的是处理多个任务的能力,并不一定要同时。比如一个人边吃饭边喝水,这两个动作并不是同时进行的,必须先停止吃饭,然后喝水,喝完水后继续吃饭。还是以CPU为例,并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行。比如我们一边听歌一边下载电影,这两个任务并不是真正的同时进行,只是CPU切换较快,我们感觉是同时进行,那么这就是并发。如下图:
在这里插入图片描述
只有一台咖啡机,所有人并不能同时使用它,只能排队,等一个人用完后另一个人才能去使用,这种就是并发。

3.3、并行与并发的区别

并发是在一段时间内宏观上多个程序同时运行,并行是在某一时刻,真正有多个程序在运行。区别总结如下:

  • 并行指多件事在同一时间点发生,并发指多件事在同一时间段内发生。
  • 并行的多个任务之间不会抢占资源,而并发的多个任务之间是互相抢占资源的。

3.4、多线程是并发还是并行

这个取决于CPU的核心数,如果CPU是单核的,那么就算是线程,也是并发的。如果CPU是多核的,多线程可以并行。

4、线程实现的两种方式

Java中主要提供了两种方式来实现线程,一种是继承java.lang.Thread类,另一种是实现java.lang.Runnable接口。下面分别介绍。

4.1、继承Thread类

java.lang.Thread类是线程类型,这个类或者它子类的任何一个实例,都代表一个线程对象。线程对象调用start方法启动一个线程,Thread类中的run方法是线程启动后自动执行的业务方法,继承Thread类通常会重写run方法。

api说明如下:
在这里插入图片描述
提供的方法如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
常用方法说明如下:

  • currentThread():获取当前正在执行的线程对象的引用。
  • getId():后去此线程的标识符,返回一个long型的。
  • getName():后去此线程的名称,返回的是String类型的。
  • getPriority():获取此线程的优先级,返回int型的。
  • getState():获取此线程的状态。
  • getThreadGroup():获取线程所属的线程组。
  • interrupt():中断这个线程。
  • interrupted():测试这个线程是否中断,返回布尔值。
  • isAlive() :线程是否存活,返回布尔值。
  • isDaemon() :是否为守护线程,返回布尔值。
  • isInterrupted():线程是否中断,返回布尔值。
  • join():线程联合,另一个线程会等待这个线程执行完毕死亡后才开始执行。。
  • run():这个方法执行线程的核心业务功能。
  • setName(String name):设置线程名称。
  • setPriority(int newPriority):设置线程的优先级,可以1-10之间int值,如果不在10以内,会抛出异常。设置为1和设为Thread.MIN_PROPERTY是等效的。设置为5和设Thread.NORM_PROPERTY是等效的。设置为10和设为Thread.MAX_PROPERTY是等效的。每个新产生的线程都会继承父线程的优先级。
  • sleep(long millis):线程休眠,使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。
  • start() :启动线程, Java虚拟机自动调用此线程的run方法。
  • stop() :线程停止,已被官方弃用。
  • toString():返回此线程的字符串表示,包括线程的名称,优先级和线程组。
  • yield():线程礼让。暂停当前线程,执行其他线程,可恢复。

以下代码示例继承Thread类实现多线程任务。

创建3个类,都继承了Thread类:

/*
 * 继承Thread类,那么此类的实例就是一个线程对象
 */
public class Thread1 extends Thread {

    /*
           * 覆盖父类中的run方法 
     * run方法里面是核心代码,只要启动线程,JVM就会自动调用该方法
     */
    @Override
    public void run() {
        for(;;) {
            System.out.println("线程名称:" + this.getName());
            try {
                //线程休眠2秒
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
    }

}
/*
 * 继承Thread类,那么此类的实例就是一个线程对象
 */
public class Thread2 extends Thread {

    /*
           * 覆盖父类中的run方法 
     * run方法里面是核心代码,只要启动线程,JVM就会自动调用该方法
     */
    @Override
    public void run() {
        for(;;) {
            System.out.println("线程名称:" + this.getName());
            try {
                //线程休眠2秒
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
    }

}
/*
 * 继承Thread类,那么此类的实例就是一个线程对象
 */
public class Thread3 extends Thread {

    /*
           * 覆盖父类中的run方法 
     * run方法里面是核心代码,只要启动线程,JVM就会自动调用该方法
     */
    @Override
    public void run() {
        for(;;) {
            System.out.println("线程名称:" + this.getName());
            try {
                //线程休眠2秒
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
    }

}

run方法里面是for循环的死循环,会一直执行,不会结束。

测试:

    public static void main(String[] args) {
        // 实例化线程对象
        Thread1 t1 = new Thread1();
        // 设置线程名称
        t1.setName("线程1");
        // 设置线程优先级
        t1.setPriority(1);
        Thread2 t2 = new Thread2();
        t2.setName("线程2");
        t2.setPriority(10);
        Thread3 t3 = new Thread3();
        t3.setName("线程3");
        t3.setPriority(5);
        // 启动这3个线程
        t1.start();
        t2.start();
        t3.start();
        System.out.println("执行main方法!");
    }

执行,控制台:
在这里插入图片描述
可以看到这3个线程是轮循着执行的,而且没有执行的固定顺序。虽然设置了线程的优先级,但是并不是按照事先设置好的优先程度来执行的,也就是说优先级只是一个期许的理想状态,设置优先级只是加大了执行这个线程的权重而已,并不是一定就先执行这个线程。而且main方法只执行了一次,那么可以推断这些线程都是在主线程中运行的。

测试Thread类的其他方法:

public class Thread4 extends Thread {

    @Override
    public void run() {
        for (int i = 3; i > 0; i--) {
            System.out.println("现在的i值是:" + i);
            // 获取当前线程
            Thread current = this.currentThread();
            System.out.println("线程ID:" + current.getId());
            System.out.println("线程名称:" + current.getName());
            System.out.println("线程的状态:" + current.getState());
            System.out.println("线程的优先级:" + current.getPriority());
            System.out.println("线程所属线程组:" + current.getThreadGroup());
            System.out.println("线程是否中断?" + current.isInterrupted());
            System.out.println("线程是否存活?" + current.isAlive());
            System.out.println("是否是守护线程:" + current.isDaemon());
            System.out.println("线程信息:" + current.toString());
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("------------------------------------");
        }

    }

}

    public static void main(String[] args) {
        // 实例化线程对象
        Thread4 t4 = new Thread4();
        // 设置线程名称
        t4.setName("线程1");
        t4.start();
        System.out.println("执行main方法!");
    }

执行,控制台:
在这里插入图片描述

4.2、实现Runnable接口

通过继承Thread类来实现线程有一个很大的弊端,就是Java只支持单继承,如果继承了Thread类,那么就不能继承其他类了。那么最好的方法是实现java.lang.Runnable接口,这样就可以继承其他类。

Runnable接口的api说明如下:
在这里插入图片描述
提供的方法:
在这里插入图片描述
这个接口提供唯一的一个run方法,实现Runnable接口必须要实现run方法。这其实和重写Thread类中的run方法是一样的,只不过一个是类,一个是接口。

以下代码说明:

/*
 * 实现Runnable接口
 */
public class MyRunnable implements Runnable{
    
    int sum = 0;
    int i = 0;
    
    //声明两个线程
    private Thread t1;
    private Thread t2;
    
    //构造方法
    public MyRunnable(Thread t1,Thread t2) {
        if(t1 == null) {
            t1 = new Thread(this);
            t1.setName("线程1");
        }
        if(t2 == null) {
            t2 = new Thread(this);
            t2.setName("线程2");
        }
        //启动t1线程
        t1.start();
        try {
            //等待t1线程死亡
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //启动t2线程
        t2.start();
    }

    @Override
    public void run() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程名称
        String tName = t.getName();
        //使用两个线程来计算1到100的和值
        while(i <=100) {
            System.out.println("当前线程:" + tName + "正在计算!");
            sum += i;
            i++;
            System.out.println("当前累加和是:" + sum);
            System.out.println("---------------------");
            if(i==50 && tName.equals("线程1")) {
                break;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
    }

}

测试:

    public static void main(String[] args) {
        Thread t1 = null;
        Thread t2 = null;
        MyRunnable myRunnable = new MyRunnable(t1, t2);
    }

控制台:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
线程1执行run方法,计算1到50的和值,i=50时,break跳出while循环,线程1的run方法执行完毕,线程1死亡,现在的i值是51。线程2继续执行run方法,计算从51到100的和值,一直到跳出循环,run方法执行完毕,然后线程2也死亡,现在的i值是101。那么最后的和值就是线程1计算的1到50的和值加上线程2计算的51到100的和值。

再以一个例子说明:

public class Classroom implements Runnable {

    // 两个线程模拟老师和学生
    private Thread teacher;
    private Thread student;

    // 构造方法
    public Classroom() {
        // 实例化两个线程对象
        teacher = new Thread(this, "上官老师");
        student = new Thread(this, "李狗蛋");
        student.setPriority(10);
        student.start();
        teacher.start();
    }

    @Override
    public void run() {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 如果是学生线程
        if (t == student) {
            System.out.println(t.getName() + "正在听课!");
            System.out.println(t.getName() + "突然不想听了,准备睡觉,眯着眯着就睡着了。");
            try {
                // 睡20秒
                Thread.sleep(20000);
            } catch (InterruptedException e) {
                System.out.println(t.getName() + "被老师叫醒了,然后被K的满头是包!");
                e.printStackTrace();
            }
        } else if (t == teacher) {// 如果是老师线程
            System.out.println(t.getName() + "正在讲人体构造课程!");
            System.out.println(t.getName() + "看到有人在睡觉!");
            for (int i = 1; i <= 5; i++) {
                System.out.println(t.getName() + "突然大叫:'下课了!下课了!");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 阻塞学生线程
            student.interrupt();
        }

    }

}

测试:

public static void main(String[] args) {
    new Classroom();
}

执行,控制台:
在这里插入图片描述

5、线程的生命周期

线程具有生命周期,大概7种状态:新建状态、就绪状态、运行状态、等待状态、休眠状态、阻塞状态、死亡状态。如下图:
在这里插入图片描述

说明如下:

  • 当新建一个线程对象时,就是新建状态。
  • 线程对象调用start()方法启动线程,进入就绪状态,等待系统调度资源。
  • 系统为线程分配CPU时间片,线程开始执行run()方法,进入运行状态。
  • 线程对象执行wait()方法,可使正在执行的线程进入等待状态,等待状态下可调用notify()方法或notifyAll()方法唤醒线程,回到就绪状态,继续等待系统调度资源。
  • 线程对象执行sleep()方法,可使正在执行的线程进入休眠状态,休眠结束后,回到运行状态,继续执行任务。
  • 线程对象执行interrupt()方法,可使正在执行的线程进入阻塞状态,该状态下线程并未死亡,会让出CPU的资源给其他线程。阻塞状态可重新回到就绪状态。
  • 当线程执行完run方法后,线程会死亡,进入死亡状态,一个死亡的线程是无法再唤醒的。

6、线程同步

在单线程程序中,是按照代码的先后顺序来执行的,后面的事情必须要等到前面的事情完成后才能开始进行。而在多线程程序中,会发生多个线程抢占当前CPU资源的问题,所以在多线程中需要防止这种资源访问的冲突,Java中提供了线程的同步机制来避免多个线程同时访问资源的冲突。

6.1、线程安全

在实际的开发过程中,使用多线程程序的情况很多,比如火车站的售票系统、银行的排号系统等。在编写多线程程序的时候,我们必须要考虑到线程的安全问题。事实上线程的安全问题来源于多个线程同时存取单一对象的数据。

现在不用线程同步机制处理,模拟一下火车站的售票系统,看会出什么问题。代码如下:

public class SaleTickets implements Runnable {

    // 假设剩下15张票
    int ticket = 15;

    @Override
    public void run() {
        //获取当前线程
        Thread t = Thread.currentThread();
        while (true) {
            if (ticket > 0) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(t.getName() + "正在售票,当前剩余票数:" + ticket-- + "张!");
            }
        }
    }

测试:

    public static void main(String[] args) {
        SaleTickets st = new SaleTickets();
        //创建四个线程对象,模拟4个卖票窗口
        Thread t1 = new Thread(st);
        t1.setName("窗口1");
        Thread t2 = new Thread(st);
        t2.setName("窗口2");
        Thread t3 = new Thread(st);
        t3.setName("窗口3");
        Thread t4 = new Thread(st);
        t4.setName("窗口4");
        //启动线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }

执行,控制台:
在这里插入图片描述
可以看到,非常的混乱,因为当前线程都在同时修改ticket的值,所以才会出现这样的情况,很明显,现实生活中,这样的情况是一定不被允许的。

6.2、线程同步机制

在处理多线程问题时必须注意,当两个或以上的线程同时访问同一个变量时,并且其中一个线程需要修改这个变量,必须要作出处理,否则会发生混乱。在处理多线程同步时,首先把修改数据的方法用关键字synchronizd来修饰,一个方法使用关键字synchronizd修饰后,当一个线程正在使用这个方法时,其他线程会暂时等待,等到该线程使用完后再使用,即存在一个优先级。而这种称为线程同步。线程同步有3种方式:同步代码块、对象同步锁、方法同步锁。

其实也很好理解,基本所有解决多线程资源冲突问题的方法都是采用给定时间内,只允许一个线程访问共享资源,就像给资源加了一道锁,当一个线程访问时,加锁,加锁时间内只有这个线程能够修改资源,其他线程无法修改。等这个线程使用完毕后,开锁,然后另一个线程再进入,再加锁,使用完毕后再开锁,下一个线程进入,再加锁,如此反复循环。Java同步机制中使用关键字synchronized来加锁。

6.2.1、同步代码块

被同步的代码块在一个线程对象对其进行访问时,其他线程是没有访问权限的,只有当前线程完全执行完该代码块后,释放了同步锁,其他线程才可以对这个代码块进行访问。

语法如下:

synchronized (Object) {
      ......
}

修改刚才的代码,如下:

public class SaleTickets implements Runnable {

    // 假设剩下15张票
    int ticket = 15;

    @Override
    public void run() {
        // 获取当前线程
        Thread t = Thread.currentThread();
        while (true) {
            //代码块加锁
            synchronized ("") {
                if (ticket >= 0) {
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(t.getName() + "正在售票,当前剩余票数:" + ticket-- + "张!");
                }
            }
        }
    }

}

测试:

    public static void main(String[] args) {
        SaleTickets st = new SaleTickets();
        //创建四个线程对象,模拟4个卖票窗口
        Thread t1 = new Thread(st);
        t1.setName("窗口1");
        Thread t2 = new Thread(st);
        t2.setName("窗口2");
        Thread t3 = new Thread(st);
        t3.setName("窗口3");
        Thread t4 = new Thread(st);
        t4.setName("窗口4");
        //启动线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }

执行,控制台:
在这里插入图片描述

可以看到,现在只有一个线程对锁住的代码块有访问权限,其他线程并没有访问权限。其他的线程没有执行是因为线程1执行完后,ticket的值是-1了,不满足if的条件了,控制台自然不会有输出。

再测一个例子:

public class SynCode implements Runnable {

    @Override
    public void run() {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 代码块加锁
        synchronized ("") {
            for (int i = 5; i > 0; i--) {
                System.out.println("当前执行代码块的是:" + t.getName() + ",当前i值为" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }

}

    public static void main(String[] args) {
        SynCode sc = new SynCode();
        // 创建四个线程对象,模拟4个卖票窗口
        Thread t1 = new Thread(sc);
        t1.setName("线程1");
        Thread t2 = new Thread(sc);
        t2.setName("线程2");
        Thread t3 = new Thread(sc);
        t3.setName("线程3");
        Thread t4 = new Thread(sc);
        t4.setName("线程4");
        // 启动线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }

控制台:
在这里插入图片描述
当上一个线程使用完加锁的代码块后,下一个线程才会再使用。

6.2.2、对象同步锁

对共享对象的访问必须同步,叫做条件变量,Java允许通过监视器使用条件变量实现线程同步,监视器阻止两个线程同时访问同一个条件变量,如同锁一样作用在数据上。线程进入卖票方法时获取监视器(加锁),当线程1的方法执行完毕后释放监视器(开锁),线程2的卖票方法才能进入。

以下用代码说明。

创建实体类:

public class Dog {

    private String name;

    public Dog() {

    }

    public Dog(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

实现Runnable接口:

public class SynObject implements Runnable {

    // 定义Dog对象
    private Dog dog;

    // 构造方法
    public SynObject() {
        if (dog == null) {
            dog = new Dog();
        }
    }

    @Override
    public void run() {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 锁住对象
        synchronized (this.dog) {
            for (int i = 1; i <= 3; i++) {
                System.out.println(t.getName() + "正在给狗改名字!");
                if (t.getName().equals("李狗蛋")) {
                    dog.setName("狗蛋" + i + "号");
                }
                if (t.getName().equals("李铁柱")) {
                    dog.setName("铁柱" + i + "号");
                }
                if (t.getName().equals("王尼玛")) {
                    dog.setName("尼玛" + i + "号");
                }
                if (t.getName().equals("云过梦无痕")) {
                    dog.setName("云儿" + i + "号");
                }
                System.out.println("狗狗现在的名字是:" + dog.getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("---------------------------------");
        }

    }

}

测试:

    public static void main(String[] args) {
        SynObject so = new SynObject();
        // 创建四个线程对象,模拟4个卖票窗口
        Thread t1 = new Thread(so);
        t1.setName("李狗蛋");
        Thread t2 = new Thread(so);
        t2.setName("王尼玛");
        Thread t3 = new Thread(so);
        t3.setName("李铁柱");
        Thread t4 = new Thread(so);
        t4.setName("云过梦无痕");
        // 启动线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }

执行,控制台:
在这里插入图片描述
可以看到,最多同时只有一个线程能够操作对象,下一个线程想要修改对象的属性值,必须等上一个线程修改完毕。

6.2.3、方法同步锁

即在方法前面加synchronized。语法如下:

修饰符 synchronized 返回值 方法名称(参数列表){
  ......
}

以下代码说明:

public class SynMethod implements Runnable {

    // 假设当前账户上有10000元
    private int money = 10000;

    @Override
    public void run() {
        doMoney();
    }

    //给方法加锁,同时只能有一个线程调用该方法
    private synchronized void doMoney() {
        // 获取当前线程
        Thread t = Thread.currentThread();
        for (int i = 1; i <= 3; i++) {
            // 会计线程,往账上加钱
            if (t.getName().equals("会计")) {
                System.out.println(t.getName() + "正在整理账户!");
                money += 10 * i;
            }
            if (t.getName().equals("出纳")) {
                System.out.println(t.getName() + "正在从账户上取钱!");
                money -= 20 * i;
            }
            System.out.println("当前账户余额:" + money + "元");
            System.out.println("----------------------------------");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

测试:

    public static void main(String[] args) {
        SynMethod sm = new SynMethod();
        //创建线程1,模拟会计
        Thread t1 = new Thread(sm);
        t1.setName("会计");
        //创建线程2,模拟出纳
        Thread t2 = new Thread(sm);
        t2.setName("出纳");
        // 启动线程
        t1.start();
        t2.start();
    }

执行,控制台:
在这里插入图片描述
可以看到,会计加钱时,出纳无法支取,出纳取钱时,会计无法往账户上加钱。说明同时只有一个线程可以调用doMoney()方法,其他线程必须要等当前线程调用完后才能调用。

7、线程并发库和线程池

7.1、线程并发库

在JDK1.5中增加了Doug Lea的并发库,这一引进给Java线程的管理和使用提供了强大的便利性,java.util.concurrent包中提供了对线程优化、管理的各项操作,使得线程的使用变得更加简单,该包中提供了线程的运行,线程池的创建,线程的生命周期的控制。

api中目录及说明如下:
在这里插入图片描述

7.2、线程池

7.2.1、线程池的概念

简单来说,线程池就是用来装线程的容器,预先在这个池子中创建了自定义数量的线程对象,要用的时候直接从这个池子中取出来用就行了,不用再创建,用完后再回收回去。使用线程池可以很好地提升性能,在系统启动时线程池可以预先创建好一些空闲的线程。当有任务时,程序将任务传递给线程池,线程池会取出一条空闲的线程来处理任务,处理完任务后,这个线程并不会死亡,而是由线程池回收回去,再次成为空闲的线程,继续等待执行下一个任务。我们以前使用的线程都是run方法执行完毕,然后线程直接死亡的,使用线程池的话,线程并不会死亡,而是继续回到线程池成为空闲线程。

以下用图说明:
在这里插入图片描述
说明:

  • 线程池预先创建好了n个线程,当没有任务时,这n个线程都是闲置的。
  • 当来了一个任务时,线程池取出一个线程来执行任务,比如上图的T1,线程池取出T1线程启动,用来执行任务1,其他的n-1个线程还是闲置的。
  • 任务1执行完毕,T1线程不会死亡,而是被回收到线程中,继续成为闲置状态,等待处理下一个任务。

7.2.2、线程池的工作机制

没用线程池的时候,任务都是提交到具体某个线程,由它来直接处理,当使用线程池的时候,任务并不是直接提交到线程的。说明如下:

  • 在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程,这个这个线程会脱离空闲状态,进行任务处理。
  • 一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务,线程池可以启动多个线程来处理。

7.2.3、使用线程池的原因

多线程运行时,系统不断的启动和关闭新线程,成本非常高,会过渡消耗系统资源,以及过渡切换线程的危险,从而可能导致系统资源的崩溃。这时可以使用线程池来避免这种情况。

7.2.4、线程池的作用

线程池的主要作用从以下3个方面来讲:

  • 限定了线程的个数,不会因线程过多而导致系统运行缓慢或崩溃。
  • 线程池不需要每次都去创建或销毁,节约了资源。
  • 线程不需要每次都去创建,响应时间更快。

7.2.5、线程池的创建使用

Java中通过Executors提供了4个不同的线程池:CachedThreadPool、FixedThreadPool、SingleThreadPool、ScheduledThreadPool。以下分别介绍这4种线程池的创建方式。

7.2.5.1、CachedThreadPool

可缓存线程池,如果线程池长度超过处理需要,可以灵活的回收线程池,若无回收,则新创建。

关于缓存线程池,说明如下:

  • 缓存线程池中的线程数量是不固定的,根据任务数量来开启线程,一个任务来了,就开启1个线程为其服务,N个任务来了就开启N个线程。
  • 一段时间后,如果只剩下1个任务,那么只留下1个线程为其服务,其他的线程都清掉。
  • 使用缓存线程池的时候,是根据任务的数量来自动产生线程数量的,线程数量跟着任务数量走。

以下用代码说明这种线程池的使用:

    // 定义方法,创建使用缓存线程池
    public static void useCacheThreadPool() {
        // 创建缓存线程池
        ExecutorService cacheThreadPool = Executors.newCachedThreadPool();
        // 假设5个任务
        for (int i = 1; i <= 5; i++) {
            int task = i;
            // 向线程池中丢任务
            cacheThreadPool.execute(new Runnable() {

                @Override
                public void run() {
                    // 获取当前线程
                    Thread t = Thread.currentThread();
                    // 每个线程循环处理任务3次
                    for (int j = 1; j <= 3; j++) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(t.getName() + "已执行任务" + j + "次,当前任务编号:" + task);
                    }

                }
            });
        }
        System.out.println("5个任务已经全部提交!");
        // 任务执行完毕后关闭线程池
        cacheThreadPool.shutdown();
    }

测试:

    public static void main(String[] args) {
        MethodsUtil.useCacheThreadPool();
    }

执行,控制台:
在这里插入图片描述

从结果可以看到,缓存线程池中的线程数量是根据任务数来创建的,n个任务就有n个线程。并且每个线程确实是处理了3次任务。

7.2.5.2、FixedThreadPool

定长的线程池,可控制线程的最大并发数,超过的线程会在队列中等待。

代码如下:

    // 定义方法,创建使用定长线程池
    public static void useFixedThreadPool() {
        // 创建定长线程池,参数为线程个数
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        // 假设5个任务
        for (int i = 1; i <= 5; i++) {
            int task = i;
            // 向池子中丢任务
            fixedThreadPool.execute(new Runnable() {

                @Override
                public void run() {
                    Thread t = Thread.currentThread();
                    // 每个任务循环处理4次
                    for (int j = 1; j <= 4; j++) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(t.getName() + "已执行任务" + j + "次,当前任务编号:" + task);
                    }

                }
            });
        }
        System.out.println("4个任务已经全部提交!");
        fixedThreadPool.shutdown();
    }

测试:

    public static void main(String[] args) {
        MethodsUtil.useFixedThreadPool();
    }

在这里插入图片描述
3个线程,5个任务,每个任务执行4次,相当于3个线程处理20个任务。可以看到,线程1和线程3都执行了8个任务,而线程2只执行了4个任务。

7.2.5.3、SingleThreadPool

单线程化线程池,只会用唯一的工作线程来执行任务,并保证所有任务按照指定的顺序来执行。

关于单线程线程池,说明如下:

  • 只会有1个线程去执行任务。
  • 跟直接new一个线程的区别是:直接new的线程在执行完任务后会死掉,不会再生,而线程池产出的单个线程执行完任务后不会死亡,而是被回收到线程池中。
  • 单线程池的好处是当线程死亡了它会再自动创建1个出来,也就是说要保证有一个线程在执行任务的话,那么newSingleThreadExecutor()是个很好的选择。

代码如下:

    // 定义方法,创建使用单线程线程池
    public static void useSingleThreadPool() {
        // 创建单线程线程池
        ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
        // 假设3个任务
        for (int i = 1; i <= 3; i++) {
            int task = i;
            // 向池子中丢任务
            singleThreadPool.execute(new Runnable() {

                @Override
                public void run() {
                    Thread t = Thread.currentThread();
                    // 每个任务循环执行3次
                    for (int j = 1; j <= 3; j++) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(t.getName() + "已执行任务" + j + "次,当前任务编号:" + task);
                    }

                }
            });
        }
        System.out.println("3个任务已经全部提交完毕!");
        singleThreadPool.shutdown();
    }

测试:

    public static void main(String[] args) {
        MethodsUtil.useSingleThreadPool();
    }

在这里插入图片描述
可以看到,自始至终只有1个线程执行任务,3个任务,每个任务执行3次,相当于1个线程执行9个任务。

7.5.2.4、ScheduledThreadPool

创建定时器线程池,支持定时及周期性任务执行。

代码如下:

    // 定义方法,创建使用定时器线程池
    public static void useScheduledThreadPool() {
        // 创建定时器线程池,池中3个线程
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
        // 假设5个任务
        // 3个线程执行5个任务,每个线程会先执行1次任务
        // 然后剩下的两个任务由其中两个线程执行
        for (int i = 1; i <= 5; i++) {
            int task = i;
            // 任务丢进池子里
            scheduledThreadPool.scheduleAtFixedRate(new Runnable() {

                @Override
                public void run() {
                    Thread t = Thread.currentThread();
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(t.getName() + "正在执行任务" + task);
                }
                // 10秒后开始执行,然后每隔3秒执行1次任务
            }, 10, 3, TimeUnit.SECONDS);
        }
    }

测试:

    public static void main(String[] args) {
        MethodsUtil.useScheduledThreadPool();
    }

在这里插入图片描述
可以看到,第一次循环线程1、2各执行了2个任务,线程3只执行了1个任务。第二次循环,线程2、3各执行了2个任务,而线程1只执行了1个任务。

7.2.6、小结

对4种线程池的特点作一个小结。如下表:
在这里插入图片描述
实际开发中如果需要使用线程池,那么可根据实际情况选用哪一种。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值