发表时间:2022-03-25来源:网络
现代操作系统(Windows、Linux、MacOS)都可以执行多任务,多任务就是同时运行多个任务。例如在我们的计算机上,一般都同时跑着多个程序,例如浏览器,视频播放器,音乐播放器,Word办公软件等等,由于CPU执行代码都是一条一条顺序执行的,即时是单核CPU也可以同时执行多个任务,操作系统执行多个任务实际上就是轮流让多个任务交替执行。即使是多核CPU,因为通常任务的数量是远多于CPU的核数,所以任务也是交替执行的。
在计算机中,我们把一个任务称为一个进程。浏览器就是一个进程,视频播放器是另外一个进程,音乐播放器也是一个进程,在某些进程内部还需要同时执行多个子任务。例如我们在使用Word时,在打字的同时需要进行拼写检查,还可以在后台进行打印,我们把子任务称为线程。
进程和线程的关系:
实现多任务的三种方法:
多进程模式(每个进程只有一个线程) 多线程模式(一个进程有多个线程) 多进程+多线程(复杂度最高)多进程和多线程的对比:
创建进程比创建线程开销大(尤其是在Windows系统上) 进程间通信比线程间通信慢 多进程稳定性比多线程高(因为在多进程的情况下,一个进程的崩溃不会影响其他进程,而在多线程的情况下,任何一个线程的崩溃,会直接导致整个进程崩溃)Java语言内置多线程支持,一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法中又可以启动多个线程,此外,JVM还有负责垃圾回收的其他工作线程等。和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。
创建一个线程对象,并启动一个新的线程。创建线程对象的方法有两种:第一种就是创建MyThread类,去继承Thread类,覆写run()方法,创建MyThread实例,调用start()启动线程。第二种:如果一个类已经从某个类派生,无法从Thread继承,就可以通过实现Runnable接口,重写run()方法,在main()方法中创建Runnable实例,创建Thread实例并传入Runnable,调用start()启动线程。注意:必须调用Thread实例的start()方法才能启动新线程,如果我们查看Thread类的源代码,会看到start()方法内部调用了一个private native void start0()方法,native修饰符表示这个方法是由JVM虚拟机内部的C代码实现的,不是由Java代码实现的。
总结:Java用Thread对象表示一个线程,通过调用start()启动一个新线程,一个线程对象只能调用一次start()方法;线程的执行代码写在run()方法中,一旦run()方法执行完毕,线程就结束了;线程调度由操作系统决定,程序本身无法决定调度顺序。
线程的状态:
线程终止的原因:
run()方法执行到return语句返回(线程正常终止) run()方法因为未捕获的异常导致线程终止(线程意外终止) 对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)通过对另一个线程对象调用join()方法可以等待其执行结束,可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;对已经运行结束的线程调用join()方法会立刻返回。
中断线程:如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发送一个信息,该线程收到信号后,结束执行run()方法。例如我们从网络下载一个100M的文件,如果网速很慢,我们等得不耐烦,就可能在下载过程中点“取消”,这时,程序就需要中断下载线程的执行。中断线程需要通过检测isInterrupted标志,其他线程需要通过调用interrupt()方法中断该线程。如果线程处于等待状态,该线程会捕获InterruptedException。捕获到InterruptedException说明有其他对其线程调用了interrupt()方法,通常情况下该线程应该立刻结束运行。
public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); t.start(); Thread.sleep(1000); t.interrupt(); // 中断t线程 t.join(); // 等待t线程结束 System.out.println("end"); } } class MyThread extends Thread { public void run() { Thread hello = new HelloThread(); hello.start(); // 启动hello线程 try { hello.join(); // 等待hello线程结束 } catch (InterruptedException e) { System.out.println("interrupted!"); } hello.interrupt(); } } class HelloThread extends Thread { public void run() { int n = 0; while (!isInterrupted()) { n++; System.out.println(n + " hello!"); try { Thread.sleep(100); } catch (InterruptedException e) { break; } } } } 还可以通过设置running标志位,线程间共享变量需要使用volatile关键字标记,确保线程能读取到更新后的变量值。为什么要对线程间共享的变量用关键字volatile声明?这涉及到Java的内存模型。在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。
volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。因此,volatile关键字的目的是告诉虚拟机:每次访问变量时,总是获取主内存的最新值,每次修改变量后,立刻回写到主内存。
守护线程:为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。因此,JVM退出时,不必关心守护线程是否已结束。如何创建守护线程呢?方法和普通线程一样,只是在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程。
Thread t = new MyThread(); t.setDaemon(true); t.start();需要注意的是:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。
当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行,如果多个线程同时读写共享变量,会出现数据不一致的问题。对共享变量进行写入时,必须保证是原子操作,原子操作是指不能被中断的一个或一系列操作。例如,对于语句:n = n + 1;看上去是一行语句,实际上对应了3条指令,因此为了保证一系列操作为原子操作,必须保证一系列操作在执行过程中不被其他线程执行。ava程序使用synchronized关键字对一个对象进行加锁,synchronized保证了代码块在任意时刻最多只有一个线程能执行。由于synchronized代码块无法并发执行,所以使用synchronized会导致性能下降。如何使用synchronized?首先找出修改共享变量的线程代码块,选择一个实例作为锁,使用synchronized(lockObject) { ... }。在使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁。
多线程同时修改变量,会造成逻辑错误,需要通过synchronized同步,同步的本质就是给指定对象加锁,注意加锁对象必须是同一个实例,对于JVM定义的单个原子操作不需要同步。JVM规范定义了几种原子操作:基本类型(long和double除外)赋值,例如:int n = m;引用类型赋值,例如:List list = anotherList。long和double是64位数据,JVM没有明确规定64位赋值操作是不是一个原子操作,不过在x64平台的JVM是把long和double的赋值作为原子操作实现的。
同步方法:当程序执行synchronized代码块时,首先要获得synchronized指定的锁。在我们添加synchronized块时,我们需要先知道锁住的哪个对象。让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把synchronized逻辑封装起来。数据封装:把同步逻辑封装到持有数据的实例中。当我们对this进行加锁时,我们可以使用synchronized来修饰方法,这样我们就可以把同步代码块自动变成方法识别。下面的两种写法是等价的。
public synchronized void add(int n) { n += 1; } public void add(int n) { synchronized(this){ n += 1; } }而静态方法锁住的是Class实例:如下
public class A { private static count; public static synchronized void add(int n){ count += n; } }等价于下面这种写法:
public class A { private static count; public static void add(int n){ synchronized(A.class) { count += n; } } }Java中线程安全的类:
不变类:例如String, Integer, LocalDate,因为这些类被final修饰,一但创建,实例成员变量就不能改变,不能写只能读 没有成员变量的类:例如Math,这些工具只提供了工具方法,自身没有成员变量 正确使用synchronized得类,例如StringBuffer 要执行 synchronized代码块,必须首先获得指定对象的锁,Java中的线程锁是可重入锁。什么叫可重入锁?JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。Java的线程可以获取多个不同对象的锁。不同的线程在获取多个不同对象的锁的时候,可能会导致死锁。例如两个线程根被执行下面两个方法,就会导致死锁。
死锁形成的条件:两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,导致死锁。
死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。如何避免死锁?线程获取锁的顺序要一致。
synchronized解决了多线程竞争的问题,但是synchronized并没有解决多线程协调的问题。
wait和notify用于多线程协调运行:在synchronized内部可以调用wait()使线程进入等待状态,必须在已获得的锁对象上调用wait()方法,在synchronized内部可以调用notify()或notifyAll()唤醒其他等待线程,必须在已获得的锁对象上调用notify()或notifyAll()方法,已唤醒的线程还需要重新获得锁后才能继续执行。
从Java 5开始,引入了一个高级的处理并发的java.util.concurrent包,它提供了大量更高级的并发功能,能大大简化多线程程序的编写。线程同步是因为多线程读写竞争资源需要同步,Java语言提供了synchronized/wait/notify来实现多线程的同步,但是编写多线程同步仍然很困难。jdk1.5提供的高级的java.util.concurrent包,提供了更高级的同步功能,可以简化多线程的编写,java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁。因为synchronized是Java语言层面提供的语法,所以我们不需要考虑异常,而ReentrantLock是Java代码实现的锁,我们就必须先获取锁,然后在finally中正确释放锁。
ReentrantLock也是可重入锁,一个线程可多次获取同一个锁,lock()方法获取锁,和synchronized不同的是,ReentrantLock可以通过tryLock()方法尝试获取锁并指定超时:
if (lock.tryLock(1, TimeUnit.SECONDS)) { try { ... } finally { lock.unlock(); } }但是有些时候,这种保护有点过头。因为我们发现,任何时刻,只允许一个线程修改,但是,对于某些方法只读取数据,不修改数据,它实际上允许多个线程同时调用,实际上我们想要的是:允许多个线程同时读,但只要有一个线程在写,其他线程就必须等待。使用ReadWriteLock可以解决这个问题,它保证只允许一个线程写入(其他线程既不能写入也不能读取),没有写入时,多个线程允许同时读(提高性能)。
public class Counter { private final ReadWriteLock rwlock = new ReentrantReadWriteLock(); private final Lock rlock = rwlock.readLock(); private final Lock wlock = rwlock.writeLock(); private int[] counts = new int[10]; public void inc(int index) { wlock.lock(); // 加写锁 try { counts[index] += 1; } finally { wlock.unlock(); // 释放写锁 } } public int[] get() { rlock.lock(); // 加读锁 try { return Arrays.copyOf(counts, counts.length); } finally { rlock.unlock(); // 释放读锁 } } }使用ReadWriteLock时,适用条件是同一个数据,有大量线程读取,但仅有少数线程修改。
使用ReentrantLock比直接使用synchronized更安全,可以替代synchronized进行线程同步。但是,synchronized可以配合wait和notify实现线程在条件不满足时等待,条件满足时唤醒,用ReentrantLock我们怎么编写wait和notify的功能呢?答案是使用Condition对象来实现wait和notify的功能。
class TaskQueue { private final Lock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); private Queue queue = new LinkedList(); public void addTask(String s) { lock.lock(); try { queue.add(s); condition.signalAll(); } finally { lock.unlock(); } } public String getTask() { lock.lock(); try { while (queue.isEmpty()) { condition.await(); } return queue.remove(); } finally { lock.unlock(); } } }可见,使用Condition时,引用的Condition对象必须从Lock实例的newCondition()返回,这样才能获得一个绑定了Lock实例的Condition实例。Condition提供的await()、signal()、signalAll()原理和synchronized锁对象的wait()、notify()、notifyAll()是一致的,并且其行为也是一样的:await()会释放当前锁,进入等待状态;signal()会唤醒某个等待线程;signalAll()会唤醒所有等待线程;唤醒线程从await()返回后需要重新获得锁。此外,和tryLock()类似,await()可以在等待指定时间后,如果还没有被其他线程通过signal()或signalAll()唤醒,可以自己醒来。
Concurrent集合:java.util.concurrent提供了线程安全的Blocking集合:ArrayBlockingQueue。BlockingQueue的意思就是说,当一个线程调用这个TaskQueue的getTask()方法时,该方法内部可能会让线程变成等待状态,直到队列条件满足不为空,线程被唤醒后,getTask()方法才会返回。
接口 线程不安全的实现类 线程安全的实现类 List ArrayList CopyOnWriteArrayList Map HashMap ConcurrentHashMap Set HashSet / TreeSet CopyOnWriteArraySet Queue ArrayDeque / LinkedList ArrayBlockingQueue / LinkedBlockingQueue Deque ArrayDeque / LinkedList LinkedBlockingDeque 使用java.util.concurrent包提供的线程安全的并发集合可以大大简化多线程编程,多线程同时读写并发集合是安全的,尽量使用Java标准库提供的并发集合,避免自己编写同步代码。Atomic:java.util.concurrent.atomic提供了一组原子类型操作,Atomic类是通过无锁(lock-free)的方式实现的线程安全(thread-safe)访问。它的主要原理是利用了CAS:Compare and Set。如果我们自己通过CAS编写incrementAndGet(),大概如下:
public int incrementAndGet(AtomicInteger var) { int prev, next; do { prev = var.get(); next = prev + 1; } while ( ! var.compareAndSet(prev, next)); return next; }CAS是指,在这个操作中,如果AtomicInteger的当前值是prev,那么就更新为next,返回true。如果AtomicInteger的当前值不是prev,就什么也不干,返回false。通过CAS操作并配合do ... while循环,即使其他线程修改了AtomicInteger的值,最终的结果也是正确的。用java.util.concurrent.atomic提供的原子操作可以简化多线程编程,原子操作实现了无锁的线程安全,适用于计数器,累加器等。
由于创建线程需要操作系统资源(线程资源、栈空间),频繁创建和销毁线程需要消耗大量时间。如果可以复用一组线程,那么我们就可以把很多小任务让一组线程来执行,而不是一个任务对应一个新线程。这种能接收大量小任务并进行分发处理的就是线程池。所以线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。Java标准库提供了ExecutorService接口表示线程池,常用用法如下:
// 创建固定大小的线程池: ExecutorService executor = Executors.newFixedThreadPool(4); // 提交任务: executor.submit(task1); executor.submit(task2); executor.submit(task3);因为ExecutorService只是接口,Java标准库提供的几个常用实现类有:
FixedThreadPool:线程数固定的线程池; CachedThreadPool:线程数根据任务动态调整的线程池; SingleThreadExecutor:仅单线程执行的线程池(只包含一个线程,所有的任务只能以单线程的形式执行)创建这些线程池的方法都被封装到Executors这个类中。我们以FixedThreadPool为例,看看线程池的执行逻辑:
import java.util.concurrent.*; public class Main { public static void main(String[] args) { // 创建一个固定大小的线程池: ExecutorService es = Executors.newFixedThreadPool(4); for (int i = 0; i < 6; i++) { es.submit(new Task("" + i)); } // 关闭线程池: es.shutdown(); } } class Task implements Runnable { private final String name; public Task(String name) { this.name = name; } @Override public void run() { System.out.println("start task " + name); try { Thread.sleep(1000); } catch (InterruptedException e) { } System.out.println("end task " + name); } } 还有一种任务,需要定期反复执行,例如,每秒刷新证券价格。这种任务本身固定,需要反复执行的,可以使用ScheduledThreadPool。放入ScheduledThreadPool的任务可以定期反复执行。Java标准库还提供了一个java.util.Timer类,这个类也可以定期执行任务,但是,一个Timer会对应一个Thread,所以,一个Timer只能定期执行一个任务,多个定时任务必须启动多个Timer,而一个ScheduledThreadPool就可以调度多个定时任务,所以,我们完全可以用ScheduledThreadPool取代旧的Timer。
总结:
Runnable接口有个问题,它的方法没有返回值。如果任务需要一个返回结果,那么只能保存到变量,还要提供额外的方法读取,非常不便。所以,Java标准库还提供了一个Callable接口,和Runnable接口比,它多了一个返回值.
class Task implements Callable { public String call() throws Exception { return longTimeCalculation(); } }并且Callable接口是一个泛型接口,可以返回指定类型的结果。现在的问题是,如何获得异步执行的结果?如果仔细看ExecutorService.submit()方法,可以看到,它返回了一个Future类型,一个Future类型的实例代表一个未来能获取结果的对象.
ExecutorService executor = Executors.newFixedThreadPool(4); // 定义任务: Callable task = new Task(); // 提交任务并获得Future: Future future = executor.submit(task); // 从Future获取异步执行返回的结果: String result = future.get(); // 可能阻塞当我们提交一个Callable任务后,我们会同时获得一个Future对象,然后,我们在主线程某个时刻调用Future对象的get()方法,就可以获得异步执行的结果。在调用get()时,如果异步任务已经完成,我们就直接获得结果。如果异步任务还没有完成,那么get()会阻塞,直到任务完成后才返回结果。
一个Future接口表示一个未来可能会返回的结果,它定义的方法有:
CompletableFuture:从Java 8开始引入了CompletableFuture,它针对Future做了改进,可以传入回调对象,当异步任务结束时,会自动回调某个对象的方法,异步任务出错时,也会自动回调某个对象的方法,所以当主线程设置好回调后,不再关心异步任务的执行。
CompletableFuture的基本用法:
创建一个CompletableFuture是通过CompletableFuture.supplyAsync()实现的,它需要一个实现了Supplier接口的对象。可见CompletableFuture的优点是:异步任务结束时,会自动回调某个对象的方法;异步任务出错时,会自动回调某个对象的方法;主线程设置好回调后,不再关心异步任务的执行。如果只是实现了异步回调机制,我们还看不出CompletableFuture相比Future的优势。CompletableFuture更强大的功能是,多个CompletableFuture可以串行执行,例如,定义两个CompletableFuture,第一个CompletableFuture根据证券名称查询证券代码,第二个CompletableFuture根据证券代码查询证券价格,这两个CompletableFuture实现串行操作如下:
public class Main { public static void main(String[] args) throws Exception { // 第一个任务: CompletableFuture cfQuery = CompletableFuture.supplyAsync(() -> { return ("中国石油"); }); // cfQuery成功后继续执行下一个任务: CompletableFuture cfFetch = cfQuery.thenApplyAsync((code) -> { return fetchPrice(code); }); // cfFetch成功后打印结果: cfFetch.thenAccept((result) -> { System.out.println("price: " + result); }); // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭: Thread.sleep(2000); } static String (String name) { try { Thread.sleep(100); } catch (InterruptedException e) { } return "601857"; } static Double fetchPrice(String code) { try { Thread.sleep(100); } catch (InterruptedException e) { } return 5 + Math.random() * 20; } }总结:CompletableFuture可以指定异步处理流程:
thenAccept()处理正常结果; exceptional()处理异常结果; thenApplyAsync()用于串行化另一个CompletableFuture; anyOf()和allOf()用于并行化多个CompletableFuture。Java 7开始引入了一种新的Fork/Join线程池,它可以执行一种特殊的任务:把一个大任务拆成多个小任务并行执行。Fork/Join线程池在Java标准库中就有应用。Java标准库提供的java.util.Arrays.parallelSort(array)可以进行并行排序,它的原理就是内部通过Fork/Join对大数组分拆进行并行排序,在多核CPU上就可以大大提高排序的速度。Fork/Join是一种基于“分治”的算法:通过分解任务,并行执行,最后合并结果得到最终结果。ForkJoinPool线程池可以把一个大任务分拆成小任务并行执行,任务类必须继承自RecursiveTask或RecursiveAction。使用Fork/Join模式可以进行并行计算以提高效率。
多线程是Java实现多任务的基础,Thread对象代表一个线程,我们可以在代码中调用Thread.currentThread()获取当前线程。Java标准库提供了一个特殊的ThreadLocal,它可以在一个线程中传递同一个对象,注意到普通的方法调用一定是同一个线程执行的。ThreadLocal实例通常总是以静态字段初始化如:static ThreadLocal threadLocalUser = new ThreadLocal();实际上,可以把ThreadLocal看成一个全局Map:每个线程获取ThreadLocal变量时,总是使用Thread自身作为key:因此,ThreadLocal相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal关联的实例互不干扰。最后,特别注意ThreadLocal一定要在finally中清除:这是因为当前线程执行完相关代码后,很可能会被重新放入线程池中,如果ThreadLocal没有被清除,该线程执行其他代码时,会把上一次的状态带进去。ThreadLocal适合在一个线程的处理流程中保持上下文(避免了同一参数在所有方法中传递)。
【说明】:本文参考了廖雪峰官方网站的Java教程,廖雪峰官方网站的多线程教程链接:https://www.liaoxuefeng.com/wiki/1252599548343744/1255943750561472
上一篇:Java编程基础(6)
皓盘云建最新版下载v9.0 安卓版
53.38MB |商务办公
ris云客移动销售系统最新版下载v1.1.25 安卓手机版
42.71M |商务办公
粤语翻译帮app下载v1.1.1 安卓版
60.01MB |生活服务
人生笔记app官方版下载v1.19.4 安卓版
125.88MB |系统工具
萝卜笔记app下载v1.1.6 安卓版
46.29MB |生活服务
贯联商户端app下载v6.1.8 安卓版
12.54MB |商务办公
jotmo笔记app下载v2.30.0 安卓版
50.06MB |系统工具
鑫钜出行共享汽车app下载v1.5.2
44.7M |生活服务
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-02-15
2022-02-14