Java多线程问题讲解

第五阶段 多线程

前言:

一个场景:周末,带着并不存在的女票去看电影,无论是现场买票也好,又或是手机买票也好,上一秒还有位置,迟钝了一下以后,就显示该座位已经无法选中,一不留神就没有座位了,影院的票是一定的,但是究竟是如何做到,多个窗口或者用户同时出票而又不重复的呢? 这就是我们今天所要讲解的多线程问题

(一) 线程和进程的概述

(1) 进程

  • 进程:进程是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源
  • 多线程:在同一个时间段内可以执行多个任务,提高了CPU的使用率

(2) 线程

  • 线程:进程的执行单元,执行路径

  • 单线程:一个应用程序只有一条执行路径

  • 多线程:一个应用程序有多条执行路径

  • 多进程的意义?—— 提高CPU的使用率

  • 多线程的意义? —— 提高应用程序的使用率

(3) 补充

并行和并发

  • 并行是物理上同时发生,指在某一个时间点同时运行多个程序
  • 并发是逻辑上同时发生,指在某一个时间段内同时运行多个程序

Java程序运行原理和JVM的启动是否是多线程的 ?

  • Java程序的运行原理:

    • 由java命令启动JVM,JVM启动就相当于启动了一个进程
    • 接着有该进程创建了一个主线程去调用main方法
  • JVM虚拟机的启动是单线程的还是多线程的 ?

    • 垃圾回收线程也要先启动,否则很容易会出现内存溢出
    • 现在的垃圾回收线程加上前面的主线程,最低启动了两个线程,所以,jvm的启动其实是多线程的
    • JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的

(二) 多线程代码实现

需求:我们要实现多线程的程序。

如何实现呢?

由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。

而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。

Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。

但是呢?Java可以去调用C/C++写好的程序来实现多线程程序。

由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,

然后提供一些类供我们使用。我们就可以实现多线程程序了。

通过查看API,我们知道了有2种方式实现多线程程序。

方式1:继承Thread类

步骤:

  • 自定义MyThread(自定义类名)继承Thread类

  • MyThread类中重写run()

  • 创建对象

  • 启动线程

public class MyThread extends Thread{
    public MyThread() {
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 100; i++){
            System.out.println(getName() + ":" + i);
        }
    }
}
public class MyThreadTest {
    public static void main(String[] args) {
        //创建线程对象
        MyThread my = new MyThread();
        //启动线程,run()相当于普通方法的调用,单线程效果
        //my.run();
        //首先启动了线程,然后再由jvm调用该线程的run()方法,多线程效果
        my.start();

        //两个线程演示,多线程效果需要创建多个对象而不是一个对象多次调用start()方法
        MyThread my1 = new MyThread();
        MyThread my2 = new MyThread();

        my1.start();
        my2.start();
    }
}

//运行结果
Thread-1:0
Thread-1:1
Thread-1:2
Thread-0:0
Thread-1:3
Thread-0:1
Thread-0:2
......
Thread-0:95
Thread-0:96
Thread-0:97
Thread-0:98
Thread-0:99

方式2:实现Runnable接口 (推荐)

步骤:

  • 自定义类MyuRunnable实现Runnable接口
  • 重写run()方法
  • 创建MyRunable类的对象
  • 创建Thread类的对象,并把C步骤的对象作为构造参数传递
public class MyRunnable implements Runnable {
    public MyRunnable() {
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 100; i++){
            //由于实现接口的方式不能直接使用Thread类的方法了,但是可以间接的使用
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}
public class MyRunnableTest {
    public static void main(String[] args) {
        //创建MyRunnable类的对象
        MyRunnable my = new MyRunnable();

        //创建Thread类的对象,并把C步骤的对象作为构造参数传递
//        Thread t1 = new Thread(my);
//        Thread t2 = new Thread(my);
        //下面具体讲解如何设置线程对象名称
//        t1.setName("User1");
//        t1.setName("User2");

        Thread t1 = new Thread(my,"User1");
        Thread t2 = new Thread(my,"User2");

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

实现接口方式的好处

可以避免由于Java单继承带来的局限性

适合多个相同程序的代码去处理同一个资源的情况,把线程同程序的代码,数据有效分离,较好的体现了面向对象的设计思想

如何理解——可以避免由于Java单继承带来的局限性

比如说,某个类已经有父类了,而这个类想实现多线程,但是这个时候它已经不能直接继承Thread类了

(接口可以多实现implements,但是继承extends只能单继承) ,它的父类也不想继承Thread因为不需要实现多线程

(三) 获取和设置线程对象

//获取线程的名称
public final String getName()

//设置线程的名称
public final void setName(String name)

设置线程的名称 (如果不设置名称的话,默认是Thread-? (编号) )

方法一:无参构造 + setXxx (推荐)

//创建MyRunnable类的对象
MyRunnable my = new MyRunnable();

//创建Thread类的对象,并把C步骤的对象作为构造参数传递
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
t1.setName("User1");
t1.setName("User2");
		
//与上面代码等价
Thread t1 = new Thread(my,"User1");
Thread t2 = new Thread(my,"User2");

方法二:(稍微麻烦,要手动写MyThread的带参构造方法,方法一不用)

//MyThread类中

public MyThread(String name){
    super(name);//直接调用父类的就好
}

//MyThreadTest类中
MyThread my = new MyThread("admin");

获取线程名称

注意:重写run方法内获取线程名称的方式

//Thread
getName()

//Runnable
//由于实现接口的方式不能直接使用Thread类的方法了,但是可以间接的使用
Thread.currentThread().getName()

使用实现Runnable接口方法的时候注意:main方法所在的测试类并不继承Thread类,因此并不能直接使用getName()方法来获取名称。

//这种情况Thread类提供了一个方法:
//public static Thread currentThread():

//返回当前正在执行的线程对象,返回值是Thread,而Thread恰巧可以调用getName()方法
System.out.println(Thread.currentThread().getName());

(四) 线程调度及获取和设置线程优先级

假如我们的计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到 CPU时间片,也就是使用权,才可以执行指令。那么Java是如何对线程进行调用的呢?

线程有两种调度模型:

分时调度模型 :所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片

抢占式调度模型 :优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。

Java使用的是抢占式调度模型

//演示如何设置和获取线程优先级

//返回线程对象的优先级
public final int getPriority()

//更改线程的优先级
public final void setPriority(int newPriority)

线程默认优先级是5。

线程优先级的范围是:1-10。

线程优先级高仅仅表示线程获取的 CPU时间片的几率高,但是要在次数比较多,或者多次运行的时候才能看到比较好的效果。

(五) 线程控制

在后面的案例中会用到一些,这些控制功能不是很难,可以自行测试。

//线程休眠
public static void sleep(long millis)

//线程加入(等待该线程终止,主线程结束后,其余线程开始抢占资源)
public final void join()

//线程礼让(暂停当前正在执行的线程对象,并且执行其他线程让多个线程的执行更加和谐,但是不能保证一人一次)
public static void yield()

//后台线程(某线程结束后,其他线程也结束)
public final void setDaemon(boolean on)

//(过时了但还可以用)
public final void stop()

//中断线程
public void interrupt()

(六) 线程的生命周期

新建 —— 创建线程对象

就绪 —— 线程对象已经启动,但是还没有获取到CPU的执行权

运行 —— 获取到了CPU的执行权

  • 阻塞 —— 没有CPU的执权,回到就绪

死亡 —— 代码运行完毕,线程消亡

(七) 多线程电影院出票案例

public class SellTickets implements Runnable {
    private int tickets = 100;

    @Override
    public void run() {
        while (true){
            if (tickets > 0){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() 
                                   + "正在出售第" + (tickets--) + "张票");
            }
        }
    }
}
public class SellTicketsTest {
    public static void main(String[] args) {
        //创建资源对象
        SellTickets st = new SellTickets();

        //创建线程对象
        Thread t1 = new Thread(st, "窗口1");
        Thread t2 = new Thread(st, "窗口2");
        Thread t3 = new Thread(st, "窗口3");

        //启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

在SellTicket类中添加sleep方法,延迟一下线程,拖慢一下执行的速度

通过加入延迟后,就产生了连个问题:

A:相同的票卖了多次

CPU的一次操作必须是原子性(最简单的)的 (在读取tickets–的原来的数值和减1之后的中间挤进了两个线程而出现重复)

B:出现了负数票

随机性和延迟导致的 (三个线程同时挤进一个循环里,tickets–的减法操作有可能在同一个循环中被执行了多次而出现越界的情况,比如说 tickets要大于0却越界到了-1)

也就是说,线程1执行的同时线程2也可能在执行,而不是线程1执行的时候线程2不能执行。

我们先要知道一下哪些问题会导致出问题:

而且这些原因也是以后我们判断一个程序是否会有线程安全问题的标准

A:是否是多线程环境

B:是否有共享数据

C:是否有多条语句操作共享数据

我们对照起来,我们的程序确实存在上面的问题,因为它满足上面的条件

那我们怎么来解决这个问题呢?

把多条语句操作共享数据的代码给包成一个整体,让某个线程在执行的时候,别人不能来执行

Java给我们提供了:同步机制

//同步代码块:

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

同步的好处

同步的出现解决了多线程的安全问题

同步的弊端

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

概述:

A:同步代码块的锁对象是谁呢?

任意对象

B:同步方法的格式及锁对象问题?

把同步关键字加在方法上

同步方法的锁对象是谁呢?

this

C:静态方法及锁对象问题?

静态方法的锁对象是谁呢?

类的字节码文件对象。

我们使用 synchronized 改进我们上面的程序,前面线程安全的问题,

public class SellTickets implements Runnable {
    private int tickets = 100;

    //创建锁对象
    //把这个关键的锁对象定义到run()方法(独立于线程之外),造成同一把锁
    private Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() 
                                       + "正在出售第" + (tickets--) + "张票");
                }
            }
        }
    }
}

(八) lock锁的概述和使用

为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

(可以更清晰的看到在哪里加上了锁,在哪里释放了锁,)

void lock() 加锁

void unlock() 释放锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SellTickets2 implements Runnable {

    private int tickets = 100;

    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                lock.lock();
                ;
                if (tickets > 0) {
                    try {
                        Thread.sleep(150);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
                }
            } finally {
                lock.unlock();
            }
        }
    }
}

(九) 死锁问题 (简单认识)

同步弊端

效率低

如果出现了同步嵌套,就容易产生死锁问题

死锁问题

是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象

(十) 等待唤醒机制

我们前面假定的电影院场景,其实还是有一定局限的,我们所假定的票数是一定的,但是实际生活中,往往是一种供需共存的状态,例如去买早点,当消费者买走一些后,而作为生产者的店家就会补充一些商品,为了研究这一种场景,我们所要学习的就是Java的等待唤醒机制

生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多进程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个进程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

我们用通俗一点的话来解释一下这个问题

Java使用的是抢占式调度模型

  • A:如果消费者先抢到了CPU的执行权,它就会去消费数据,但是现在的数据是默认值,如果没有意义,应该等数据有意义再消费。就好比买家进了店铺早点却还没有做出来,只能等早点做出来了再消费
  • B:如果生产者先抢到CPU的执行权,它就回去生产数据,但是,当它产生完数据后,还继续拥有执行权,它还能继续产生数据,这是不合理的,你应该等待消费者将数据消费掉,再进行生产。 这又好比,店铺不能无止境的做早点,卖一些,再做,避免亏本

梳理思路

  • A:生产者 —— 先看是否有数据,有就等待,没有就生产,生产完之后通知消费者来消费数据
  • B:消费者 —— 先看是否有数据,有就消费,没有就等待,通知生产者生产数据

解释唤醒——让线程池中的线程具备执行资格

Object类提供了三个方法:

//等待
wait()
//唤醒单个线程
notify()
//唤醒所有线程
notifyAll()

注意:这三个方法都必须在同步代码块中执行 (例如synchronized块),同时在使用时必须标明所属锁,这样才可以得出这些方法操作的到底是哪个锁上的线程

为什么这些方法不定义在Thread类中呢 ?

这些方法的调用必须通过锁对象调用,而我们刚才使用的锁对象是任意锁对象。

所以,这些方法必须定义在Object类中。

我们来写一段简单的代码实现等待唤醒机制

public class Student {
    String name;
    int age;
    boolean flag;// 默认情况是没有数据(false),如果是true,说明有数据

    public Student() {
    }
}
public class SetThread implements Runnable {
    private Student s;
    private int x = 0;

    public SetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        while (true){
            synchronized (s) {
                //判断有没有数据
                //如果有数据,就wait
                if (s.flag) {
                    try {
                        s.wait(); //t1等待,释放锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                //没有数据,就生产数据
                if (x % 2 == 0) {
                    s.name = "admin";
                    s.age = 20;
                } else {
                    s.name = "User";
                    s.age = 30;
                }
                x++;
                //现在数据就已经存在了,修改标记
                s.flag = true;

                //唤醒线程
                //唤醒t2,唤醒并不表示你立马可以执行,必须还得抢CPU的执行权。
                s.notify();
            }
        }
    }
}
package cn.bwh_05_Notify;

public class GetThread implements Runnable {
    private Student s;

    public GetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        while (true){
            synchronized (s){
                //如果没有数据,就等待
                if (!s.flag){
                    try {
                        s.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                System.out.println(s.name + "---" + s.age);

                //修改标记
                s.flag = false;
                //唤醒线程t1
                s.notify();
            }
        }
    }
}
package cn.bwh_05_Notify;

public class StudentTest {
    public static void main(String[] args) {
        Student s = new Student();

        //设置和获取的类
        SetThread st = new SetThread(s);
        GetThread gt = new GetThread(s);

        //线程类
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(gt);

        //启动线程
        t1.start();
        t2.start();
    }
}
//运行结果依次交替出现

生产者消费者之等待唤醒机制代码优化

最终版代码(在Student类中有大改动,然后GetThread类和SetThread类简洁很多)

public class Student {
    private String name;
    private int age;
    private boolean flag;

    public synchronized void set(String name, int age) {
        if (this.flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        this.name = name;
        this.age = age;

        this.flag = true;
        this.notify();
    }

    public synchronized void get() {
        if (!this.flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println(this.name + "---" + this.age);

        this.flag = false;
        this.notify();
    }
}
public class SetThread implements Runnable {
    private Student s;
    private int x = 0;

    public SetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        while (true) {
            if (x % 2 == 0) {
                s.set("admin", 20);
            } else {
                s.set("User", 30);
            }
            x++;
        }
    }
}
public class GetThread implements Runnable{
    private Student s;

    public GetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        while (true){
            s.get();
        }
    }
}
public class StudentTest {
    public static void main(String[] args) {
        Student s = new Student();
        //设置和获取的类

        SetThread st = new SetThread(s);
        GetThread gt = new GetThread(s);

        Thread t1 = new Thread(st);
        Thread t2 = new Thread(gt);

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

最终版代码特点:

  • 把Student的成员变量给私有的了。
  • 把设置和获取的操作给封装成了功能,并加了同步。
  • 设置或者获取的线程里面只需要调用方法即可

(十一) 线程池

程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池

线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用

在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池

JDK5新增了一个Executors工厂类来产生线程池,有如下几个方法
//创建一个具有缓存功能的线程池
//缓存:百度浏览过的信息再次访问
public static ExecutorService newCachedThreadPool()

//创建一个可重用的,具有固定线程数的线程池
public static ExecutorService newFixedThreadPool(intnThreads)
  					 
//创建一个只有单线程的线程池,相当于上个方法的参数是1 
public static ExecutorService newSingleThreadExecutor()
  					 
这些方法的返回值是ExecutorService对象,该对象表示一个线程池,可以执行Runnable对象或者Callable对象代表的线程。它提供了如下方法

Future<?> submit(Runnable task)
<T> Future<T> submit(Callable<T> task)
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorDemo {
    public static void main(String[] args) {
        //创建一个线程池对象,控制要创建几个线程对象
        ExecutorService pool = Executors.newFixedThreadPool(2);

        //可以执行Runnalble对象或者Callable对象代表的线程
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());

        //结束线程池
        pool.shutdown();
    }
}

(十二) 匿名内部类的方式实现多线程程序

匿名内部类的格式:

new 类名或者接口名( ) {
              重写方法;
          };

本质:是该类或者接口的子类对象

public class ThreadDemo {
    public static void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + i);
                }
            }
        }.start();
    }
}
public class RunnableDemo {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + i);
                }
            }
        }).start();
    }
}

(十三) 定时器

定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。在Java中,可以通过Timer和TimerTask类来实现定义调度的功能

Timer

·public Timer()

public void schedule(TimerTask task, long delay)

public void schedule(TimerTask task,long delay,long period)

TimerTask

abstract void run()

public boolean cancel()

开发中

Quartz是一个完全由java编写的开源调度框架

结尾:

如果内容中有什么不足,或者错误的地方,欢迎大家给我留言提出意见, 蟹蟹大家 !_

如果能帮到你的话,那就来关注我吧!(系列文章均会在公众号第一时间更新)

在这里的我们素不相识,却都在为了自己的梦而努力 ❤

一个坚持推送原创Java技术的公众号:理想二旬不止

版权声明:本文为ideal-20原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/ideal-20/p/11205147.html