Java多线程

一、线程的简介

想要了解线程是什么,我们需先从进程开始,学过操作系统或者计算机组成原理的人应该知道,在早期的操作系统是批处理系统,为了更好的管理以及调度作业,我们引进了进程这一概念
因为所有作业要被CPU执行就必须分配相应的内存资源,所以在早期没有线程机制的操作系统中,进程作为系统资源分配和调度的最小单位,引进了进程机制的好处是能够让作业并发地执行,提高作业的执行效率,但是因为进程每次被切换都需要保存进程上下文环境,所以进程切换的开销很大,因此对于这种进程作为CPU调度的最小单位的机制,在单核CPU中执行效果还好但是开销大,在多核CPU中则完全发挥不出多核的优势,所以我们就引进了线程机制

既然引进了线程机制,那么线程到底是什么呢?其实线程就是小一级的进程,只不过线程几乎不分配系统资源,多个线程共享同一个进程中的系统资源,此时线程就作为CPU调度最小的执行单位,而进程就作为最小的资源分配单位,这样就充分发挥了的多核CPU的优势,提高了程序的并行性

线程既然是小一级的进程,那么其也具备和进程一样的性质——动态性,线程同进程一样具有生命周期,其生命周期如图
线程生命周期图
线程同进程一样具有创建态、就绪态、运行态、阻塞态、以及终止态,其每个状态会发生什么或者怎么由一个状态转移到另一个状态,在这我就不过多赘述了,有兴趣的可以看看操作系统中进程管理相关的知识


二、Java开启线程方式

Java虚拟机允许一个Java程序开启多个线程,其中提供了两个主要的实现方式,下面我就来介绍介绍

2.1 继承Thread类

如果需要类需要开启多线程可以继承Thread的基类,其步骤如下

  1. 创建一个类继承Thread类

    public class Thread01 extends Thread{
        ...
    }
    
  2. 重写Thread类中的run ( ) 方法

    public class Thread01 extends Thread{
        @Override
        public void run() {
            for (int i = 1;i<=20;i++){
                try {
                    System.out.println("--Thread01线程在执行--");
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
  3. 创建对象,通过start ( ) 方法实现多线程开启

    public class Thread01 extends Thread{
        ...
        public static void main(String[] args) {
            Thread thread = new Thread01();
            thread.start();							//通过start方法开启线程
            for (int i=0;i<=20;i++){
                try{
                    System.out.println("主线程在执行");
                    sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
    }
    

了解第一种方法实现线程的开启,那么我们来看看Thread类的源码是怎么实现的

public class Thread implements Runnable {
    ....
}

查看源码就很容易发现Thread类其实继承了Runnable接口,我们创建一个线程类实际上就是继承了Runnable接口,那么我们能不能直接直接继承Runnable接口呢?所以Java语言又提供了第二种方式去开启创建线程

2.2 继承Runnable接口

第二种方式开启线程方式其步骤如下:

  1. 创建一个类继承Runnable接口

    class PrimeRun implements Runnable {
        long minPrime;
        PrimeRun(long minPrime) {
            this.minPrime = minPrime;
        }
    }
    
  2. 然后重写run ( ) 方法

    class PrimeRun implements Runnable {
        public void run() {
            // compute primes larger than minPrime
            . . .
        }
    }
    
  3. 通过Thread对象的start方法开启线程

    PrimeRun p = new PrimeRun(143);
    new Thread(p).start();			//创建Thread对象调用start方法开启线程
    

可见第二种方式开启线程和第一种方式的最大区别就是第一种我们可以直接调用自己创建的类的对象的start方法开启线程,而第二种则是需要new一个Thread对象然后通过该对象的start方法开启线程

其原因很简单,就是第二种方法继承的Runnable接口只有run方法

public interface Runnable {
    public abstract void run();
}

因此,我们创建的线程类只能重写run方法,没有start方法

2.3 继承Callable接口

Java中第三种开启线程的方式是通过继承Callable接口实现的,其步骤如下:

  1. 创建一个类继承Callable接口,并覆写 call( ) 方法

    class MyCallable implements Callable<T>{
        @Override
        public T call() throws Exception {
            ...
           	return T;
        }
    }
    
  2. 通过FutureTask作为代理调用其 run( ) 方法来开启线程(也可通过线程池来开启线程)

    public class Demo03Callable {
        public static void main(String[] args) {
            MyCallable callable = new MyCallable();
            FutureTask<Integer> futureTask = new FutureTask<Integer>(callable);
            futureTask.run();
        }
    }
    

Callable接口和Runnable接口的比较:

  • Callable接口的call方法可以在线程运行结束时返回一个值,而Runnable的run方法则不能返回
  • Callable接口的call方法需要监测异常处理,而Runnable的run方法则不需要监测
  • 二者同时通过另一个类代理实现来开启线程,Callable接口是需要Future对象来开启,而Runnable需要Thread对象开启
  • 二者都可以使用线程池

三、线程礼让、休眠、插队、优先级设置

3.1 线程礼让 yield( )

在Java中,线程礼让的是用过Thread类提供的的静态方法 yield( ) 实现的

class MyRunnable01 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (i%3==0)
                Thread.yield();//如果是继承Thread类则可直接使用this.yield调用
            System.out.println(Thread.currentThread().getName()+"运行了"+i+"次");
        }
    }
}

注:因为CPU调度具有随机性,线程礼让不一定会成功,而且当有多个线程时,线程礼让之后获得CPU资源的线程也不确定

3.2 线程休眠 sleep()

在Java中线程休眠的是通过sleep函数实现的

 try {
     Thread.sleep(1000);
 } catch (InterruptedException e) {
     e.printStackTrace();
 }

注:线程的休眠属于线程中断一类,因此需要进行异常监测

3.3 线程插队

线程插队其实就是中断当前线程,转而运行使用join方法的线程对象

在Java中,线程插队的是用过Thread类提供的的方法 join( ) 实现的(注意是属于对象的方法,而不是类的方法)

public class Demo04Yield {
    public static void main(String[] args) {
        MyRunnable01 runnable01 = new MyRunnable01();
        Thread t1 = new Thread(runnable01);
        Thread t2 = new Thread(runnable01);
        t1.start();
        t2.start();
        for (int i = 0; i < 100; i++) {
            if (i==50){
                try {
                    t1.join();			//t1线程对象调用join方法,中断当前main线程,转而执行t1线程
                    t2.join();			//t2线程对象调用join方法,中断当前main线程,转而执行t2线程
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+"运行了"+i+"次");
        }
    }
}

3.4 优先级设置

有时候我们编写的程序需要开启多线程,但是线程很多,CPU数量一定,那么CPU调用线程则会按照它自己的一定的调度算法进行调度,那么就可能会出现很重要的线程会最后处理,而不重要的线程则会先处理,那么就会出现性能倒置,因此我们需要对开启的线程设置优先级,让重要的线程有更大的可能获得CPU资源

在Java中,Thread类提供了setPriority ( ) 方法对线程进行优先级的设置,系统给了如下三个级别的常量值

  • public static final int MIN_PRIORITY = 1;
  • public static final int NORM_PRIORITY = 5;(线程默认)
  • public static final int MAX_PRIORITY = 10;
public final void setPriority(int newPriority) {
        ThreadGroup g;
        checkAccess();
        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
            throw new IllegalArgumentException();
        }
        if((g = getThreadGroup()) != null) {
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }
            setPriority0(priority = newPriority);
        }
    }

从setPriority ( ) 方法的源码可见,优先级的参数不一定是给定的三个参数值,也可以自己在1~10之间设定

在给线程设置优先级之后,我们通过getPriority()方法查看线程的优先级

注:必须在线程启动之前设置优先级!!!


四、守护线程

守护线程,顾名思义是守护程序执行的线程,该线程可以伴随程序启动到程序结束,甚至程序结束,守护线程都不会结束,因此Java虚拟机(JVM)不会将守护线程的结束视作程序的结束,在Java中,gc(内存回收)就是通过守护线程形式存在的完成程序中的内存回收的工作

那么在Java中如何创建一个守护线程呢?

其实很简单,Thread类提供了setDaemon(boolean b)方法,当传入参数是true则该线程就是一个守护线程,当传入参数是false时则该线程就是一个用户线程,例如下面SystemThread中的run方法按道理是个死循环,但是因为将其设置为守护线程,JVM不会将其视为用户线程,因此不会将其结束判定为程序结束,而是将main线程结束视为程序结束

public class Demo06Daemon {
    public static void main(String[] args) {
        UserThread userThread = new UserThread();
        SystemThread systemThread = new SystemThread();
        Thread t1 = new Thread(userThread);
        Thread t2 = new Thread(systemThread);
        t2.setDaemon(true);
        t1.start();
        t2.start();
    }
}
class UserThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+"运行了"+i+"次");
        }
    }
}
class SystemThread implements Runnable{
    @Override
    public void run() {
        int count=0;
        while (true){
            System.out.println("系统线程运行了"+count+"次");
            count++;
        }
    }
}

五、线程锁

因为程序的多线程并发执行使得程序失去了封闭性与再现性,但是有时侯我们必须考虑程序的顺序执行,例如在买票系统中,两个人不能抢购同一张票,所以我们需要对一些资源进行限制,每次只允许一个人访问该资源,因此我们就需要借助Java提供的锁对资源加锁,每次只让一个线程进行访问,从而实现互斥访问

5.1 Synchronized

Synchronized关键字使Java提供的用于解决线程互斥访问临界资源的,其作用域临界资源或者临界区(临界资源和临界资源就是每次只允许一个线程 访问的资源变量或者代码区)

  • 作用于临界资源(作用于需要互斥访问的临界资源)

    class TicketSystem implements Runnable {
        private Integer tickets = 10;
        
        @Override
        public void run() {
            while(true){
                synchronized (tickets){
                    if (tickets<0) break;
                    System.out.println(Thread.currentThread().getName()+"抢到了第"+tickets+"张票");
                    tickets--;
                }
            }
        }
    }
    
  • 作用临界区(作用于方法上)

    class TicketSystem implements Runnable {
        private Integer tickets = 10;
    
        @Override
        public void run() {
            while(buy()){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        
        public synchronized boolean buy(){		//作用于方法上
            if (tickets<0) return false;
            System.out.println(Thread.currentThread().getName()+"抢到了第"+tickets+"张票");
            tickets--;
            return true;
        }
    }
    

    注:Synchronized关键字必须作用于共享的临界资源或者临界区,加了Synchronized关键字的临界区,线程只能排队访问,例如我们对Run方法使用Synchronized关键字,那么线程只能排队调用run方法

5.2 lock

在Java中,Synchronized关键字提供的是隐式锁,其可以锁临界资源以及锁临界区,但是Java中又提供了另一种有效的灵活且复杂的方式对共享区进行加锁,那就是Lock接口,我们需要了解的是其实现类ReentrantLock,其有两个基本方法

  • lock方法,上锁
  • unlock方法,解锁

当我们需要对一个共享区进行上锁解锁时,就用这两个方法,但是只能锁临界区,不能锁临界资源

class BuyTicket implements Runnable{
    private int tickets=10;
    private final ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true){
            try {
                lock.lock();
                if (tickets <= 0) break;
                System.out.println(Thread.currentThread().getName() + "抢到了第" + tickets + "张票");
                tickets--;
            }
            finally {
                lock.unlock();
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

六、线程通信

在多线程程序中,我们不仅需要考虑线程之间的互斥访问临界资源与临界区的问题,还要考虑线程之间的互相通信的问题,可能是一个线程急需CPU资源,因此需要打断当前线程,又亦或是一个线程完成之后需要唤醒其他阻塞线程获取CPU资源继续执行等等

为了解决这些问题,Java中提供了相关的方法,例如:

  • Wait()方法是用来阻塞线程
  • notify()方法则是用来唤醒其他线程
  • notifyAll()方法则是用来唤醒其他所有线程
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿Halley

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值