小言_互联网的博客

Java并发编程

317人阅读  评论(0)

线程的一些概念

线程,程序、进程的基本概念

线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程是进程划分成的更小的运行单位。

线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。

线程有哪些状态

形成死锁的四个必要条件

说线程安全问题:什么是线程安全,如何实现线程安全

线程安全 - 如果线程执行过程中不会产生共享资源的冲突,则线程安全。

实现线程安全的三种方式:

  1. 互斥同步
    临界区(悲观锁)):syncronized、ReentrantLock
    信号量:semaphore
    互斥量:mutex
  2. 非阻塞同步:CAS(Compare And Swap)

JUC中提供了几个 Automic 类以及每个类上的原子操作就是乐观锁机制。不激烈情况下,性能比synchronized略逊,而激烈的时候,也能维持常态。激烈的时候,Atomic 的性能会优于 ReentrantLock 一倍左右。但是其有一个缺点,就是只能同步一个值,一段代码中只能出现一个 Atomic 的变量,多于一个同步无效。因为他不能在多个 Atomic 之间同步。
非阻塞锁是不可重入的,否则会造成死锁

  1. 无同步方案

可重入代码 使用Threadlocal 类来包装共享变量 或者 volatile 关键字修饰共享变量,做到每个线程有自己的copy 线程本地存储

实现多线程的方法

继承 Thread 类

实现 Runnabel 接口

实现 Callable 接口

线程同步和死锁

手写一段死锁的代码,注意static关键字的用法,保证资源的唯一

生产者和消费者模型-线程通信

代码参考我的另一篇博客

线程同步 和 线程通信 是两个概念,并且 synchronized 只能用于线程同步,不能用于线程通信(体现在代码中的现象是,虽然线程是同步执行的,但是会出现其中一个线程重复拿到时间片的现象)。线程通信 可以结合 synchronized + 信号灯法使用。

面试题: 为什么线程通信方法wait(),notify(),notifyAll()要被定义到Object类中?

Java中任何对象都可以被当作锁对象,调用wait方法,那么线程便会处于该对象的等待池中,调用notify(),notifyAll()方法,用于唤醒线程去获取对象的锁。Java中没有提供任何对象使用的锁,但是任何对象都继承于Object类,所以定义在Object类中最合适。

线程池

线程池的优点

  • 线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。
  • 可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。

线程池的创建

public ThreadPoolExecutor(int corePoolSize,
						  int maximumPoolSize,
						  long keepAliveTime, 
						  TimeUnit unit, 
						  BlockingQueue<Runnable> workQueue,
						  RejectedExecutionHandler handler)

corePoolSize: 线程池核心线程数量

maximumPoolSize: 线程池最大线程数量

核心线程数和最大线程数动画理解

keepAliverTime: 当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间

unit: 存活时间的单位

workQueue: 存放任务的队列

handler: 超出线程范围和队列容量的任务的处理程序

线程池的实现原理
提交一个任务到线程池中,线程池的处理流程如下:

  1. 判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
  2. 线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
  3. 判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

线程池的源码解读:

  1. ThreadPoolExecutor的execute()方法

从结果可以观察出:

  1. 创建的线程池具体配置为:核心线程数量为5个;全部线程数量为10个;工作队列的长度为5。
  2. 我们通过queue.size()的方法来获取工作队列中的任务数。
  3. 运行原理:刚开始都是在创建新的线程,达到核心线程数量5个后,新的任务进来后不再创建新的线程,而是将任
    务加入工作队列,任务队列到达上线5个后,新的任务又会创建新的普通线程,直到达到线程池最大的线
    程数量10个,后面的任务则根据配置的饱和策略来处理。我们这里没有具体配置,使用的是默认的配置
    AbortPolicy:直接抛出异常。

线程池中Callable异常处理分析
见参考

并发编程的3个基本概念

原子性
定义: 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。Java中的原子性操作包括:

(1)基本类型的读取和赋值操作,且赋值必须是值赋给变量,变量之间的相互赋值不是原子性操作。

(2)所有引用reference的赋值操作

(3)java.concurrent.Atomic.* 包中所有类的一切操作

可见性
定义: 指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。

当然,synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

有序性
定义: 即程序执行的顺序按照代码的先后顺序执行。

Java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指 “线程内表现为串行语义”,后半句是指"指令重排序"现象和"工作内存主主内存同步延迟"现象。

在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。Java提供volatile来保证一定的有序性。

补充:指令重排
见转载

重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则:

  1. 重排序操作不会对存在数据依赖关系的操作进行重排序

比如:a=1; b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时不会被重排序这两个操作。

  1. 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变

比如:a=1; b=2; c=a+b; 这三个操作,第一步 a=1; 和第二步 b=2; 由于不存在数据依赖关系, 所以可能会发生重排序,但是 c=a+b 这个操作是不会被重排序的,因为需要保证最终的结果一定是 c=a+b=3

重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果。

下例中的 1 和 2 由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中 a=2 这个操作还未被执行,所以 b=a+1 的结果也有可能依然等于2。

public class TestVolatile {
   
	int a = 1;
	boolean status = false;//状态切换为true 
	public void changeStatus {
    
		a = 2;   					//1
		status = true;  			//2
	}
	//若状态为true,则为running
	public void run() {
   
		if(status) {
      				//3
			int b = a + 1; 			//4
			System.out.println(b);
		}
	}
}

volatile、ThreadLocal的使用场景和原理

volatile 原理

volatile关键字最全总结

(1)volatile 变量进行写操作时,JVM 会向处理器发送一条 Lock 前缀的指令,将这个变量所在缓存行的数据写会到系统内存。
Lock 前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。

(2)它会强制将对缓存的修改操作立即写入主存;

(3)如果是写操作,它会导致其他CPU中对应的缓存行无效

volatile 保证可见性、有序性,不保证原子性, 所以 volatile 不适合复合操作

例如,inc++ 不是一个原子性操作,可以由读取、加、赋值3步组成,所以结果并不能达到30000。


分析:

开启10个线程,每个线程都自加1000次,如果不出现线程安全的问题最终的结果应该就是:10*1000= 10000;可是运行多次都是小于10000的结果,问题在于 volatile 并不能保证原子性,在前面说过 counter++ 这并不是一个原子操作,包含了三个步骤:1.读取变量inc的值;2.对inc加一;3.将新值赋值给变量inc。如果线程 A 读取 inc 到工作内存后,其他线程对这个值已经做了自增操作后,那么线程A的这个值自然而然就是一个过期的值,因此,总结果必然会是小于100000的。

如果让volatile保证原子性,必须符合以下两条规则:

  1. 运算结果并不依赖于变量的当前值,或者能够确保只有一个线程修改变量的值;
  2. 变量不需要与其他的状态变量共同参与不变约束

解决方法:

  1. 采用synchronized
  2. 采用Lock
  3. 采用java并发包中的原子操作类,原子操作类是通过CAS循环的方式来保证其原子性的

volatile的适用场景:

  1. 状态标志,如:初始化或请求停机
  2. 一次性安全发布,如:单列模式
  3. 独立观察,如:定期更新某个值
  4. “volatile bean” 模式
  5. 开销较低的“读-写锁”策略,如:计数器
synchronized 和 volatile 区别
  1. volatile主要应用在多个线程对实例变量更改的场合,刷新主内存共享变量的值从而使得各个线程可以获得最新的值,线程读取变量的值需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。另外,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中(即释放锁前),从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作
  2. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
  3. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞,比如多个线程争抢 synchronized 锁对象时,会出现阻塞。
  4. volatile 仅能实现变量的修改可见性,不能保证原子性;而synchronized 则可以保证变量的修改可见性和原子性,因为线程获得锁才能进入临界区,从而保证临界区中的所有语句全部得到执行。
  5. volatile 标记的变量不会被编译器优化,可以禁止进行指令重排;synchronized标记的变量可以被编译器优化。
ThreadLocal 原理

ThreadLocal 是用来维护本线程的变量的,并不能解决共享变量的并发问题。
ThreadLocal 是各线程将值存入该线程的map中,以 ThreadLocal 自身作为key,需要用时获得的是该线程之前存入的值。如果存入的是共享变量,那取出的也是共享变量,并发问题还是存在的。

ThreadLocal的适用场景:

  1. 数据库连接
  2. Session管理
ThreadLocal 源码分析

参考三太子敖丙——ThreadLocal

Thread 类中维护了成员变量 threadLocals,类型是ThreadLocalMap。而 ThreadLocalMap 又是 ThreadLocal 类中的 内部类。

每一个线程维护自己的 threadLocals 成员变量,及 ThreadLocalMap 类型的 map 集合用来存放 ThreadLocal<?> 类型的对象。

ThreadLocalMap底层结构是怎么样子的呢?
为什么需要数组呢?没有了链表怎么解决Hash冲突呢?
ThreadLocal 存在什么安全问题?

ThreadLocal 涉及到的两个层面的内存自动回收
1)在 ThreadLocal 层面的内存回收

当线程死亡时,那么所有的保存在的线程局部变量就会被回收,其实这里是指线程Thread对象中的 ThreadLocal.ThreadLocalMap threadLocals 会被回收,这是显然的。

2)ThreadLocalMap 层面的内存回收

如果线程可以活很长的时间,并且该线程保存的线程局部变量有很多(也就是 Entry 对象很多),那么就涉及到在线程的生命期内如何回收 ThreadLocalMap 的内存了,不然的话,Entry对象越多,那么ThreadLocalMap 就会越来越大,占用的内存就会越来越多,所以对于已经不需要了的线程局部变量,就应该清理掉其对应的Entry对象。

使用的方式是,Entry对象的 key 是WeakReference 的包装,当ThreadLocalMap 的 private Entry[] table ,已经被占用达到了三分之二时 threshold = 2/3 (也就是线程拥有的局部变量超过了10个) ,就会尝试回收 Entry 对象

if (!cleanSomeSlots(i, sz) && sz >= threshold)
	rehash();

cleanSomeSlots 就是进行回收内存:

ThreadLocal 源码总结

通过源代码可以看到每个线程都可以独立修改属于自己的副本而不会互相影响,从而隔离了线程和线程。避免了线程访问实例变量发生安全问题。同时我们也能得出下面的结论:
(1)ThreadLocal 只是操作 Thread 中的 ThreadLocalMap 对象的集合;
(2)ThreadLocalMap 变量属于线程的内部属性,不同的线程拥有完全不同的 ThreadLocalMap 变量;
(3)线程中的ThreadLocalMap变量的值是在ThreadLocal对象进行set或者get操作时创建的;
(4)使用当前线程的ThreadLocalMap的关键在于使用当前的ThreadLocal的实例作为key来存储value值;
(5) ThreadLocal模式至少从两个方面完成了数据访问隔离,即纵向隔离(线程与线程之间的 ThreadLocalMap不同)和横向隔离(不同的ThreadLocal实例之间的互相隔离);
(6)一个线程中的所有的局部变量其实存储在该线程自己的同一个map属性中;
(7)线程死亡时,线程局部变量会自动回收内存;
(8)线程局部变量时通过一个 Entry 保存在map中,该Entry 的key是一个 WeakReference包装的ThreadLocal, value为线程局部变量,key 到 value 的映射是通过:ThreadLocal.threadLocalHashCode & (INITIAL_CAPACITY - 1) 来完成的;
(9)当线程拥有的局部变量超过了容量的2/3(没有扩大容量时是10个),会涉及到ThreadLocalMap中Entry的回收

对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

synchronized

面试题

补充:volatile、ThreadLocal、synchronized等3个关键字区别

见转载


转载:https://blog.csdn.net/weixin_45005846/article/details/125146685
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场