转载

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

什么是Java多线程?

原文  https://segmentfault.com/a/1190000019796744
正文到此结束
Loading...