【多线程】基础
1. 概念
1.1 使用多线程目的
提高系统的吞吐速度(提高CPU的利用率):充分利用多CUP多核(一个核可以对应多个线程数,如四核八线程),一个核对应一个线程。单核CPU时代,只能有一个线程利用CPU,使用多线程提高系统利用率不明显。
1.2 多任务多进程
过去单核CPU时代,计算机能在同一时间点并行执行多任务或多进程。并不是真正意义上的“同一时间点”,而是多个任务或进程共享一个CPU,并交由操作系统来完成多任务间对CPU的运行切换,以使得每个任务都有机会获得一定的时间片运行。
而现在多核CPU的情况下,同一时间点可以执行多个任务,具体到这个任务在CPU哪个核上运行,这个就跟操作系统和CPU本身的设计相关了
1.3 程序线程数
比如说tomcat为每个请求启动一个线程处理,其中maxThreads设置为1000,有1000个用户并发,那么Tomcat就会起1000个线程来处理。那么假如实际CPU线程数只有30,1000个线程分时间片段使用30个CPU线程处理,从外部上看是同一时间。类似单核CUP多任务。
1.4如何设置程序线程数
(1)活跃线程数为 CPU(核)数时最佳。
过少的活跃线程导致 CPU 无法被充分利用,过多的活跃线程导致过大的线程上下文切换开销,同时更多的内存开销,更多的CPU开销。
活跃线程数:处于 IO 的线程,休眠的线程等均不消耗 CPU,不属于活跃线程。在实际环境中,当前活跃线程数一直在变化,很多活跃线程可能因为需要进行 IO 处理或等待资源而处于非活跃状态,假如说线程的数量等于 CPU(核)数,这就意味着活跃线程数小于 CPU(核)数。
(2)确定线程数的原则
提高CPU的中签率:多线程编程中,在确保内存不溢出的情况下提升线程数是可以提高CPU中签率的,也就是能提高你的程序处理数据的速度。
不是线程数越大越好:即使没有溢出,也不是线程数越大越好,线程切换毕竟需要时间,应该找到瓶颈所在
(3)依据cpu核数设置合适的线程数
获取cpu核心数:Runtime.getRuntime().availableProcessors();
// 根据CPU数量创建线程池
private static ExecutorService printJobthreadPool = Executors
.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
2. 线程安全
2.1 线程安全是由什么引起
线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;
若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。以下情况是线程安全的:
- 常量始终是线程安全的,因为只存在读操作。
- 每次调用方法前都新建一个实例是线程安全的,因为不会访问共享的资源。
- 局部变量是线程安全的。因为每执行一个方法,都会在独立的空间创建局部变量,它不是共享的资源。局部变量包括方法的参数变量和方法内变量。
2.2 解决线程安全的方式
为了解决多线程中相同变量的访问冲突问题,有两种方法:ThreadLocal和线程同步机制。
(1)同步机制
对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
(2)ThreadLocal
ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
ThreadLocal具体使用:
private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>()
//我们有时候会使用静态变量来存放某些值。但是并发情况下静态变量是不安全的。因此使用ThreadLocal创建独立的副本。
private static final ThreadLocal contextHolder=new ThreadLocal();
然后使用常用方法操作:
set:设置当前线程的线程局部变量的值。
get:返回当前线程所对应的线程局部变量。
remove:将当前线程局部变量的值删除。
(3) 什么是线程安全的类
线程安全的类 ,指的是类内共享的全局变量的访问必须保证是不受多线程形式影响的。如果由于多线程的访问(比如修改、遍历、查看)而使这些变量结构被破坏或者针对这些变量操作的原子性被破坏,则这个类就不是线程安全的。
2.4 无状态Bean
(1)无状态bean和有状态的定义
- 无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是
没有实例变量的对象
,不能保存数据,是不变类,是线程安全的。无状态的Bean适合用不变模式,技术就是单例模式,这样可以共享实例,提高性能。 - 有状态对象:有状态的Bean,多线程环境下不安全,那么适合用Prototype原型模式。
package com.sw;
public class TestManagerImpl implements TestManager{
private User user; //实例变量
public void deleteUser(User e) throws Exception {
user = e ; //1
prepareData(e);
}
public void prepareData(User e) throws Exception {
user = getUserByID(e.getId()); //2
.....
//使用user.getId(); //3
.....
.....
}
}
TestManagerImpl是一个有状态的Bean,如果该Bean配置为singleton,会出现什么样的状况呢?
3. 使用流程
多线程使用步骤主要有:
3.1 建立线程任务
实现方式主要有两种:实现Runnable接口和继承Thread类。实现Runnable接口相比继承Thread类有如下好处:
- 避免继承的局限,一个类可以继承多个接口。
- 适合于资源的共享。
在程序开发中只要是多线程肯定永远以实现Runnable接口为主。两种方式都要实现run()方法。
(1)实现Runnable接口
主要的实现步骤很简单:构造函数和实现run方法
public class RefundNotify implements Runnable
{
private RefundNotifyMapper mRefundNotifyMapper;
private LinkedBlockingQueue<String> queue;
private String url;
private static Logger logger = LoggerFactory.getLogger(AutoRepayServiceImpl.class);
//构造函数
public RefundNotify(LinkedBlockingQueue<String> queue, RefundNotifyMapper mRefundNotifyMapper, String url01)
{
System.out.println("退款通知线程开始工作!");
this.queue = queue;
this.mRefundNotifyMapper = mRefundNotifyMapper;
this.url = url01;
}
// 实现run方法
@SneakyThrows
@Override
public void run()
{
}
}
Runnable里面没有start方法可以通过Thread类进行启动Runnable多线程,Runnable可以用同一个对象实例化的,可以资源共享,Thread不可以
//同一个实例
MyThreadWithImplements myRunnable = new MyThreadWithImplements();
hread thread1 = new Thread(myRunnable, "窗口一");
Thread thread2 = new Thread(myRunnable, "窗口二");
Thread thread3 = new Thread(myRunnable, "窗口三");
thread1.start();
thread2.start();
thread3.start();
(2)继承Thread类
使用不多,不详细介绍
3.2 创建线程池
使用ThreadPoolExecutor创建线程池并执行
//获取系统处理器个数,作为线程池数量
int nThreads = Runtime.getRuntime().availableProcessors();
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("member-pool-%d").build();
//创建线程池
ExecutorService pool = new ThreadPoolExecutor(5, 200,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
//传入任务,执行
pool.execute(refundNotify);
pool.execute(invoiceNotify);
pool.execute(timingRetryNotify);