超详细JAVA基础——多线程

本文详细介绍了Java中的多线程编程,包括线程的概念、创建线程的步骤、Thread类的使用,以及线程池和线程同步的关键技术。通过示例展示了如何创建任务和线程,使用ExecutorService创建线程池以提高效率,并探讨了synchronized关键字和锁机制在避免竞争状态中的作用。
摘要由CSDN通过智能技术生成

一、引言

多线程使得程序中的多个任务可以同时执行

Java的重要功能之一就是内部支持多线程——在许多程序设计语言中,多线程都是通过调用依赖于系统的过程或函数来实现的。

二、线程的概念

一个程序可能包含多个可以同时运行的任务。线程是指一个任务从头到尾的执行流程。

在一个java程序中可以并发地启动多个线程。这些线程可以在多处理器系统上并行运行,也可以在单处理器系统上并发执行。并发是指多个任务在宏观上同时运行,微观上不同时运行;并行是指多个任务在宏观和微观上都同时运行。
多线程可以使程序的反应更快,交互性更强,执行效率更高。比如某任务在等待用户的输入操作时,系统便可以将CPU资源分配给其他任务。因此,即使在单处理器系统上,多线程程序的运行速度也也比单线程更快。

在这里插入图片描述

三、创建任务和线程

可以在程序中创建附加的线程以执行并发任务。在Java中,每个任务都是Runnable接口的一个实例,也称为可运行对象(runnable object)。线程本质上讲就是便于任务执行的对象。
一个任务类必须实现Runnable接口。任务必须从线程运行。

3.1 步骤一:创建类,实现Runnable接口

创建一个实现循环打印字符功能的类PrintChar,并实现Runnable接口。类的数据域有char c和int n,分别为打印的目标的字符和打印的次数。实现run方法告诉系统该线程将如何运行。类的定义代码如下:

public class PrintChar implements Runnable{
    private char c;
    private int n;

    public PrintChar() {
    }

    public PrintChar(char c, int n) {
        this.c = c;
        this.n = n;
    }

    @Override
    public void run() {
        for(int i = 0; i < n; i ++) {
            System.out.print(c + " ");
        }
    }
}

3.2 创建一个任务

通过类的构造方法生成类的实例化对象,这便是创造了一个任务。使用下面的语句创建PrintChar的任务:

PrintChar printChar1 = new PrintChar('a', 1000);

3.3 创建任务的线程

任务必须在线程中执行。Thread类包括创建线程的构造方法以及控制线程的很多有用的方法。使用下面的语句创建任务的线程:

Thread thread1 = new Thread(printChar1);

然后调用start()方法告诉Java虚拟机该线程准备运行,如下所示:

thread1.start();

3.4 编写程序

根据以上步骤创建一个Java程序,它创建了三个任务以及三个运行这些任务的线程:

  • 第一个任务printclass1打印字符 a 10次。
  • 第二个任务printclass2打印字符 b 10次。
  • 第三个任务printclass3打印字符 c 10次。

代码如下:

public class TaskThreadTest{

    public static void main(String[] args) {

        PrintChar printChar1 = new PrintChar('a', 100);
        PrintChar printChar2 = new PrintChar('b', 100);
        PrintChar printChar3 = new PrintChar('c', 100);

        Thread thread1 = new Thread(printChar1);
        Thread thread2 = new Thread(printChar2);
        Thread thread3 = new Thread(printChar3);

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

该程序创建了三个任务。为了同时运行它们,创建三个线程。调用start()方法启动一个线程,它会导致任务中的run()方法被执行。当run()方法执行完毕,线程就终止。

截取一小段输出结果如下图所示:
在这里插入图片描述
从输出结果可以看到,字符a、b、c的打印是没有顺序的,展示了三个进程并发运行的效果。

注意:任务中的run()方法指明如何完成这个任务。Java虚拟机会自动调用该方法,无需特地调用它。

四、Thread类

Thread类包含为任务而创建的线程的构造方法,以及控制线程的方法。

Thread类的方法和描述如下表所示:

方法描述
+Thread()创建一个空的线程
+Thread(task: Runnable)为指定的任务创建一个线程
+star():void开始一个线程导致JVM 调用run()方法
+isAlive(): boolean测试线程当前是否在运行
+setPriority(p:int): void为该线程指定优先级p(取值从1到10)
+join(): void等待该线程结束
+sleep(millis: long): void让一个线程休眠指定时间,以毫秒为单位
+yield(): void引发一个线程暂停并允许其他线程执行
+interrupt(): void中断该线程
  • 可以使用yield()方法为其他线程临时让出 CPU时间。例如,将PrintChar类的run()方法的代码做如下修改:
@Override
    public void run() {
        for(int i = 0; i < n; i ++) {
            System.out.print(c + " ");

            Thread.yield();
        }
    }

每次打印一个字符后,printchar任务的线程会让出时间给其他线程。

  • 方法sleep(long mills)可以将该线程设置为休眠以确保其他线程的执行,休眠时间为指定的毫秒数。例如,PrintChar类的run方法代码可以修改如下:
@Override
    public void run() {
        for(int i = 0; i < n; i ++) {
            System.out.print(c + " ");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

每打印一个字符之后,线程就休眠1秒。

五、线程池

通过上面的学习,我们学会了通过实现Runnable接口来定义一个任务类:

public class TaskClass implements Runnable {

	//通过构造器创建任务对象
	public TaskClass(...) {...}
	
	//在run方法中定义任务流程
 	@Override
    public void run() {...}
}

以及创建一个线程来运行任务:

Thread thread = new Thread(new TaskClass(...));
thread.start();

该方法对单一任务的执行是很方便的,但是由于必须为每个任务创建一个线程,因此对大量的任务而言是不够高效的,为每个任务开始一个新线程可能会限制吞吐量并且造成性能降低。

这就引出了线程池的概念。线程池是管理并发执行任务个数的理想方法。Java提供Executor接口来执行线程池中的任务,提供ExecutorService接口来管理和控制任务。其中,ExecutorService是 Executor的子接口。两者关系如下图所示:
在这里插入图片描述
可以使用Executors类中的静态方法创建一个ExecutorService对象,如下图所示:

在这里插入图片描述

  • newFixedThreadPool方法在池中创建固定数目的线程。如果线程完成了任务的执行,它可以被重新使用以执行另外一个任务。
  • 如果线程池中所有的线程都不是处于空闲状态,而且有任务在等待执行,那么在关闭之前,如果由于一个错误终止了一个线程,就会创建一个新线程来替代它。
  • 如果线程池中所有的线程都不是处于空闲状态,而且有任务在等待执行,那么newCachedThreadPool()方法就会创建一个新线程。
  • 如果缓冲池中的线程在60秒内都没有被使用就该终止它。对许多小任务而言,一个缓冲池已经足够。

因此,我们可以使用Executors类中的静态方法newFixedThreadPool() 返回ExecutorService类型的线程池对象,使用对象的execute() 方法向线程池中添加任务。最后使用close() 方法关闭线程池。使用线程池重写先前的程序,代码如下:

public class TestThreadPool {

    public static void main(String[] args) {

        //得到一个线程数为3的线程池对象executor
        ExecutorService executor = Executors.newFixedThreadPool(3);

        //通过从Executor接口继承的execute方法,向进程池对象executor中添加任务
        executor.execute(new PrintChar('a', 100));
        executor.execute(new PrintChar('b', 100));
        executor.execute(new PrintChar('c', 100));

        //关闭进程池
        executor.shutdown();
    }
}

如果将进程池中的线程数量设为1,那么这三个任务将会顺序执行,结果就是先输出100次字符a,再输出100次字符b,最后输出100次字符c。
如果将第六行代码改为ExecutorService executor = Executors.newCachedThreadPool();,程序将会为每个等待的任务创建一个新线程,所有的任务都将并发执行。

六、线程同步

如果一个共享资源同时被多个线程访问,那么可能会遭到破坏。下面的程序将作为例子说明这个问题:

public class AccountWithoutSync {

    //创建一个账户
    private static Account account = new Account();

    public static void main(String[] args) {

        ExecutorService executor = Executors.newCachedThreadPool();

        for(int i = 0; i < 100; i ++) {
            executor.execute(new AddAPennyTask());
        }

        executor.shutdown();
        while(!executor.isTerminated()) {}

        System.out.println(account.getBalance());
    }

    //AddAPennyTask类实现Runnable接口,在run方法中定义运行内容为向账户加一块钱
    private static class AddAPennyTask implements Runnable{

        @Override
        public void run() {
            try {
                 account.deposit(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
    
    private static class Account {

        private int balance = 0;

        public int getBalance() {
            return balance;
        }
        
        public void deposit(int num) throws InterruptedException {
            int newBalance = balance + num;
            Thread.sleep(5);
            balance = newBalance;
        }
    }
}

10~12行代码将100个任务添加到了线程池,每个任务都实现了向账户内添加一块钱的操作。那么正常情况下,account.getBalance()方法返回的值应该是100。但是执行该代码得到的结果如下:在这里插入图片描述

那么为什么会出现这种情况呢,下面给出一个可能得场景:
在这里插入图片描述
在线程1中,任务获得的balance为0。在线程1休眠的时候,线程2的任务获得的balance依然为0,因为此时线程1还没有将balance的值更新。时间线结束的时候,线程2为balance赋值1,覆盖了线程1的赋值。

很明显,问题就是任务1和任务2以一种会引起冲突的方式访问一个公共资源。这是多线程程序中的一个普遍问题,称为竞争状态(race condition)。如果一个类的对象在多线程程序中没有导致竞争状态,则称这样的类为线程安全的 (thread-safe)。如上例所示,Acount类不是线程安全的。

6.1 synchronized关键字

为避免竞争状态,应该防止多个线程同时进入程序的某一特定部分,程序中的这部分称为临界区(criticalregion)。

Account类中的deposit方法是临界区,可以使用synchronized关键字同步方法,以便一次只有一个线程访问这个方法。下面介绍一种能够解决上述问题的方法:

在Account类中的deposit 方法中添加关键字synchronized,使Account类成为线程安全的,如下所示:

public synchronized void deposit(int num) throws InterruptedException {
    int newBalance = balance + num;
    Thread.sleep(5);
    balance = newBalance;
}

原理:一个同步方法在执行之前需要加锁。锁是一种实现资源排他使用的机制。对于实例方法,要给调用该方法的对象加锁。对于静态方法,要给这个类加锁。如果一个线程调用一个对象上的同步实例方法(静态方法),首先给该对象(类)加锁,然后执行该方法,最后解锁。在解锁之前,另一个调用那个对象(类)中方法的线程将被阻塞,直到解锁。

6.2 同步语句

下面介绍另一种解决竞争状态问题的方法——同步语句。

当执行方法中某一个代码块时,同步语句不仅可用于对 this对象加锁,而且可用于对任何对象加锁。这个代码块称为同步块(synchronized block)。同步语句的一般形式如下所示:

synchronized (expr) {
    statement;
}

表达式expr求值结果必须是一个对象的引用。如果对象已经被另一个线程锁定,则在解锁之前,该线程将被阻塞。当获准对一个对象加锁时,该线程执行同步块中的语句,然后解除给对象所加的锁。同步语句允许设置同步方法中的部分代码,而不必是整个方法,这大大增强了程序的并发能力。

对程序中AddAPennyTask类的run方法进行如下修改:

@Override
public void run() {
    try {
        synchronized (account) {
            account.deposit(1);
        }
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

经过修改,在某个任务进入account对象的deposit方法时,其他任务就无法进入deposit方法。

6.3 利用加锁同步

加锁同步可以显式地采用锁和状态来同步线程

上面介绍的synchronized关键字和同步语句都是隐式地对实例加上锁。Java也支持显式地加锁,以实现线程间的同步。一个锁是一个Lock接口的实例,定义了加锁和解锁的方法。
ReentrantLock是Lock的一个具体实现,用于创建相互排斥的锁。按如下方式修改程序中Account类的代码,便可显式加锁,实现线程同步:

private static class Account {
    
    private static Lock lock = new ReentrantLock(); //创建一个锁对象
    private int balance = 0;

    public int getBalance() {
        return balance;
    }

    public void deposit(int num) throws InterruptedException {
        
        lock.lock(); //加锁
        
        int newBalance = balance + num;
        Thread.sleep(5);
        balance = newBalance;
        
        lock.unlock(); //解锁
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值