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类所具有的优势:
使用线程池:
- 线程池是一种更高级的多线程管理方式,它可以重复使用线程来执行多个任务。使用
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
时,将线程工厂和拒绝策略作为额外的参数传递进去。
通过自定义线程工厂和拒绝策略,我们可以更灵活地控制线程池中线程的创建过程和任务的拒绝处理。
3.守护线程
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 没啥区别
使用锁 会引起 ---- 死锁 : 线程间的互相等待。
多线程死锁:同步中嵌套同步,导致锁无法释放
如何避免 : 尽量方式锁中嵌套锁
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实现内存可见性原理:
写操作时,通过在写操作指令后加入一条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,否则什么都不做。
假如说有3个线程并发的要修改一个AtomicInteger的值,他们底层的机制如下:
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会好些。
读写锁ReentrantReadWriteLock (乐观锁的实现)
读写锁维护着一对锁,一个读锁和一个写锁。通过分离读锁和写锁,使得并发性比一般的互斥锁有了较大的提升:在同一时间可以允许多个读线程同时访问,但是在写线程访问时,所有读线程和写线程都会被阻塞。