likes
comments
collection
share

Java—— 多线程

作者站长头像
站长
· 阅读数 8

Java—— 多线程

一、前言

多线程是计算机编程中的一个重要概念,它允许程序在同一时间内执行多个任务,以实现更高的性能和并发处理能力。而Java中的多线程作为高级技巧无论是在实际开发中还是在面试中都是很重要的存在!虽然Java的多线程是些陈词滥调,但是热热还能吃。

二、什么是进程?线程?

  • 进程是指在操作系统中正在运行的一个程序的实例。一个进程可以看作是一个独立的执行环境,包含了程序代码、数据、资源和执行状态等。一个进程可以包含多个线程,每个线程可以独立地执行不同的任务,共享同一进程的资源和内存空间。
  • 线程是操作系统能够进行运算调度的最小单位,它拥有自己的程序计数器、寄存器集合和栈空间。不同的线程可以在同一进程中并发执行,通过切换线程的执行来实现多个任务的交替进行,从而达到并发处理的效果。

不太懂没关系我们举个例子:

我们使用QQ音乐听歌时,我们可以边听歌边浏览今日推荐、或者下载歌曲。 其中QQ音乐就是一个进程,浏览今日推荐、搜索音乐就是两个线程。还记得我们之前使用的非智能手机吗,如果你想下载歌曲,那么就得退出正在播放的歌,这种模式就是单线程的。而多线程就是这些任务可以同时进行。

三、我们为什么要使用线程?线程的优缺点?

优点:

  1. 使程序执行效率更高。能够让任务实现并行执行。
  2. 更充分的利用CPU资源。更好地利用系统资源。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,大大提高程序的效率。
  3. 增加了程序应用性和灵活性。由于同一进程的所有线程是共享同一内存,所以不需要特殊的数据传送机制,不需要建立共享存储区或共享文件,从而使得不同任务之间的协调操作与运行、数据的交互、资源的分配等问题更加易于解决。
  4. 提升用户体验。比如那些比较耗时的任务,可以起一个单独线程放到后台进行执行,而用户可以做其他的事情。

缺点:

  1. 多线程消耗的内存空间比较多。
  2. 线程的切换需要消耗资源。
  3. 多线程下容易造成死锁和数据不安全的情况。

这也就是为什么我们的手机内存越做越大越流畅,也越贵的原因。

四、Java的多线程

4.1 Java实现线程的方式

(1)继承Thread重写run方法

public class ThreadTest extends Thread{
    @Override
    public void run() {
        System.out.println("A");
    }
}
public class TestThread {
    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        threadTest.start();
    }
}

特点:

  • 无法获取子线程返回值。
  • run方法不可以抛出异常。

(2)实现Runnable接口

public class ThreadTest03 implements Runnable{
    @Override
    public void run() {
        System.out.println("实现Runnable接口重写run方法");
    }
}
public class TestThread02 {
    public static void main(String[] args) {
        ThreadTest03 threadTest03 = new ThreadTest03();
        Thread thread = new Thread(threadTest03);
        thread.start();
    }
}

通过查看源码发现,当创建线程继承Thread的时候,其实Thread也是实现Runnable接口的,但是Runnable接口只有一个方法,那就是run方法Thread在实现Runnable接口后又在自己的类中写了很多方法。

特点:

  • 无法获取子线程返回值。
  • run方法不可以抛出异常。

(3)实现Callable接口

import java.util.concurrent.Callable;
public class ThreadTest04 implements Callable<String> {
    String s;
    @Override
    public String call() throws Exception {
        s = "返回值";
        System.out.println(s);
        return s;
    }
}
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class TestThread03 {
    public static void main(String[] args) {
        ThreadTest04 threadTest04 = new ThreadTest04();
        FutureTask futureTask = new FutureTask<String>(threadTest04);
        try {
            new Thread(futureTask).start();
            System.out.println(futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

特点:

  • 可以获取子线程返回值。
  • run方法可以抛异常。
  • 可以取消任务。

(4)使用线程池

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestThreadPool {
    private static int THREAD_POOL_NUM = 10;//线程池数量
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);//创建执行服务
        for (int i = 0; i < THREAD_POOL_NUM; i++) {
            ThreadPool threadPool = new ThreadPool();
            executorService.execute(threadPool);
        }
        //关闭线程池
        executorService.shutdown();
    }
}
public class ThreadPool implements Runnable {
    @Override
    public void run() {
        System.out.println("通过线程池方式创建的线程:" + Thread.currentThread().getName());
    }
}

特点:

  • 池中的线程将一直存在,直到显式地关闭它。
  • 如果任何线程在关闭之前的执行过程中由于失败而终止,那么如果需要执行后续任务,一个新的线程将取代它的位置

4.2 线程的优先级

线程的优先级一共被分为10个等级,线程默认的优先级是5。

  • 在有时间片轮询机制中,“高优先级”的线程的被CUP分配时间片的概率要大于“低优先级”的线程。但是并不是“高优先级”的线程一定会先执行。
  • 在无时间片轮询机制中,“高优先级”的线程会被先执行。如果有“低优先级”的线程正在运行,有“高优先级”的线程处于可运行状态的话,就执行完“低优先 级”的线程后,再执行”高优先级“的线程

设置线程的优先级,代码如下:

lass ThreadTest extends Thread{
    public void run(){
        ...
    }
}
...
Thread t1=new ThreadTest("thread1");    // t1线程
Thread t2=new ThreadTest("thread2");    // t2线程
t1.setPriority(1);  // 设置t1的优先级为1
t2.setPriority(5);  // 设置t2的优先级为5

4.3 线程常用的方法以及其作用

  1. threadTest.run() —— 线程体 线程的执行体,一般要重写,执行体中的内容就是当前线程执行的内容。
public class ThreadTest extends Thread{
    @Override
    public void run() {
        System.out.println("A");
    }
}
  1. threadTest.start()——开始 启动线程的方法,调用此方法那么该线程就处于了就绪状态。
public class ThreadTest extends Thread{
    @Override
    public void run() {
        System.out.println("A");
    }
}
...
//启动线程
threadTest.start()
  1. Object.wait()——等待

使当前线程处于等待(阻塞)状态,并且释放所持有的对象的锁。在使用线程的此方法时,需要处理Interrupted(中断异常)异常。wait()方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。 调用该方法后当前线程进入睡眠状态,直到以下事件发生。

  • 其他线程调用了该对象的notify方法。
  • 其他线程调用了该对象的notifyAll方法。
  • 其他线程调用了interrupt中断该线程。
  • 时间间隔到了。
public class TestThread {
    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        try {
            threadTest.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  1. threadTest.suspend()——挂起 将线程挂起,必须由其他线程调用resume()该线程才会被唤醒,这个方法在过去被广泛使用,但目前已经不推荐使用了,因为它可能导致一些严重的问题。如下:
  • 死锁问题: 如果在一个线程执行过程中调用了 suspend() 方法,而另一个线程需要访问被挂起线程的资源,那么可能会导致死锁,即两个线程都在等待对方的释放。
  • 不安全的状态: suspend() 方法会导致线程的状态被暂时冻结,但线程仍然占用资源,可能会阻止其他线程的执行。如果被挂起的线程持有某个资源的锁,其他线程将无法获得该锁。
  • 破坏性: 如果一个线程在执行 suspend() 方法的时候被中断,那么该线程的状态可能会被破坏,导致程序出现不可预测的行为
  1. Thread.join()——插队 join()方法把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。 比如在线程B中调用了线程Ajoin()方法,直到线程A执行完毕后才会执行线程Bjoin的主要作用就是:让主线程等待子线程结束才能继续运行。join的底层是调用wait()方法,wait()方法在调用后释放锁资源,所以join方法在调用的时候也是释放锁资源的。使用join()方法也是需要处理Interrupted(中断异常)异常。
// 主线程
public class Father extends Thread {
    public void run() {
        Son s = new Son();
        s.start();
        s.join();
        ...
    }
}
// 子线程
public class Son extends Thread {
    public void run() {
        ...
    }
}
  1. Thread.yield()——禅让 暂停当前正在运行的线程对象放弃当前CPU资源,让其他具有相同优先级的线程有机会执行,故yield()的另一个作用是,让具有相同优先级或者高于当前优先级的线程轮替执行。但是调用了yield()方法后,并不能一定让步,因为让步的线程还有可能被其它线程调度程序再次选中,所以可能达不到让步的效果。

  2. Thread.sleep()——睡觉 sleep()是一个静态本地方法。让线程睡眠,此期间线程不消耗CPU资源。此方法和wait()方法很像,但是唯一点不同的是:wait()会释放锁资源,而sleep()不会。它会抱着资源睡,俗称——占着茅坑不拉屎。

  3. object.nodify()——叫醒 唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;

  4. object.nodifyAll()——全部叫醒 唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

  5. threadTest.setPriority() 设置线程的优先级。

  6. Thread.currentThread() 输出当前线程。

4.4 Java线程的状态有几种?

(1)新生状态(NEW):

执行new Thread()时创建线程,线程对象一旦被创建就会处于新生状态。

新生状态调用方法:

//创建线程代码
new Thread()

(2)运行状态(RUNNABLE)

执行Start()后,线程处于就绪状态,但是不会立即执行,抢占CUP时间片。线程在抢占到CPU时间片后,就会进入到运行中状态。运行状态包含了运行中和就绪状态。

进入运行状态调用的方法:

Thread.start()

(3)阻塞状态(BLOCKED)

在等待monitor(监视器)锁的线程的状态。假如在同一个JVM中,有A、B两个线程并行执行,A首先进入了同步块或者是方法后,线程B此时就处于阻塞状态。

调用下面方法会进入BLOCKED状态:

Object.wait()

(4)等待状态(WAITING)

处于等待中的线程的状态。处于该状态下的线程需要等待另一个线程做 “唤醒” 操作后才能解除WAITING状态。

调用下面的方法会使线程处于等待状态:

Object.wait()
Thread.join()
LockSupport.park()

(5)限时等待(TIMED_WAITING)

指定了等待时间的线程状态。超过了等待时间后,该线程直接解除限时等待状态。

调用下面的指定等待时间的方法,让线程进入限时等待状态:

Object.wait(Long)
Thread.join(Long)
Thread.sleep(long)
LockSupport.parkNanos()
LockSupport.parkUntil()

调用下面方法让线程解除限时等待状态:

Object.nodify()
Object.nodifyAll()
LockSupport.unpark(Thread)

(6)终止状态(TERMINATED)

已经终止的或者已经结束的线程的状态。

调用下面的方法,让线程进入终结状态:

Thread.stop()

Java线程的六种状态如下图所示: Java—— 多线程

五、Java线程调试方法

我们想要知道线程如何执行的,可以使用idea工具进行调试查看。下面我就使用实现多线程调试下进入同步代码块的场景:

5.1 调试代码:

创建线程:

public class MyThread implements Runnable {
    @Override
    public void run() {
        SyncClass syncClass = new SyncClass();
        syncClass.yncStaticMethod();
    }
}

同步代码块:

public class SyncClass {
    public synchronized static void yncStaticMethod() {
        System.out.println("synchronized 修饰静态方法! Thread:" + Thread.currentThread().getName());
    }
}

启动线程:

public class TestMain {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        MyThread myThread2 = new MyThread();
        Thread thread = new Thread(myThread);
        thread.setName("线程1");
        Thread thread2= new Thread(myThread2);
        thread2.setName("线程2");
        thread.start();
        thread2.start();
    }
}

5.2 调试

在同步代码块中打一个断点: Java—— 多线程 鼠标放到断点上,然后点击鼠标右键: Java—— 多线程 Java—— 多线程 Java—— 多线程