前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入理解多线程

深入理解多线程

作者头像
栋先生
发布2018-09-29 16:30:58
6580
发布2018-09-29 16:30:58
举报
文章被收录于专栏:Java成长之路Java成长之路

多线程是java中比较重要的一部分内容,使用多线程有许多的优点: - 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。 - 程序需要实现一些需要等待的任务时,可以提高计算机系统CPU的利用率 - 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

本文就多线程来做一个总结,希望可以给予大家一些帮助。

一、多线程的概述和创建

1. 基本概念:程序-进程-线程

  • 程序(program) 是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
  • 进程(process) 是程序的一次动态执行过程,它经历了从代码加载、执行到完毕的一个完整过程。动态过程:有它自身的产生、存在和消亡的过程。 如:运行中的QQ,运行中的MP3播放器 程序是静态的,进程是动态的
  • 线程(thread)进程可进一步细化为线程,是一个程序内部的一条执行路径。 是实现并发的机制的一种有效手段。 若一个程序可同一时间执行多个线程,就是支持多线程的

2. 线程的创建和启动

在java中实现多线程可以采用两种方式:继承Thread类、实现Runnable接口

3. Thread类

  1. Thread类是在java.lang包中定义的, 所以不需要导入,一个类只要继承了继承Thread类, 就是一个多线程操作类
  2. 子类中重写Thread类中的run方法, 此方法为线程的主体。
  3. 创建Thread子类对象,即创建了线程对象。
  4. 调用线程对象start方法:启动线程,并调用run方法;并且start方法只能被执行一次。
代码语言:javascript
复制
class MyThread extends Thread { // 继承Thread类,作为线程的实现类
    private String name; // 表示线程的名称

    public MyThread(String name) {
        this.name = name; // 通过构造方法配置name属性
    }

    public void run() { // 覆写run()方法,作为线程 的操作主体
        for (int i = 0; i < 10; i++) {
            System.out.println(name + "运行,i = " + i);
        }
    }
};

public class ThreadDemo02 {
    public static void main(String args[]) {
        MyThread mt1 = new MyThread("线程A "); // 实例化对象
        MyThread mt2 = new MyThread("线程B "); // 实例化对象
        mt1.start(); // 调用线程主体
        mt2.start(); // 调用线程主体
    }
};

Output: 线程A 运行,i = 0 线程B 运行,i = 0 线程A 运行,i = 1 线程B 运行,i = 1 线程A 运行,i = 2 线程B 运行,i = 2 线程B 运行,i = 3 线程B 运行,i = 4 线程A 运行,i = 3 线程B 运行,i = 5 线程A 运行,i = 4 线程B 运行,i = 6 线程A 运行,i = 5 线程B 运行,i = 7 线程A 运行,i = 6 线程B 运行,i = 8 线程A 运行,i = 7 线程B 运行,i = 9 线程A 运行,i = 8 线程A 运行,i = 9

从例子来看, 确实是并发执行的, 哪个线程先抢到cpu资源, 哪个线程就先执行。

线程A和线程B的启动步骤
线程A和线程B的启动步骤

问题:为什么不直接调用run()方法, 而是通过start()方法来调用(或者说start()方法是怎么实现能启动多线程这个功能的)?-批注1

4. Runnable 接口

  1. 定义子类,实现Runnable接口。
  2. 子类中重写Runnable接口中的run方法。
  3. 通过Thread类含参构造器创建线程对象。
  4. 将Runnable接口的子类对象作为实际参数传递Thread类的构造方法中。
  5. 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
代码语言:javascript
复制
class MyThread implements Runnable { // 实现Runnable接口,作为线程的实现类
    private String name; // 表示线程的名称

    public MyThread(String name) {
        this.name = name; // 通过构造方法配置name属性
    }

    public void run() { // 覆写run()方法,作为线程 的操作主体
        for (int i = 0; i < 10; i++) {
            System.out.println(name + "运行,i = " + i);
        }
    }
};
public class RunnableDemo01 {
    public static void main(String args[]) {
        MyThread mt1 = new MyThread("线程A "); // 实例化对象
        MyThread mt2 = new MyThread("线程B "); // 实例化对象

        Thread t1 = new Thread(mt1); // 实例化Thread类对象
        Thread t2 = new Thread(mt2); // 实例化Thread类对象
        t1.start(); // 启动多线程
        t2.start(); // 启动多线程
    }
};

OUtput: 线程B 运行,i = 0 线程A 运行,i = 0 线程B 运行,i = 1 线程A 运行,i = 1 线程B 运行,i = 2 线程A 运行,i = 2 线程A 运行,i = 3 线程A 运行,i = 4 线程A 运行,i = 5 线程A 运行,i = 6 线程A 运行,i = 7 线程A 运行,i = 8 线程A 运行,i = 9 线程B 运行,i = 3 线程B 运行,i = 4 线程B 运行,i = 5 线程B 运行,i = 6 线程B 运行,i = 7 线程B 运行,i = 8 线程B 运行,i = 9

5. Thread类与Runnable接口的区别

实现Runnable接口比继承Thread类有如下明显优点

  • 适合多个相同程序代码的线程去处理同一个资源
  • 可以避免单继承局限所带来的影响
  • 增强了代码的健壮性, 代码能够被多个线程共享,代码与数据是独立的

综合以上来看,开发中使用Runnable接口是最合适的,在以后的文章中,使用多线程时都将以Runnable接口的实现为操作的重点。

6. 线程的调度

调度策略 - 时间片

这里写图片描述
这里写图片描述

- 抢占式:高优先级的线程抢占CPU

java的调度方法 - 同优先级线程组成先进先出队列(先到先服务),使用时间片策略 - 对高优先级,使用优先调度的抢占式策略

二、多线程的常用方法

  • void start(): 启动线程,并执行对象的run()方法
  • run(): 线程在被调度时执行的操作
  • String getName(): 返回线程的名称
  • void setName(String name):设置该线程名称,线程的名称一般在启动线程前设置, 但也允许为已经运行的线程设置名称。允许两个Thread对象有相同的名字, 但为了清晰, 应避免这种情况发生。如果没为线程指定名称, 系统会进行自动命名(从Thread-0、Thread-1依次编号)
  • static currentThread(): 返回当前线程
  • boolean isAlive():判断线程是否活着,活着返回true, 否则返回false
  • void join():可以使用join()方法让一个线程强制运行,线程强制执行期间, 其他线程无法运行,必须等待此线程完成后才可以继承执行。
  • static void sleep(long millis):另当前活动线程在指定的时间段内放弃对cpu控制, 使其他线程有机会被执行, 时间到后重排队
  • void interrupt():当一个线程运行的时候, 另外一个线程可以直接通过interrupt()方法, 中断其运行状态。
  • void setPriority(int newPriority):设置线程的优先级。在java的线程操作中, 所有线程在运行前都会保持在就绪状态,那么此时,哪个线程优先级高,哪个线程就有可能会先被执行。 X_PRIORITY:最高优先级(10) RM_PRIORITY:中等优先级(5),主方法(main)的优先级就是NORM_PRIORITY N_PRIORITY:最低优先级(1)
  • int getPriority():返回线程的优先级
  • static void yield():线程让步,暂停当前正在执行的线程,把执行机会让给优先级相同或这更高的线程;若队列中没有同优先级的线程,忽略此方法
  • void stop():强制线程生命期结束
  • final void setDaemon(boolean on):设置守护进程

三、死锁和同步机制

1. 问题的产生

以卖火车票为例,如果现在要想买火车票的话可以去火车站或者去各个售票点,但是不管有多少个地方可以买火车票,最终一趟列车的火车票数量是固定的,如果把各个售票点理解为各个线程的话,则所有线程应该共同拥有同一份的票数。

代码语言:javascript
复制
class MyThread implements Runnable {
    private int ticket = 5; // 假设一共有5张票

    public void run() {
        for (int i = 0; i < 100; i++) {
            if (ticket > 0) { // 还有票
                try {
                    Thread.sleep(300); // 加入延迟,可以使问题暴露的更加明显(会让线程执行到这里的时候必须切换出去, 不加切换出去的几率小)
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("卖票:ticket = " + ticket--);
            }
        }
    }
};
public class SyncDemo01 {
    public static void main(String args[]) {
        MyThread mt = new MyThread(); // 定义线程对象
        Thread t1 = new Thread(mt); // 定义Thread对象
        Thread t2 = new Thread(mt); // 定义Thread对象
        Thread t3 = new Thread(mt); // 定义Thread对象
        t1.start();
        t2.start();
        t3.start();
    }
};

Output: 卖票:ticket = 5 卖票:ticket = 3 卖票:ticket = 4 卖票:ticket = 2 卖票:ticket = 1 卖票:ticket = 0 卖票:ticket = -1

从执行的结果来看, 卖出的票数成负数,程序代码出现了问题。

程序分析:

这里写图片描述
这里写图片描述

如图所示,线程当ticket等于1的时候,假设线程t1先进入if语句,紧接着执行睡眠, 线程被挂起,此时ticket还没有被减1。同理t2、t3线程就会在t1睡眠时候进入if语句,所以在最后,t1、t2、t3都执行了-1,ticket最后就变成了-1。 如果出现重票,原因也是类似的情况。

  • 产生问题的原因:由于一个线程在操作共享数据过程中,未执行完毕的情况下,另外的线程参与进来,导致共享数据存在了安全问题。
  • 如何解决线程的安全问题:必须让一个线程操作共享数据完毕以后,其它线程才有机会参与共享数据的操作。使用线程的同步机制来解决。

2. 同步机制

  • 在代码块上加上“sysnchronized”关键字的话,则此代码块就称为同步代码块
代码语言:javascript
复制
sysnchronized(同步监视器){
        //需要被同步的代码(即为操作共享数据的代码)
}

  • 共享数据:多个线程共同操作的同一个数据(变量)
  • 同步监视器:由一个类的对象来充当。哪个线程获取此监视器,谁就执行大括号里被同步的代码。俗称:锁 要求:所有的线程必须共用同一把锁!

注:在实现的方式中,考虑同步的话,可以使用this来充当锁。但是在继承的方式中,慎用this!

  • synchronized还可以放在方法声明中,表示整个方法为同步方法。
代码语言:javascript
复制
public synchronized void show (String name){ 
        //代码
}

注:同步方法里面的同步监视器是this

3. 卖火车票问题的解决:

代码语言:javascript
复制
//使用同步方法实现
class MyThread1 implements Runnable{
    private int ticket = 5 ;    // 假设一共有5张票  
    public void run(){  
        for(int i=0;i<100;i++){  
            this.sale() ;   // 调用同步方法  
        }  
    }  
    public synchronized void sale(){    // 声明同步方法  
        if(ticket>0){    // 还有票  
            try{  
                Thread.sleep(300) ; // 加入延迟  
            }catch(InterruptedException e){  
                e.printStackTrace() ;  
            }  
            System.out.println("卖票:ticket = " + ticket-- );  
        }  

    }  
};  
//使用同步代码块
class MyThread implements Runnable {
    private int ticket = 5; // 假设一共有5张票

    public void run() {
        for (int i = 0; i < 100; i++) {
            synchronized (this) { // 要对当前对象进行同步
                if (ticket > 0) { // 还有票
                    try {
                        Thread.sleep(300); // 加入延迟
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("卖票:ticket = " + ticket--);
                }
            }
        }

    }
};
public class SyncDemo02 {
    public static void main(String args[]) {
        //分别测试同步代码块和同步方法
        //测试同步代码块
    /*  MyThread mt = new MyThread(); // 定义线程对象
        Thread t1 = new Thread(mt); // 定义Thread对象
        Thread t2 = new Thread(mt); // 定义Thread对象
        Thread t3 = new Thread(mt); // 定义Thread对象
        System.out.println("使用同步代码块:");
        t1.start();
        t2.start();
        t3.start();*/
        //测试同步方法
        MyThread1 mt1 = new MyThread1(); // 定义线程对象
        Thread t11 = new Thread(mt1); // 定义Thread对象
        Thread t21 = new Thread(mt1); // 定义Thread对象
        Thread t31 = new Thread(mt1); // 定义Thread对象
        System.out.println("使用同步方法:");
        t11.start();
        t21.start();
        t31.start();
    }
};

Output: 卖票:ticket = 5 卖票:ticket = 3 卖票:ticket = 4 卖票:ticket = 2 卖票:ticket = 1 卖票:ticket = 0

程序分析:

这里写图片描述
这里写图片描述

如图所示,图中的②就是同步监视器,也就是俗称的锁。当没有线程进入时,②是绿色的,表示可以进入。当t1线程进入到run()方法时, ②将变成红色,阻止t2、t3线程的进入。直到t1线程的run()方法执行完毕,释放了②的3(也就是②变成绿色)。这样就保证了共享数据只会被一个线程操作,程序就不会出现重票、错票的情况了。

4. 死锁

产生原因:过多的同步可能导致死锁,不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。

代码语言:javascript
复制
//死锁的问题:处理线程同步时容易出现。
//不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
public class DeadLock {
    static StringBuffer sb1 = new StringBuffer();
    static StringBuffer sb2 = new StringBuffer();

    public static void main(String[] args) {
        //线程1
        new Thread() {
            public void run() {
                synchronized (sb1) {
                    try {
                        sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    sb1.append("A");
                    synchronized (sb2) {
                        sb2.append("B");
                        System.out.println(sb1);
                        System.out.println(sb2);
                    }
                }
            }
        }.start();
        //线程2
        new Thread() {
            public void run() {
                synchronized (sb2) {
                    try {
                        sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    sb1.append("C");
                    synchronized (sb1) {
                        sb2.append("D");
                        System.out.println(sb1);
                        System.out.println(sb2);
                    }
                }
            }
        }.start();
    }
}

Output: ……

  • 程序分析: 程序执行过程中,假设线程1先抢到cpu,并且对sb1上锁,执行到sleep(10)的时侯;线程2又抢到cpu,并且对sb2上锁,然后也执行到了sleep(10)。此时线程1没有释放sb1的锁, 线程2无法向下执行;而线程2也没有释放sb2的锁,所以线程1也无法向下执行。就形成了死锁。
  • 解决方法: 专门的算法、原则 尽量减少同步资源的定义

四、线程通信

1. Object类对线程的支持

唤醒和等待:notify、notifyAll、wait Object是所有类的父类,在此类中有以下几个方法是对线程操作有所支持的

No.

方法

类型

描述

1

public final void wait() throws InterruptedException

普通

线程等待

2

public final void wait(long timeout,int nanos) throws InterruptedException

普通

线程等待,并指定等待的最长时间,以毫秒为单位

3

public final void wait(long timeout) throws InterruptedException

普通

线程等待,并制定等待的最长毫秒及纳秒

4

public final void notifyAll()

普通

唤醒第一个等待的线程

5

public final void notify()

普通

唤醒全部等待的线程

  • wait() 与 notify() 和 notifyAll() wait():令当前线程挂起并放弃CPU、同步资源,使别的线程可访问并修改共享资源,而当前线程排队等候再次对资源的访问 notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待 notifyAll ():唤醒正在排队等待资源的所有线程结束等待.
  • Java.lang.Object提供的这三个方法只有在synchronized方法或synchronized代码块中才能使用, 否则会报java.lang.IllegalMonitorStateException异常

2. 简单应用:使用两个线程打印 1-100. 线程1, 线程2 交替打印

代码语言:javascript
复制
//使用同步代码块
class PrintNum1 implements Runnable {
    int num = 1;
    Object obj = new Object();

    public void run() {
        while (true) {
            //synchronized()里面可以填写任意可以唯一确定的对象,也可以直接写this
            synchronized (obj) {
                obj.notify();
                if (num <= 100) {
                    try {
                        Thread.currentThread().sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //输出num的值并且让num加一
                    System.out.println(Thread.currentThread().getName() + ":" + num++);
                } else 
                    break;

                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
//使用同步方法
class PrintNum2 implements Runnable {
    private int num = 1;

    @Override
    public void run() {
        while (true) {
            print();
            //1.为什么在这里使用break不能终止线程?
            if(num == 101)
                break;
            System.out.println(num);
        }
    }

    private synchronized void print() {
        notify();
        if(num <= 100){
            try {
                Thread.currentThread().sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":" + num++);
        }
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class TestCommunication {
    public static void main(String[] args) {
        //测试同步代码块
//      PrintNum1 p = new PrintNum1();
//      Thread t1 = new Thread(p, "甲");
//      Thread t2 = new Thread(p, "乙");
        //测试同步方法
        PrintNum2 p = new PrintNum2();
        Thread t1 = new Thread(p, "甲");
        Thread t2 = new Thread(p, "乙");

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

3. 经典例题:生产者/消费者问题

生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。 这里可能出现两个问题: 生产者比消费者快时,消费者会漏掉一些数据没有取到。 消费者比生产者快时,消费者会取相同的数据。

代码语言:javascript
复制
/*
    分析:
    1.是否涉及到多线程的问题?是!生产者、消费者
    2.是否涉及到共享数据?有!考虑线程的安全
    3.此共享数据是谁?即为产品的数量
    4.是否涉及到线程的通信呢?存在这生产者与消费者的通信
 */
class Clerk{//店员
    int product;

    public synchronized void addProduct(){//生产产品
        if(product >= 20){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            product++;
            System.out.println(Thread.currentThread().getName() + ":生产了第" + product + "个产品");
            notifyAll();
        }
    }
    public synchronized void consumeProduct(){//消费产品
        if(product <= 0){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            System.out.println(Thread.currentThread().getName() + ":消费了第" + product + "个产品");
            product--;
            notifyAll();
        }
    }
}

class Producer implements Runnable{//生产者
    Clerk clerk;

    public Producer(Clerk clerk){
        this.clerk = clerk;
    }
    public void run(){
        System.out.println("生产者开始生产产品");
        while(true){
            try {
                Thread.currentThread().sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.addProduct();

        }
    }
}
class Consumer implements Runnable{//消费者
    Clerk clerk;
    public Consumer(Clerk clerk){
        this.clerk = clerk;
    }
    public void run(){
        System.out.println("消费者消费产品");
        while(true){
            try {
                Thread.currentThread().sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.consumeProduct();
        }
    }
}
public class TestProduceConsume {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Producer p1 = new Producer(clerk);
        Consumer c1 = new Consumer(clerk);
        Thread t1 = new Thread(p1);//一个生产者的线程
        Thread t3 = new Thread(p1);
        Thread t2 = new Thread(c1);//一个消费者的线程

        t1.setName("生产者1");
        t2.setName("消费者1");
        t3.setName("生产者2");

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

4. 多线程的生命周期

线程状态转换图
线程状态转换图

JDK中用Thread.State枚举表示了线程的几种状态 要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:

  • 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
  • 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件
  • 运行:当就绪的线程被调度并获得处理器资源时,便进入运行状态, run()方法定义了线程的操作和功能
  • 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
  • 死亡:线程完成了它的全部工作或线程被提前强制性地中止

批注一:Question: Thread中start()和run()的区别 批注二:native关键字

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2015年11月08日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、多线程的概述和创建
    • 1. 基本概念:程序-进程-线程
      • 2. 线程的创建和启动
        • 3. Thread类
          • 4. Runnable 接口
            • 5. Thread类与Runnable接口的区别
              • 6. 线程的调度
              • 二、多线程的常用方法
              • 三、死锁和同步机制
                • 1. 问题的产生
                  • 2. 同步机制
                    • 3. 卖火车票问题的解决:
                      • 4. 死锁
                      • 四、线程通信
                        • 1. Object类对线程的支持
                          • 2. 简单应用:使用两个线程打印 1-100. 线程1, 线程2 交替打印
                            • 3. 经典例题:生产者/消费者问题
                              • 4. 多线程的生命周期
                              领券
                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档