多 线 程

一、线程概述

在我们看来,计算机可以同时执行多个任务,尽管是单核的CPU也能够做到,这是因为计算机的操作系统在执行多个任务的时,实际上就让CPU对多个任务轮流交替执行,但计算机执行速度很快,所以给人一种同时处理多任务的感觉。

进程

在操作系统中,一个独立执行的程序可以称做一个进程,也就是“正在运行的程序”。一个进程是由多个线程执行而成,一个进程至少有一个线程。

线程

我们知道,代码运行时都是自上而下的,这样的程序叫做单线程。

出现多段代码交替运行时,这样的程序叫做多线程,指一个进程在执行过程中可以产生多个单线程,这些单线程程序在运行时是相互独立的,它们可以并发执行,多线程意味着需要创建多个线程。

对比:对进程比多线程稳定,多进程中,一个进程的奔溃不会影响其他进程,而多线程中,任何一个线程奔溃都会直接导致整个进程奔溃。但是,进程的创建开销比较大,并且进程间的通信比线程的通信慢,为线程间通信就是读写同一个变量,速度很快。

二、线程创建

两种方式,一种继承java.lang包下的Thread类,一种实现java.lang.Runnable接口。

1、继承Thread类

public class example {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();//创建继承类MyThread线程对象
        myThread.start();//开启线程
        //main方法线程输出
        while (true){
            System.out.println("main()方法在运行");
        }
    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        while (true){
            System.out.println("MyThread类的run()方法在运行");
        }
    }
}

运行结果:

通过死循环打印,可以看出两个线程子在不断交叉执行。

2、实现Runnable接口

当一个类继承,比如说Student类继承了Person父类,那么就不能实现再继承Thread类,无法创建线程。因此Thread提供了另一个构造方法TThread(Runnable target),其中Runnable是一个接口,接口中只有一个run()方法。当通过Thread(Runnable target)构造方法创建线程对象时,只需为该方法传递一个实现Runnable接口的实例对象,这样创建的线程将调用实现了Runnable接口的类中的run()方法作为运行代码,而不需要调用Thread类中的run()方法。

public class example {
    public static void main(String[] args) {
        MyThread myThread = new MyThread(); //创建MyThread的实例对象
        Thread thread = new Thread(myThread); //创建线程对象
        thread.start();  // 开启线程,执行线程中的run()方法
        while (true) {
            System.out.println("main()方法在运行");
        }
    }
}

class MyThread1 implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("MyThread类的run()方法运行");
        }
    }
}

区别

经典售票问题

继承Thread

public class example03 {
    public static void main(String[] args) {
        new Ticket().start();//第一个线程对象
        new Ticket().start();//第二个线程对象
        new Ticket().start();//第三个线程对象
        new Ticket().start();//第四个线程对象
    }
}
class Ticket extends Thread{
    private int tickets = 100;

    @Override
    public void run() {
        while (true){
            if (tickets > 0){
                System.out.println(Thread.currentThread().getName() + "正在售票:" + tickets-- + "张票");
            }
        }
    }
}

结果 

明显的,可以看到一张票被卖了两次,显然是不正确的。

Runnable接口

public class example04 {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(ticket,"窗口1").start();
        new Thread(ticket,"窗口2").start();
        new Thread(ticket,"窗口3").start();
        new Thread(ticket,"窗口4").start();
    }
}

class Ticket1 implements Runnable {
    private int tickets = 100;

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                System.out.print(Thread.currentThread().getName() + "正在售票:" + tickets-- + "张票" + " ");
            }
        }
    }
}

运行结果四个窗口正常卖票,没有重复。这是因为总票数tickets是共享的,创建的四个线程里,每个线程都调用实现类Ticket1中的run()方法,这样就可以确保四个线程访问的是同一个tickets变量,共享100张车票。

线程创建简化

Thread t = new Thread(() -> {

            //main方法代码            

}         });

public class example5 {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            while (true){
                System.out.println("start new thread");
            }
        });
        thread.start();
        MyThread2 myThread = new MyThread2();//创建继承类MyThread2线程对象
        myThread.start();//开启线程
    }
}
class MyThread2 extends Thread{
    @Override
    public void run() {
        while (true){
            System.out.println("MyThread类的run()方法正在运行");
        }
    }
}

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

线程整个生命周期可以分为五个阶段,分别是新建状态(New)、就绪状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)和死亡状态(Terminated),线程的不同状态表明了线程当前正在进行的活动。

1.新建状态(New)

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

2.就绪状态(Runnable)

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

3.运行状态(Running)

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

4.阻塞状态(Blocked)

一个正在执行的线程在某些特殊情况下,如被人为挂起或执行耗时的输入/输出操作时,会让出CPU的使用权并暂时中止自己的执行,进入阻塞状态。线程从阻塞状态只能进入就绪状态,而不能直接进入运行状态,也就是说结束阻塞的线程需要重新进入可运行池中,等待系统的调度。

5.死亡状态(Terminated)

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

四、线程调度

线程调度有两种模型,分别是分时调度模型抢占式调度模型

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

Java虚拟机默认采用抢占式调度模型。

线程优先级

线程的优先级用1~10之间的整数来表示,数字越大优先级越高。

静态常量表示线程的优先级

通过Thread类的setPriority(int newPriority)方法进行设置,参数newPriority接收的是1~10之间的整数或者Thread类的三个静态常量。

public class example06 {
    public static void main(String[] args) {
        Thread min = new Thread(new MinPriority(),"优先级较低");
        Thread max = new Thread(new MaxPriority(),"优先级较高");
        min.setPriority(Thread.MIN_PRIORITY);
        max.setPriority(Thread.MAX_PRIORITY);
        min.start();
        max.start();
    }
}
class MaxPriority implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "正在输出" + i);
        }
    }
}
class MinPriority implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "正在输出" + i);
        }
    }
}

线程休眠

想使正在执行的线程暂停,将CPU让给别的线程,这时可以使用静态方法sleep(long millis),该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。这样其他的线程就可以得到执行的机会了。

public class example07 {
    public static void main(String[] args) {
        new Thread(new SleepThread()).start();
        for (int i = 0; i < 10; i++) {
            if (i == 5){
                try {
                    Thread.sleep(2000);// 当前线程休眠2秒
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("主线程正在输出:" + i);
            try {
                Thread.sleep(500);// 当前线程休眠500毫秒
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
class SleepThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (i ==3){
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("SleepThread线程正在输出" + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

 

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

线程让步

public class example08 {
    public static void main(String[] args) {
        YieldThread threadA = new YieldThread("线程A");
        YieldThread threadB = new YieldThread("线程B");
        threadA.start();
        threadB.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();// 线程运行到此,作出让步
            }
        }
    }
}

 

调用Thread的yield()方法,使当前线程暂停,这时另一个线程就会获得执行。

但在上述代码,当线程A==3,线程A做出让步后,线程B并没有获得执行权,虽然yield()可以让线程又“运行状态”到“就绪状态”,但是并不一定会让其他线程获得执行权。

线程插队

 在Thread类中提供了一个join()方法,当在某个线程中调用其他线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后它才会继续运行。

public class example09 {
    public static void main(String[] args) {
        Thread t = new Thread(new JoinThread(),"线程1");
        t.start();
        for (int i = 0; i < 6; i++) {
            System.out.println(Thread.currentThread().getName() + "正在输出:" + i);
            if (i == 2){
                try {
                    t.join();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
class JoinThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 6; i++) {
            System.out.println(Thread.currentThread().getName() + "正在输出:" + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

 

两个线程的循环体中都调用了Thread的sleep(500)方法,以实现两个线程的交替执行。当main线程中的循环变量为2时,调用t线程的join()方法,这时,t线程就会“插队”优先执行。从运行结果可以看出,当main线程输出2以后,线程一就开始执行,直到线程一执行完毕,main线程才继续执行。   

五、多线程同步

多线程可以提高效率,但是多个线程去访问同一个资源,会引发一些安全问题。因此,需要实现多线程的同步,即限制某个资源在同一时刻只能被一个线程访问。

线程安全问题

在上面介绍Runnable接口时,引用了售票的例子,但售票的过程中会出现意外,下面通过使用sleep()方法进行模拟。

public class example10 {
    public static void main(String[] args) {
        SaleThread saleThread = new SaleThread();
        new Thread(saleThread,"线程一").start();
        new Thread(saleThread,"线程二").start();
        new Thread(saleThread,"线程三").start();
        new Thread(saleThread,"线程四").start();
    }
}
class SaleThread implements Runnable{
    private int tickets = 10;
    @Override
    public void run() {
        while (tickets > 0){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "--卖出的票:" + tickets--);
        }
    }
}

出现了明显的线程安全问题,重复卖票和票号为负数。原因是在售票程序的while循环中添加了sleep()方法,由于线程有延迟,当票号减为1时,假设线程1此时出售1号票,对票号进行判断后,进入while循环,在售票之前通过sleep()方法让线程休眠,这时线程二会进行售票,由于此时票号仍为1,因此线程二也会进入循环,同理,四个线程都会进入while循环,休眠结束后,四个线程都会进行售票,这样就相当于将票号减了四次,结果中出现了0、-1、-2这样的票号。

同步代码块

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

java中提供了同步机制,将共享资源的代码放在一个使用synchronized关键字修饰的代码块中,这个代码块被称作同步代码块。

格式:

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

lock是一个锁对象,是同步代码块的关键。lock可以是任意对象,它的作用是用来定义临界区,确保同一时刻只有一个线程可以执行被同步的代码块。 当一个线程进入synchronized块时,它会尝试获取lock对象的锁,如果锁被其他线程持有,那么该线程会被阻塞,直到锁被释放。只有当该线程成功获取到锁时,才能继续执行synchronized块中的代码。其他线程在获取到锁之前,将被阻塞在synchronized块外部,等待锁的释放。循环往复,直达共享资源被处理完成。这样保证同一时刻只有一个线程可以执行被同步的代码块,防止多个线程同时访问共享资源造成的数据不一致或冲突。

将售票进行修改,添加同步代码块

public class example10 {
    public static void main(String[] args) {
        SaleThread saleThread = new SaleThread();
        new Thread(saleThread,"线程一").start();
        new Thread(saleThread,"线程二").start();
        new Thread(saleThread,"线程三").start();
        new Thread(saleThread,"线程四").start();
    }
}
class SaleThread implements Runnable{
    private int tickets = 10;
    Object lock = new Object(); // 定义任意一个对象,用作同步代码块的锁
    @Override
    public void run() {
        while (true){
            synchronized (lock){
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            if (tickets > 0){
                System.out.println(Thread.currentThread().getName() + "卖出的票:" + tickets--);
            }else{
                break;
            }
        }
    }
}

将有关tickets变量的操作全部都放到同步代码块中。为了保证线程的持续执行,将同步代码块放在死循环中,直到ticket<0时跳出循环。从运行结果可以看出,售出的票不再出现0和负数的情况,这是因为售票的代码实现了同步,之前出现的线程安全问题得以解决。

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

同步方法

在方法前面同样可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能。

格式:

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

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

public class example11 {
    public static void main(String[] args) {
        TicketThread ticketThread = new TicketThread();
        new Thread(ticketThread,"线程1").start();
        new Thread(ticketThread,"线程2").start();
        new Thread(ticketThread,"线程3").start();
        new Thread(ticketThread,"线程4").start();
    }

}
class TicketThread implements Runnable{
    private int tickets = 10;
    @Override
    public void run() {
        while (true){
            saleTicket();
            if (tickets <= 0){
                break;
            }
        }
    }
    //定义同步方法saleTicket()
    private synchronized void saleTicket(){
        if ( tickets> 0){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "卖出的票:" + tickets--);
        }
    }
}


死锁问题

面试官:如果你能告诉我什么是死锁,我就录用你。

面试者:只要你录用我,我就告诉你什么是死锁。

....

上面对话就形成了一个死锁,两个线程都在等待对方的锁,这样造成了程序停滞,称为死锁。

六、线程通信

线程中不只是争夺锁,也有协作机制,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些通信机制,可以协调它们的工作,以此实现多线程共同操作一份数据——等待唤醒机制。

 在一个线程满足某个条件时,就进入等待状态(wait() / wait(time)),  等待其他线程执行完他们的指定代码过后再将其唤醒(notify());或可以指定 wait 的时间,等时间到了自动唤醒;在有多个线程进行等待时,如果需要,可以使用 notifyAll()来唤醒所有的等待线程。wait/notify 就是线程间的一种协作机制。  

注意:被通知的线程被唤醒后也不一定能立即恢复执行,因为它当初中断的 地方是在同步块内,而此刻它已经不持有锁,所以它需要再次尝试去 获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。

如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE(可运行)状态。否则,线程就从 WAITING 状态又变成 BLOCKED(等待锁) 状态。

public class example13 {
    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        new Thread(thread1,"线程1").start();
        new Thread(thread1,"线程2").start();
    }
}
class Thread1 implements Runnable{
    int i = 1;

    @Override
    public void run() {
        while (true){
            synchronized (this){
                notify();
                if(i <= 10){
                    System.out.println(Thread.currentThread().getName() + ":" + i++);
                }else{
                    break;
                }
                try {
                    wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

 

从结果可以知道,两个线程交叉执行,当线程1打印完后进入等待状态,这时线程2对其进行唤醒,打印完后进行等待状态,线程1对其进行唤醒...直到结束。

注意:

 wait 方法与 notify 方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过 notify 唤醒使用同一个锁对象调用的 wait 方法后的线程。

wait 方法与 notify 方法是属于 Object 类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了 Object 类的。

wait 方法与 notify 方法必须要在同步代码块或者是同步函数中使用因为:必须 要通过锁对象调用这 2 个方法。否则会报java.lang.IllegalMonitorStateException 异常。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值