文章目录
一、并发编程
-
线程、进程
线程:是进程的一个执行单元,是CPU执行的最小单位;
进程:运行中的程序是计算机分配内存资源的最小单位。 -
创建线程方式
- 类 继承 Thread 重写run()
- 实现Runnable接口 重写run() ,再创建Thread对象,去执行任务
- 实现Callable接口,重写call()(可以有返回值,可以抛异常),再创建Thread对象,去执行任务
- 线程池
-
线程常用方法
run() yield() wait()
start() sleep() notify()
join() -
线程的状态
新建、就绪、运行、阻塞、死亡 -
多线程的安全问题
多线程不一定有问题,共享操作是有问题的 -
解决方案
加锁;排队;并发执行;一个一个执行 -
加锁方式
synchronized关键字+同步对象(对多个线程来说是同一个对象):
可以添加在代码上synchronized(同步锁/同步对象){ }
还可以加在方法上, 同步不需要我们提供了,静态方法,锁是类的Class对象,非静态方法,锁是this。
ReentrantLock实现了Lock接口,所以可以称为lock锁,
实现原理不同:
ReentrantLock是一种java代码层面的控制实现 ,而synchronized是关键字,依靠的是底层编译后的指令实现
加锁范围不同:
ReentrantLock只能对某一段代码块加锁,而synchronized可以对代码块和方法加锁
加锁释放锁方式不同:
ReentrantLock需要手动的加锁释放锁,而synchronized是隐式的自动的加锁,自动释放锁(代码执行完了,出现异常了) -
线程死锁
指俩个或多个线程,分别持有对方需要的资源,相互僵持等待。
死锁条件:- 互斥:在一段时间内某一资源仅为一个进程占用
- 请求和保持:当进程因请求另一资源而阻塞时,对已获得的资源保持不放
- 不可抢占:进程已获得的资源在未使用完之前,不可被抢占,只能在使用完时由自己释放
- 循环等待:存在一个进程请求资源的循环链
措施: - 预防死锁:设置某些限制条件破坏死锁的几个必要条件
- 避免死锁:在资源的动态分配中,用某种方法防止系统进入不安全状态
- 检测死锁: 允许程序出现死锁,出现后及时检测然后采取措施,将进程从死锁中解脱出来
- 解除死锁: 当检测到死锁发生时,可以撤销一些进程,回收它们的资源.
-
线程通信
同步情况下 、notify()、wait()
二、线程进阶
围绕着线程安全问题,用户在手机上买票,抢购,秒杀
a.多线程
优点:提高程序的响应速度,可以多个线程完成自己的工作,提高硬件设备的利用率
缺点:可能会出现资源争夺问题
并发执行:在一段时间内,多个线程依次执行
并行执行:真正意义上的同时执行,俩个线程在同一个时间点上一起执行
高并发指的是很多用户一起访问
b.并发编程核心问题
不可见性:一个线程对共享变量的修改,另外一个线程不能够立刻看到
由于想让程序响应处理速度更快, java内存模型设计有主内存和工作内存(线程使用的内存)。线程中不能直接对主内存中的数据进行操作,必须将主内存数据加载到工作内存(本地内存), 这样在多核cpu下就会产生不可见性。
乱序性:指令在执行过程中,为了优化性能,可能将一些语句的顺序改变。
volatile
可以解决不可见性, volatile修饰的变量在一个线程修改后,对其他线程立即可见,还可以解决乱序性, volatile修饰的变量在执行时禁止指令重排序,但是不能解决非原子性问题。
非原子性:线程切换带来的非原子性问题,A先执行被B插了一脚。
原子性:一个或多个操作在 CPU 执行的过程中不被中断的特性
措施:
- 加锁:互斥的,A线程执行时加锁,此时其他线程就不能执行了。
- 原子变量(解决++之类的问题)
在java.util.concurrent包下面提供一些类,可以在不加锁的情况下,实现++操作的原子性,这些类称为原子类AtomicInteger,原子类内部实现是 volatile+CAS机制
CAS机制:比较与交换,乐观锁(不加锁)实现,采用自旋思想(一个循环中不断地尝试更新操作,直到成功或者放弃)
包含三个操作数:内存值、预估值、更新值,判断预估值和主内存中的值是否一致,如果一致说明没有其他线程修改过, 如果不一致说明其他线程修改过。重新获取主内存的共享变量,重复操作。
CAS缺点:原子类内部实现使用了不加锁的CAS机制,线程不会被阻塞,所有的线程都会不断的重试进行操作,在访问量大的情况下,会导致cpu消耗过高,原子类适合在低并发情况下使用。
ABA问题:一个线程读取一个共享变量的值为A,然后另一个线程将该值修改为B,再改回A。此时,线程再次尝试进行CAS操作时,会发现该变量的值仍然是A,因此它会认为该值没有被修改过。
解决ABA问题:使用有版本号的原子类
c.Java中的锁分类
- 乐观锁/悲观锁
乐观锁:不加锁,乐观锁认为不加锁的并发操作是没有问题的,并发修改时进行比较,满足条件进行更新,否则再次进行比较,例如:原子类
悲观锁:悲观锁认为不加锁的是肯定会出问题的,使用java中提供的各种锁实现加锁。
悲观锁适合写操作比较多的情况,乐观锁则适合读多写少的情况。 - 可重入锁
当一个线程进入到一个同步方法中,然后在此方法中要调用另一个同步方法,而且两个方法共用同一把锁,此时线程是可以进入到另一个同步方法中的。 - 读写锁(ReentrantReadWriteLock)
读读不互斥(共享),读写互斥,写写互斥 - 分段锁
用于将数据分段,并在每一个分段上单独加锁,提高并发效率。
jdk8之后,去除了真正的分段锁,现在的分段锁不是锁,是一种实现的思想。将锁的粒度更小化。
例如ConcurrentHashMap 没有给方法加锁,用hash表中的第一个节点当做锁的,这样就可以有多把锁,提高了并发操作效率 - 自旋锁
也不是锁,是获取锁的方式。
如: 原子类,,需要改变内存中的变量,需要不断尝试,synchronized 加锁 其他线程不断尝试获取锁 - 共享锁/独占锁
共享锁: 多个线程共享一把锁, 读写锁中的读锁,都是读操作时,多个线程可以共用
独占锁: synchronized ReentrantLock 互斥的 读写锁的写锁 - 公平锁/非公平锁
公平锁:按照请求锁的顺序分配 谁先来,先获得锁
ReentrantLock底层可以设置为公平锁,也可以设置为非公平锁,默认非公平锁
非公平锁:不按照请求锁的顺序分配,谁抢到 谁获得
如:synchronized
偏向锁/轻量级锁/重量级锁
针对synchronized锁的状态,优化synchronized锁- 无锁 : 没有任何线程使用的锁对象
- 偏向锁 : 就是当前只有一个线程访问,在对象头(Mark Word)中记录线程id,下次此线程访问时,可以直接获取锁,偏向于这把锁
- 轻量级锁 : 当锁的状态为偏向锁时,此时继续有线程来访问,升级为轻量级锁,会让线程以自旋的方式获取锁,先程不会阻塞
- 重量级锁 : 当锁的状态为轻量级锁时,线程自旋获取锁的次数到达一定数量时,锁的状态升级为重量级锁,会让自旋次数多的线程进入到阻塞状态,因为访问大时,线程都自旋获得锁,cpu消耗大。
对象头:
mark word
记录对象的一些相关信息:
hash值,分代年龄(4个bit记录),锁的状态,偏向锁,线程id…
synchronized实现:
通过底层指令控制实现
synchronized修饰方法
底层指令会添加ACC_SYNCHRONIZED
进入方法时使用monitorenter检测,执行完毕使用monitorexit释放锁
synchronized修饰代码块
进入代码块时使用monitorenter检测,执行完毕使用monitorexit释放锁
d.ReentrantLock锁实现
AQS:抽象同步队列是一个底层具体的同步实现者。
AbstractQueuedSynchronizer类中有一个int类型state变量,记录锁的状态
volatile int state; //标记有没有线程在访问共享资源
内部类
static final class Node{
volatile Node prev;
volatile Node next;
volatile Thread thread;
}
protected final int getState() {
return state;
}
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
ReentrantLock源码:
abstract static class Sync extends AbstractQueuedSynchronizer {
lock();
unlock();
}
static final class NonfairSync extends Sync {
实现加锁
final void lock() {
if (compareAndSetState(0, 1)) //先尝试获取锁,修改锁的状态
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
}
static final class FairSync extends Sync {
final void lock() {
acquire(1); //走正常流程获取锁, 如果当前锁状态为0,获取锁,状态改为1
如果当前锁状态为1,把线程放入到队列
}
}
无参构造方法
public ReentrantLock() {
sync = new NonfairSync(); //使用的是非公平锁
}
有参构造方法
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();//FairSync公平锁,NonfairSync非公平锁
}
e.ConcurrentHashMap
- HashMap:键值对 双链集合 键是不重复的,值可以重复,多线程不安全,单线程安全
- HashTable:是线程安全的,因为底层方法加了synchronized
- ConcurrentHashMap:是线程安全的,采用css+synchronized保证安全, 底层方法没直接加synchronized, put时,先用key计算hash值,在计算位置,如果位置上没有元素(null), 采用cas机制尝试去放入,如果此位置上已经有元素了,那么使用第一元素作为锁对象,使用synchronized加锁,这样就会降低锁的粒度,可以同时有多个方法进入到put中操作,提高并发的效率,如果有多个线程是对同一个位置操作,那么就必须一个一个操作。
Hashtable和ConcurrentHashMap不支持存储为null的键和为null的值
f.CopyOnWriteArrayList
List
- ArrayList 单列集合 底层是数组实现 可以存储重复元素,是有序(添加顺序),可以自动扩容 默认是10个长度 扩容为原来的1.5倍,是线程不安全的
- Vector 单列集合 底层是数组实现 可以存储重复元素, 是有序(添加顺序),可以自动扩容 默认是10个长度 扩容是2倍,是线程安全的 锁是添加到方法上的,并发效率低,读取数据时也进行加锁,是一种资源的浪费,读是不改变数据的。
- CopyOnWriteArrayList:添加,修改方法会加锁,每次改变值时会复制一个新数组,在新数组上进行修改,如果此时有读操作,可以从原数组读取,修改完成后,将新复制的数组 重新赋给原数组;
读的方法不用加锁,提高了读的效率,适合读操作多 写操作少的情况
是线程安全的 不存储重复元素,底层使用CopyOnWriteArrayList,添加元素时判断元素是否存在
g.CountDownLatch
允许一个线程等待其他线程执行完毕后再执行,创建CountDown Latch对象时指定一个初始值是线程数量,执行完一个,AQS内部state-1,当state为0表示所有线程执行完毕,然后等待的线程就可以工作了
三、线程池
池(容器 集合 ArrayList): 每次连接数据库都要创建一个连接对象,用完之后就销毁了,频繁创建和销毁占用一定的开销。 可以创建出一定数量的连接对象放到池子中, 有连接到来时,从池子中获得一个连接对象使用,用完之后不销毁,还回到池子中即可,减少创建销毁的开销。如:数据库的连接池
线程池:频繁的创建线程,销毁线程也是需要开销的。
有两个类实现线程池:
ThreadPoolExecutor
Executors
其中阿里巴巴开发规约中规定使用ThreadPoolExecutor实现
ThreadPoolExecutor中可以准确的控制创建的数量,最大等待数量,拒绝策略等
ThreadPoolExecutor构造方法中的7个参数:
- corePoolSize 核心池子的数量(大小),默认是先不创建线程,有任务到达后,再创建,之后就不销毁了
- maximumPoolSize 10 最大池子数量
- keepAliveTime 非核心线程池中的线程,在一定时间内没有任务执行时,销毁非核心线程池中的线程
- TimeUnit 时间单位
- workQueue 等待队列 设置队列数量 20
- threadFactory 创建线程工厂
- handler 拒绝策略
核心线程池满了,再看队列满没满,再看非核心线程池满没满,再看线程池满没满。
拒绝策略:请求任务不断过来,此时系统又处理不过来就采取相对应的策略拒绝服务 - AbortPolicy策略:直接抛出异常,阻止系统正常工作(用的不多)
- CallerRunsPolicy:由提交任务的线程执行, 例如我们的main线程
- DiscardOldestPolicy: 丢弃队列中等待时间最长的任务
- DiscardPolicy: 直接丢弃任务 不予理睬
向线程池提交任务有两种方式:
execute : 提交实现了Runnable接口的任务 没有返回值
submit : 提交实现了Callable接口 和Runnable接口 的任务 可以接收返回值
关闭线程池:
shutdown: 关闭线程池时,先不会再接收新的任务, 会等待所有任务执行完成
shutdownNow: 会终止正在执行的任务 返回还未执行的任务列表
ThreadLocal
//创建一个ThreadLocal对象,复制保用来为每个线程会存一份变量,实现线程封闭
private static ThreadLocal<Integer> localNum = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 0;
}
};
ThreadLocal是用来为每个线程提供一个变量副本,每个线程中的变量是相互隔离的,所以称为本地线程变量
ThredLocal底层结构:
ThreadLocal内部维护一个ThreadLocalMap的内部类
为每个线程创建一个ThreadLocalMap对象,用来保存此线程拥有的变量,在每一个线程中的ThreadLocal对象中,用当前操作的ThreadLocal对象作为键
每次为线程赋值时,首先要获取当前线程对象,拿到当前线程对象中的ThreadLocalMap
ThreadLocal内存泄漏问题,什么原因造成内存泄漏?
当本地变量不在线程中继续使用时,但是value值还与外界保持引用关系,这样一来,垃圾回收器就不能回收,ThreadLoaclMap对象,会造成内存泄漏问题
解决办法?
用完之后删除 threadLocal.remove();下次垃圾回收时,就可以回收ThreadLoaclMap了。