JAVA多线程
2022/1/16 20:06:58
本文主要是介绍JAVA多线程,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
一、有关多线程的概念
1、程序、进程、线程
JAVA
-
程序(programm)
概念:是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码。 -
进程(process)
概念:程序的一次执行过程,或是正在运行的一个程序。
说明:进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域 -
线程(thread)
概念:进程可进一步细化为线程,是一个程序内部的一条执行路径。
说明:线程作为调度和执行的单位,每个线程拥独立的运行栈和程序计数器(pc),线程切换的开销小。
2、并行与并发
并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事
二:线程的创建和启动
Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread
类来体现。
Thread类
- 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常 把run()方法的主体称为线程体
- 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
Thread类的构造器
Thread()
:创建新的Thread对象Thread(String threadname)
:创建线程并指定线程实例名Thread(Runnable target)
:指定创建线程的目标对象,它实现了Runnable接 口中的run方法Thread(Runnable target, String name)
:创建新的Thread对象
三、创建多线程的两种方式
方式一:继承Thread类
步骤:
- 创建一个继承于Thread类的子类
- 重写Thread类的run() --> 将此线程执行的操作声明在run()中
- 创建Thread类的子类的对象
- 通过此对象调用start()(注:start有两个作用①启动当前线程 ② 调用当前线程的run())
代码实现:
package com.xawl.thread; //①创建一个继承于Thread类的子类 public class MyThread extends Thread { //②重写Thread类的run() --> 将此线程执行的操作声明在run()中 @Override public void run() { for (int i = 0; i < 100; i+=2) { System.out.println("MyThread:"+i); } } public static void main(String[] args) { //③创建Thread类的子类的对象 MyThread t = new MyThread(); //④通过此对象调用start()(注:start有两个作用①启动当前线程 ② 调用当前线程的run()) t.start(); } }
方式二:实现Runnable接口
步骤:
- 创建一个实现了Runnable接口的类
- 实现类去实现Runnable中的抽象方法:run()
- 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 通过Thread类的对象调用start()
代码实现:
package com.xawl.thread; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; //①创建一个实现了Runnable接口的类 public class MyRunnable implements Runnable{ @Override //②实现类去实现Runnable中的抽象方法:run() public void run() { for (int i = 0; i < 100; i+=2) { System.out.println("MyThread:"+i); } } public static void main(String[] args) { //③创建实现类的对象 MyRunnable r = new MyRunnable(); //④将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象 //⑤通过Thread类的对象调用start() new Thread(r).start(); } }
两种方式的对比
开发中优先选择实现Runnable接口的方式
- 原因一:实现的方式没类的单继承性的局限性
- 原因二:实现的方式更适合来处理多个线程共享数据的情况。
**两者的联系:**Thread 实现了Runnable接口(public class Thread implements Runnable
)
两者的相同点:
- 两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
- 目前两种方式,要想启动线程,都是调用的Thread类中的start()。
两者的不同点:
- Thread 是类,而Runnable是接口
- 因为Thread是类,而Runnable是接口,所以使用实现Runnable的方式具有更好的扩展性。
- 实现Runnable的方式由于多个线程都是基于某一个Runnable对象建立的,它们会共享Runnable对象上的资源。
Thread类的有关方法
void start()
: 启动线程,并执行对象的run()方法run()
: 线程在被调度时执行的操作String getName()
: 返回线程的名称void setName(String name)
:设置该线程名称static Thread currentThread()
: 返回当前线程。在Thread子类中就 是this,通常用于主线程和Runnable实现类static void yield()
:线程让步 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程 若队列中没有同优先级的线程,忽略此方法join()
:当某个程序执行流中调用其他线程的 join() 方法时,调用线程将 被阻塞,直到 join() 方法加入的 join 线程执行完为止static void sleep(long millis)
:(指定时间:毫秒) 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后 重排队。stop()
: 强制线程生命期结束,不推荐使用boolean isAlive()
:返回boolean
,判断线程是否还活着
线程的优先级:
MAX_PRIORITY
:10MIN _PRIORITY
:1NORM_PRIORITY
:5 -->默认优先级
如何获取和设置当前线程的优先级:getPriority()
:获取线程的优先级setPriority(int p)
:设置线程的优先级
说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只当高优先级的线程执行完以后,低优先级的线程才执行。
补充:
Java中的线程分为两类:一种是守护线程,一种是用户线程。
它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。
守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)
可以把一个用户线程变成一个守护线程。
Java垃圾回收就是一个典型的守护线程。
若JVM中都是守护线程,当前JVM将退出。
四、线程的生命周期
JDK中用Thread.State
类定义了线程的几种状态(下面是总结的几种状态,并不完全对应State
类中的状态)
Java语言使用Thread类 及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五 种状态:
- 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建 状态
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已 具备了运行的条件,只是没分配到CPU资源
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线 程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中 止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
五、线程的同步
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态。
线程同步的目的计算解决线程安全问题
在JAVA中我们有三种方式实现线程同步:①同步代码块②同步方法③Lock锁
方式一:同步代码块
synchronized(同步监视器){ //需要被同步的代码 }
说明:
- 操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。
- 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
- 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
- 要求:多个线程必须要共用同一把锁。
补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类(Class)充当同步监视器。
方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法使用synchronized
关键字声明为同步的。
关于同步方法的总结:
- 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
- 非静态的同步方法,同步监视器是:this
- 静态的同步方法,同步监视器是:当前类本身
方式三:Lock锁( JDK5.0新增)
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同 步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的 工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象 加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock
类实现了 Lock ,它拥有与synchronized
相同的并发性和 内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock
,可以 显式加锁、释放锁。
class A{ private final ReentrantLock lock = new ReenTrantLock(); public void m(){ lock.lock(); try{ //保证线程安全的代码; } finally{ lock.unlock(); } } } //注意:如果同步代码有异常,要将unlock()写入finally语句块
synchronized与lock的异同
相同:二者都可以解决线程安全问题
不同:
- synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
- Lock是显式锁(手动开启lock()和关闭锁unlock()),synchronized是 隐式锁,出了作用域自动释放
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有 更好的扩展性(提供更多的子类)
六、线程通信
线程与线程之间不是相互独立的个体,它们彼此之间需要相互通信和协作,最典型的例子生产者-消费者问题就是互相通信的过程,就是线程间的协作。
生产者-消费者问题:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。因此一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。同样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。
线程通信涉及到的三个方法:
wait()
:一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。notify()
:一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。notifyAll()
:一旦执行此方法,就会唤醒所有被wait的线程。
说明:
wait()
,notify()
,notifyAll()
三个方法必须使用在同步代码块或同步方法中。wait()
,notify()
,notifyAll()
三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException
异常wait()
,notify()
,notifyAll()
三个方法是定义在java.lang.Object
类中。
sleep() 和 wait()的异同?
相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
不同点:
- 两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
- 调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
- 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
七、JDK5.0新增线程创建方式
方式一:实现Callable接口。
Future
接口 可以对具体Runnable
、Callable
任务的执行结果进行取消、查询是 否完成、获取结果等。
FutrueTask
是Futrue
接口的唯一的实现类FutureTask
同时实现了Runnable
,Future
接口。它既可以作为Runnable
被线程执行,又可以作为Future
得到Callable
的返回值
步骤:
- 创建一个实现
Callable
的实现类 - 实现
call
方法,将此线程需要执行的操作声明在call()中 - 创建
Callable
接口实现类的对象 - 将此
Callable
接口实现类的对象作为传递到FutureTask
构造器中,创建FutureTask
的对象 - 将
FutureTask
的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start() - 获取
Callable
中call
方法的返回值(get()返回值即为FutureTask
构造器参数Callable实现类重写的call()的返回值。)
代码:
//1.创建一个实现Callable的实现类 class NumThread implements Callable{ //2.实现call方法,将此线程需要执行的操作声明在call()中 @Override public Object call() throws Exception { int sum = 0; for (int i = 1; i <= 100; i++) { if(i % 2 == 0){ System.out.println(i); sum += i; } } return sum; } } public class ThreadNew { public static void main(String[] args) { //3.创建Callable接口实现类的对象 NumThread numThread = new NumThread(); //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象 FutureTask futureTask = new FutureTask(numThread); //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start() new Thread(futureTask).start(); try { //6.获取Callable中call方法的返回值 //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。 Object sum = futureTask.get(); System.out.println("总和为:" + sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
实现Callable
接口的方式比实现Runnable
接口方式更强大
原因:
- call()可以返回值的。
- call()可以抛出异常,被外面的操作捕获,获取异常的信息
- Callable是支持泛型的
方式二:使用线程池
经常创建和销毁、使用量特别大的资源,比如并发情况下的线程, 对性能影响很大。
提前创建好多个线程,放入线程池中,使用时直接获取,使用完 放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交 通工具。
使用线程池的好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
JDK 5.0起提供了线程池相关API:ExecutorService
和 Executors
ExecutorService
:真正的线程池接口。常见子类ThreadPoolExecutor
void execute(Runnable command)
:执行任务/命令,没有返回值,一般用来执行 RunnableFuture submit(Callable task)
:执行任务,有返回值,一般又来执行 Callablevoid shutdown()
:关闭连接池
Executors
:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool()
:创建一个可根据需要创建新线程的线程池Executors.newFixedThreadPool(n)
; 创建一个可重用固定线程数的线程池Executors.newSingleThreadExecutor()
:创建一个只有一个线程的线程池Executors.newScheduledThreadPool(n)
:创建一个线程池,它可安排在给定延迟后运 行命令或者定期地执行。
ThreadPoolExecutor
它提供了四种构造函数来创建线程池,其中最为核心的构造函数如下所示:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
这7个参数的含义如下:
corePoolSize
线程池核心线程数。即线程池中保留的线程个数,即使这些线程是空闲的,也不会 被销毁,除非通过ThreadPoolExecutor
的allowCoreThreadTimeOut(true)
方法开启了核心线程 的超时策略;maximumPoolSize
线程池中允许的最大线程个数;keepAliveTime
用于设置那些超出核心线程数量的线程的最大等待时间,超过这个时间还没有新 任务的话,超出的线程将被销毁unit
超时时间单位;workQueue
线程队列。用于保存通过execute
方法提交的,等待被执行的任务;threadFactory
线程创建工程,即指定怎样创建线程;public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
handler
拒绝策略。即指定当线程提交的数量超出了maximumPoolSize
后,该使用什么策略处理 超出的线程。
代码
class NumberThread implements Runnable{ @Override public void run() { for(int i = 0;i <= 100;i++){ if(i % 2 == 0){ System.out.println(Thread.currentThread().getName() + ": " + i); } } } } class NumberThread1 implements Callable{ @Override public void run() { for(int i = 0;i <= 100;i++){ if(i % 2 != 0){ System.out.println(Thread.currentThread().getName() + ": " + i); } } } } public class ThreadPool { public static void main(String[] args) { //1. 提供指定线程数量的线程池 ExecutorService service = Executors.newFixedThreadPool(10); ThreadPoolExecutor service1 = (ThreadPoolExecutor) service; //设置线程池的属性 // System.out.println(service.getClass()); // service1.setCorePoolSize(15); // service1.setKeepAliveTime(); //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象 service.execute(new NumberThread());//适合适用于Runnable service.submit(new NumberThread());//适合使用于Callable //3.关闭连接池 service.shutdown(); } }
这篇关于JAVA多线程的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-10-05小米13T Pro系统合集:性能与摄影的极致融合,值得你升级的系统ROM
- 2024-10-01基于Python+Vue开发的医院门诊预约挂号系统
- 2024-10-01基于Python+Vue开发的旅游景区管理系统
- 2024-10-01RestfulAPI入门指南:打造简单易懂的API接口
- 2024-10-01初学者指南:了解和使用Server Action
- 2024-10-01Server Component入门指南:搭建与配置详解
- 2024-10-01React 中使用 useRequest 实现数据请求
- 2024-10-01使用 golang 将ETH账户的资产平均分散到其他账户
- 2024-10-01JWT用户校验课程:从入门到实践
- 2024-10-01Server Component课程入门指南