并发编程-
一、简述
线程:线程是cpu可执行的最小单位,而进程是操作系统可分配的最小资源单位。一个进程中可以有多个线程。
线程的五个状态:
新建(new Thread())
就绪 (thread.start())
运行(cpu开始执行该线程)
阻塞(线程在等待获得锁)
销毁(线程执行完毕或出现异常)。
创建线程:
1.继承Thread类并重写run方法
2.实现runnable接口并重写run方法
3.实现callable接口并重写call方法
call方法和run方法不同之处在于,run方法时无返回值的,call方法有返回值,是个泛型。
4.使用线程池
二、多线程
并发执行:所谓的并发执行就是多个线程交替执行。
并发执行的优点:
1.多个线程共享资源,可以减少内存的加载和释放,从而提高执行效率
2.多个线程因为操作系统的调度可能会被分配到不同的cpu核心上执行,更好的利用了多核处理器的资源,从而提升了程序的可扩展性。
并发执行遇到的问题:
1.不可见性:
由于Java内存模型(JMM)的原因,JMM分为主内存区和本地内存区,每个线程都会有自己的本地内存区。多核处理器允许多个线程并行执行在不同的核心上,如果多个线程同时对主内存区中的数据进行操作,就需要现将其加载到该线程的本地内存区中,当第一个线程对数据修改完写回到本地内存后,第二个线程不知道本地内存中的数据已经被修改,此时就会导致堆数据的操作出现错误。
2.乱序性
为了优化性能,有时候cpu会将后面的指令提前执行,这样指令的运行顺序就被打乱了。
3.非原子性
cpu执行指令,指令是原子性的,但是高级语言的语句却是非原子性的,一条高级语句往往可以拆成多条指令。非原子性就会导致线程交叉操作,使得结果错误。
总结:java缓存模型导致了不可见性。编译器优化导致了乱序性。线程交换导致了非原子性。
4.解决方法:
1.volatile关键字
volatile关键字对共享变量修饰后
1.该变量一旦被线程修改,对其他线程来说是立即可见的。
2.禁止了指令的重排序
3.仍不能解决非原子性
2.如何保证原子性
2.1synchronized锁
synchronized锁是一种独占锁,因此只有持有锁的线程才能被执行,虽然不能阻止线程交换,但是当其他线程想要执行时,会因为没有锁而阻塞,变相的保证了原子性。
2.2原子类
该方式是以volatile+CAS来实现的,volatile保证了主内存数据的可见性,而CAS即比较和交换,在具体实现中,当多个线程对同一数据进行操作,当一个线程加载主内存的数据到对本地内存时,此时会对该数据记录此时的值为预期值,当对数据进行修改后,写回到主内存区之前会对预期值和当前主内存区的值进行比较,如果已被更改就重新进行修改操作。
CAS缺点:CAS使用自旋锁的方式,由于该锁会不断循环判断,因此不会类似synchronize 线程阻塞导致线程切换。但是不断的自旋,会导致CPU的消耗,在并发量大的时候容易导致CPU跑满。
5.Java中的锁分类
1.乐观锁/悲观锁
乐观锁和悲观锁并非是真实存在的锁,而是一种思想。
乐观锁认为多线程开发中不需要加锁,例如使用原子类,采用不加锁的方式解决问题。
悲观锁认为多线程开发中一定要加锁,否则会出现问题。
2.可重入锁
可重入锁又名递归锁,是指在外层方法获得锁后,进入内层方法后会再次获得锁。reentrantlock就是可重入锁。reentrantlock锁可以避免死锁。
3.读写锁
读写锁有以下特点,多个线程同时读取数据时不会互斥,但是一旦有线程进行写操作,就会互斥,阻止该操作。
4.共享锁/独占锁
共享锁是允许多个获得该锁的线程,在不发生写操作的前提下,可以同时对数据进行读取操作。而独占锁一次只允许一个线程进入锁代码块中。
5.分段锁
分段锁也是一种思想,主张将数据分段,在每个分段上都加锁,以提高并发效率。如ConcurrentHashMap,ConcurrentHashMap底层哈希表有16个空间,可以用每一个位置上的第一个节点当做锁,这样可以同时由不同的线程操作不同的位置,只是同一个位置多个线程不能同时操作。
6.自旋锁
自旋锁是指,在线程进行抢锁的过程中,如果没抢到锁会多次进行抢锁操作,实在抢不到锁才会将该线程阻塞。但是自旋过程中不会释放cpu资源,因此比较耗费cpu,但是在低并发情况下会有较高的效率。
7.公平锁/非公平锁
公平锁是指按照请求锁的顺序分配,拥有稳定获得锁的机会。
非公平锁是指不按照请求锁的顺序分配,不一定拥有获得锁的机会。
8.偏向锁/ 轻量级锁/重量级锁
锁的状态:无锁,偏向锁,轻量级锁、重量级锁
无锁状态: 没有任何线程获取锁
偏向锁状态: 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。 降低获取锁的代价。
轻量级锁状态: 当锁状态为偏向锁时, 继续有其他线程过来获取锁,锁状态升级为轻量级锁,线程不会进入到阻塞状态,一直自旋获得锁。
重量级锁状态: 当锁状态为轻量级锁时, 线程数量持续增多,且线程自旋次数到一定数量时,锁状态升级为重量级锁,线程会进入到阻塞状态,等待操作系统调度执行。
6.synchronized锁实现
synchronized锁相当于一个监视器,当线程进入锁修饰的代码块或方法中时,就会自动获得锁,其他线程再访问该方法/代码块时就会阻塞,直到有锁的线程运行完毕或被wait,释放了锁。
1.原子性:synchronized锁不能同时加到多个线程上,因此其保证了操作的原子性。
2.可见性:synchronized锁会在线程释放锁后才会将本地内存中的数据刷新到主内存中。等到其他线程从主内存中读取数据时,已经被最新的值了。
3.有序性:synchronized锁会阻止操作系统对线程中的指令进行重排序,以确保操作的有序性
synchronized控制同步,是依靠底层的指令实现的.
如果是同步方法,在指令中会为方法添加ACC_SYNCHRONIZED标志
如果是同步代码块,在进入到同步代码块时,会执行monitorenter, 离开同步代码块时或者出异常时,执行monitorexit
7.AQS(AbstractQueuedSynchronizer)
抽象同步队列,并发包中很多类的底层都用到了AQS,在该队列中,将线程放到Node类中的thread变量上。
class AbstractQueuedSynchronizer {
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state; //表示有没有线程访问共享数据 默认是0 表示没有线程访问
//修改状态的方法(CAS)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
static final class Node {
volatile Node prev;
volatile Node next;
volatile Thread thread;
}
}
Reentrantlock
Reentrantlock类公平锁和非公平锁都可以实现,可以对资源进行共享同步,和synchronized一样是支持可重入的。其内部有三个类Sync、FairSync、NonfairSync
class ReentrantLock{
abstract static class Sync extends AbstractQueuedSynchronizer {
abstract void lock();
}
//非公平锁
static final class NonfairSync extends Sync {
void lock(){
}
}
//公平锁
static final class FairSync extends Sync {
void lock(){
}
}
}
7.1获取锁的时机(acquire()调用时机)
1.线程被唤醒
2.调用lock方法
3.线程在同步队列中等待,直到被前驱节点唤醒或者轮到自己尝试获取锁。
7.2公平锁和非公平锁
线程进队列时,如果是公平锁,则会默默等待前驱结点被唤醒或轮到自己尝试获取锁。而在非公平锁中,线程进来会调用方法比较当前锁的状态是否为0,如果是为其设置状态为1
在ReentrantLock
中,acquire
方法的作用是尝试获取锁。如果获取锁成功,则当前线程获得锁并继续执行;如果获取锁失败,则当前线程会被加入到同步队列中等待。
8.JUC常用类
8.1ConcurrentHashMap
该类相比HashMap的优点在于,相对于HashMap而言是线程安全的;相对于HashTable是高效的,原因在于,他加synchronized锁方式是分段锁(JDK5-7之前),在JDK8以后是在哈希表中每个位置的头一个元素上加锁,这样如果多个线程如果操作不同的位置,那么相互不影响,只有多个线程操作同一个位置时,才会等待,如果位置上没有任何元素,那么采用cas机制插入数据到对应的位置。
ConcurrentHashMap中的元素键和键值都不可以为null,这是为了防止产生歧义,如
map.put("b","b")
System.out.println(map1.get("a"));//null 值是null 还是键不存在返回null
map.put("a",null)
8.2CopyOnWriteArrayList
CopyOnWriteArrayList 是对写方法加了Reentrantlock锁,他的效率比Vector高,一是因为读取数据没有了锁,多个线程就可同时对数据进行读操作。而写操作不会直接在原数组上修改,是先复制一个数组,然后对复制出来的数组进行修改,最后将底层数组切换为新的数组。
因此CopyOnWriteArrayList写的效率较低,适合高并发读多写少的情况。但是因写方法的特殊性,CopyOnWriteArrayList可以在迭代过程中直接对集合的底层数组进行修改,而不需要使用迭代器的对象对数组修改。普通数组如果在迭代过程中对集合底层数组进行修改,java为了保证迭代操作的一致性和安全性,会禁止该操作,抛出ConcurrentModificationException异常。
9.线程池
在高并发过程中,频繁地创建线程和销毁线程都会大大降低运行的效率,因此在jdk5引入了线程池,一般使用ThreadPoolExecutor来创建线程池。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
9.1线程池中的参数
corePollSize:记录核心线程池的线程最大数量
maximumPollSize:记录线程池中的线程最大数量(包含核心线程池中的线程数)
keepAliveTime:记录非核心线程池中的空闲线程的空闲生命,如果非核心线程池中的线程长时间未被调用,就会被销毁。
unit:时间单位
workQueue:等待队列,当核心线程池中的线程都在被使用时,如果再进来新的任务,就会存放在等待队列中,如果等待队列也满了,就会在非核心线程池中创建新的线程。
threadFactory:线程工厂,负责创建新的线程。
handler:拒绝策略,当核心线程池和非核心线程池中的线程都在使用中,且等待队列也已经满了,此时会执行拒绝策略。
9.2线程池工作流程
当有任务进入线程池,如果核心线程池中有空闲的线程,就交给核心线程池中的线程执行。否则判断等待队列是否有空,如果有则放进等待队列,否则判断非核心线程池是否还有容量,如果有就使用线程工厂创建新的线程来执行该任务,否则执行拒绝策略。
9.3四种拒绝策略
AbortPolicy: 抛异常
CallerRunsPolicy: 将新来的线程交给提交任务的线程去执行
DiscardOldestPolicy: 丢弃等待时间最长的任务
DiscardPolicy: 丢弃最后的任务
9.4提交任务的方法
执行任务除了可以使用execute方法和submit方法。它们的主要区别 是:execute没有返回值,而submit会有返回值。
9.5关闭线程池
shutdown(),执行该方法后,会停止接收新的任务,会将线程池中的所有任务执行完毕再彻底关闭线程池。
shutdownNow(),执行该方法后会立刻关闭线程池,正在执行的任务也会停止。
10.ThreadLocal
ThreadLocal是本地线程变量,他会为每个线程设置一个本地变量,初始值由ThreadLocal构造器生成。本地线程变量只在当前线程中使用。