Java并发编程_java

线程的一些概念

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

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

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

进程是程序的一次执行过程是系统运行程序的基本单位因此进程是动态的。系统运行一个程序即是一个进程从创建运行到消亡的过程。简单来说一个进程就是一个执行中的程序它在计算机中一个指令接着一个指令地执行着同时每个进程还占有某些系统资源如 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的赋值操作

(3java.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关键字最全总结

(1volatile 变量进行写操作时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 会被回收这是显然的。

2ThreadLocalMap 层面的内存回收

如果线程可以活很长的时间并且该线程保存的线程局部变量有很多(也就是 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 源码总结

通过源代码可以看到每个线程都可以独立修改属于自己的副本而不会互相影响从而隔离了线程和线程。避免了线程访问实例变量发生安全问题。同时我们也能得出下面的结论:
(1ThreadLocal 只是操作 Thread 中的 ThreadLocalMap 对象的集合;
(2ThreadLocalMap 变量属于线程的内部属性不同的线程拥有完全不同的 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个关键字区别

见转载

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Java