多线程
仅个人笔记
进程与线程
-
进程:是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
-
线程:是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的 资源。
-
虽然系统是把资源分给进程,但是CPU很特殊,是被分配到线程的,所以线程是CPU分配的基本单位。
-
进程与线程的关系:一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。
-
程序计数器:是一块内存区域,用来记录线程当前要执行的指令地址 。
-
栈:用于存储该线程的局部变量,这些局部变量是该线程私有的,除此之外还用来存放线程的调用栈祯。
-
堆:是一个进程中最大的一块内存,堆是被进程中的所有线程共享的。
-
方法区:则用来存放 NM 加载的类、常量及静态变量等信息,也是线程共享的 。
-
进程与线程的区别:
- 进程:有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响。
- 线程:是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉。
总结1
-
一个程序至少有一个进程,一个进程至少有一个线程.
-
线程的划分尺度小于进程,使得多线程程序的并发性高。
-
另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
-
每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
-
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别
并发与并行
- 并发:是指同一个时间段内多个任务同时都在执行,并且都没有执行结束。并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行 。
- 并行:是说在单位时间内多个任务同时在执行 。
- 在多线程编程实践中,线程的个数往往多于CPU的个数,所以一般都称多线程并发编程而不是多线程并行编程。
并发过程中常见的问题
-
线程安全问题:
多个线程同时操作共享变量i时,会出现线程1更新共享变量i的值,但是其他线程获取到的是共享变量没有被更新之前的值。就会导致数据不准确问题。
-
共享内存不可见性问题
Java 内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存,线程读写变量时操作的是自己工作内存中的变量 。(当线程A里有缓存,线程B修改变量后,线程A并不知道,即线程B写入的值对线程A不可见)
-
synchronized 的内存语义:
这个内存语义就可以解决共享变量内存可见性问题。进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存。会造成上下文切换的开销,独占锁,降低并发性。
-
Volatile的理解:
该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时-,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。volatile的内存语义和synchronized有相似之处,具体来说就是,当线程写入了volatile变量值时就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存),读取volatile变量值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)。不能保证原子性。
创建线程
-
继承Thread类
重写run方法:使用继承方式的好处是,在run()方法内获取当前线程直接使用this就可以了,无须使用Thread.currentThread()方法;不好的地方是Java不支持多继承,如果继承了Thread类,那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码。
-
实现Runable接口
实现run方法:解决继承Thread的缺点,没有返回值
-
实现Callable接口
实现call方法:前两种方式都没办法拿到任务的返回结果,但是Callable方式可以
public class TestCall implements Callable { @Override public Object call() throws Exception { return "hhh"; } public static void main(String[] args){ FutureTask<String> futureTask = new FutureTask<String>(new TestCall()); new Thread(futureTask).start(); try { String result = futureTask.get(); System.out.println(result); } catch (Exception e) { e.printStackTrace(); } } }
线程特性
- 线程能被标记为守护线程,也可以是用户线程
- 每个线程均分配一个name,默认为(Thread-自增数字)的组合
- 每个线程都有优先级.高优先级线程优先于低优先级线程执行. 1-10,默认为5
- main所在的线程组为main,构造线程的时候没有现实的指定线程组,线程组默认和父线程一样
- 当线程中的run()方法代码里面又创建了一个新的线程对象时,新创建的线程优先级和父线程优先级一样.
- 当且仅当父线程为守护线程时,新创建的线程才会是守护线程.
- 当JVM启动时,通常会有唯一的一个非守护线程(这一线程用于调用指定类的main()方法)
- JVM会持续执行线程直到下面情况某一个发生为止:
- 类运行时exit()方法被调用 且 安全机制允许此exit()方法的调用.
- 所有非守护类型的线程均已经终止,or run()方法调用返回or在run()方法外部抛出了一些可传播性的异常.
- JVM会持续执行线程直到下面情况某一个发生为止:
线程同步
为了解决并发问题,Java的多线程支持引入同步监视器,使用同步监视器的通用方法就是同步代码块。
-
同步代码块
//使用obj为同步监视器,任何线程进入下面同步代码块之前 //必须先获得对obj的锁定----其它线程无法获得锁,也就无法修改它 //符合“加锁->修改->释放锁”的逻辑 synchronized(obj){ //此处的代码块就是同步代码块 }
-
同步方法
使用synchronized关键字来修改某个方法,称为同步方法。同步方法的同步监视器就是this,也就是调用该方法的对象。
public synchronized void draw(double drawAmount){ }
-
同步锁(Lock)
class x{ //定义锁对象 private final ReentrantLock lock = new ReentrantLock(); //... //需要保证线程安全的方法 public void m(){ //加锁 lock.lock(); try{ //..需要保证线程安全的代码 }finally{ lock.unlock(); } } }
-
死锁
当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有检测,也没有采用措施来处理死锁的情况。
线程通信
-
传统的线程通信
Object提供了三个方法:wait(),notify()和notifyAll(),必须由同步监视器调用。
- wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()或notifyAll()方法来唤醒该线程。
- notify():唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则任意唤醒一个线程。
-
使用Condition控制线程通信
当使用Lock对象来保证同步时,Java提供了一个Condition类来保持协调。Lock替代了同步方法或同步代码块,Condition替代了同步监视器的功能。
- await():导致当前线程等待,知道其他线程调用该Condition的Signal()方法或signalAll()方法
- signal():唤醒在此Lock上的单个线程。若有多个线程在Lock对象上等待,任意唤醒一个。
- signalAll():唤醒在此Lock对象上等待的所有线程
//显示定义Lock对象 private final Lock lock = new ReentrantLock(); //获得指定Lock对象对应的Condition private final Condition cond = lock.newCondition(); int flag = false; public void X(int a){ lock.lock(); try{ if(!flag){ cond.await(); }else{ //... cond.signalAll(); }finally{ lock.unlock(); } } }
-
使用阻塞队列(BlockingQueue)控制线程通信
class Consumer extends Thread{ private BlockingQueue<String> bq; public Consumer(BlockingQueue<String> bq) { this.bq = bq; } public void run() { while(true) { System.out.println(getName() + "消费者准备消费集合元素!"); try { Thread.sleep(200); //尝试取出元素,如果队列已空,则线程被阻塞 bq.take(); }catch(Exception ex){ ex.printStackTrace(); } System.out.println(getName() + "消费完成" + bq); } } } public class Producer extends Thread{ private BlockingQueue<String> bq; public Producer(BlockingQueue<String> bq) { this.bq = bq; } public void run() { String[] strArr = new String[]{"Java","Struts","Spring"}; for(int i = 0; i < 5; i++) { System.out.println(getName() + "生产者准备生产集合元素!"); try { Thread.sleep(200); //尝试放入元素,如果队列已满,则线程被阻塞 bq.put(strArr[i%3]); }catch(Exception ex){ ex.printStackTrace(); } System.out.println(getName() + "生产完成" + bq); } } public static void main(String[] args) { //创建一个容量为1的BockingQueue BlockingQueue<String> bq = new ArrayBlockingQueue<>(1); new Producer(bq).start(); new Producer(bq).start(); new Producer(bq).start(); new Consumer(bq).start(); } }
线程池
当我们线程创建过多时,容易引发内存溢出,因此就有必要使用线程池的技术
线程池的优势
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池的使用
//线程池的真正实现类是ThreadPoolExecutor,其构造方法有如下4种:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler);
}
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||maximumPoolSize <= 0 ||maximumPoolSize < corePoolSize ||keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
参数 | 含义 |
---|---|
corePoolSize(必需) | 核心线程数。默认情况下,核心线程会一直存活,但是当将allowCoreThreadTimeout设置为true时,核心线程也会超时回收。 |
maximumPoolSize(必需) | 线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。 |
keepAliveTime(必需) | 线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将allowCoreThreadTimeout设置为true时,核心线程也会超时回收。 |
unit(必需) | 指定keepAliveTime参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。 |
workQueue(必需) | 任务队列。通过线程池的execute()方法提交的Runnable对象将存储在该参数中。其采用阻塞队列实现。 |
threadFactory(可选) | 线程工厂。用于指定为线程池创建新线程的方式。 |
handler(可选) | 拒绝策略。当达到最大线程数时需要执行的饱和策略。 |
//线程池的使用流程如下:
// 创建线程池
Executor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE,
MAXIMUM_POOL_SIZE,
KEEP_ALIVE,
TimeUnit.SECONDS,
sPoolWorkQueue,
sThreadFactory
);
// 向线程池提交任务
threadPool.execute(new Runnable() {
@Override
public void run() {
... // 线程执行的任务
}
});
// 关闭线程池
threadPool.shutdown(); // 设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程
threadPool.shutdownNow(); // 设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
线程池的工作原理
任务队列(workQueue)
任务队列是基于阻塞队列实现的,即采用生产者消费者模式,在Java中需要实现BlockingQueue接口。但Java提供了7种阻塞队列的实现:
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)。
- LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为Integer.MAX_VALUE。
- PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现Comparable接口也可以提供Comparator来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务。
- DelayQueue:类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现Delayed接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
- SynchronousQueue: 一个不存储元素的阻塞队列,消费者线程调用take()方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用put()方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。
- LinkedBlockingDeque: 使用双向队列实现的有界双端阻塞队列。双端意味着可以像普通队列一样FIFO(先进先出),也可以像栈一样FILO(先进后出)。
- LinkedTransferQueue: 它是ConcurrentLinkedQueue、LinkedBlockingQueue和SynchronousQueue的结合体,但是把它用在ThreadPoolExecutor中,和LinkedBlockingQueue行为一致,但是是无界的阻塞队列。
注意有界队列和无界队列的区别:如果使用有界队列,当队列饱和时并超过最大线程数时就会执行拒绝策略;而如果使用无界队列,因为任务队列永远都可以添加任务,所以设置maximumPoolSize没有任何意义。
线程工厂(threadFactory)
/*
线程工厂指定创建线程的方式,需要实现**ThreadFactory**接口,并实现newThread(Runnable r)方法。该参数可以不用指定,Executors框架已经为我们实现了一个默认的线程工厂
*/
/**
* The default thread factory.
*/
private static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup():Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
拒绝策略(handler)
当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现RejectedExecutionHandler接口,并实现rejectedExecution(Runnable r, ThreadPoolExecutor executor)方法。不过Executors框架已经实现了4种拒绝策略:
- AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException异常。
- CallerRunsPolicy:由调用线程处理该任务。
- DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
- DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。
功能线程池
Executors封装了4种常见的功能线程池(现在已经不建议使用),如下:
- 定长线程池(FixedThreadPool)
- 特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。
- 应用场景:控制线程最大并发数。
- 定时线程池(ScheduledThreadPool )
- 特点:核心线程数量固定,非核心线程数量无限,执行完闲置10ms后回收,任务队列为延时阻塞队列。
- 应用场景:执行定时或周期性的任务。
- 可缓存线程池(CachedThreadPool)
- 特点:无核心线程,非核心线程数量无限,执行完闲置60s后回收,任务队列为不存储元素的阻塞队列。
- 应用场景:执行大量、耗时少的任务。
- 单线程化线程池(SingleThreadExecutor)
- 特点:只有1个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。
- 应用场景:不适合并发但可能引起IO阻塞性及影响UI线程响应的操作,如数据库操作、文件操作等。
并发工具工具
CountDownLatch、CyclicBarrier、Semophore
CountDownLatch
(Latch,门闩)
public class ThreadPoolTest {
private static AtomicInteger ai = new AtomicInteger();
private static CountDownLatch count = new CountDownLatch(4);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 4; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(ai.getAndIncrement());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count.countDown();
}
}).start();
}
count.await();
System.out.println("门开了");
}
}
(跟Thread.join()类似,就是当前线程等待别的线程执行完了再执行。原理就是await时加共享锁,countDown时释放共享锁。)
CyclicBarrier
(循环使用的屏障)
public class ThreadPoolTest {
private static AtomicInteger ai = new AtomicInteger();
private static CyclicBarrier barrier = new CyclicBarrier(4, new Runnable() {
@Override
public void run() {
System.out.println("到我了?");
}
});
public static void main(String[] args) {
for (int i = 0; i < 4; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(ai.getAndIncrement());
try {
Thread.sleep(3000);
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
}
(可以在屏障里指定如果所有线程都到达屏障了,后面应该执行哪个线程。)
Semophore
(信号量,控制并发量,分布式系统也有相似概念,称为“限流”。限流是有必要的,比如系统里有一个导出汇总excel的功能,不限制并发量,系统立马就OOM了。)
public class ThreadPoolTest {
private static AtomicInteger ai = new AtomicInteger();
//我们新建一条三车道的高速公路
private static Semaphore semaphore = new Semaphore(3);
public static void main(String[] args) {
//高速入口堵了20辆车
ExecutorService executors = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
executors.execute(new Runnable() {
@Override
public void run() {
try {
//放行一辆
semaphore.acquire();
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
} finally {
//车道空出来了
semaphore.release();
}
System.out.println(ai.getAndIncrement());
}
});
}
}
}
线程组
在java的多线程处理中有线程组ThreadGroup的概念,ThreadGroup是为了方便线程管理出现了,可以统一设定线程组的一些属性,比如setDaemon,设置未处理异常的处理方法,设置统一的安全策略等等;也可以通过线程组方便的获得线程的一些信息。
每一个ThreadGroup都可以包含一组的子线程和一组子线程组,在一个进程中线程组是以树形的方式存在,通常情况下根线程组是system线程组。system线程组下是main线程组,默认情况下第一级应用自己的线程组是通过main线程组创建出来的。
线程组和线程池的区别
线程组和线程池是两个不同的概念,他们的作用完全不同,前者是为了方便线程的管理,后者是为了管理线程的生命周期,复用线程,减少创建销毁线程的开销。
AQS
所谓AQS,指的是AbstractQueuedSynchronizer,它提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架,ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等并发类均是基于AQS来实现的,具体用法是通过继承AQS实现其模板方法,然后将子类作为同步组件的内部类。
(头大,先略了)
参考自:
https://www.cnblogs.com/zsql/p/11144688.html
https://www.cnblogs.com/chenzida/articles/9515084.html
https://blog.csdn.net/u013541140/article/details/95225769
https://blog.csdn.net/u010266988/article/details/83960541