一、多线程安全与应用
1、程序、进程与线程的关系
进程是程序运行的实例;
当一个程序执行进入内存运行时,即变成一个进程;
进程的资源是彼此隔离的,其他进程不允许访问
线程是进程内执行的“任务”;
线程是进程内的一个“基本任务”,每个线程都有自己的功能,是CPU分配与调度的基本单位;
一个进程内可以包含多个线程,反之一个线程只能隶属于某一个线程;
进程内至少拥有一个“线程”,这个线程叫“主线程”,主线程消亡则进程结束;
单核CPU通过时间片使所有线程达到一个并发执行的效果;
多核CPU达到并行执行的效果;
2、创建多线程的三种方式
(1)继承Thread类创建线程【不推荐】
package org.example;
import java.util.Random;
public class ThreadExample1 {
class Runner extends Thread {
@Override
public void run() {
Integer speed = new Random().nextInt(10);
for (int i = 1; i <= 10; i++) {
System.out.println("第" + i + "秒:" + this.getName() + "跑到:" + (i * speed));
}
}
}
public void start() {
Runner runnerA = new Runner();
runnerA.setName("参赛者A");
Runner runnerB = new Runner();
runnerB.setName("参赛者B");
runnerA.start();
runnerB.start();
}
// 主线程存在,进程存在,主线程消失,进程消失
public static void main(String[] args) {
new ThreadExample1().start();
}
}
当调用thread.start()方法时,线程对象A和B中的run方法执行权移交给了操作系统,由操作系统的线程调度策略来决定先执行哪个方法
(2)实现Runnable接口创建线程
package org.example;
import java.util.Random;
public class ThreadExample2 {
class Runner implements Runnable {
@Override
public void run() {
Integer speed = new Random().nextInt(10);
for (int i = 1; i <= 10; i++) {
System.out.println("第" + i + "秒:" + Thread.currentThread().getName() + "跑到:" + (i * speed));
}
}
}
public void start() {
Runner runnerA = new Runner();
Thread threadA = new Thread(runnerA);
threadA.setName("参赛者A");
Runner runnerB = new Runner();
Thread threadB = new Thread(runnerB);
threadB.setName("参赛者B");
threadA.start();
threadB.start();
}
// 主线程存在,进程存在,主线程消失,进程消失
public static void main(String[] args) {
new ThreadExample2().start();
}
}
(3)Callable接口创建线程
package org.example;
import java.util.Random;
import java.util.concurrent.*;
public class ThreadExample3 {
// callable线程执行后将结果返回
class Runner implements Callable<Integer> {
public String name;
@Override
public Integer call() throws Exception {
Integer speed = new Random().nextInt(10);
Integer result = 0;
for (int i = 1; i <= 10; i++) {
System.out.println("第" + i + "秒:" + this.name + "跑到:" + (i * speed));
result += i * speed;
}
return result;
}
}
public void start() throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(3);
Runner threadA = new Runner();
threadA.name = "参赛者A";
Runner threadB = new Runner();
threadB.name = "参赛者B";
Runner threadC = new Runner();
threadC.name = "参赛者C";
Future<Integer> submit1 = executorService.submit(threadA);
Future<Integer> submit2 = executorService.submit(threadB);
Future<Integer> submit3 = executorService.submit(threadC);
executorService.shutdown();
System.out.println(threadA.name + "累计跑了" + submit1.get() + "米");
System.out.println(threadB.name + "累计跑了" + submit2.get() + "米");
System.out.println(threadC.name + "累计跑了" + submit3.get() + "米");
}
// 主线程存在,进程存在,主线程消失,进程消失
public static void main(String[] args) throws ExecutionException, InterruptedException {
new ThreadExample3().start();
}
}
3、线程的生命周期
Thread类定义的线程六种状态:
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called {@code Object.wait()}
* on an object is waiting for another thread to call
* {@code Object.notify()} or {@code Object.notifyAll()} on
* that object. A thread that has called {@code Thread.join()}
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
4、初识线程同步并使用sychronized实现线程同步
(1)初识线程同步
代码中的同步机制:
- sychronized(同步锁)关键字的作用就是利用一个特定的对象设置一个锁(绣球),在多线程(游客)并发访问的时候,同时只允许一个线程(游客)可以获得这个锁,执行特定的代码(迎娶新娘)。执行后释放锁,继续由其他线程争抢。
(2)sychronized的锁对象
- sychronized代码块:任意对象即可
- sychronized方法:this当前对象【实际使用更多】
- sychronized静态方法:该类的字节码对象
package org.example;
public class SyncSample {
class Printer {
Object lock = new Object();
public void print() throws InterruptedException {
// 代码块
synchronized (lock) {
Thread.sleep(1000);
System.out.println("你");
Thread.sleep(1000);
System.out.println("好");
Thread.sleep(1000);
System.out.println("北");
Thread.sleep(1000);
System.out.println("京");
}
}
// 方法【实际使用更广泛】
public synchronized void print2() throws InterruptedException {
// 代码块
Thread.sleep(1000);
System.out.println("你");
Thread.sleep(1000);
System.out.println("好");
Thread.sleep(1000);
System.out.println("北");
Thread.sleep(1000);
System.out.println("京");
}
// 方法【实际使用更广泛】
public synchronized void print3() throws InterruptedException {
// 代码块
Thread.sleep(1000);
System.out.println("你");
Thread.sleep(1000);
System.out.println("好");
Thread.sleep(1000);
System.out.println("北");
Thread.sleep(1000);
System.out.println("京");
}
}
class PrintTask implements Runnable {
private Printer printer;
@Override
public void run() {
try {
printer.print2();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public void start() {
Printer printer = new Printer();
for(int i = 0; i < 10; i++) {
PrintTask printTask = new PrintTask();
printTask.printer = printer;
Thread thread = new Thread(printTask);
thread.start();
}
}
public static void main(String[] args) {
new SyncSample().start();
}
}
5、死锁产生的原因与解决方式
死锁是在多线程情况下最严重的问题,在多线程对公共资源(文件、数据)等进行操作时,彼此不释放自己的资源,而去试图操作其他线程的资源,而形成交叉引用,就会产生死锁。
解决死锁最根本的建议是:
- 尽量减少公共资源的引用
- 用完马上释放公共资源
- 增加超时失败机制
6、初识线程池及其基本使用
并发是伴随着多核处理器的诞生而产生的,为了充分利用硬件资源,诞生了多线程技术。但是多线程又存在资源竞争的问题,引发了同步和互斥的问题,JDK1.5推出的java.util.concurrent(并发工具包)来解决这些问题。
(1)Runnable接口的弊端:
- Runnable新建线程,性能差
- 线程缺乏统一管理,可能无限制的新建线程,相互竞争,严重时会占有过多系统资源导致死机或内存溢出
(2)线程复用的理念
ThreadPool线程池
- 重用存在的线程,减少线程对象创建、消亡的开销
- 线程总数可控,提高资源的利用率
- 提供额外功能,定时执行、定期执行、监控等
JUC支持的线程池种类
在Java.util.concurrent中,提供了工具类Executors(调度器)对象来创建线程池,可创建的线程池有四种:
1、FixedThreadPool:定长线程池
public static void main(String[] args) {
// 创建一个定长线程池
// 定长线程池的特点是固定线程总数,空闲线程用于执行任务,如果线程都在使用,后续任务则处理等待状态
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for(int i = 0; i < 100; i++) {
// 执行runnable对象
final int index = i;
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "---" + index);
}
});
}
// 需要返回值,使用submit callable接口实现
threadPool.shutdown();
}
2、CachedThreadPool:可缓存线程池
// 创建可缓存线程池
ExecutorService threadPool = Executors.newCachedThreadPool();
// 可缓存线程池的特点,无限大,如果线程中没有可用的线程则创建,有空闲则利用起来
for(int i = 0; i < 100; i++) {
// 执行runnable对象
final int index = i;
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "---" + index);
}
});
}
// 需要返回值,使用submit callable接口实现
threadPool.shutdown();
3、SingleThreadPool:单线程池
4、ScheduledThreadPool:调度线程池
public static void main(String[] args) {
// 创建可调度线程池:定时执行任务
ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(5);
threadPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(new Date() + "延迟执行1秒,每三秒执行一次");
}
}, 1, 3, TimeUnit.SECONDS);
}
二、线程池
1、线程池——治理线程的最大法宝
(1)什么是“池”
需要的时候可以快速使用,不用重新收集
(2)线程池
- 如果不使用线程池,每个任务都新开一个线程处理
- 加快响应速度
- 合理利用CPU和内存
- 统一管理
(3)线程池适合应用的场合
批量计算任务、服务器处理请求、excel解析
实际上,在开发中,如果需要创建5个以上的线程,那么就可以使用线程池来管理
2、创建和停止线程池:
(1)线程池构造方法的参数
参数名 | 类型 | 含义 |
---|---|---|
corePoolSize | int | 核心线程数 |
maxPoolSize | int | 最大线程数 |
keepAliveTime | long | 保持存活时间 |
workQueue | BlockingQueue | 任务存储队列 |
threadFactory | threadFactory | 当线程池需要新的线程的时候,会使用ThreadFactory来创建新的线程 |
Handler | RejectExecutionHandler | 当线程池无法接受你所提交的任务的拒绝策略 |
- corePoolSize指的是核心线程数:线程池在完成初始化后,默认情况下,线程池中并没有任何线程,线程池会等待有任务到来时,再创建新线程去执行任务
- 最大量maxPoolSize:在核心线程数的基础上,额外增加的线程数的上限
- 添加线程规则:
最开始线程总数小于corePoolSize时,即使有线程处于空闲状态,新任务到来时,也会创建一个新线程;直到线程数等于corePoolSize,再来任务时,会把任务放入队列去等待;直到队列也满了,如果此时线程数小于maxPoolSize,则会再创建新线程来执行任务;如果队列已满,并且线程数已经扩大到等于maxPoolSize时,再尝试添加任务时,会被拒绝;
(2)增减线程的特点
- 通过设置corePoolSize和maxPoolSize相同,就可以创建固定大小的线程池
- 线程池希望保持较少的线程数,并且只有在负载变得很大时才增加它
- 通过设置maxPoolSize为很高的值,可以允许线程池容纳任意数量的并发任务
- 只有在队列填满时才创建多于corePoolSize的线程,如果使用的是无界队列,那么线程数就不会超过corePoolSize
(3)线程存活时间:keepAliveTime
如果线程池当前的线程数多于corePoolSize,那么如果多于的线程空闲时间超过keepAliveTime,它们就会被终止
(4)创建线程:ThreadFactory
默认使用Executors.defaultThreadFactory()
创建出来的线程都在同一个线程组
如果自己指定ThreadFactory,那么就可以改变线程名、线程组、优先级是否是守护线程等
通常情况下,使用默认的线程工厂基本上就可以满足绝大多数的需要
(5)工作队列:
有3种最常见的队列类型
- 直接交换:SynchronousQueue
- 无界队列:LinkedBlockingQueue
- 有界队列:ArrayBlockingQueue
3、线程池应该手动创建还是自动创建:
(1)四种线程池自动创建弊端
- newFixedThreadPool:容易造成大量内存占用,可能会导致OOM
- newSingleThreadExecutor:当请求堆积的时候,可能会占用大量的内存
- CachedThreadPool可缓存线程池
特点:具有自动回收多余线程的功能
弊端在于第二个参数maximumPoolSize被设置为了Integer.MAX_VALUE,这可能会创建数量非常多的线程,甚至导致OOM - newScheduledThreadPool
(2)正确的创建线程池的方法:
- 根据不同的业务场景,设置线程池参数;
- 比如:内存有多大,给线程取什么名字等等
(3)线程池里的线程数量设定为多少比较合适:
-
CPU密集型(加密、计算hash等):最佳线程数为CPU核心数的1-2倍
-
耗时IO型(读写数据库、文件、网络读写等):最佳线程数一般会大于CPU核心数很多倍
参考Brain Goetz推荐的计算方法: 线程数 = CPU核心数*(1 + 平均等待时间/平均工作时间)
(4)常见线程池的特点:
-
FixedThreadPool
核心线程池和最大线程数相同,队列满后,无法再增加线程
-
CachedThreadPool
具有自动回收多余线程的功能,队列没有容量,线程数可以很多
-
ScheduledThreadPool
支持定时及周期性任务执行的线程池
-
SingleThreadExecutor
只会用唯一的工作线程来执行任务(用到的场景并不多)
(5)停止线程池的正确方法:
1、shutdown
运行这个方法之后并不一定会停止,事实上,这个方法仅仅是初始化整个关闭过程。运行这个方法,会把正在执行的和队列中等待的都执行完毕之后再关闭,但是不会再增加任务。
2、isShutdown
3、isTerminated:线程都结束了返回true
4、awaitTermination
5、shutdownNow
4、拒绝策略与钩子方法
任务太多,怎么拒绝:
- 拒绝时机
当Executor关闭时,提交新任务会被拒绝,以及当Executor对最大线程和工作队列容量使用有限边界并且已经饱和时
4种拒绝策略:
- AbortPlicy:抛出异常
- DiscardPolicy:默默丢弃
- DiscardOlderPolicy:丢弃掉老的
- CallerRunsPolicy:谁调用谁执行
钩子方法,给线程池加点料:
- 每个执行任务前后
- 日志、统计
5、线程池实现原理
(1)线程池组成部分:
- 线程池管理器
- 工作线程
- 任务队列
- 任务接口(Task)
(2)Executor家族:
- 线程池、ThreadPoolExecutor、ExecutorService、Executor、Executors等这么多和线程池先关的类,都是什么关系
- Executor:是一个接口,里面只有一个方法execute
- ExecutorService:继承Executor,增加了一些新的方法,拥有初步管理线程池的方法
- Executors:工具类,帮我们快速创建线程池用的
(3)线程池实现任务复用的原理:
- 相同线程执行不同任务
- runWorker 方法(进入到源码)
6、 线程池五种状态:
(1)execute方法:执行任务
(2)使用线程池的注意点:
- 手动创建线程池
- 线程数合理
- 考虑多个线程池间的影响
7、关于ReentrantLock和Condition
(1)ReentrantLock
ReentrantLock是java中一种常见的“锁‘,可以”锁住“一部分资源,让这部分资源在只有获取锁之后才能被操作,这就保证了线程安全,否则的话,如果有多个线程同时操作该资源,那么这个数据就可能会出错。
(2)Condition
Condition是ReentrantLock内部的工具,可以实现”等待“、”唤醒”的效果,比较典型的应用场景就是当执行任务A的线程获取到锁后,但是还没达到运行条件,那么进进入“等待”,而执行任务B的线程在完成任务后,再去“唤醒”之前“等待”状态的线程。
三、ThreadLocal
2、典型场景2
- 用ThreadLocal保存一些业务内容(用户权限信息、从用户系统获取到的用户名、userId等)
- 这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的
- 在线程生命周期内,都通过这个静态ThreadLocal实例的get()方法取的自己set过的那个对象,避免了将这个对象作为参数传递的麻烦
- 强调同一个请求(同一个线程内)不同方法间的共享
- 不需要重写initialValue()方法,但是必须手动调用set()方法
public class ThreadLocalSample2 {
public static void main(String[] args) {
Service1 service1 = new Service1();
service1.process();
new Service2().process();
}
}
class Service1 {
public void process() {
User user = new User("三三");
UserHolderContext.holder.set(user);
}
}
class Service2 {
public void process() {
UserHolderContext.holder.get();
}
}
class UserHolderContext {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
3、ThreadLocal的两个作用
1、让某个需要用到的对象在线程间隔离(每个线程都有自己独立的对象)
2、在任何方法中都可以轻松获取到该对象
场景一:initialValue
在ThreadLocal第一次get的时候把对象初始化出来,对象的初始化时机可以由我们控制
场景二:set
ThreadLocal里面对象的生成时机不由我们控制,例如拦截器生成的用户信息,用ThreadLocal.set直接放到ThreadLocal中,以便后续使用
使用ThreadLocal带来的好处:
-
达到线程安全
-
不需要加锁,提高执行效率
-
更高效地利用内存、节省开销
相比于每个任务都新建一个SimpleDateFormat,显然用ThreadLocal可以节省内存和开销
-
免去传参的繁琐
–不需要每次都传同样的参数
–ThreadLocal使得代码耦合度更低、更优雅
4、ThreadLocal主要方法:
(1)initialValue() 初始化
- 该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候,才会触发
- 当线程第一次使用get方法访问变量时,将调用此方法
- 每个线程最多调用一次此方法,但如果已经调用了remove()后,再调用get(),则可以再次调用此方法
- 如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue()方法
(2)void set(T t):为这个线程设置一个新值
(3)T get():得到这个线程对应的Value,如果是首次调用get(),则会调用initialize来得到这个值
(4)void remove():删除对应这个线程的值
5、Thread、ThreadLocal以及ThreadLocalMap三者之间的关系
6、ThreadLocal注意点
1、内存泄漏
某个对象不再使用,但是占有的内存无法回收
2、Value的泄漏
(1)ThreadLocalMap的每个Entry都是一个对key的弱引用,同时,每个Entry都包含一个value的强引用
(2)正常情况下,当一个线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了
(3)但是,如果线程不终止(需要保持很久),那么key对应的value就不能被回收,这种情况下就会发生内存泄漏,就可能出现OOM
3、如何避免内存泄漏
调用remove方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用ThreadLocal之后,应该调用remove()方法
【注:学习来源——慕课网】
三、在Spring boot中开启多线程
1、@EnableAsyn
2、ThreadPool bean配置
3、方法上面加上@Aysnc(“threadPoolname”)
五、如何实现线程控制
1、线程的创建与启动:使用 Thread 类或者实现 Runnable 接口来创建线程,并调用 start() 方法来启动线程。
2、线程的休眠:使用 Thread 类的 sleep() 方法来使线程进入休眠状态,指定休眠的时间。
3、线程的等待和唤醒:使用 Object 类的 wait() 和 notify()/notifyAll() 方法来实现线程的等待和唤醒操作。wait() 方法会使当前线程进入等待状态,直到其他线程调用 notify() 或 notifyAll() 方法来唤醒它。
4、线程的中断:使用 Thread 类的 interrupt() 方法来中断线程。可以通过判断线程的中断状态来决定是否终止线程的执行。
5、线程的加入:使用 Thread 类的 join() 方法来等待其他线程的结束。调用 join() 方法会使当前线程进入等待状态,直到被等待的线程执行完毕。
6.、线程的优先级:使用 Thread 类的 setPriority() 方法来设置线程的优先级。可以通过设置优先级来影响线程调度的顺序。【在绝大数时间里,所有线程都应该以默认的优先级运行。试图操纵线程优先级通常是一种错误,优先级是在run()的开头部分设置的】
7、线程的同步:使用 synchronized 关键字来实现线程之间的同步操作。可以使用 synchronized 关键字修饰方法或代码块,确保同一时间只有一个线程可以访问被同步的代码。
8、线程的死锁避免:通过合理的设计和编写代码,避免出现死锁现象。例如,避免线程之间互相持有对方所需的资源而无法释放。
六、并发
1、事实上,从性能的角度看,如果没有任务会阻塞,那么在单处理器机器上使用并发就没有任何意义。
2、编写多线程程序最基本的困难在于协调不同线程驱动的任务之间对这些资源的使用,以使得这些资源不会同时被多个任务访问。
3、在绝大数时间里,所有线程都应该以默认的优先级运行。试图操纵线程优先级通常是一种错误,优先级是在run()的开头部分设置的
4、让步:Thread.yield() 暗示工作差不多了,可以让别的线程使用CPU了,只是一个暗示,没有任何机制保证它会被执行。对于任何重要的控制或在调整应用时,都不能依赖yield()
5、isDaemon() 守护线程,GC
6、在Java中,Thread类自身不执行任何操作,它只是驱动赋予它的任务
7、不需要了解线程组的任何知识