7. Java多线程

1. 基本概念

1.1 程序/进程/线程
1.1.1 程序(program):一段静态的代码,静态对象。

1.1.2 进程(process):是程序的一次执行过程,或是正在运行的一个程序。

  • 进程是一个动态的过程,有它自身的产生、存在、消亡的过程。
  • 程序是静态的,进程是动态的。
  • 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。

1.1.3 线程(thread):进程可进一步细化为线程,一个程序内部的一条执行路径。

  • 若一个进程同一时间并行执行多个线程,就是支持多线程的。
  • 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
  • 一个进程中的多个线程共享相同的内存单元。它们从同一堆中分配对象,可以访问相同的变量和对象,使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能会带来安全隐患。
  • 一个java应用程序至少有3个线程:main主线程、垃圾回收线程、异常处理线程。

1.2 线程相关特性
1.2.1 单核CPU和多核CPU

  • 单核CPU是一种假的多线程,但是因为CPU很快,感觉不出来。
  • 单核CPU只用单个线程先后完成多个任务,比用多个线程来完成用的时间更短,因为切换线程需要花费时间。
  • 多核CPU才是真正意义上的多线程

1.2.2 并行与并发

  • 并行:多个CPU同时执行多个任务。
  • 并发:一个CPU采用时间片策略“同时”执行多个任务。

1.2.3 使用多线程的优点

  • 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
  • 提高计算机系统的CPU利用率。
  • 改善程序结构。将长而复杂的进程分为多个线程,独立运行,利于理解和修改。

1.2.4 何时需要多线程

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

1.2.5 线程的分类
Java中线程分为两类:守护线程&用户线程。两者唯一的区别是判断JVM何时离开,若JVM中都是守护线程,当前JVM退出。守护线程是用来服务用户线程的,Java垃圾回收就是一个守护线程。

2. 线程的创建和使用

2.1 继承Thread类的方式创建多线程
2.1.1 创建步骤
Java可通过继承java.lang.Thread类来实现多线程,具体步骤如下:

  • 创建一个继承与Thread的子类
  • 重写Thread类中的run()方法,方法内实现此子线程要完成的功能
  • 创建该子类的对象
  • 调用该对象的start()方法:启用线程,且会调用当前线程的run()方法
public class ThreadTest {
    
    
    public static void main(String[] args) {
    
    
        MyThread myThread = new MyThread();	//对象由主线程创建
        myThread.start();	//两个循环在不同线程中运行
        for (int i = 0; i < 100; i++) {
    
    
            if (i % 2 != 0) {
    
    
                System.out.println(i);
            }
        }
    }
}

class MyThread extends Thread {
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            if (i % 2 == 0) {
    
    
                System.out.println(i);
            }
        }
    }
}

2.1.2 问题说明

  • 不能直接调用run()方法来启动线程,此时只是单纯地调用了一个方法。
  • 不能让已经start()的线程再次执行,需要重新创建一个线程的对象执行start()。

2.1.3 Thread类的常用方法

  • start():启动当前线程,并调用当前线程的run()方法
  • run():存放子线程要执行的代码
  • currentThread():静态方法,返回执行当前代码的线程
  • getName()/setName():获取/设置当前线程的名字
  • yield():当前线程释放CPU的执行权(释放后CPU可能又分配给了该线程)
  • join():在A线程中调用B线程的join方法,则A线程进入阻塞状态,直至B线程执行完毕,A线程才结束阻塞状态
  • sleep(long millitime):显示地让当前线程睡眠millitime毫秒,睡眠过程中当前线程阻塞
  • isAlive():判断当前线程是否还存活

2.1.4 线程的优先级
Java的线程调度方法的特点:

  • 时间片策略:同优先级线程组成先进先出队列
  • 抢占式策略:对高优先级线程优先调度

Java的线程优先级:

  • Java中线程的优先级分为10档,最大(MAX_PRIORITY)为10,最小(MIN_PRIORITY)为1,默认(NORM_PRIORITY)为5
  • 线程创建时继承父线程的优先级
  • 高优先级仅表示抢到CPU的可能性大,并非一定是高优先级线程在低优先级线程前调用
  • getPriority()方法返回线程优先级,setPriority(int newPriority)设置线程的优先级

2.2 实现Runnable接口的方式创建多线程
2.2.1 创建步骤

  • 创建一个实现了Runnable接口的类
  • 实现类取实现Runnable中的抽象方法run()
  • 创建实现类的对象
  • 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
  • 通过Thread类的对象调用start()方法
public class RunnableTest {
    
    
    public static void main(String[] args) {
    
    
        MyThread1 myThread1 = new MyThread1();
        Thread thread1 = new Thread(myThread1);
        Thread thread2 = new Thread(myThread1);
        thread1.start();
        thread2.start();
    }
}

class MyThread1 implements Runnable {
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            if (i % 2 == 0) {
    
    
                System.out.println(i);
            }
        }
    }
}

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

  • 开发中优先选择实现Runnable接口的方式,该方式避免了类的单继承的局限性
  • 实现Runnable的方式创建的线程共用实现类中的属性,而用继承Thread的方式创建的线程想要共用类中的属性需要static
  • Thread类本身也实现了Runnable接口

2.3 实现Callable接口的方式创建多线程
2.3.1 与实现Runnable接口的比较
为JDK5.0新增的多线程创建方式,与使用Runnable相比功能更强大:

  • 相比run()方法,call()方法可以有返回值
  • 方法可以抛出异常
  • 支持泛型的返回值
  • 需要借助FutureTask类,比如获取返回结果

2.3.2 Future接口

  • 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
  • FutureTask是Future接口的唯一实现类。
  • FutureTask同时实现了Runnable、Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。

2.3.3 创建步骤

  • 创建一个实现Callable接口的实现类
  • 实现call()方法,将此线程需要执行的操作声明在call方法中
  • 创建实现类的对象
  • 将实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
  • 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread类的对象,并调用start()方法
  • 调用FutureTask对象的get()方法获取call()方法的返回值
public class CallableTest {
    
    
    public static void main(String[] args) {
    
    
        MyThread2 myThread2 = new MyThread2();
        FutureTask futureTask = new FutureTask<>(myThread2);
        new Thread(futureTask).start();
        try {
    
    
            Object sum = futureTask.get();
            System.out.println(sum);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        } catch (ExecutionException e) {
    
    
            e.printStackTrace();
        }
    }
}

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

2.4 使用线程池的方式创建多线程
2.4.1 线程池介绍

  • 背景:线程池为JDK5.0新增的线程创建方式。若经常创建和销毁、使用量很大的资源,对性能影响很大。
  • 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现重复利用。
  • 好处:提高响应速度、降低资源消耗、便于线程管理。

2.4.2 线程池相关API
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor。

  • void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
  • < T >Future< T >submit(Callable< T >task):执行任务,有返回值,一般用来执行Callable
  • void shutdown():关闭连接池

Ececutors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

  • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
  • Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
  • Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
  • Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或定期执行

2.4.3 创建步骤

  • 创建线程池
  • 通过线程池执行线程操作
  • 关闭连接池
public class ThreadPoolTest {
    
    
    public static void main(String[] args) {
    
    
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1= (ThreadPoolExecutor) service;
        service1.setCorePoolSize(15);
        service1.execute(new MyThread3());
        service1.shutdown();
    }
}

class MyThread3 implements Runnable {
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            if (i % 2 == 0) {
    
    
                System.out.println(i);
            }
        }
    }
}

可用于管理的线程池属性有:

  • corePoolSize:核心池的大小
  • maxmiumPoolSize:最大线程数
  • keepAliveTime:线程没有任务时最多保持多长时间后终止

3. 线程的生命周期

JDK中用Thread.State类定义了线程的几种状态,Java中线程的一个完整的声明周期通常要经历如下5种状态:

  • 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
  • 就绪:处于新建状态的线程调用start()后,将进入线程队列等待CPU时间片,此时它以具备了运行的条件,只是没分配到CPU资源。
  • 运行:当就绪的线程被调度并获得CPU资源时,并进入运行状态,开始调用run()方法。
    失去CPU执行权、或调用yield()方法到就绪状态。执行完run()方法、调用stop()方法、出现异常未处理到死亡状态。调用sleep()/join()/wait()/suspend()方法、等待同步锁时进入阻塞状态。
  • 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态。
    sleep()时间到、join()结束、获取了同步锁、调用notify/notifyAll()/resume()方法进入就绪状态。
  • 死亡:线程完成了它的全部工作,或被提前强制性中止,或出现异常结束。
    在这里插入图片描述

4. 线程的同步机制

出现线程安全问题的原因:由于一个线程在操作共享数据的过程中,未执行完成时另外的线程参与进来。在Java中通过同步机制解决线程安全问题,有多种方式实现同步机制。

4.1 方式一:同步代码块

synchronized(同步监视器){
    
    
	需要同步的代码
}
  • 操作共享数据的代码,即为需要同步的代码。共享数据指多个线程共同操作的变量。
  • 同步监视器:俗称。任何一个类的对象都可以充当锁,要求多个线程必须共用同一把锁。
  • 通过实现Runnable接口创建多线程时,通常用this充当同步监视器。通过继承Thread类创建多线程时,常用“类名.class”充当同步监视器(类也是对象)。
  • 被同步的那部分代码相当于单线程运行,效率变低。

4.2 方式二:同步方法
如果操作共享数据的代码完整地声明在一个方法中,不妨将此方法声明为同步方法。

public synchronized void show(){
    
    
}
  • 同步方法也有锁,不需要显式地声明。非静态的同步方法以this为锁,静态的同步方法以当前类为锁(继承方式实现多线程要用静态的同步方法)。

4.3 方式三:使用Lock锁

  • 从JDK5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁用Lock对象充当。
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
  • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,可以显式加锁、释放锁。
  • synchronized在执行完同步代码后,自动释放同步锁。Lock需要手动启动和结束同步。
ReentrantLock lock = new  ReentrantLock();//继承的方式时要申明为static
try{
    
    
	lock.lock();
	同步代码块
}finally{
    
    
	lock.unlock();
}

4.4 懒汉式的线程安全

class Bank{
    
    
	private Bank(){
    
    }

	private  static Bank instance = null;
	//方式一:效率低
	public static Bank getInstance(){
    
    
		synchronized(Bank.class){
    
    
			if(instance == null){
    
    
				instance = new Bank();
			}
			return instance;
		}
	}
	//方式二:效率高
	public static Bank getInstance(){
    
    
		if(instance == null){
    
    
			synchronized(Bank.class){
    
    
				if(instance == null){
    
    
					instance = new Bank();
				}
			}
		}
		return instance;
	}
}

4.5 线程的死锁

  • 死锁的产生:不同的线程分别占用对方需要的同步资源不放弃。(当两个线程同时需要两个锁,且各占用一个锁时发生死锁)
  • 死锁的解决:使用专门的算法、尽量减少同步资源的定义、尽量避免嵌套同步。

5. 线程的通信

5.1 线程通信的例题
使用两个线程打印1-100,交替打印。

public class CommunicationTest {
    
    
    public static void main(String[] args) {
    
    
        Number number = new Number();
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);
        t1.setName("线程1");
        t2.setName("线程2");
        t1.start();
        t2.start();
    }
}

class Number implements Runnable {
    
    
    private int number = 1;

    @Override
    public void run() {
    
    
        while (true) {
    
    
            synchronized (this) {
    
    
                notify();
                if (number <= 100) {
    
    
                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;
                    try {
    
    
                        wait();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                } else {
    
    
                    break;
                }
            }
        }
    }
}

5.2 线程通信的方法
线程通信的以下3个方法必须使用在同步代码快或同步方法中(Lock中使用其他方法),这三个方法的调用者为同步监视器。

  • wait():当前线程进入阻塞状态,并释放同步监视器。
  • notify():唤醒被wait的一个线程。若有多个线程被wait,则唤醒优先级高的那个。
  • notifyAll():唤醒所有被wait的线程。

sleep()和wait()的异同:

  • 相同点:都能使当前线程进入阻塞状态。
  • 不同点:① 声明位置不同:Thread类中声明sleep(),Object类中声明wait();
    ② 调用的要求不同:sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块或同步方法中;
    ③ sleep()不会释放锁,wait()会释放锁。

猜你喜欢

转载自blog.csdn.net/qq_43221336/article/details/107799039
今日推荐