复习线程基础内容
线程的概念
线程是进程中的一个最小执行单元,是一个独立的任务,是cpu执行的最小单元.
把一些独立的任务放在线程中执行,多个线程可以同时并行的执行,提高了程序效应处理的速度.
创建线程
-
类 继承 Thread类
-
类 实现Runnable接口 重写public void run() { } new Thread(任务)
-
类 实现Callable接口 重写public T call()throws Exception{} 可以抛出异常
-
使用线程池创建
线程方法
run() 执行的任务 start() 启动线程 sleep(时间);让线程休眠 join() ; 线程加入 让其他线程等待当前线程执行完后再执行
currentThread() 获得当前正在执行的线程
线程的状态
创建 new Thread 还不能执行
就绪状态 start() 把线程注册到操作系统
运行状态 获得了cpu的执行
阻塞状态 sleep() join wait 等待同步锁 期间操作系统就不再调用了 等待阻塞动作完成后,再回到就绪状态
死亡状态 任务运行结束,出异常没有处理
多线程访问共享数据
存在资源竟用问题
加锁
使用synchronized关键字修饰代码块和方法
同步锁对象,任意类的对象都可以,但是只能是唯一的一个对象,记录有没有线程进入到同步代码块
synchronized(同步锁对象){ }
synchronized修饰方法时,对象是自动提供的,
synchronized修饰方法时,同步锁对象不需要我们指定 同步锁对象会默认提供: 1.非静态的方法--锁对象默认是this 2.静态方法--锁对象是当前类的Class对象(类的对象,一个类的对象只有一个)
public synchronized void print(){ if(num>0){ System.out.println(Thread.currentThread().getName()+"买到了第"+num+"张票"); num--; } }
ReentrantLock类实现 java类实现
lock()
unlock()
synchronized 和 ReentrantLock区别:
synchronized是一个关键字,控制依靠底层编译后的指令去实现 synchronized可以修饰一个方法,还可以修饰一个代码块 synchronized是隐式的加锁和释放锁,一旦方法或代码块中运行结束或出现异常,会自动释放锁 ReentrantLock是一个类,是依靠java代码去控制(底层有一个同步队列) ReentrantLock只能修饰代码块 ReentrantLock需要手动的加锁,手动的释放锁,所以释放锁最好写在finally中,一旦出现异常,保证锁能释放
线程通信(生产者,消费者模型)
wait() notify() notifyAll() 都只能在同步代码块中使用, 他们是Object类中定义的方法, 调用的对象只能是锁对象
sleep()和wait()区别
sleep(时间) 休眠指定的时间,时间到了后,会自动进入到就绪状态, 休眠期间不会释放锁 是Thread类中的方法
wait() 线程等待,不会自己醒来,需要其他线程唤醒, wait()是会释放锁的 是Object类中的方法
多线程优点
提高程序的响应处理速度,提高cpu的利用率,压榨硬件的剩余价值
问题:
多线程访问同一资源
现在cpu是多内核的,在理论上是可以同时执行多个线程的.
并行执行: 在同一个时间节点上,多个线程同时执行
并发执行: 在一个时间段内,多个线程交替执行 微观上是一个一个的执行, 宏观上感觉是同时执行
并发编程核心问题
不可见性,乱序性,非原子性
也就是多个线程访问共享数据时,出现问题的根本原因.
不可见性
java内存模型 Java Memory Model,JMM
java内存模型是变量数据(票数)都存在主内存中,每个线程还有自己的工作内存(本地内存),
规定线程不能直接对主内存中的数据操作,只能把主内存数据加载到自己的工作内存中操作,
操作完成后,再写回主内存.
这样的设计就会引发不可见性问题. 一个线程在自己的工作内存中操作了数据后,另一个线程也正在操作,但是不知道数据已经被另一个线程修改了,
乱序性 cpu 读指令的同时可以同时执行不影响其他的指令
a = 3
a= 4
c =a +b
为了优化指令执行,在执行一些等待时间长的执行时,可以把其他的一些指令提前执行,提高速度.
但是在多线程场景下,就可能会出现问题
非原子性
线程的切换执行带来非原子性问题
cpu保证的原子性执行时cpu指令级别的,但是对于高级语言的一条代码,有时是要拆分成多条执令的
线程在执行到某条执行时,操作系统会切换到其他线程去执行,这样这条高级语言指令执行就是非原子性的.
例如: ++操作 分成3条指令, 1.加载主内存数据到工作内存,2.在工作内存操作数据,3.写回主内存
总结: 工作内存的缓存导致了不可见性, 指令的优化导致了乱序(无序)性,线程的切换执行导致非原子性
volatile关键字 - 解决不可见性 乱序性
volatile关键字修饰的变量,在一个工作内存中操作后,底层会将工作内存中的数据同步到其他线程的工作内存,使其立即可见. 解决了不可见性的问题
volatile关键字修饰后的变量,在执行时,不会让优化重排序,解决了乱序性.
但是volatile无法解决非原子性问题.
如何保证原子性 ----加锁
解决非原子性问题,可以通过加锁的方式实现,
synchronized 和 ReentrantLock都可以实现.
java中还提供一种方案,在不加锁的情况下,实现++操作的原子性,
就是原子类.AtomicInteger
在java.util.concurrent包下,定义了许多与并发编程相关的处理类, 此包一般大家也简称JUC.
private static AtomicInteger atomicInteger = new AtomicInteger(0); public static void main(String[] args) {for (int i = 0; i < 10; i++) {new Thread() {@Overridepublic void run() {//不加锁的方式,在多线程中实现++操作,满足原子性的System.out.println(atomicInteger.incrementAndGet());}}.start();}
AtomicInteger
实现方式: 采用CAS(比较并交换)思想,当多个线程对同一个内存数据库操作时,
假设A线程把主内存数据加载到自己工作内存中,这个工作内存中的值就是预期值,
然后在自己的工作内存中操作后,当写回主内存时,先判断预期值和主内存的值是否一致,如果一致,说明还没有其他线程修改,直接写回主内存,
一旦预期值和主内存中的值不一样,说明有其他线程已经修改过了,线程A需要重新获取主内存中值,重新操作,判断.
直到预期值和主内存值相同,才结束,否则自旋一直判断.
由于采用自旋方式实现,使得线程都不会阻塞,一直自旋,适合并发量低的情况.
如果并发了过大,线程一直自旋,会导致cpu开销大.
还会有一个ABA问题: 线程A拿到主内存值后,期间有其他线程已经多次修改内存数据,最终又修改的和线程A拿到值相同,
可以通过带版本号的原子类,每次操作时改变版本号即可.
private AtomicStampedReference atomicStampedReference= new AtomicStampedReference(100,0);//预期值和版本号解决aba问题
A->B->A
A C
锁分类
锁分类,不全是指java中的锁,有的指锁的特征,有的指锁的实现,有的指锁的状态
乐观锁,悲观锁
乐观锁: 是一种不加锁的实现,例如原子类, 认为不加锁,采用自旋方式尝试修改共享数据,是不会有问题的.
悲观锁: 是一种加锁实现,例如synchronized 和 ReentrantLock, 认为不加锁修改共享数据会出问题
可重入锁
可重入锁又名递归锁, 当同一个线程,获取锁进入到完成方法后,可以在内存进入到另一个方法(内存方法与外层方法使用的是同一把锁)
synchronized 和 ReentrantLock都是可以重入的锁
读写锁
ReentrantReadWriteLock 读写锁实现
ReentrantReadWriteLock.WriteLock ReentrantReadWriteLock.ReadLock读读不互斥 读写互斥 写写互斥 只要有写操作,其他线程就不能写,也不能读,保证读不到脏数据 最大的保证读的效率
分段锁
将锁的粒度进一步细化,提高并发效率
Hashtable是线程安全的,方法上都加了锁 假如有两个线程同时读,也只能一个一个的读,并发效率低
ConcurrentHashMap没有给方法上加锁,使用hash表中的每个位置上的第一个对象作为锁对象,这样就可以多个线程对不同的位置进行操作,相互不影响,只有对同一个位置操作时,才互斥.
有多把锁,提高并发操作的效率
自旋锁
线程尝试不断的获取锁,当第一次获取不到时,线程不阻塞,尝试继续获取锁,有可能后面几次尝试后,有其他线程释放了锁,此时就可以获取锁, 当尝试获取到一定次数后(默认10次),任然获取不到锁,那么可以进入阻塞状态.
synchronized 就是一种自旋锁
并发量的低的情况下适合自旋
共享锁/独占锁
共享锁: 锁可以内多个线程共享, 读写锁中读锁就是共享锁
独占锁: 一把锁只能有一个线程使用,读写锁的写锁,synchronized 和 ReentrantLock都是独占锁.
公平锁/非公平锁
公平锁: 可以按照请求获得锁的顺序来得到锁
非公平锁:不按照请求获得锁的顺序得到锁
synchronized 是非公平
ReentrantLock默认是非公平
public ReentrantLock() {sync = new NonfairSync(); }
可以通过构造方法参数设置选择公平实现或非公平实现
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync(); }
锁状态
java中为了synchronized 进行优化,提供了3种锁状态,
偏向锁: 一段同步代码块一直由一个线程执行,那么会在锁对象中记录下了线程信息,可以直接获得锁.
轻量级锁: 当锁状态为偏向锁时,此时又有其他线程访问,锁状态升级为轻量级锁,线程不阻塞,采用自旋方式获取锁.
重量级锁: 当锁状态为轻量级锁时,如果有大量的线程到来,大量的线程自旋,锁状态升级为重量级锁,自旋的线程会进入到阻塞状态,由操作系统去调度管理.
synchronized
是一个关键字,实现同步,还需要我们提供一个同步锁对象,记录锁状态,记录线程信息
控制同步,是依靠底层的指令实现的.
如果是同步方法,在指令中会为方法添加ACC_SYNCHRONIZED标志
如果是同步代码块,在进入到同步代码块时,会执行monitorenter, 离开同步代码块时或者出异常时,执行monitorexit
AQS
AQS(AbstractQueuedSynchronizer 抽象同步队列) 是一个实现线程同步的框架
并发包中很多类的底层都用到了AQS
链表 状态,改变状态的方法
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实现
三个内部类 公平非公平 都继承sync sync继承aqs
class ReentrantLock{abstract static class Sync extends AbstractQueuedSynchronizer {abstract void lock();}//非公平锁static final class NonfairSync extends Sync {void lock(){}}//公平锁static final class FairSync extends Syn c {void lock(){}}}
公平和非公平区别
JUC 常用类
在集合类中,像Vector,Hashtable这些类加锁时都是直接把锁加载方法上了,性能就低, 在并发访问量小的情况下,还可以使用, 大并发访问量下,性能就太低了.
ConcurrentHashMap
hashmap 双列集合键不能重复 ,如何判断键不能重复 ,hashcode 和equals 先用hashcode方法判断不同肯定不同,然后相同用equals方法 HashMap适合单线程场景下的,不允许多个线程同时访问操作,如果有多线程访问会报异常Hashtable 是线程安全的 直接给方法加锁,效率低ConcurrentHashMap 是线程安全的,没有直接给方法加锁, 用哈希表中每一个位置上的第一个元素(第一个是存在元素)作为锁对象哈希表长度是16,那么就有16把锁对象,锁住自己的位置即可,这样如果多个线程如果操作不同的位置,那么相互不影响,只有多个线程操作同一个位置时,才会等待如果位置上没有任何元素,那么采用cas机制插入数据到对应的位置Hashtable ,ConcurrentHashMap 键值都不能为null为什么这样设计,键值都不能为null?map.put("b","b")为了消除歧义 System.out.println(map1.get("a"));//null 值是null 还是键不存在返回null
CopyOnWriteArrayList 写少读多
ArrayList 是单线程场景下使用的,在多线程场景下会报异常 Vector 是线程安全的,在方法上加了锁,效率低 CopyOnWriteArrayList 写方法操作加了锁(ReentrantLock实现的), 在写入数据时,先把原数组做了备份,把要添加的数据写入到备份数组中,当写入完成后,再把修改的数组赋值到原数组中去 给写加了锁,读没有加锁,读的效率变高了, 这种适合写操作少,读操作多的场景
CopyOnWriteArraySet
CopyOnWriteArraySet 的实现基于 CopyOnWriteArrayList,不能存储重复数据。
辅助类 CountDownLatch
.countDown(); 计数器的一种操作 让前面的线程先执行,然后执行后面的线程 底层也是 aqs同步队列
池的概念 缓冲
字符串常量池
String s1 = "abc"; String s2="abc"; s1==s2//true
Integer自动装箱 缓存了-128 --+127之间的对象
Integer a = 100; Integer b = 100; a==b //true IntegerCache.cache[i + (-IntegerCache.low)];
数据库连接池
阿里巴巴Druid数据库连接池
帮我们缓存一定数量的链接对象,放在池子里,用完还回到池子中,
减少了对象的频繁创建和销毁的时间开销
线程池
为减少频繁的创建和销毁线程,
jdk5开始引入了线程池,
建议使用ThreadPoolExecutor类来创建线程池, 这样提高效率.
Java.uitl.concurrent.ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
7个参数
corePoolSize: 核心线程池中的数量(初始化的数量) 5
maximumPoolSize:线程池中最大的数量 10 5
keepAliveTime: 空闲线程存活时间 当核心线程池中的线程足以应付任务时, 非核心线程池中的线程在指定空闲时间到期后,会销毁.
unit: 时间单位
workQueue: 5 等待队列, 当核心线程池中的线程都在使用时,如果有任务继续到来,会先将等待的任务放到队列中,如果队列也满了,才会创建新的线程(非核心线程池中的线程)
threadFactory:线程工厂,用来创建线程池中的线程
handler:拒绝处理任务时的策略 4种拒绝策略
线程池工作流程
当有大量的任务到来时,先判断核心线程池中的线程是否都忙着,
有空闲的,直接让核心线程中的线程执行任务没有空闲的, 判断等待队列是否已满,如果没满,把任务添加到队列等待如果已满,判断非核心线程池中的线程是否都忙着如果有空闲的,没满,交由非核心线程池中的线程执行如果非核心线程池野已经满了,那么就使用对应的拒绝策略处理.
4种拒绝策略:
AbortPolicy: 抛异常
CallerRunsPolicy: 由提交任务的线程执行 例如在main线程提交,则由main线程执行拒绝的任务 DiscardOldestPolicy: 丢弃等待时间最长的任务
DiscardPolicy: 丢弃最后不能执行的任务
提交任务的方法
void execute(任务); 提交任务没有返回值Future<?> submit = executor.submit(myTask);//提交任务可以接收返回值 submit.get();
关闭线程池
shutdown(); //执行shutdown()后,不再接收新的任务,会把线程池中还有等待队列中已有的任务执行完,再停止
shutdownNow(); //立即停止,队列中等待的任务就不再执行了.
ThreadLocal
是什么
本地线程变量,为每个线程提供一个变量副本,只在当前线程中使用,相互是隔离的
底层构造
为每个线程对象创建ThreadLocalMap对象,赋给Thread类中threadLocals,用threadLocals存储每个线程的变量副本
内存泄漏
对象已经不用了,但是垃圾回收不能回收该对象.(例如数据库连接对象,流对象,socket....)
对象引用分为四种:
强引用
Object obj = new Object(); 强引用
obj.hashCode();
obj=null; 没有引用指向对象
对象如果有强引用关联,那么肯定是不能被回收的
软引用
被SoftReference类包裹的对象, 当内存充足时,不会被回收,当内存不足时,即使有引用指向,也会被回收
Object o1 = new Object();SoftReference<Object> softReference = new SoftReference<Object>(o1);
弱引用
被WeakReference类包裹的对象,只要发送垃圾回收,该类对象都会被回收掉,不管内存是否充足
Object o1 = new Object();WeakReference<Object> weakReference = new WeakReference<Object>(o1);
ThreadLocal 被弱引用管理static class Entry extends WeakReference<ThreadLocal<?>> {}
当发生垃圾回收时,被回收掉,但是value还与外界保持引用关系,不能被回收. 造成内存泄漏
threadLocal.remove();//不再使用时,调用remove方法,删除键值对,可以避免内存泄漏问题
虚引用
被PhantomReference类包裹的对象,随时都可以被回收,
通过虚引用对象跟踪对象回收的状态
深层拷贝的概念
深层拷贝指的是创建一个新对象,新对象和原对象不但拥有不同的内存地址,而且它们内部所包含的所有引用类型的成员变量也会被递归地复制,这就保证了新对象和原对象在任何层次上都是相互独立的,对其中一个对象的修改不会影响到另一个对象。
序列化与反序列化的原理
-
序列化:把对象的状态转换为字节序列,这些字节序列包含了对象的所有信息,像对象的类名、成员变量的值等。这个字节序列能够存储在文件中,也可以通过网络传输。
-
反序列化:把字节序列再转换回对象。在反序列化的过程中,Java 会依据字节序列里的信息创建一个新的对象,并且把字节序列中的值赋给新对象的成员变量。
序列化实现深层拷贝的原因
-
创建新对象:反序列化时会创建一个全新的对象,这个对象和原对象在内存中的地址是不同的,这就保证了两者在最外层是相互独立的。
-
递归复制引用类型成员变量:序列化过程会递归地处理对象的所有成员变量,包括引用类型的成员变量。当对引用类型的成员变量进行序列化时,会把其内部的状态也转换为字节序列;在反序列化时,会依据这些字节序列创建新的对象,并将其赋值给新对象的对应成员变量。这样一来,新对象和原对象内部的引用类型成员变量也是相互独立的