Java多线程基础学习

前言:继续复习java基础知识,网上也看了不少,当看到多线程这边的时候发现荒废了一年基本都忘光了,这篇文章就把多线程的一些基础知识重新过一边,就当是复习了。


在讲解多线程之前先简要说说线程与进程的概念:

  • 进程:进程是程序的一次执行过程,是资源分配的最小单元,进程切换开销较大,一个进程包含多个线程
  • 线程:线程是进程内部的一个执行序列,是操作系统调度的最小单位,线程间切换开销较小,多个线程共享进程的资源

那为什么需要多线程呢?

  • 线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
  • 多线程并发编程是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。

本次主要讲解多线程的创建、线程状态、线程的同步和线程的死锁。

1.多线程创建

java要实现多线程有三种方式:继承Thread类、实现Runnable接口、实现Callable接口,下面分别举例说明:

  • 继承Thread类
package com.multiThreading.learn;

/**
 * Created on 2020/2/26
 * Package com.multiThreading.learn
 *
 * @author dsy
 */
public class MyThread extends Thread {   //继承Thread类并重写run()方法

    private String name ;

    public MyThread(String name){
        this.name = name;
    }

    @Override
    public void run() {
        for (int i =0 ;i<5;i++){
            try {
                sleep(1000);      //使当前正在执行的线程进入休眠状态(暂时停止执行)以免当前线程强制占用CPU资源,单位为毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("执行了线程"+this.name);
        }
    }

}

测试类:

public class ThreadTest {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()); //打印当前正在执行的线程名称
        MyThread thread = new MyThread("A");
        MyThread thread1 = new MyThread("B");
        thread.start();   //通过调用Thread类的start()方法启动线程
        thread1.start();
        //thread1.start();   一个线程start()方法只能调用一次,否则会抛出java.lang.IllegalThreadStateException异常

    }
}

运行结果:

main
执行了线程B
执行了线程A
执行了线程B
执行了线程A
执行了线程A
执行了线程B
执行了线程B
执行了线程A
执行了线程A
执行了线程B

再次执行:

main
执行了线程A
执行了线程B
执行了线程B
执行了线程A
执行了线程B
执行了线程A
执行了线程A
执行了线程B
执行了线程A
执行了线程B

说明:由于main()方法就是一个主线程,所以在调用Thread.currentThread().getName()方法时打印出了main。当MyThread类两个对象调用start()方法时就启动了两个线程(并没有开始运行),虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度后就会执行run()方法。仔细看这两次的打印结果,我们发现这两个线程执行的顺序不太一样,这并不是我们能控制的,是由操作系统来控制的。

  • 实现Runnable接口(推荐用此方法)
package com.multiThreading.learn;

import static java.lang.Thread.sleep;

/**
 * Created on 2020/2/26
 * Package com.multiThreading.learn
 *
 * @author dsy
 */
public class RunnableThread implements Runnable {  //实现Runnable接口并重写run()方法
    private String name ;

    public RunnableThread(String name){
        this.name = name;
    }
    @Override
    public void run() {
        for (int i =0 ;i<5;i++){
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("执行了线程"+this.name);
        }
    }
}

测试类:

public class ThreadTest {
    public static void main(String[] args) {
        RunnableThread thread = new RunnableThread("A");   //创建RunnableThread 实例
        RunnableThread thread1 = new RunnableThread("B");
        new Thread(thread).start();  //利用RunnableThread 实例thread来创建Thread实例并调用Thread类的start()方法来启动线程
        new Thread(thread1).start();
    }
}

打印输出:

执行了线程A
执行了线程B
执行了线程A
执行了线程B
执行了线程B
执行了线程A
执行了线程A
执行了线程B
执行了线程B
执行了线程A

在Runnable接口里面只有一个方法就是run()方法

继承Thread类与实现Runnable接口来实现多线程的区别:
在这里插入图片描述

  • 实现Runnable接口避免了Java中单继承的局限
  • 使用Thread方法每个线程都会创建一个唯一的对象并与之关联;使用Runnable接口多个线程共享一个对象。简而言之就是使用Runnable接口可以实现资源的共享
  • 因为Runnable接口中只有一个run()方法,而Thread类中还有很多其他方法,若只是想执行run()方法实现Runnable接口即可,继承Thread类开销太大

下面介绍第三种方法

  • 实现Callable接口

Callable接口与Runnable接口类似,唯一的区别就是使用Runnable接口没有返回值,而实现Callable接口可以有返回值(通过Future类的get()方法)
关系图如下(参考李兴华老师的讲解)
在这里插入图片描述
示例:使用Callable接口实现多线程并获得返回值


package com.multiThreading.learn;

import java.util.concurrent.Callable;

/**
 * Created on 2020/2/26
 * Package com.multiThreading.learn
 *
 * @author dsy
 */
public class MyThread implements Callable<String> {  //实现Callable接口

    @Override
    public String call() throws Exception {
        return "Hello world !";
    }

}

测试类:

package com.multiThreading.learn;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * Created on 2020/2/26
 * Package com.multiThreading.learn
 *
 * @author dsy
 */
public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyThread myThread = new MyThread();
        FutureTask<String> task = new FutureTask<String>(myThread);
        Thread thread = new Thread(task);
        thread.start();
        System.out.println(task.get());   //使用get()方法获得线程中的返回值
    }
}

打印输出:

Hello world !

注意:上面哪一种方式实现多线程,线程的启动都是通过Thread.start()方法实现的。

2.线程的几种状态

一个线程主要包括5种状态:新建状态、可运行状态、运行状态、阻塞状态、终止状态,如下图所示:
在这里插入图片描述

  • 新建状态(New):一个线程实例被创建但在start()方法调用之前处于新建状态。
  • 可运行状态(Runnable):当一个线程实例调用了start()方法后,还未被操作系统选中调度,此时处于可运行状态。
  • 运行状态(Running):该线程被系统调度获得CPU资源,进入运行状态,执行run()方法。
  • 阻塞状态(Blocked):运行中的线程因某种原因放弃了CPU资源,如等待I/O完成、执行了sleep()方法、执行了wait()方法等
  • 终止状态(Terminated):线程执行了stop()方法或run()方法执行完毕,此时线程生命周期结束

3.线程的同步

现在我们先用一个例子来理解什么是同步:
现在我们假设要实现一个卖票的程序,有三个售票窗口要出售共10张车票:

package com.multiThreading.learn;

/**
 * Created on 2020/2/27
 * Package com.multiThreading.learn
 *
 * @author dsy
 */
public class SaleTicketsThread implements Runnable {

    private Integer total = 10;  //共十张车票
    
    @Override
    public void run() {
        for (int i=0;i<20 ;i++){
            if (total>0){
	            try {
	                    Thread.sleep(100);  //模拟线程的网络延迟,让问题暴露的更快
	            } catch (InterruptedException e) {
	                    e.printStackTrace();
	            }
                System.out.println(Thread.currentThread().getName()+"成功售出一张票,还剩"+--total+"张");
            }
        }
    }
}

测试类:

package com.multiThreading.learn;

/**
 * Created on 2020/2/27
 * Package com.multiThreading.learn
 *
 * @author dsy
 */
public class SaleTicketsTest {

    public static void main(String[] args) {
        SaleTicketsThread saleTicketsThread = new SaleTicketsThread();
        Thread thread1 = new Thread(saleTicketsThread,"窗口A");
        Thread thread2 = new Thread(saleTicketsThread,"窗口B");
        Thread thread3 = new Thread(saleTicketsThread,"窗口C");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

打印输出:

窗口C成功售出一张票,还剩9张
窗口B成功售出一张票,还剩8张
窗口A成功售出一张票,还剩7张
窗口C成功售出一张票,还剩6张
窗口A成功售出一张票,还剩5张
窗口B成功售出一张票,还剩4张
窗口C成功售出一张票,还剩3张
窗口B成功售出一张票,还剩2张
窗口A成功售出一张票,还剩2张
窗口C成功售出一张票,还剩1张
窗口A成功售出一张票,还剩0张
窗口B成功售出一张票,还剩-1张
窗口C成功售出一张票,还剩-2

观察上述输出结果,发现票数出现了负数,用下面这张图来解释上面发生的情况:
在这里插入图片描述
当服务器中还剩最后一张票的时候,此时total = 1,其中某一个售票线程在判断时通过,但此时并没有立刻修改票数,而是进入延迟状态,随后(时间很短很短可能只有几毫秒的时间)另外两个线程也立刻进行票数的判断并都进入延迟状态,随后三个线程都修改票数导致total出现-2的情况。这就出现了不同步操作,也就是数据的访问是不安全的操作。

解决办法:synchronized关键字

synchronized关键字可以实现“锁”的功能,还是以上述卖票的例子来说,当一个售票线程进入时,将售票操作进行上锁,其他两个售票线程过来想要进行售票操作,发现该操作被锁上了,只能在外面进行等待了,只有主动上锁的那个售票线程操作完成后释放锁,其他售票进程才能进入。
代码如下:

package com.multiThreading.learn;

/**
 * Created on 2020/2/27
 * Package com.multiThreading.learn
 *
 * @author dsy
 */
public class SaleTicketsThread implements Runnable {

    private Integer total = 10;

    @Override
    public void run() {
        for (int i=0;i<20 ;i++){
            synchronized (this){  //将下面的售票操作进行上锁
                if (total>0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"成功售出一张票,还剩"+--total+"张");
                }
            }
        }
    }
}

测试代码不变

打印输出:

窗口A成功售出一张票,还剩9张
窗口C成功售出一张票,还剩8张
窗口B成功售出一张票,还剩7张
窗口C成功售出一张票,还剩6张
窗口C成功售出一张票,还剩5张
窗口A成功售出一张票,还剩4张
窗口A成功售出一张票,还剩3张
窗口A成功售出一张票,还剩2张
窗口A成功售出一张票,还剩1张
窗口A成功售出一张票,还剩0

从打印输出发现此时已经实现了数据的同步,但是步虽然可以保证数据的完整性,但是其执行速度明显较慢。

4.线程的死锁

线程同步的本质是一个线程等待另一个线程执行完毕后再去继续执行,但是如果所有线程彼此之间都在等待着,此时虽然实现了同步,但是彼此之间都在等待无法继续往下执行,就出现了死锁状态。举个例子来理解:

package com.multiThreading.learn;

/**
 * Created on 2020/2/27
 * Package com.multiThreading.learn
 *
 * @author dsy
 */
public class DeadLockDemo {
    private static final Object resource1 = new Object();//资源 1
    private static final Object resource2 = new Object();//资源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread().getName() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()  + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread().getName()  + "get resource2");
                }
            }
        }, "线程 1 ").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread().getName()  + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()  + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread().getName()  + "get resource1");
                }
            }
        }, "线程 2 ").start();
    }
}

打印输出:
在这里插入图片描述
分析:我们看到此时线程1获得了资源1的锁,然后调用sleep(1000)方法让线程1休眠1s,为的是让线程二获得了资源2锁。等到两个线程休眠都结束了后,线程1在等待资源2,线程2在等待资源1,程序还在运行并没有结束,两者陷入了无限等待的僵局就产生了死锁。符合产生死锁的四个必要条件:

  • 互斥条件:即资源的互斥使用。进程一旦获得资源就不允许别的进程使用该资源。
  • 不可剥夺条件:当进程获得资源后就一直占有该资源直到使用完毕后释放。
  • 请求与保持条件:如果进程运行共需申请若干资源,但只获得其中一部分资源,就必须等待另外一部分资源。而如果等待的这部分资源被其他进程占有,则该进程继续运行的机会显然减少。
  • 循环等待条件:当若干进程对资源的等待构成等待环路时,就发生了死锁。

死锁的避免:

我们只要破坏产生死锁的四个必要条件之一就可以避免死锁:

  • 破坏互斥条件:这个一般不太可能实现,因为一些资源就是要互斥去访问的。
  • 破坏不可剥落条件:强行的从别的进程手中夺走自身需要的资源,则该进程就可以运行来避免死锁。
  • 破坏请求和保持条件:一次性将该进程所需的资源全部分配掉,就避免了死锁。
  • 破坏循环等待条件:多个进程按照一定的顺序来申请所需的资源,破坏环路等待来避免死锁。

在上述死锁案例中,我们修改线程2代码如下,即可避免死锁:

new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread().getName()  + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()  + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread().getName()  + "get resource2");
                }
            }
        }, "线程 2 ").start();

打印输出:

线程 1 get resource1
线程 1 waiting get resource2
线程 1 get resource2
线程 2 get resource1
线程 2 waiting get resource2
线程 2 get resource2

分析:线程1获得resource1的锁后进入睡眠状态,随后线程2启动想要获得resource1的锁,但是已经被线程1拿走了,所以只能等待。线程1休眠完成后获得resource2的锁,随后线程1的生命周期结束并释放对resource1与resource2的占用,线程2就可以获取到了,这样就避免了死锁。

发布了5 篇原创文章 · 获赞 6 · 访问量 189

猜你喜欢

转载自blog.csdn.net/weixin_39296283/article/details/104523739