什么是Java多线程?

第五阶段 多线程

前言:

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

(一) 线程和进程的概述

(1) 进程

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

(2) 线程

  • 线程:进程的执行单元,执行路径
  • 单线程:一个应用程序只有一条执行路径
  • 多线程:一个应用程序有多条执行路径
  • 多进程的意义?—— 提升CPU的使用率
  • 多线程的意义? —— 提升应用程序的使用率

(3) 补充

并行和并发java

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

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

  • Java程序的运行原理:安全

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

    • 垃圾回收线程也要先启动,不然很容易会出现内存溢出
    • 如今的垃圾回收线程加上前面的主线程,最低启动了两个线程,因此,jvm的启动实际上是多线程的
    • JVM启动至少启动了垃圾回收线程和主线程,因此是多线程的

(二) 多线程代码实现

需求:咱们要实现多线程的程序。并发

如何实现呢?框架

因为线程是依赖进程而存在的,因此咱们应该先建立一个进程出来。jvm

而进程是由系统建立的,因此咱们应该去调用系统功能建立一个进程。ide

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技术的公众号:理想二旬不止