入门多线程

本秂侑毒 提交于 2019-12-01 08:30:59

进程的概念

在一个操作系统中,每个独立执行的程序都可称为一个进程,也就是 " 正在运行的程序 "。目前大多数计算机上安装的都是多任务操作系统,即能够同时执行多个应用程序,最常见的有Windows、Linux、Unix等。

在多任务操作系统中,表面上看是支持进程并发执行的,例如可以一边听音乐,一边聊天,但实际上这些进程并不是同时运行的。在计算机中,所有的应用程序都是由CPU执行。对于一个CPU而言,在某个时间点只能运行一个程序,也就是说只能执行一个进程。操作系统会为每一个进程分配一段有限的CPU使用时间,CPU在这段时间中执行某个进程,然后会在下一段时间切换到另一个进程中去执行。由于CPU运行速度很快,能在极短的时间内在不同的进程之间进程切换,所以给人以同时执行多个程序的感觉。

线程的概念

每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元同时进行。这些执行单元可以看做程序执行的一条条线索,被称为线程。操作系统中的每一个进程中都至少存在一个线程。例如当一个Java程序启动时,就会产生一个进程,该进程中会默认创建一个线程,在这个线程上会运行main()方法中的代码。

所谓的多线程是指一个进程在执行过程中可以产生多个单线程,这些单线程程序在运行时是相互独立的,它们可以并发执行。

 

 

多线程看似是同时执行的,其实不然,它们和进程一样,也是由CPU轮流执行的,只不过CPU运行速度很快,故而给人同时执行的感觉。

线程的创建

在Java中提供了两种多线程实现方式,一种是继承java.lang包下的Treand类,覆写Thread类的run()方法,在run()方法中实现运行在线程上的代码;另一种是实现java.lang.Runnable接口,同样是在run()方法中实现运行在线程上的代码。接下来就对创建多线程的两种方式分别进行讲解,并比较他们的优缺点。

1.继承Thread类创建多线程

先来看一个简单的单进程实例

public class Example01 {

    public static void main(String[] args) {
        MyThread myThread = new MyThread();        //创建MyThread实例对象
        myThread.run();                        //调用MyThread类的run()方法
        for (int i = 0; i < 10; i++) {
            System.out.println("Main类的run方法在运行");
        }
    }
    
}
class MyThread{
    public void run() {
        for (int i = 0; i < 10; i++) {        //打印输入语句
            System.out.println("MyThread类的run方法在运行");
        }
    }
}

通过继承Thread类,并重写Thread类中的run()方法便可实现多线程。在Thread类中,提供了一个start()方法,用于启动新线程。线程启动后,虚拟机会自动调用run()方法,如果子类重写了,便会执行子类中的方法。

接下来通过修改上面程序,通过继承Thread类的方式来实现多线程

public class Example02 {

    public static void main(String[] args) {
        MyThread1 myThread1 = new MyThread1();        //创建线程MyThread的线程对象
        myThread1.start();        //开启线程
        for (int i = 0; i < 10; i++) {        //通过循环语句打印输出
            System.out.println("Main方法中的run()方法在运行"+i);
        }
    }
}

class MyThread1 extends Thread{
    public void run() {
        for (int i = 0; i < 10; i++) {        //通过循环语句打印输出
            System.out.println("MyThread的run()方法在运行--"+i);
        }
    }
}

单线程的程序在运行时,会按照代码的调用顺序执行,而在多线程中,main()方法和MyThread类的run()方法却可以同时运行,互不影响,这正是单线程和多线程的区别

2.实现Runnable接口创建多线程

Thread类实现的多线程有一定的局限性。因为Java只支持单继承,一个类一旦继承了某个类就无法再继续继承Thread类

Thread类提供了另外一个构造方法Thread(Runnable target),其中,Runnable是一个接口,它只有一个run()方法。当通过Thread(Runnable target)构造方法创建线程对象时,只需为该方法传递一个实现了Runnable接口的实例对象,这样创建的线程将调用实现了Runnable接口的类中run()方法作为运行代码,而不需要调用Thread类中的run()方法

接下来通过一个案例来演示如何实现Runnable接口的方式来创建多线程

public class Example01 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();            //创建MyThread的实例对象
        Thread thread = new Thread(myThread);        //创建线程对象
        thread.start();        //开启线程,执行线程中的run()方法
        for (int i = 0; i < 10; i++) {
            System.out.println("Main类的run()方法在运行"+i);
        }
    }
}
class MyThread implements Runnable{
    @Override
    public void run() {        //线程的代码段,当调用start()方法时,线程从此处开始执行
        for (int i = 0; i <10; i++) {
            System.out.println("MyThread类的run()方法在运行"+i);
        }
    }
}

两种实现多线程方法的对比分析

既然直接继承Thread类和实现Runnable接口都能实现多线程,那么这两种实现多线程的方法在实际应用中又有什么区别呢?

假设售票厅有4个窗口可发售某日某次列车的100张车票,这时,100张车票可以看做共享资源,4个售票窗口需要创建4个线程。为了更直观的显示窗口的售票情况,可以通过Thread的currentThread()方法得到当前线程的实例对象,然后调用getName()方法可以获取到线程的名称

首先通过继承Thread类的方式来实现多线程的创建

public class Example01 {

    public static void main(String[] args) {
        new TicketWindow().start();        //创建第1个线程对象TicketWindow并开启
        new TicketWindow().start();        //创建第2个线程对象TicketWindow并开启
        new TicketWindow().start();        //创建第3个线程对象TicketWindow并开启
        new TicketWindow().start();        //创建第4个线程对象TicketWindow并开启
    }
}
class TicketWindow extends Thread{
    private int TICKETS=100;
    public void run() {
        for (int i = 0; i <100; i++) {
            //通过死循环打印语句
            if(TICKETS > 0) {
                String th_name = Thread.currentThread().getName();    //获取当前线程的名字
                System.out.println(th_name+"正在发票第"+TICKETS--+"张票");
            }
        }
    }
}

由于现实中铁路系统的票资源是共享的,因此上面的运行结果显然不合理。为了保证资源共享,在程序中只能创建一个售票对象,然后开启多个线程去运行同一个售票对象的售票方法。简单来说就是4个线程运行同一个售票程序,这时就需要用到另外一种多线程实现方式。

通过使用Runnable接口的方式来实现多线程的创建。并使用构造方法Thread (Runnable targett,String name) 在创建线程对象的同时制定线程的名称。

public class Example02 {
    public static void main(String[] args) {
        TicketWindow2 ticketWindow2 = new TicketWindow2();
        new Thread(ticketWindow2, "窗口1").start();        //创建线程并起名为窗口1,开启线程
        new Thread(ticketWindow2, "窗口2").start();        //创建线程并起名为窗口2,开启线程
        new Thread(ticketWindow2, "窗口3").start();        //创建线程并起名为窗口3,开启线程
        new Thread(ticketWindow2, "窗口4").start();        //创建线程并起名为窗口4,开启线程
    }
}
class TicketWindow2 implements Runnable{
    private int tickets=100;
    @Override
    public void run() {
        while(true) {
            if(tickets>0) {
                String name = Thread.currentThread().getName();    //获取线程名字
                System.out.println(name+"正在发售第"+tickets--+"张票");
            }
        }
    }
}

每个线程都去调用这个TicketWindow2 对象中的run()方法,这样就可以确保4个线程访问的是同一个tickets变量,共享100张车票

通过上面两个文件可以看出,实现Runnable接口相对于继承Thread类来说,有如下显著的好处:
1. 适合多个相同程序代码的线程去处理同一个资源的情况,把线程与程序代码、数据有效的分离,很好地体现了面向对象的设计思想。
2. 可以避免由于Java的单继承带来的局限性。在开发中经常碰到这样一种情况,就是使用一个已经继承了某一个类的子类创建线程,由于一个类不能同时有两个父类,所以不能用继承Thread类的方式,那么就只能采用Runnable接口的方式。

事实上,大部分的多线程应用都会采用第2中方式,即实现Runnable接口。

线程的生命周期及状态转换

在Java中,任何对象都有生命周期,线程也不例外,它也有自己的生命周期。当Thread对象创建完成时,线程的生命周期便开始了。当run()方法中代码正常执行完毕或者线程抛出一个未捕获的异常(Exception)或者错误(Error)时,线程的生命周期便会结束。线程整个生命周期可以分为5个阶段,分别是新建状态(New)、就绪状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)和死亡状态(Terminated),线程的不同状态表明了线程当前正在进行的活动。在程序中,通过一些操作,可以使线程在不同的状态之间转换

 

 

 接下来针对线程生命周期中的5种状态分别进行详细讲解,具体如下:

1. 新建状态(New)
创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,没有表现出任何线程的动态特征

2. 就绪状态(Runnable)
当线程对象调用了start()方法后,该线程就进入就绪状态。处于就绪状态的线程位于线程队列中,此时它只是具备了运行的条件,能否获得CPU的使用权并开始运行,还需要等待系统的调度

3. 运行状态(Running)
如果处于就绪状态的线程获得了CPU的使用权,并开始执行run()方法中的线程执行体,则该线程处于运行状态。一个线程启动后,它可能不会一直处于运行状态,当运行状态的线程使用完系统分配的时间后,系统就会剥夺该线程占用的CPU资源,让其他线程获得执行的机会。需要注意的是,只有处于就绪状态的线程才可能转换到运行状态

4. 阻塞状态(Blocked)
一个正在执行的线程在某些特殊情况下,如被人为挂起或执行耗时的输入/输出操作时,会让出CPU的使用权并暂时中止自己的执行,进入阻塞状态。线程进入阻塞状态后,就不能进入排队队列。只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。

  下面就列举一下线程由运行状态转换成阻塞状态的原因,以及如何从阻塞状态转换成就绪状态。

    - 当线程试图获取某个对象的同步锁时,如果该锁被其他线程所持有,则当前线程会进入阻塞状态。如果想从阻塞状态进入就绪状态就必须获取到其他线程锁持有的锁。
    - 当线程调用了一个阻塞式的IO方法时,该线程就会进入阻塞状态,如果想进入就绪状态就必须要等到这个阻塞的IO方法返回。
    - 当线程调用了某个对象的wait()方法时,也会使线程进入阻塞状态,如果想进入就绪状态就需要使用notity()方法唤醒该线程。
    - 当线程调用了Thread的sleep(long millis)方法时,也会使线程进入阻塞状态。在这种情况下,只需等到线程睡眠的时间到了以后,线程就会自动进入就绪状态。
    - 当在一个线程中调用了另一个线程的join()方法时,会使当前线程进入阻塞状态。在这种情况下,需要等到新加入的线程运行结束后才会结束阻塞状态,进入就绪状态。

    需要注意的是,线程从阻塞状态只能进入就绪状态,而不能直接进入运行状态,也就是说结束阻塞的线程需要重新进入可运行池中,等待系统的调度。

5. 死亡状态
当线程调用stop()方法或run()方法正常执行完毕后,或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他状态。

线程的调度

程序中的线程是并发执行的,某个线程弱项被执行必须要得到CPU的使用权。Java虚拟机会按照特定的机制为程序中的每个线程分配CPU使用权,这种机制被称为线程的调度。

在计算机中,线程调度有两种模型,分别是分时调度模型和抢占式调度模型。所谓分时调度模型是指让所有的线程轮流获得的CPU的使用权,并且平均分配每个线程占用的CPU时间片。抢占式调度模型是指让可运行池中优先级高的有限占用CPU,而对于优先级相同的线程,随机选择一个线程使其占用CPU,当它失去了CPU的使用权后,再随机选择其他线程获取CPU使用权。

Java虚拟机默认采用抢占式调度模型,通常情况下程序员不需要去关心它,但在某些特定的需求下需要改变这种模式,由程序自己来控制CPU的调度。

1.线程的优先级

在程序中,如果要对线程进行调度,最直接的方式就是设置线程的优先级。优先级越高的线程获得CPU执行的机会越大,而优先级越低的线程获得CPU机会越小。线程的优先级用1~10的整数来表示,数字越大优先级越高。除了可以直接使用数字表示线程的优先级,还可以使用Thread类中提供的静态常量表示线程的优先级。

 

 

 

程序在运行期间,处于就绪状态的每个线程都有自己的优先级,例如main线程具有普通优先级。然而线程优先级并不是固定不变的,可以通过Thread类的setPriority(int newPriority)方法对其进行设置,该方法中的参数newPriority接收的是1~10之间的整数或者Thread类的3个静态常用。

下面通过一个案例来演示不同优先级的两个线程在程序中的运行情况

public class Example01 {
    public static void main(String[] args) {
        Task task = new Task();
        //创建两个线程
        Thread minPriority = new Thread(task, "优先级较低的线程");
        Thread maxPriority = new Thread(task, "优先级较高的线程");
        //设置优先级
        minPriority.setPriority(Thread.MIN_PRIORITY);
        maxPriority.setPriority(Thread.MAX_PRIORITY);
        //开启线程
        minPriority.start();
        maxPriority.start();
    }
}
//定义一个线程的任务类
class Task implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"正在输出:"+i);
        }
    }
}

需要注意的是,虽然Java中提供了10个线程优先级,但是这些优先级需要操作系统的支持。不同的操作系统对优先级的支持是不一样的,不会与Java中线程优先级一一对应。因此,这设计多线程应用程序时,其功能的实现一定不能依赖于线程的优先级,而只能把线程优先级作为一种提高程序效率手段。

2.线程休眠

优先级高的程序会先执行,而优先级低的程序会后执行。如果希望人为地控制线程,使正在执行的线程暂停,将CPU让给别的线程,这时可以使用静态方法sleep(long millis),该方法可以让当前正在执行的线程暂停一段时间,进入休眠状态。当前线程调用sleep(long millis)方法后,在指定时间(参数millis)内该线程是不会执行的,这样其他的线程就可以得到执行的机会了。

Sleep(long millis) 方法声明会抛出InterruptedException(中断异常),因此在调用该方法时应该捕获异常,或者声明抛出该异常。

接下来通过一个案例来演示一下sleep(long millis)方法在程序中的使用。

public class Example02 {
    public static void main(String[] args) throws InterruptedException {
        Task1 task = new Task1();
        new Thread(task).start();
        for (int i = 0; i < 10; i++) {
            if(i==5) {
                Thread.sleep(2000);        //当前线程休眠2秒
            }else {
                Thread.sleep(500);
            }
            System.out.println("父线程正在输出:"+i);
        }
    }
}
class Task1 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                if(i==3) {
                    Thread.sleep(2000);        //当前线程休眠2秒
                }else {
                    Thread.sleep(500);
                }
                System.out.println("子线程正在输出:"+i);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

需要注意的是,sleep()是静态方法,只能控制**当前**正在运行的线程休眠,而不是控制其他线程休眠。当休眠时间结束后,线程就会返回到就绪状态,而不是立即开始运行。

3.线程让步

所谓的线程让步是指正在执行的线程,在某些情况下将CPU资源让给其他线程执行

线程让步可以通过yield()方法来实现,该方法和sleep()方法有点相似,都可以让当前正在运行的线程暂停,区别在于yield()方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。当某个线程调用yield()方法之后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会。

接下来通过一个案例来演示一下yield()方法的使用

public class Example03 {
    public static void main(String[] args) {
        //创建两个线程
        YieldThread t1 = new YieldThread("线程A");
        YieldThread t2 = new YieldThread("线程B");
        //开启两个线程
        t1.start();
        t2.start();
    }
}
class YieldThread extends Thread{
    //定义一个有参的构造方法
    public YieldThread(String name) {
        super(name);    //调用父类的构造方法 传入线程名字
    }
    @Override
    public void run() {
        for (int i = 0; i < 6; i++) {
            System.out.println(Thread.currentThread().getName()+"---"+i);
            if(i==3) {
                System.out.println("线程让步:");
                Thread.yield();
            }
        }
    }
}

4.线程插队

现实生活中经常碰到 " 插队 " 的情况,同样,在Thread类中也提供了一个join()方法来实现这个 " 功能 " 。当在某个线程中调用其他线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完后它才会继续执行。

接下来通过案例来演示join()方法的使用

public class Example04 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new EmergencyThread(), "子线程");
        t.start();        //开启线程
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"---"+i);
            if(i==2) {
                t.join();    //调用join方法
            }
            Thread.sleep(500);        //线程休眠500毫秒
        }
    }
}

class EmergencyThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"---"+i);
            try {
                Thread.sleep(500);        //线程休眠500毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

多线程同步

1.线程安全问题

首先想为什么出现问题?(也是我们判断是否有问题的标准)

  1. 是否是多线程环境
  2. 是否有共享数据源
  3. 是否有多条语句操作共享数据源

如何解决多线程安全问题呢?

基本思想: 让程序没有安全问题的环境。

怎么实现呢?

把多个语句操作共享数据源的代码给锁起来,让任意时刻只能有一个线程执行即可。

2.同步代码块

线程安全问题其实就是由多个线程同时处理共享资源所导致的,要想解决而线程安全问题,就得保证用于处理共享资源的代码在任何时刻只能有一个线程访问

为了实现这种限制,Java中提供了同步机制。当多个线程使用同一个供需昂资源时,可以将处理共享资源的代码放在一个使用synchronized关键字修饰的代码块中,这个代码块被称作同步代码块,其语法格式如下:

    synchronized(lock){
        操作共享资源代码块
    }

上面的代码中,lock是一个锁对象,他是同步代码块的关键。当某一个线程执行同步代码块时,其他线程将无法执行当前同步代码块,会发生阻塞,等当前线程执行完同步代码块后,所有的线程开始抢夺线程的执行权,抢到执行权的线程将进入同步代码块,执行其中的代码。循环往复,直到共享资源被处理完为止。这个过程就如同一个同乡电话亭,只有前一个人打完电话出来后,后面的人才可以打开

接下来演示使用同步代码块的售票程序

public class Example01 {
    public static void main(String[] args) {
        TicketWindow task = new TicketWindow();        //创建线程的任务类对象
        new Thread(task, "窗口1").start();         //创建线程并起名为窗口1
        new Thread(task, "窗口2").start();         //创建线程并起名为窗口2
        new Thread(task, "窗口3").start();         //创建线程并起名为窗口3
        new Thread(task, "窗口4").start();         //创建线程并起名为窗口4
    }
}
//线程的任务类
class TicketWindow implements Runnable{
    private int tickets=50;        //10张票
    Object obj = new Object();        //任意定义一个对象,用作同步代码块的锁
    @Override
    public void run() {
        while(true) {
            synchronized (obj) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if(tickets>0) {
                    System.out.println(Thread.currentThread().getName()+"---卖出的票"+tickets--);
                }else {
                    break;
                }
            }
        }
    }
}

将有关tickets变量的操作全部都放到同步代码块中。

同步代码块中的锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是唯一的。

" 任意 " 指的是共享锁对象的类型。锁对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁,每个锁都有自己的标志位,这样线程之间便不能产生同步的效果。

3.同步方法

同步代码块可以有效的解决线程的安全问题,当把共享资源的操作放在synchronized定义的区域内时,便为这些操作加了同步锁。在方法面前同样可以同synchronized关键字来修饰,被修饰的方法为同步方法,它能实现与同步代码块相同的功能,具体语法格式如下:

  synchronized 返回值类型 方法名([参数1,...]){}

 


被synchronized修饰的方法在某一刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行该方法

接下来使用同步方法对文件进行修改

public class Example01 {
    public static void main(String[] args) {
        TicketWindow task = new TicketWindow();        //创建线程的任务类对象
        new Thread(task, "窗口1").start();         //创建线程并起名为窗口1
        new Thread(task, "窗口2").start();         //创建线程并起名为窗口2
        new Thread(task, "窗口3").start();         //创建线程并起名为窗口3
        new Thread(task, "窗口4").start();         //创建线程并起名为窗口4
    }
}
//线程的任务类
class TicketWindow implements Runnable{
    private int tickets=50;        //10张票
    @Override
    public void run() {
        while(true) {
            sendTicket();
        }
    }
    private synchronized void sendTicket() {
        try {
            Thread.sleep(10);  //线程休眠10秒模拟延迟
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if(tickets>0) {
            System.out.println(Thread.currentThread().getName()+"--卖出的票"+tickets--);
        }else {
            System.exit(0);
        }
    }
}

将售票代码抽取为售票方法sendTicket(),并用synchronized关键字把sendTicket修饰为同步方法,然后在run()方法中调用该方法。


解决线程安全问题实现1

同步代码块:

synchronized(对象){需要同步的代码;}

同步可以解决安全问题的根本原因就是在那个对象上。该对象如同锁的功能

同步代码块的对象可以使哪些?
  Object()

同步的特点

1.同步的前提:
  多个线程
  多个线程使用的是同一个锁对象

2.同步的好处:
  同步的出现解决了多线程的安全问题

3.同步的弊端:
  当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

 

解决线程安全问题实现2

同步方法,就是把同步关键字加到方法上

同步方法的锁对象是什么呢?
  同步方法锁对象是谁,锁对象就是this

如果是静态方法,同步方法的锁对象又是什么呢?
  静态方法锁对象是谁,MyCinema.class

那么,我们到底使用谁?
  如果锁对象是this,就可以考虑使用同步方法。
  否则能使用同步代码块的尽量使用同步代码块

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!