并发编程
一.线程复习
1.什么是线程,进程
进程是操作系统分配资源的基本单位
线程是进程中的一个执行单元(一个独立执行的任务),是cpu执行的最小单元
2.Java中如何创建线程
1.继承Thread类,重写run(),直接创建子类的对象
2.类实现Runnable接口,重写run(), 任务类 new Thread(任务类对象)
3.类实现Callable接口
4.线程池
3.线程中常用的方法
run() call()
Thread类中的方法:
start();启动线程的,把线程注册到操作系统private native void start0();
sleep();让线程休眠指定的时间
join();让其他线程等待当前线程执行完成后再执行
yeild();线程礼让
setDaemon();设置线程为守护线程
currentThread();获得当前正在执行的线程
wait();让线程等待,只能被其他线程唤醒
notify();notifyAll();唤醒被wait等待的线程
4.线程的状态
5.多线程
(1)什么是多线程
在程序中(进程)可以创建多个线程来分别执行不同的任务
(2)多线程优缺点
优点:可以提高程序执行效率
缺点:线程多了,需要操作系统进行管理的,占用开销的(不是啥事情都创建线程执行的.)
多个线程同时访问共享资源(数据)会出现问题
(3)线程同步
加锁排队 (饭堂买饭 排队+加锁 一次只能有一个买饭)
synchronized关键字:
synchronized修饰方法 非静态方法锁对象是this 静态方法是类的Class对象
synchronized修饰代码块
ReentrantLock
public class ReentrantLockDemo implements Runnable{int num = 10;/*ReentrantLock是java.util.concurrent.locks包下的类,是java代码实现的一种锁控制只能手动的加锁和手动的释放锁只能对某段代码块加锁,不能给整个方法加法*/ReentrantLock reentrantLock = new ReentrantLock();@Overridepublic void run() {while (true){try { reentrantLock.lock();//加锁 if (num > 0) {System.out.println(Thread.currentThread().getName() + "买到第" + num + "张票");num--;} else {break;}}finally {reentrantLock.unlock();//释放锁}}}
}
(4)死锁
多个线程相互持有对方需要的锁对象不释放,二形成的一种相互等待获取锁的现象,
死锁发生后,程序不会报错,只能相互等待,不继续向后执行了.
如何避免死锁发生:
避免锁的嵌套使用
避免多个同步代码块中的锁相互使用
口语描述死锁:
线上1和线程2 同时访问两个代码块, 线程1访问的同步代码块使用A锁,在A锁的同步代码块又使
用了B锁.
线程2在访问的同步代码块中使用B锁,在B锁的同步代码块中又用到了A锁,有可能形成死锁.
(5)线程通信(生产者,消费者模型)
在线程同步的基础上进行的,两个线程相互牵制执行
wait(); 线程等待
notify(); 唤醒等待的线程
只能在同步代码块(同步方法)中使用, 还只能通过同步锁对象调用
案例:交替打印数字
public class PrintNum implements Runnable{int num = 0;Object obj = new Object();@Overridepublic void run() {while (true){synchronized(obj){obj.notify();System.out.println(Thread.currentThread().getName()+":"+num++);try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}try {obj.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}}}
}
wait()和sleep()区别:
所属的类不同: wait()属于Object类 sleep()属于Thread类
阻塞后唤醒方式不同: wait()让线程等待后,需要让另一个线程通过notify()唤醒,如果不唤醒一直
等待
sleep()休眠执定的时间,时间到了后,自动唤醒
释放锁不同: sleep()不会释放锁
wait()会释放锁
二.线程进阶---并发编程
后面谈论的线程内容,几乎都是在多线程对同一共享数据操作的基础上进行.
1.多线程优缺点
优点: 可以提高程序执行效率
缺点: 线程多了,需要操作系统进行管理的,占用开销的(不是啥事情都创建线程执行的.)
多个线程同时访问共享资源(数据)会出现问题
并发(高并发): 计算机领域指的是同一时刻只能有一个任务执行,多个任务依次执行
汉语并发指的是同时执行
并发执行: 在很多用户同时访问时,应当让多个用户并发执行(一个时间段内依次执行)
并行执行: 在同一个时间点上,多个任务同时执行
2.并发编程核心问题
多个线程同时对同一个资源访问的情况下. 为什么会出现问题.
(1)不可见性
首先了解java程序运行的内存模型(JMM)
java在线程操作时,先把主内存中的数据加载到线程的工作内存中,然后对数据进行操作, 但是
线程A在自己的工作内存中操作玩出,线程B不知道,做了同样的操作,最终结果和预期结果不一样
(2)乱序性(无序性)重排序
系统为了优化,在执行某些指令时,会将一些看起来没关系的指令执行顺序改变,但是在,某些情况下会
产生问题.
1 int a = 10;
2 int b = 从数据库读取
3 int c = a+b;
(3)非原子性
i++;在多线程情况下时线程安全的吗?
i++高级语言,在底层执行时,会被分为3条件
加载i
i+1
i=2
线程的切换执行会带来原子性问题
java内存模型为每个线程提供工作内存(缓存),导致不可见性问题
系统指令优化会把一些指令顺序重排序(在执行一些较为耗时的指令时,把一些其他指令先行执行),
可能导致程序无法正常执行
线程切换执行会打破指令执行的原子性 ,例如++操作 分为三条指令, 但是在执行这3条指令时,cpu可
能在执行时,切换到其他线程执行,打破原子性执行.
(4)如何解决以上3个问题:
volatile
volatile关键可以解决不可见性和乱序性
**volatile**修饰的共享变量,在被一个线程操作后,可以做到立即对其他线程可见 (volatile底层
实现内存屏障)
volatile修饰的变量禁止对其的执行顺序重排序
volatile只能解决不可见性和重排序问题, 不能解决非原子性问题
(5)如何解决非原子性问题:
1.加锁(ReentrantLock和synchronized)
2.使用原子类来解决++在多线程中非原子性问题
AtomicInteger 是一个原子类,能够在不加锁的情况下,实现多线程++操作不出问题,结果正确
private static AtomicInteger atomicInteger = new AtomicInteger(0);atomicInteger.incrementAndGet(); 自增并获得
CAS 机制
CAS(Compare-And-Swap) :比较并交换
特点: 不加锁实现对变量++操作保证原子性.
优点: 线程不会进入到阻塞状态,一直自旋,效率高
缺点: 线程一直自旋,对cpu开销大, 所以原子类适用于线程少的情况
3.Java中的锁分类
(1)乐观锁/悲观锁
乐观锁: 就是没有加锁的实现. AtomicInteger中的实现就是不加锁的,通过自旋比较实现(CAS)
悲观锁: 就是加锁的实现,认为不加锁是会出问题的 ,ReentrantLock和synchronized都是悲观锁
(2)可重入锁
ReentrantLock和synchronized都是可重入锁
可重入锁又名递归锁, 指的是一个线程在外层方法获得锁时,可以直接进入到内层的加锁的方法中.
(3)自旋锁
指的是对synchronized获得锁的一种描述(特点), 线程在获得锁时,是自旋的不断尝试去获得锁
(4)公平锁/非公平锁
公平锁: 就是排队获得锁,有先来后到 ReentrantLock 既可以是公平锁也可以是非公平锁
非公平锁: 就是抢锁,谁抢到谁执行, 有可能后来的线程先抢到锁 synchronized ReentrantLock
(5)读写锁
ReentrantReadWriteLock
特点: 读读不互斥, 读写互斥, 写写互斥
适合读(查询)多,写少的场景, 提高读的效率
(6)共享锁和独占锁
独占锁: synchronized ReentrantLock都是独占锁,就是有我没他,一次只能有一个线程执行.
共享锁: 一个锁可以被多个线程持有, 读写锁中的读锁就是共享锁
4.synchronized锁的实现
jdk1.7之后,对synchronized锁进行了优化(jdk7之前synchronized锁没有状态,都是自旋的获取锁),
jdk7之后为synchronized锁设计了不同的状态.
无锁状态: 没有线程进入到同步代码块就是无锁状态
偏向锁状态: 只有一个线程访问同步代码块时,同步锁中记录线程id,下次线程访问时,可以快速的获
得锁.
轻量级锁状态: 当线程数量大于1个之后,锁状态由偏向锁升级为轻量级锁, 线程不会阻塞,以自旋方
式获得锁,提高获取锁的效率.
重量级锁状态: 当锁状态为轻量锁时,如果线程自旋到一定次数还获取不到锁,那么锁会升级为重量级
锁,让获取不到锁的线程进入到阻塞状态,等待操作系统调度.
使用synchronized锁的时候,必须为锁提供一个同步锁对象的,此对象就是用来记录锁状态的
对象中有一个区域叫对象头,对象头中中有一块区域叫mark word,记录对象运行时的一些数据,
如锁状态,哈希值,GC分代年龄,当前线程id.
synchronized时java中内置的一种的锁,底层实现是靠底层指令进行控制的,
使用时必须提供一个同步锁对象,用来记录锁的各种状态.
5.AQS
AbstractQueuedSynchronizer 抽象同步队列, 并发包下面很多类的底层实现都会用到
内部有一个int类的变量state,用来记录有没有线程使用
内部会构建一个队列,用来存储没有获得锁的线程
6.ReentrantLock锁实现
ReentrantLock 基于 AQS的,
ReentrantLock 可以实现公平锁和非公平锁
内部结构
公平和非公平的区别
三.Java集合类复习
javaSE中3个重点: 面向对象相关知识
集合类 (数据结构,线程)
多线程
次重点: 网络 (计网)
IO
常用类
异常
单列集合 Collection
List
ArrayList 底层数组实现 查询块
LinkedList 底层链表 查询慢
Vector 底层也是数组,是线程安全的
Set
HashSet
TreeSet
双列集合 Map
HashMap
TreeMap
Hashtable
1.你能讲一下HashMap吗?
基本特点: 双列集合,键不能重复,值可以重复,可以有一个key为null
数据结构:
源码
put方法源码分析
public V put(K key, V value) {//通过key计算哈希值return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {int h; 为了减少哈希值冲突return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; //哈希表Node<K,V> p; //记录之前int n, i; //n是哈希表长度, i是索引if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//创建哈希表if ((p = tab[i = (n - 1) & hash]) == null) // == 97%16 根据哈希值计算位置//如果该位置为null,则直接将key,value包装到一个node对象中,直接存放到第i个位的第一个tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;//判断key是否重复, 先判断哈希值(快),但是哈希有问题,值相同,但内容不同,//所以在哈希值相同时,需要调用equals(),判断内容是否相同.if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;//当key不重复时,判断类型是树类型还是链表类型else if (p instanceof TreeNode)//已经是树类型,向红黑树上添加元素e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {//链表for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);//添加完成后,判断链表长度if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);//转红黑树break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;//key相同时,会用后面key的值,覆盖之前的相同的key的值afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;}
重点参数:
1.哈希表长度: 默认是16
2.哈希表每次扩容原来 2 倍
3.哈希表的负载因子 0.75, 哈希表不会装满的,装满会影响查询效率,所以会牺牲一定的空间而换取查询效率
4.链表长度上限是 8 ,尝试将链表转为红黑树,但是不一定转成功, 还会判断哈希表长度,哈希表长度小于64 会先扩容哈希表,扩容后所有元素位置需要重新计算,这样链表会变短, 只有当链表长度大于等于8且哈希表长度大于64,链表才会转成红黑树.
5.当红黑树节点数量减少为6个时,红黑树退化成链表
2.ConcurrentHashMap
ConcurrentHashMap是一个线程安全的map,加锁的方式与Hashtable不同
Hashtable直接在方法上加锁,一次只能有一个线程进入方法操作.
ConcurrentHashMap不是给方法加的锁,个每个哈希表中的位置加锁
四.JUC常用类
Java5.0在java.util.concurrent包中提供了多种并发容器类来改进同步容器的性能。
1.ConcurrentHashMap
ConcurrentHashMap是一个线程安全的map,加锁的方式与Hashtable不同
Hashtable直接在方法上加锁,一次只能有一个线程进入方法操作.
ConcurrentHashMap不是给方法加的锁,个每个哈希表中的位置加锁
2.CopyOnWriteArrayList
ArrayList在多线程中是不安全的
Vector是线程安全的,效率低,add();get();
CopyOnWriteArrayList
add();加锁,有线程写的时候,不影响读,写的时候,先把数组复制备份一下,在备份的数
组上进行写(修改),写完之后再把备份的数组赋给原数组
get();没加锁,可以有多个线程同时查询,提高了查询效率
3.CopyOnWriteArraySet
CopyOnWriteArraySet底层使用了CopyOnWriteArrayList,也是线程安全的,但是里面不能存储重
复数据的
4.辅助类CountDownLatch
CountDownLatch 辅助类 递减计数器
使一个线程 等待其他线程执行结束后再执行
相当于一个线程计数器,是一个递减的计数器
先指定一个数量,当有一个线程执行结束后就减一,直到为0,关闭计数器
这样线程就可以执行了
五.线程池
理解池的概念
字符串常量池 String s1 = "abc"; String s2="abc"; s1==s2;
数据库连接池
Connection 每次链接创建,每次用完销毁 创建对象是要花费时间的
Integer -128---+127也有缓存池
Integer a = 1270; //-128 -- +127 返回的都是同一个对象,超出范围,每次才会创建新的对象
Integer b = 1270; //== Integer.valueOf(1270);System.out.println(a==b);//false
1.线程池
如果每次执行任务时,都去创建线程对象,用完销毁,频繁的创建也是比较占资源的.
jdk5之后,提供ThreadPoolExecutor类来实现线程池.
好处: 避免了频繁的创建也是比较占资源的, 统一管理线程
(1)ThreadPoolExecutor类
Java.uitl.concurrent.ThreadPoolExecutor 类是线程池中最核心的一个类,因此如果要透彻地
了解Java中的线程池,必须先了解这个类。
ThreadPoolExecutor继承了AbstractExecutorService 类,并提供了四个构造器,事实上,通
过观察每个构造器的源码具体实现,发现前面三个构造器都是调用的第四个构造器进行的初始化工
作。
ThreadPoolExecutor构造方法中的7个参数:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
corePoolSize: 核心线程池数量 5
maximumPoolSize: 最大线程池数量 10
keepAliveTime: 非核心线程池中的线程, 空闲多久后销毁
unit: 为keepAliveTime指定时间单位
workQueue: 等待的线程队列
threadFactory: 创建线程的工厂
RejectedExecutionHandler: 拒绝策略
(2)线程池的执行流程
(3)线程池中的队列
线程池有以下工作队列:
ArrayBlockingQueue
LinkedBlockingQueue
(4)线程池的拒绝策略
1.AbortPolicy 直接抛出异常
2.CallerRunsPolicy 拒绝后,由提交任务的线程执行此任务(如main线程)
3.DiscardOldestPolicy 丢弃队列中等待时间最长的那一个
4.DiscardPolicy 丢弃最后来的无法执行的任务
(5)向线程池提交任务的两种方法
execute 与 submit 的区别
void execute 适用于不需要关注返回值的场景
submit 方法适用于需要关注返回值的场景。
(6)关闭线程池
shutdownNow 立刻关闭,即使还有未执行完的任务
shutdown 等待所有任务执行完了再关闭
(7)java中创建线程方式:
1.继承Thread类
2.实现Runnable接口
3.实现Callable接口
4.线程池
2.ThreadLocal
作用: 为每个线程提供一个变量副本
使用:
//创建ThreadLocal对象,为每个线程自动的提供一个变量副本static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){@Overrideprotected Integer initialValue() {return 1; //初始化变量}};public static void main(String[] args) {/*这种方式在两个线程中对num进行操作,这个num是同一个*///线程1new Thread(new Runnable() {@Overridepublic void run() {threadLocal.set(10);threadLocal.set(threadLocal.get()+5);System.out.println(Thread.currentThread().getName()+":"+threadLocal.get());}}).start();//线程2new Thread(new Runnable() {@Overridepublic void run() {threadLocal.set(20);threadLocal.set(threadLocal.get()+10);System.out.println(Thread.currentThread().getName()+":"+threadLocal.get());}}).start();}
(1)内部实现
(2)ThreadLocal内存泄漏问题
内存溢出: 内存不够用了
内存泄漏: 一些对象已经不再使用,但是虚拟机又不能回收的对象( 例如: 数据库连接对象,IO流
对象,Socket 提供close)
如果使用ThreadLocal不当,会造成内存泄漏问题。
对象与引用关系
Object obj = new Object(); 强引用
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被弱引用管理的, 下次垃圾回收到来时,ThreadLocal会被回收掉, 造成ThreadLocalMap
中的不存在了,但是value还被外界引用, 所以ThreadLocalMap就不能被回收, 造成了内存泄漏.
所以, 正确的使用ThreadLocal方式是在用完之后, 主动删除ThreadLocalMap中的数据.