JAVA中多线程超详细讲解、看完你就会了 | 您所在的位置:网站首页 › java多线程编程描述正确的一项是 › JAVA中多线程超详细讲解、看完你就会了 |
JAVA多线程
1.多线程基础
1.1 线程和进程
进程:
是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用 程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基 本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。 线程:进程内部的一个独立执行单元;一个进程可以同时并发的运行多个线程,可以理 解为一个进程便相当于一个单 CPU 操作系统,而线程便是这个系统中运行的多个任 务。 2.多线程的创建方式第一种继承Thread类 重写run方法 (无法设置返回值) 创建一个继承自java.lang.Thread类的子类,并重写run()方法来定义线程执行的任务。然后,创建子类的实例并调用start()方法启动线程。 class MyThread extends Thread { public void run() { // 线程执行的任务 } } MyThread thread = new MyThread(); thread.start();第二种实现Runnable接口,重写run方法 (无法设置返回值) 创建一个实现java.lang.Runnable接口的类,实现其run()方法来定义线程执行的任务。然后,创建一个Thread对象,并将Runnable对象传递给它,最后调用start()方法启动线程。 class MyRunnable implements Runnable { public void run() { // 线程执行的任务 } } Runnable runnable = new MyRunnable(); Thread thread = new Thread(runnable); thread.start();第三种实现 implements Callable接口(可以存在线程返回值 Object) 创建一个实现java.util.concurrent.Callable接口的类,实现其call()方法来定义线程执行的任务,并可以返回一个结果。使用ExecutorService来提交Callable任务并获取执行结果。 import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; class MyCallable implements Callable { public String call() { // 线程执行的任务,并返回结果 return "Task completed"; } } ExecutorService executor = Executors.newFixedThreadPool(2); Callable callable = new MyCallable(); Future future = executor.submit(callable); 实现Runnable接口比继承Thread类所具有的优势: 适合多个相同的程序代码的线程去共享同一个资源。 可以避免java中的单继承的局限性。 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和数 据独立。 线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread 的类使用线程池: 线程池是一种更高级的多线程管理方式,它可以重复使用线程来执行多个任务。使用ExecutorService接口来创建和管理线程池,然后通过submit()方法提交任务。 import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; ExecutorService executor = Executors.newFixedThreadPool(2); // 创建一个包含两个线程的线程池 Runnable runnable = () -> { // 线程执行的任务 }; executor.submit(runnable);这些方式都可用于创建多线程,具体的选择取决于你的需求和设计。线程池是一种高效的方式,可以减少线程创建和销毁的开销,并更好地管理线程的生命周期。同时,使用Callable接口可以获得任务的执行结果,而Runnable则用于执行无需返回结果的任务。 线程池的工作流程线程池是一种用于管理和复用线程的机制,它可以提高多线程应用程序的性能和资源管理效率。以下是典型的线程池的工作流程: 初始化线程池: 创建一个线程池并初始化其参数,包括最小线程数、最大线程数、任务队列大小、线程空闲时间等。线程池的大小通常根据应用需求和系统资源来确定。提交任务: 当需要执行任务时,将任务提交给线程池。任务可以是一个Runnable或Callable对象,表示需要执行的工作单元。任务队列: 线程池维护一个任务队列,所有提交的任务都会排队在这个队列中等待执行。如果线程池中有可用的线程,它们会从队列中取出任务并执行。如果没有可用线程,任务会等待,直到有线程可用。线程执行任务: 线程池中的线程会循环地从任务队列中取出任务并执行它们。一旦任务完成,线程将返回线程池中,准备执行下一个任务。线程复用: 线程池会复用线程,而不是在每个任务之后销毁线程。这减少了线程创建和销毁的开销,提高了执行效率。线程池管理: 线程池负责管理线程的数量和状态。它可以根据需要动态调整线程数量,以适应不同的工作负载。例如,可以根据队列中的任务数量来增加或减少线程的数量。任务完成: 当任务执行完成后,可以获取任务的执行结果(如果任务是Callable类型的)。然后可以对结果进行处理或返回给调用者。关闭线程池: 当不再需要线程池时,应该显式地关闭它。关闭线程池会停止接受新任务,并等待已提交的任务执行完成。然后线程池中的线程会被终止。关闭线程池是为了释放资源并避免内存泄漏。线程池的主要优点在于可以有效地管理和复用线程,降低了线程创建和销毁的开销,提高了应用程序的性能和响应速度。它还可以控制并发线程的数量,避免资源耗尽问题。因此,在多线程应用程序中,使用线程池通常是一种良好的实践。 import java.util.concurrent.*; public class ThreadPoolExample { public static void main(String[] args) { // 创建线程工厂 ThreadFactory threadFactory = Executors.defaultThreadFactory(); // 创建拒绝策略 RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy(); // 创建线程池,设置参数 int corePoolSize = 5; int maxPoolSize = 10; long keepAliveTime = 60; // 线程空闲时间 TimeUnit unit = TimeUnit.SECONDS; // 时间单位 int queueCapacity = 100; // 任务队列大小 ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maxPoolSize, keepAliveTime, unit, new LinkedBlockingQueue(queueCapacity), threadFactory, rejectedExecutionHandler ); // 提交任务给线程池 for (int i = 0; i < 10; i++) { final int taskId = i; // 任务ID(仅用于示例) executor.execute(new Runnable() { public void run() { System.out.println("Task " + taskId + " is executing by " + Thread.currentThread().getName()); // 执行任务的具体逻辑 // ... } }); } // 关闭线程池 executor.shutdown(); } }在这个示例中,我们首先通过Executors.defaultThreadFactory()创建了一个默认的线程工厂,用于创建线程池中的线程。 然后,我们创建了一个拒绝策略ThreadPoolExecutor.AbortPolicy(),它表示当线程池饱和时(线程池和任务队列都满了),拒绝接受新的任务并抛出RejectedExecutionException异常。 最后,我们在创建ThreadPoolExecutor时,将线程工厂和拒绝策略作为额外的参数传递进去。 通过自定义线程工厂和拒绝策略,我们可以更灵活地控制线程池中线程的创建过程和任务的拒绝处理。 Java中有两种线程,一种是用户线程,另一种是守护线程。 用户线程是指用户自定义创建的线程,主线程停止,用户线程不会停止。 守护线程当进程不存在或主线程停止,守护线程也会被停止 守护线程(daemon thread)是在计算机程序中运行的一种特殊线程。它的主要特点是当所有非守护线程结束时,守护线程会自动退出,而不会等待任务的完成。 守护线程通常被用于执行一些后台任务,如垃圾回收、日志记录等。它们在程序运行过程中默默地执行任务,不会阻塞主线程或其他非守护线程的执行。 与普通线程不同,守护线程的生命周期并不影响整个程序的生命周期。当所有非守护线程结束时,守护线程会被强制退出,无论它的任务是否完成。 需要注意的是,守护线程不能用于执行一些重要的任务,因为它们可能随时被强制退出。此外,守护线程也无法捕获或处理异常。 总结来说,守护线程是一种在后台执行任务的线程,当所有非守护线程结束时会自动退出。它们通常用于执行一些不重要或周期性的任 thread1.setDaemon(true); //设置守护线程 4.线程安全相关问题线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静 态变量只有读操作,而无写 操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一 般都需要考虑线程同步, 否则的话就可能影响线程安全。 5.如何解决当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容 易出现线程安全问题。 要解决上述多线程并发访问一个资源的安全问题,Java中提供了同步机制 (synchronized)来解决。 一.同步代码块 (自动锁) (重量锁) 二.同步方法 三.lock锁同步 (手动锁) ReentrantLock lock = new ReentrantLock(); lock.lock(); sell(name); lock.unlock();面试题: JVM指令集 lock 锁和 syn 哪个锁的性能更好呢? 1.8之前lock 锁更强 1.8(包含) syn 和 lock 没啥区别 同步代码块 与 同步方法有什么区别? 锁对象不同 同步方法锁对象为this 同步代码块的锁对象为任意对象(必须保证唯一) synchronized 实现原理? monitorenter和monitorexit字节码指令 lock 锁 与 synchronized 区别? lock 是乐观锁还是悲观锁? 得看实现类 ReentrantLock 悲观锁 读写锁 乐观锁 ReentrantLock 是公平锁还是非公平锁 ? 无参非公平,代参公平锁使用锁 会引起 ---- 死锁 : 线程间的互相等待。 多线程死锁:同步中嵌套同步,导致锁无法释放 如何避免 : 尽量方式锁中嵌套锁 6.线程状态状态描述: NEW(新建) :线程刚被创建,但是并未启动。 RUNNABLE(可运行) :线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。 BLOCKED(锁阻塞) :当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 WAITING(无限等待) :一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 TIMED_WAITING(计时等待) :同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 TERMINATED(被终止) :因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 wait() 让线程处于等待状态,并且释放当前锁资源 需要手动唤醒 sleep() 不会释放锁 让线程处于等待状态 自然醒来对于sleep()方法,首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。 sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。 wait()是把控制权交出去,然后进入等待此对象的等待锁定池处于等待状态,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。 在调用sleep()方法的过程中,线程不会释放对象锁。而当调用wait()方法的时候,线程会放弃对象锁。 7.线程结束:结束线程有以下三种方法: (1)设置退出标志,使线程正常退出。 (2)使用interrupt()方法中断线程。 (3)使用stop方法强行终止线程(不推荐使用Thread.stop, 这种终止线程运行的方法已经被废弃,使用它们是极端不安全的!) Thread.sleep(1000l); //中断线程 t.interrupt(); t.stop(); //废弃 8.线程优先级现今操作系统基本采用分时的形式调度运行的线程,线程分配得到时间片的多少决定了线程使用处理器资源的多少,也对应了线程优先级这个概念。 在JAVA线程中,通过一个int priority来控制优先级,范围为1-10,其中10最高,默认值为5。 线程优先级 并不能觉得线程的执行顺序,只是让当前线程能够获得更多的cpu资源而已 优先级可以增加线程获取cpu资源的多少,但是不能决定线程的执行顺序 t.setPriority(1); //获得的cpu资源多于 线程2 join()方法 (让线程顺序执行)join作用是让其他线程变为等待。thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。 yield方法Thread.yield()方法的作用:暂停当前正在执行的线程,并执行其他线程。(可能没有效果) yield()让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。 9.多线程并发的3个特性 (重点)原子性 :即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要 么就都不执行 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即 看得到修改的值 (volitale) 有序性:程序执行的顺序按照代码的先后顺序执行 解决可见性问题方案:1.同步方式解决可见性问题 while (flag) { synchronized (this) { } }线程解锁前(退出同步代码块时):必须把自己工作内存中共享变量的最新值刷新到主内存中 线程加锁时(进入同步代码块时):将清空本地内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(加锁与解锁是同一把锁) 自旋锁所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是 否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。 适应自旋锁即自适应自旋锁。所谓自适应就意味着自旋的 次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来 决定。 锁消除 (JDK对象Syn 优化的实现)为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但 是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进 行锁消除。锁消除的依据是逃逸分析的数据支持 。 JVM可以明显检测到变量vector没有逃逸出方法vectorTest() 之外,所以JVM可以大胆地将vector内部的加锁操作消除。 关于 Java 逃逸分析的定义: 逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术。 锁粗话但是如果一系列的连续加锁解锁操作, 可能会导致不必要的性能损耗,所以引入锁粗化的概念。 就是将多个连续的加锁、解锁操作连接在一起,扩展成 一个范围更大的锁。 重量锁 (SYN) 操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。 10.Volatile介绍 (面试点)面试问题:volatile 能够保证线程安全问题吗?为什么? 不能,volatile 只能保证可见性和顺序性,不能保证原子性。 作用:解决内存可见性的问题 public volatile boolean flag = true;Volatile实现内存可见性的过程 线程写Volatile变量的过程: 改变线程本地内存中Volatile变量副本的值; 将改变后的副本的值从本地内存刷新到主内存 线程读Volatile变量的过程: 从主内存中读取Volatile变量的最新值到线程的本地内存中 从本地内存中读取Volatile变量的副本 Volatile实现内存可见性原理:写操作时,通过在写操作指令后加入一条store屏障指令,让本地内存中变量的值能够刷新到主内存中 读操作时,通过在读操作前加入一条load屏障指令,及时读取到变量在主内存的值 Volatile 无法保证原子性解决方案: 使用synchronized (不推荐) public synchronized void addCount() { for (int i = 0; i < 10000; i++) { count++; } }使用ReentrantLock(可重入锁) //可重入锁 private Lock lock = new ReentrantLock(); public void addCount() { for (int i = 0; i < 10000; i++) { lock.lock(); count++; lock.unlock(); } }使用AtomicInteger(原子操作) public static AtomicInteger count = new AtomicInteger(0); public void addCount() { for (int i = 0; i < 10000; i++) { //count++; count.incrementAndGet(); } } CAS介绍什么是CAS? CAS:Compare and Swap,即比较再交换。 jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。 CAS算法理解 对CAS的理解,CAS是一种无锁算法 (乐观锁),CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
1.首先,每个线程都会先获取当前的值。接着走一个原子的CAS操作,原子的意思就是这个CAS操作一定是自己完整执行完的,不会被别人打断。 2.然后CAS操作里,会比较一下,现在你的值是不是刚才我获取到的那个值。如果是,说明没人改过这个值,那你给我设置成累加1之后的一个值。 3.同理,如果有人在执行CAS的时候,发现自己之前获取的值跟当前的值不一样,会导致CAS失败,失败之后,进入一个无限循环,再次获取值,接着执行CAS操作。 CAS缺陷CAS虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方 法:循环时间太长、只能保证一个共享变量原子操作、ABA问题 存在问题: 1.可能cas 会一直失败,然后自旋 2.如果一个值原来是A,变成了B,然后又变成了A,那么在CAS检查的 时候会发现没有改变,但是实质上它已经发生了改变,这就是所谓的ABA问题。 对于ABA问题其解决方案是加上版本号,即在每个变量都加上一个版本号,每次 改变时加1,即A —> B —> A,变成1A —> 2B —> 3A。 JAVA之AQS什么是AQS? (锁获取和锁释放) 它只是一个抽象类 ,但是JUC中的很多组件都是 基于这个抽象类,也可以说这个AQS是多数JUC组件的基础。 用于JUC包下的,核心组件 AQS(AbstractQueuedSynchronizer),即队列同步器。 JAVA之锁 ReentrantLock 可重入锁 (悲观锁)获取锁 sync.lock(); 释放锁 sync.release(1); ReentrantLock与synchronized的区别 1.功能比synchronized 要多,拓展性更强 2.对待线程等待,唤醒操作更加详细和灵活。 3.ReentrantLock提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而synchronized则一旦进入锁请求要么成功要么阻塞,所以相比synchronized而言,ReentrantLock会不容易产生死锁些。 4.ReentrantLock支持更加灵活的同步代码块,但是使用synchronized时,只能在同一个synchronized块结构中获取和释放。 5.RentrantLock支持中断处理,且性能较synchronized会好些。 读写锁维护着一对锁,一个读锁和一个写锁。通过分离读锁和写锁,使得并发性比一般的互斥锁有了较大的提升:在同一时间可以允许多个读线程同时访问,但是在写线程访问时,所有读线程和写线程都会被阻塞。 |
今日新闻 |
推荐新闻 |
专题文章 |
CopyRight 2018-2019 实验室设备网 版权所有 |