Java基础之多线程

2021-03-09 17:28

阅读:669

标签:head   fixed   throws   monit   mmu   lse   被锁   finally   用户   

多线程

1、简介

多线程,简而言之就是一段时间内同时干多件事(多条线程运行),而线程就是一条工作线,比如做饭,一个人干一系列有先后顺序的事情就是一条线程(比如洗米、洗锅、煮饭),多线程可以是多个人在同一个时间做多件可以同时进行的事情,比如一个人在煮饭的同时另一个人在炒菜。同时多线程也可以表示一个人在一段时间内干多件事情,比如这个人同时起两个锅炒两个菜,这个菜炒一会,那个菜炒一会,让两个菜都不会糊(正常工作)。

而在电脑中,多线程可以理解成同一时间运行多个程序,每个程序可以看成一个线程,当然有些程序也可以看成多线程,比如浏览器同时打开两个视频。

2、一个线程的生命周期

技术图片

  • 新建状态:

    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

  • 就绪状态:

    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  • 运行状态:

    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

  • 阻塞状态:

    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  • 死亡状态:

    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

3、创建一个线程

方式一:继承Thread类

// 1.创建一个继承于Thread类的子类
class MyThread extends Thread{
    // 2.重写Thread类的run()方法
    @Override
    public void run() {
        // 重写方法,遍历1000以内的偶数,遍历1000效果明显点
        for (int i = 0; i 

多线程解析

程序先开启了第一条线程----用于执行Multithreading的main方法,当执行到MyThread thread1 = new MyThread();时会创建第二条线程,但是不会开启这条线程,而当第一条线程运行到thread1.start();时才会开启第二条线程,并调用前面编写好的run()方法,注意:thread1.start();这里有两个操作,第一个是启动一个新的线程,第二个是执行这个线程的run()方法,同时第一条线程并不会暂停,而是和第二条线程同时运行的。
技术图片

案例:创建两个分线程,一个遍历偶数,一个遍历奇数

class Thread1 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i 

共享数据

class TicketThread1 extends Thread{
    public static int ticket = 100;
    @Override
    public void run() {
        while (true){
            if (ticket>0){
                System.out.println(ticket);
                ticket--;
            }
            else{
                break;
            }
        }
    }
}
public class Multithreading {
    public static void main(String[] args) throws InterruptedException {
        TicketThread1 thread3 = new TicketThread1();
        TicketThread1 thread4 = new TicketThread1();
        TicketThread1 thread5 = new TicketThread1();
        thread3.start();
        thread4.start();
        thread5.start();
    }
}

方式二:实现Runnable接口(更常用,因为方式一是通过继承来实现的,而一个类只能继承一个类,同时在共享数据上面也有优势)

// 1. 创建一个实现了了Runnable接口的类
class RunnableThread implements Runnable{
    // 2. 实现Runnable的抽象方法:run()
    public void run() {
        for (int i = 0; i 

源码解析

public class Thread implements Runnable {
	// 方式二在Thread类中调用的构造函数
	public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
	// 这是init方法中进行的操作
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals){
        //......
        this.target = target;
        //......
    }
    // 这是Thread类中的start()方法
    public synchronized void start() {
        //......
        // 这个方法会启动新线程并调用run()方法
        start0();
        //......
    }
    // 这是Thread类中的run()方法
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
}
// 在方式1中,我们通过继承Thread类并重写run()方法,实现了当我们启动新线程并运行时并调用时(thread1.start();)
// 将会调用我们重写的run()方法
// 而在方式二中,我们使用了`public Thread(Runnable target)`这个构造函数,传入Runnable target参数
// 而在启动新线程时,将会调用Thread类中的run()方法,即target.run();也就是我们自己写的实现了Runnable接口的类的run方法。

共享数据

class TicketThread implements Runnable{
    private int ticket = 100;
    public void run() {
        while (ticket>0){
            System.out.println(ticket);
            ticket--;
        }
    }
}
public class Multithreading2 {
    public static void main(String[] args) {
        TicketThread ticketThread = new TicketThread();
        Thread thread2 = new Thread(ticketThread);
        Thread thread3 = new Thread(ticketThread);
        Thread thread4 = new Thread(ticketThread);
        thread2.start();
        thread3.start();
        thread4.start();
    }
}
// 在方式二中,我们不需要在实现了Runnable接口的类中将多个线程的共享数据(ticket)定义为static实现数据共享
// 我们只需要在多个线程中使用同一个对象进行线程初始化即可
// 而在方式一中必须要将共享数据定义为static才行

方式三:实现Callable接口---JDK 5.0新增方式

  • 与Runnable类似但功能更加强大
    • 需要实现call()方法而不是run()方法
    • 相比于run()方法,可以有返回值
    • call()可以抛出异常
    • 支持泛型的返回值
    • 需要借助FutureTask类,比如获取返回结果
// 1. 创建一个实现了Callable接口的实现类
class CallableThread implements Callable{
    // 2. 实现Callable的call()方法,将线程需要执行的操作声明在这里,相当于原来Runnable的run方法,实际上就是run方法会调用call方法
    // 这里的返回值与Callable中指定的类型对应
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 0; i  futureTask = new FutureTask(callableThread);
        // 新建线程并启动
        // 5. 将FutureTask对象作为参数传递到Thread类的构造器中,创建Thread类的对象,并调用start()方法
        Thread thread = new Thread(futureTask);
        thread.start();

        try {
            // 6. 获取Callable接口实现类对象的call()方法的返回值
            Object sum = futureTask.get();
            System.out.println(sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

方式四:使用线程池---JDK 5.0新增方式

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回线程池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

好处:

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 便于线程管理
    • corePoolSize:核心池的大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持长时间后终止
class ThreadPool1 implements Runnable{
    public void run() {
        for (int i = 0; i 

4、Thread类的一些重要方法

序号 方法 描述
1 public void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
2 public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
3 public final void setName(String name) 改变线程名称,使之与参数 name 相同。
4 public final String getName() 获取线程名称。
5 public final void setPriority(int newPriority) 更改线程的优先级。
6 public final int getPriority() 获取线程的优先级。
7 public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。
8 public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。
9 public void interrupt() 中断线程。
10 public final boolean isAlive() 测试线程是否处于活动状态。
11 public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。
12 public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
13 public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
14 public static Thread currentThread() 返回对当前正在执行的线程对象的引用。
15 public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流。

5、线程优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。

Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。

默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。

具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。简而言之就是,如果有两个线程,一个线程的优先级是1,一个线程的优先级是9,系统运行10s,可能会分给第一个线程1s的时间用于运行,分给第二个线程9s时间,这个具体分配是看平台的。

6、线程的安全性(同步)

买票问题

class TicketThread implements Runnable{
    private int ticket = 100;
    public void run() {
        while (ticket>0){
            System.out.println(ticket);
            ticket--;
        }
    }
}
public class Multithreading2 {
    public static void main(String[] args) {
        TicketThread ticketThread = new TicketThread();
        Thread thread2 = new Thread(ticketThread);
        Thread thread3 = new Thread(ticketThread);
        Thread thread4 = new Thread(ticketThread);
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

在上面的买票问题中,我们会遇到同一张票卖了多次,或者票超买(余票到-1的情况),这是因为我们创建的三个线程是同时运行的。

试想一下,如果其中的多个线程同时执行到关键的语句

  • 即第一个线程执行了语句System.out.println(ticket);

  • 但是还没来得及执行ticket--;

  • 而其他线程也执行了语句System.out.println(ticket);

  • 这就会导致同一个ticket输出两次

  • 结果同一张票卖了多次

  • 或者当ticket=1时,第一个线程准备执行语句ticket--;

  • 在还没来得及将ticket减到0,其他线程执行了语句ticket>0进入到了输出语句

  • 与此同时第一个线程恰好执行了ticket--;

  • 然后第二个线程执行了System.out.println(ticket);

  • 结果线程二会打印0,而这个打印是违法的(超卖)。

这里先想象一个场景,在进行多人排队上厕所厕所,多个要上厕所的用户需要看着门口的同一个标志位,厕所中有人一种标识(红灯亮),厕所中没人一种标识(绿灯亮),当厕所没人时,队伍中的用户才能去上厕所,否则只能在门口排队,甚至时不时的要判断一下标志位是否改变。

锁就相当于标志位,而在java的对象中,每一个对象都有一个锁标志位信息,用于标志这个对象是否被锁住,显然我们需要所有的线程关注同一个锁(不管这个锁是否是本身的锁),当锁释放时,就可以有线程去抢夺资源,反之线程只能在外面等待,等待占用的线程释放资源,因此锁有时会和对象划上等号。

// 锁的模拟
class TicketThread2 implements Runnable{
    private int ticket = 100;
    // 锁标志位,true表示锁打开,可以被线程占用
    private boolean flag = true;
	public void run() {
        while (ticket>0) {
            if (flag){
            	if (ticket>0){
                    flag=false;
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":"+ticket);
                    ticket--;
                    flag=true;
                }
                else {
                    System.out.println(Thread.currentThread().getName()+":抢不到票");
                }
            }
            else {
                break;
            }
        }
    }
}
public class Multithreading2 {
    public static void main(String[] args) {
        TicketThread ticketThread = new TicketThread2();
        Thread thread2 = new Thread(ticketThread);
        Thread thread3 = new Thread(ticketThread);
        Thread thread4 = new Thread(ticketThread);
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

线程同步方式一:同步代码块

实现Runnable接口的多线程同步

synchronized(同步监视器){
    // 需要被同步的代码
}
// 说明:1. 操作共享数据的代码,就是需要被同步的代码
//     2. 共享数据:多个线程共同操作的变量,比如ticket就是共享数据
//     3. 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
//         要求:多个线程必须要共用同一把锁(同一个对象)
// 注意:下面这种方法只能避免重复卖,但不能解决超卖的问题,
// 因为第一个线程在sleep的时候,其他线程通过了while判断,在第一个线程释放时,二三个线程会先后执行ticket--操作
// 想要改善就在 synchronized内部加入一个票量判断即可(将synchronized内部全部包围)

class TicketThread2 implements Runnable{
    private int ticket = 100;
    public void run() {
        while (ticket>0){
            synchronized(this) {
                if (ticket>0){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 这里也相当于操作了共享数据,与下面的代码实际上是一体的
                    System.out.println(Thread.currentThread().getName()+":"+ticket);
                    // 操作了共享数据
                    ticket--;
                }
                else {
                    break;
                }
            }
        }
    }
}

继承Thread类的多线程同步

class TicketThread3 extends Thread{
    private static int ticket = 100;
    private static Object object = new Object();
    @Override
    public void run() {
        while (ticket>0){
            // 与实现Runnable接口不同,由于继承Thread类后创建多线程是需要new多个TicketThread3类对象的
            // 而不过将object设置为非静态的属性,将会导致每个线程使用自己的object,无法使用同一个锁
            // 因此需要将object设置为静态的,或者这里也可以将object替换成TicketThread3.class
            // 继承Thread类需要额外注意同步监视器的选择
            synchronized(object) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + ticket);
                    ticket--;
                }
                else {
                    break;
                }
            }
        }
    }
}
public class Multithreading2 {
    public static void main(String[] args) {
        TicketThread3 thread5 = new TicketThread3();
        TicketThread3 thread6 = new TicketThread3();
        TicketThread3 thread7 = new TicketThread3();
        thread5.start();
        thread6.start();
        thread7.start();
    }
}

线程同步方法二:同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们就可以将这个方法声明为同步的。

实现Runnable接口的多线程同步

class TicketThread4 implements Runnable{
    private int ticket = 100;
    public void run() {
        while (ticket > 0) {
            show();
        }
    }
    // 同步监视器(锁)就是this
    private synchronized void show(){
        if(ticket>0){
            try {
            	Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":"+ticket);
            ticket--;
        }
    }
    // 相当于
    private void show(){
        synchronized(this){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":"+ticket);
            ticket--;
        }
    }
}

public class Multithreading2 {
    public static void main(String[] args) {
        TicketThread ticketThread = new TicketThread4();
        Thread thread5 = new Thread(ticketThread);
        Thread thread6 = new Thread(ticketThread);
        Thread thread7 = new Thread(ticketThread);
        thread5.start();
        thread6.start();
        thread7.start();
    }
}

继承Thread类的多线程同步

class TicketThread5 extends Thread{
    private static int ticket = 100;
    @Override
    public void run() {
        while (ticket>0){
            show();
        }
    }
    // 如果方法为非静态的,那么同步监视器会是this,那么三个线程对象将使用三个锁
    // 如果设置为static,同步监视器将会是TicketThread5.class(唯一)
    private static synchronized void show(){
        if (ticket>0){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":"+ticket);
            ticket--;
        }
    }
}
public class Multithreading2 {
    public static void main(String[] args) {
        TicketThread5 thread11 = new TicketThread5();
        TicketThread5 thread12 = new TicketThread5();
        TicketThread5 thread13 = new TicketThread5();
        thread11.start();
        thread12.start();
        thread13.start();
    }
}

方式三:Lock锁------JDK 5.0新增的线程同步机制

class LockThread implements Runnable{
    private int ticket = 100;
    // 1. 实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();

    public void run() {
        while (ticket>0){
            try {
                // 2. 调用锁定方法lock()---加锁
                lock.lock();

                if (ticket>0){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"售卖第:"+ticket+"张票");
                    ticket--;
                }
            } finally {
                // 3. 调用解锁方法:unlock()----如果上面代码出现异常,也会调用解锁
                lock.unlock();
            }

        }
    }
}
public class Multithreading3 {
    public static void main(String[] args) {
        LockThread lockThread = new LockThread();
        Thread thread1 = new Thread(lockThread);
        Thread thread2 = new Thread(lockThread);
        Thread thread3 = new Thread(lockThread);
        thread1.start();
        thread2.start();
        thread3.start();

    }
}

synchronized和lock的区别:

synchronized在执行完相应的同步代码之后会自动释放同步监视器(锁),而Lock则需要手动地启动同步(加锁lock())以及结束同步(解锁unlock())。

Lock只有代码块锁,而synchronized还有方法锁。

使用Lock锁,JVM将花费较少时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)。

建议的优先级

Lock>同步代码块>同步方法

7、线程间通信(参考第2部分---线程的生命周期)

class CommunicationThread implements Runnable{
    private int ticket = 100;
    public void run() {
        while (ticket>0){
            synchronized (this){
                // 随机通知一个等待阻塞的线程去等待锁的释放,其他的线程依旧处于等待阻塞状态(wait)
                // notify();
                // 通知其他所有的线程解除等待阻塞状态,并在执行接下去的代码之前,去竞争对象锁,决定哪个线程真正地执行代码
                notifyAll();
                if (ticket>0){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"售卖第:"+ticket+"张票");
                    ticket--;
                    try {
                        // 让本线程进入登台阻塞状态,等待其他线程唤醒本线程
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                else {
                    break;
                }
            }
        }
    }
}
public class Multithreading4 {
    public static void main(String[] args) {
        CommunicationThread communicationThread = new CommunicationThread();
        Thread thread1 = new Thread(communicationThread);
        Thread thread2 = new Thread(communicationThread);
        Thread thread3 = new Thread(communicationThread);
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

技术图片

注意事项

  • notify()、notifyAll()、wait()必须写在同步代码块或者同步方法中

  • 同时这三个方法的调用者必须是同步代码块或者同步方法中的同步监视器(即都是this.notify()....../object.notify()......)

  • 否则将会报java.lang.IllegalMonitorStateException错

  • 三个方法是定义在Object类中的
    技术图片

Java基础之多线程

标签:head   fixed   throws   monit   mmu   lse   被锁   finally   用户   

原文地址:https://www.cnblogs.com/whether/p/14167685.html


评论


亲,登录后才可以留言!