深入理解多线程

多线程是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方法只能被执行一次。
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资源, 哪个线程就先执行。

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

4. Runnable 接口

  1. 定义子类,实现Runnable接口。
  2. 子类中重写Runnable接口中的run方法。
  3. 通过Thread类含参构造器创建线程对象。
  4. 将Runnable接口的子类对象作为实际参数传递Thread类的构造方法中。
  5. 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
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. 问题的产生

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

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”关键字的话,则此代码块就称为同步代码块
sysnchronized(同步监视器){
        //需要被同步的代码(即为操作共享数据的代码)
}

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

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

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

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

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

//使用同步方法实现
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. 死锁

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

//死锁的问题:处理线程同步时容易出现。
//不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
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 交替打印

//使用同步代码块
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),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。 这里可能出现两个问题: 生产者比消费者快时,消费者会漏掉一些数据没有取到。 消费者比生产者快时,消费者会取相同的数据。

/*
    分析:
    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关键字

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏架构之路

深入探讨 Java 类加载器

看到一篇写得非常好的加载器文章,转过来与大家分享一下(https://www.ibm.com/developerworks/cn/java/j-lo-class...

37140
来自专栏Python小屋

Python+pickle读写二进制文件小案例

对于二进制文件,不能使用记事本或其他文本编辑软件进行正常读写,也无法通过Python的文件对象直接读取和理解二进制文件的内容。必须正确理解二进制文件结构和序列化...

34860
来自专栏java一日一条

JAVA 动态代理

为了使代理类和被代理类对第三方有相同的函数,代理类和被代理类一般实现一个公共的interface,该interface定义如下

10130
来自专栏java一日一条

Java代码编译和执行的整个过程

JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:

9110
来自专栏JMCui

Linux 学习记录 四(Bash 和 Shell scirpt).

一、什么是 Shell?        狭义的shell指的是指令列方面的软件,包括基本的Linux操作窗口Bash等,广义的shell则包括 图形接口的软件,...

42040
来自专栏java一日一条

Java代码编译和执行的整个过程

JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:

13020
来自专栏微信公众号:Java团长

《深入理解Java虚拟机》笔记

也就是说,我们完全可以做一个工具,从一个文件中读入指令,然后将这些指令运行起来。上面代码中“编好的机器指令”当然指的是能在CPU上运行的,如果这里我还实现了一个...

6710
来自专栏博客园

Asp.Net Web API(三)

    在Asp.Net Web API中,一个控制器就是一个处理HTTP请求的类,控制器的public方法就被叫做action方法或简单的Action。当We...

17650
来自专栏Hongten

java多线程系列_线程的生命周期(4)

与人有生老病死一样,线程也同样要经历开始(等待)、运行、挂起和停止四种不同的状态。这四种状态都可以通过Thread类中的方法进行控制。下面给出了Thread类中...

10720
来自专栏决胜机器学习

PHP面向对象核心(三)——反射、异常处理

PHP面向对象核心(三) (原创内容,转载请注明来源,谢谢) 五、反射 1、反射即PHP运行过程中,提取出关于类、方法、属性、参数等信息,包括 注释信息。动态获...

355120

扫码关注云+社区

领取腾讯云代金券