万字梳理java多线程的基础知识

关于java的线程的这些基本知识你真的掌握了吗

文章目录

  • 关于java的线程的这些基本知识你真的掌握了吗
    • 前言
    • 一、线程是什么?
    • 二、多线程的引入
        • 1.为什么引入多线程
        • 2. 何时需要多线程
    • 多线程的四种创建方式
        • 方式一:继承Thread类
        • 方式二:实现Runnable接口
        • 前面两种创建多线程方式的对比
        • Thread类中的常用方法
        • 方式三:**实现Callable接口**
        • 方式四:**使用线程池**
      • 线程的同步
      • 线程通信
  • 总结


前言

作者本身是一个以Java为主语言的学习者,本篇文章将会为你分享我在学习java这门语言过程中,我们应该必须掌握的一些关于线程方面的基本知识,这篇文章作为你查缺补漏的一个参考利器,希望可以对你有所帮助。


一、线程是什么?

首先我们要知道什么是程序,什么是进程?

  • 程序:程序是为完成特定任务、用某种语言编写的一组指令的集合。是一段静态的代码,即静态对象。记住这么个公式:程序=数据结构+算法+文档。
  • 进程:进程是程序的一次执行过程,或是一个正在运行的程序,是一个动态的过程,即有自身的生命周期。进程是竞争计算机资源分配的基本单位

知道了上面这两个基本的概念以后我们来讲一讲什么是线程:

  • 线程:进程可以进一步细化为线程,是一个程序内部的一条执行路径。线程作为调度和执行的基本单位,每个线程都拥有自己独立的运行栈和程序计数器。

本文对程序、进程、线程的基本概念介绍到此。如果你对进程,线程等相关的基础知识还有一个更为细致的了解,可以去参考教材《操作系统》。

二、多线程的引入

1.为什么引入多线程

首先我们要知道多线程的优点:
①提高应用程序的响应。对图形化界面更有意义,可增强用户的体验。
②提高计算机系统的CPU的利用率。
③改善程序结构。

2. 何时需要多线程

  • 程序需要同时执行两个或多个任务。
  • 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
  • 需要一些后台运行的程序时。

补充一点:线程的生命周期

调用start方法
获取CPU执行权/运行
执行完run/调用线程的stop/出现Error/Exception并且没有处理
失去CPU执行权
sleep/join/等待同步锁/wait/suspend
sleep时间到/join结束/获取同步锁/notify/nptifyAll/resume
新建
就绪
运行
死亡
阻塞

多线程的四种创建方式

方式一:继承Thread类

1,创建一个Thread类的子类

2,重写thread类中的方法run() 将此线程要执行的操作写在run方法中

3,创建thread类的子类的对象

4,通过此对象调用start()方法启动线程 。
start()方法的作用:
①启动当前线程 ②调用当前线程的run()方法

第一种方式示例代码
下面这段代码相信大家都看得懂,无非就是创建了一个打印1-100内的偶数的线程。

class ThreadTest extends Thread{
    public static final int MAX=100;
    @Override
    public void run(){
      for(int i=0;i<MAX;i++){
          if(i%2==0){
        System.out.println(Thread.currentThread().getName()+" "+i+",");
         }
      }
    }

那么线程创建好了我们一个怎么样去启动线程呢?前面我们也提到过 怎么样去启动,即调用start()方法。首先实例化继承Thread类的类,让它的对象去调用start()方法启动线程。
大家结合我在代码中的注释去理解代码。
话不多说,上代码:

public class MyThread{
    public static void main(String[] args) {
        int max=100;
        ThreadTest threadTest=new ThreadTest();
        threadTest.start();
        
        //不能直接调用run()方法去启动线程
        
        //在启动一个线程,遍历100以内的偶数,
        //不可以让已经执行过start()的线程重新去执行
        //此时需要重新new一个对象去调用start()方法
        //如下:
        ThreadTest threadTest1=new ThreadTest();
        threadTest1.start();
        //如下操作在mian线程中执行
        System.out.println(Thread.currentThread().getName()+" " +"hello!");
        for(int i=0;i<max;i++){
            if(i%2!=0){
              System.out.println(Thread.currentThread().getName()+" "+i+","+"*******");
            }
        }
    }
}

方式二:实现Runnable接口

①:创建一个实现Runnable接口的类

②:实现类去重写Runnable中的抽象方法:run()

③:创建实现类的对象

④:将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象

⑤:通过Thread类的对象去调用start()方法
代码示例
大家不必过度给关注代码的内容,只需理解创建多线程的方式即可。

class Wicket implements Runnable{
    private int ticket=100;
    @Override
    public void run(){
        while (true){
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":卖票,票数为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }

前面两种创建多线程方式的对比

开发中优先选择实现Runnable接口的方式

原因: ①实现的方式没有单继承的局限性

②实现的方式更适合用来处理多个线程的

相同点:两种方式都需要重写run()方法,并将线程的操作放在run()方法中。

Thread类中的常用方法

紧接着我们先看Thread类中的一些常用方法,方便大家对多线程的一些代码有一个更好的理解。
1,start() :启动当前线程

2, run(): 通常需要重写此方法,将创建的线程要
执行的操作放在其中

3,currentThread() :静态方法,返回当前代码的线程

4, getName() :获取当前线程的名字

5, setName() :设置当前线程的名字

6,yield() :释放当前CPU的执行权

7,join() :在线程 A中调用线程B的join(),此时线程A进入阻塞状态
直到线程B执行完以后,线程A才结束阻塞状态

8,stop() :当执行此方法时,强制结束线程的生命周期

9,sleep() :让当前线程“睡眠”指定的毫秒数
当前线程是阻塞状态

10,isALive() :判断线程是否还存活
关于这些方法的测试大家自己可以去实验一下,体会编程的快乐,哈哈哈!

方式三:实现Callable接口

1,创建一个实现Callable接口的实现类

2,实现call()方法,将此线程要执行的操作声明在call()方法中

3,创建Callable接口的实现类的对象

4,将此Callable接口的实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象

5,将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法。
上代码:还是老打印偶数
6,获取call()方法中的返回值
如何理解实现Callable接口的方式创建多线程比实现Runnable接口的方式创建多线程更强大

①call()方法有返回值

②call()可以抛出异常,被外面捕获,获取异常的信息

③Callable是支持泛型的

class CallableThreadTest implements Callable {
    private int sum = 0;
    private static final int MAX = 100;
    private int i = 0;
    @Override
    public Object call() throws Exception {
        for (; i <=MAX; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}

方式四:使用线程池

1,提供指定线程数量的线程池

2,执行指定的线程操作,需要提供实现Runnable接口或者实现Callable接口的实现类的对象

3,关闭连接池

corePoolSize:线程池大小

maxmumPoolSize:最大线程数

keepAliveTime:线程没有任务时最多保持多长时间会终止

几点说明:
newFixedThreadPool():静态方法,指定创建的线程池的大小
execute():参数放的是实现类的对象,比如实现Thread类的类,或者是实现Runnable接口的类。
executor.shutdown():关闭线程池。
代码示例:

public class ThreadPollDemo {
    public static void main(String[] args) {
        ExecutorService executor= Executors.newFixedThreadPool(10);
        executor.execute(new ThreadPoolTest());//适用Runnable
        executor.execute(new ThreadPoolTestNumber());
        executor.shutdown();
        // executor.submit()适用于Callable

    }
}
class ThreadPoolTest implements Runnable{
private static final int MAX=100;
private int i=0;
    @Override
    public void run() {
        for(;i<=MAX;i++){
            if(i%2==0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}

class ThreadPoolTestNumber implements Runnable{
    private static final int MAX=100;
    private int i=0;
    @Override
    public void run() {
        for(;i<=MAX;i++){
            if(i%2!=0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}

提到这儿,不得不说使用线程池创建多线程的几个好处:

①提高响应速度

②降低资源消耗

③便于线程管理

下面我们来了解一下关于线程同步安全方面的一些问题

线程的同步

通过同步机制来解决线程的安全问题

方式一:同步代码块

synchronized (同步监视器){
//需要被同步的代码(操作共享数据的代码)

}
什么叫做共享数据?什么叫做同步监视器?
请看解释
弄清楚这两个名词,对于我们处理线程安全问题有有益处。
①共享数据:多个线程共同操作的变量

②同步监视器:俗称 锁。
任何一个类的对象都可以充当锁(要求:多个线程必须共用一个锁)

下面我直接给打家上了一段代码,你可以复制到自己的电脑上跑一跑

package com.threadtest;

/**
 * @author wch
 * @version jdk 1.8
 * @date 2020/12/27 22:42
 * @Description 使用实现Runnable接口的方式实现三个窗口卖票
 * 存在线程安全问题,出现了重票和错票
 * 使用synchronized解决,同步代码块
 *
 */
public class WicketTest {
    public static void main(String[] args) {
        Wicket wicket=new Wicket();
        Thread thread=new Thread(wicket);
        Thread thread1=new Thread(wicket);
        Thread thread2=new Thread(wicket);
        thread.setName("窗口一");
        thread1.setName("窗口二");
        thread2.setName("窗口三");
        thread.start();
        thread1.start();
        thread2.start();
    }
}
class Wicket implements Runnable{
    private int ticket=100;
    private  final  Object obj=new Object();
    @Override
    public void run(){
        while (true){
            /*
            可以用this
             */
            synchronized(this){
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":卖票,票数为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

在代码中相信你很容易看出哪儿时被同步的代码块,但是有一个问题你必须清楚,那就是,在使用关键字synchronized包代码块时不能暴多也不能包少,当然,具体问题具体分析。

方式二:同步方法

如果操作共享数据的代码完整的声明在一个类中,我们不妨将此方法声明为同步的

同步方法仍涉及到同步监视器,只是不需要我们显式的声明

非静态的同步方法,同步监视器是this

静态的同步方法,同步监视器是当前类本身

package com.threadtest;

/**
 * @author wch
 * @version jdk 1.8
 * @date 2020/12/29 19:03
 * @Description  使用同步方法处理继承Thread类的售票线程安全问题
 */
public class ThreadTestOfTickets {
    public static void main(String[] args) {
        WindowOfTickets windown=new WindowOfTickets();
        WindowOfTickets windown1=new WindowOfTickets();
        WindowOfTickets windown2= new WindowOfTickets();
        windown.setName("窗口一");
        windown1.setName("窗口二");
        windown2.setName("窗口三");
        windown.start();
        windown1.start();
        windown2.start();
    }
}
class WindowOfTickets extends Thread{
    private static int ticket=100;

    /**
     * 同步监视器是当前类本身  WindowOfTickets.class
     */
    private static synchronized void show(){
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + ":卖票,票数为:" + ticket);
            ticket--;
        }
    }
    @Override
    public void run(){
                while (ticket>1){
                    show();
                }
        }
}

方式三:Lock锁

package com.locktest;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author wch
 * @version jdk 1.8
 * @date 2020/12/29 20:21
 * @Description 用Lock解决线程安全问题
 */
public class LockDemo {
    public static void main(String[] args) {
        LockTest lockTest=new LockTest();
        Thread thread=new Thread(lockTest);
        Thread thread1=new Thread(lockTest);
        Thread thread2=new Thread(lockTest);
        thread.setName("窗口一");
        thread1.setName("窗口二");
        thread2.setName("窗口三");
        thread.start();
        thread1.start();
        thread2.start();

    }
}
class LockTest implements Runnable{
    private int ticket=100;
    private ReentrantLock reentrantLock=new ReentrantLock();
    @Override
    public void run(){
        while (true){
            reentrantLock.lock();
            try{
                if(ticket>1) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":卖票,票数为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
            finally {
                reentrantLock.unlock();

            }

            }
        }
}

使用的优先顺序:Lock->同步代码块->同步方法

synchronized与Lock方法的异同

相同点:都是用来解决线程的同步安全问题

不同点:synchronized机制在执行完相应的同步代码块以后,自动的释放同步监视器。Lock需要手动的启动(lock()),同时结束同步也需要手动的实现(unLock())

同步的方式,解决线程的安全的问题,操作同步代码时,只能有一个线程参与,其他线程等待,相当于是一个单线程的过程,效率低

说明:

1,在实现Runnable接口创建的线程方式中,我们可以考虑用this来充当同步监视器

2,在继承Thread类创建的单线程中,慎用this来充当同步监视器,考虑当前类来充当同步监视器

线程通信

线程通信中涉及到的方法

wait()方法:一旦执行此方法,当前线程进入阻塞状态,并释放同步监视器

notify()方法:一旦执行此方法,就会唤醒被wait()的一个线程,如果有多个,则唤醒优先级高的线程

notifyAll()方法:一旦执行此方法,就会唤醒所有被wait()的线程

注意点:
①wait(),notify(),notifyAll(),三个方法必须使用在同步代码块或同步方法中。

②wait(),notify(),notifyAll(),三个方法的调用者,必须是同步代码或同不=步方法的同步监视器。

③wait(),notify(),notifyAll(),三个方法是定义在java.lang.Object类中的。

sleep() 和wait()的异同

相同点:一旦执行这两个方法都可以让线程进入阻塞状态

不同点:

①两个方法声明的位置不一样,Thread类中声明sleep(),Object类中声明wait()。

②调用的要求不同,sleep()可以在任何场景下调用,wait()必须使用在同步代码块或同步方法中

③关于是否释放同步监视器,如果两个方法都是使用在同步代码块或者是同步方法中,sleep()不会释放,wait()会释放同步监视器 。

package com.numbertest;



/**
 * @author wch
 * @version jdk 1.8
 * @date 2020/12/29 21:46
 * @Description  两个线程交替打印1-100之间的数字
 */
public class NumberTest {
    public static void main(String[] args) {
        CreateNumber createNumber=new CreateNumber();
        Thread thread=new Thread(createNumber);
        Thread thread1=new Thread(createNumber);
        thread.setName("线程一:");
        thread1.setName("线程二:");
        thread.start();
        thread1.start();
    }
}
class CreateNumber implements Runnable{
    private int number=1;
    @Override
    public  void run(){
        while (true){
            synchronized (this) {
                notify();
                if (number <= 100) {
                    try {
                        Thread.sleep(100);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    break;
                }
            }
        }
    }
}

总结

以上就是今天要讲的内容,

你可能感兴趣的