目录
概念
进程和线程:
并发与并行
同步与异步
临界区
线程安全:
面试题:线程安全三大特性
java内存模型
创建多线程
1.继承thread
2.runnable
3.Callable
多线程中的同步机制sync
线程的五种状态
线程池
CountDownLatch 倒计时锁
Semaphore 信号量
JUC CyclicBarrier 循环屏障
CyclicBarrier使用场景
ReentrantLock 重入锁
Condition 条件唤醒
Callable & Future
JUC 同步容器
ConcurrentSkipListMap
JUC 之Atomic 包与CAS 算法
CAS 算法
总结
volatile
volatile和synchronized区别
并发就是指程序同时处理多个任务的能力
并发编程的根源在于对多任务情况下对访问资源的有效控制
concurrent包:
调度器:ExecutorService
并发导致的问题:脏读
java程序:jar包或者war包
进程是动态的概念,是程序运行的状态
线程是进程内的一个基本任务,每个线程都有自己的功能,是CPU分配和调度的基本单位。共享进程的资源
#请求之后必须等待得到响应,同步,是阻塞的
同步:拥有公共资源和共享数据的线程之间合理有序执行,合作完成某项任务的过程叫做同步
请求之后不用等待,系统会自动传入到回调函数,叫异步,是非阻塞的
临界资源用来表示一种公共资源与共享数据,可以被多个线程使用
访问临界资源的代码块叫临界区。
同一时间只能有一个线程访问临界区(阻塞状态),其他线程必须等待
死锁、饥饿、活锁
在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行。不会出现数据污染等意外情况。
多用户环境下结果和单用户结果应一致
原子性:一个操作或者是多个操作,要不全部执行并且执行的过程中不会被任何因素打断,要么都不执行
可见性:当多个线程访问一个变量的时候,一个线程修改了变量的值,其他线程应立即能够看到。
有序性:如果在本线程内观察,所有操作都是有序的。如果在一个线程观察另一个线程,所有的操作都是无序的
用volatile防止重排序,但JVM已经有防止重排序的机制,所以用到的机会不多
java虚拟机内存
当线程里的栈帧被销毁了之后,线程就执行完成了。就不会有栈里边的方法引用堆中的对象。所以堆中刚刚创建的对象就成了垃圾,需要回收。而方法区是静态区,里面还有常量区,不会被回收。虽然也有GC去扫描,但是明显频率很低
面试题:创建线程的三种方式
1.继承thread类创建线程
2.实现runnable 接口创建线程
3.使用callable 和 future 创建线程
重写run方法,是具体线程的执行的方法
通过threadx.start() 启动线程
垃圾回收的是监听线程
继承runnable接口重写run方法
但是不能直接.start(),要传给thread,让thread去启动线程
jdk1.5 以后为我们专门提供了一个并发工具包 java.util.concurrent
java.util.concurrent 包含许多线程安全,测试良好,高性能的并发构建块。创建concurrent的目的就是要实现Collection框架对数据结构所执行的并发操作。通过提供一组可靠的,高性能并发构建块,开发人员可以提高并发类的线程安全、可伸缩性,性能,可读性和可靠性。
java.util.concurrent.Callable
实现Callable接口可以允许我们的线程返回值或抛出异常
实现call方法的时候返回值和抛出异常
要用到线程池去运行calllable对象
Future 用于接收线程内部call方法的返回值
不涉及临界区使用的可以用runnable,否则不推荐
synchronized(同步锁)关键字的作用就是利用一个特定的对象设置一个锁lock,在多线程并发访问的时候,同时只允许一个线程可以获得这个锁,执行特定的代码。执行后释放锁
如果两个同步代码块用同一块锁,那两个代码块也是互斥的,不能同时执行
面试题:synchronized 使用场景,对应不同的锁对象
synchronized代码块-- 任意对象即可
synchronized方法--this当前对象
synchronized静态方法--该类的字节码对象
如果用不同的同步代码块用同一把锁,那两个代码块是同步的。用不同的锁就是异步的
最常用的是锁方法
synchronized 锁静态方法--用的锁是该类的字节码对象。
以下两个方法用的同一把锁----该类的字节码文件
如果有数据共享的话那线程必须是安全的,否则可以不安全,毕竟不安全速度快。
面试题
对于new Thread() 创建线程,性能差,且缺乏统一管理,可能无限制的新建线程。相互竞争,严重时会占用过多系统资源导致死机或OOM
concurrent 包中,提供了工具类Executors(调度器) 对象来创建线程池,可创建的有四种
CachedThreadPool 可缓存线程池 无限大,如果没有可用线程则创建,有空闲则利用起来
FixedThreadPool 定长线线程池 固定线程总数,空闲线程用于执行任务,如果没有多余线程,任务等待,等待算法为FIFO(先进先出)
SingleThreadExecutor 单线程池 只有一个线程
ScheduleThreadPool 调度线程池
ScheduleThreadPool 调度线程池 适用于定时任务之类的
不常用,因为框架自带调度方法
数据库的连接池是线程池的经典应用,每一个数据库连接都由线程来管理
应用场景:写excel表,例如要写第10个表,第11个表是前十个sheet的汇总表,可以定义十个线程写10个sheet,同时用CountDownLatch 倒计时锁监听,当十个线程运行完后汇总 再输出第十一个表。
用于限制获取某种资源的线程数量
即线程池中虽然有足够的线程,但是使用超过semaphore 也不可以运行。限制线程数为10,剩下的进不去的十个人要等前边的十个人玩完才可以获得线程执行run
下面是 当信号量满的情况下 直接退出不等待获得线程
就是解决了一个问题: 让所有的线程同一时刻开始执行,而不是用上述的循环方式创建多线程
CyclicBarrier 是一个同步工具类,它允许一组线程互相等待,直到到达某个公共屏障点。
与CountDownLatch(倒计时锁)不同的是该CyclicBarrier 在释放等待线程后可以重用
包含Semephore 的功能,可以限制同时运行的进程数。但是与Semephore 不同的是实现了线程复用,即不去创建新线程而是去用以前用过的空闲下来的线程
适用于多线程必须同时开始的场景
重入锁是指任意线程在获取到锁之后再次获取该锁不会被该锁阻塞
ReentrantLock 设计的目标是用来代替synchronized关键字。但是还没实现。只会在面试的时候问到
使用
代码:四个线程用的是同一把锁,不会有并发并行问题。 那还用多线程有什么用呢????
代码示例:
computer必须要实现callable接口,定义返回值类型为boolean
之前说的线程安全的类性能差,线程不安全的又不安全,如何鱼与熊掌兼得呢?
线程安全-并发容器
hashMap 是线程不安全的,hashTable 是线程安全的。concurrentHashMap 是线程安全的,效率高的
底层原理实现要搞清楚,面试要考
hashTable是整个table加锁,效率自然低,而CHM是分段加锁,段与段之间支持并发。段内要排队获取锁
CHM的速度 是hashTable 的16倍
对于一个单链表,即使链表是有序的,如果我们想要在其中查找某个数据,也只能从头到尾遍历链表,这样效率自然就会很低,跳表就不一样了。跳表是一种可以用来快速查找的数据结构,有点类似于平衡树(B+树)。它们都可以对元素进行快速的查找。但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整。而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,你只需要部分锁即可。这样,在高并发环境下,你就可以拥有更好的性能。而就查询的性能而言,跳表的时间复杂度也是 O(logn) 所以在并发数据结构中,JDK 使用跳表来实现一个 Map。
跳表的本质是同时维护了多个链表,并且链表是分层的,
最低层的链表维护了跳表内所有的元素,每上面一层链表都是下面一层的子集。有点像B+树
跳表内的所有链表的元素都是排序的。查找时,可以从顶级链表开始找。一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续找。这也就是说在查找过程中,搜索是跳跃式的。如上图所示,在跳表中查找元素18。
查找18 的时候原来需要遍历 18 次,现在只需要 7 次即可。针对链表长度比较大的时候,构建索引查找效率的提升就会非常明显。
从上面很容易看出,跳表是一种利用空间换时间的算法。
使用跳表实现Map 和使用哈希算法实现Map的另外一个不同之处是:哈希并不会保存元素的顺序,而跳表内所有的元素都是排序的。因此在对跳表进行遍历时,你会得到一个有序的结果。所以,如果你的应用需要有序性,那么跳表就是你不二的选择。JDK 中实现这一数据结构的类是ConcurrentSkipListMap。
原子性:要么做完要么不做
JUC包下包含多个原子操作类
都是基本数据类型的封装类,只不过具有原子性。这样我们在一些场景下就不用自己手动对数据加锁了。
Atomic类的底层应用的是CAS算法,所以只适合轻量级数据,计数器等
是乐观锁最著名的实现算法
如果在并发很高的项目上用CAS, 效率不但不会提高反而会降低。因为一直有冲突一直要重试,非常占用计算资源
http://www.importnew.com/23535.html
volatile保证可见性的原理是在每次访问变量时都会进行一次刷新,因此每次访问都是主内存中最新的版本。所以volatile关键字的作用之一就是保证变量修改的实时可见性。
volatile关键字通过提供“内存屏障”的方式来防止指令被重排序,为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
1、volatile不会进行加锁操作:
volatile变量是一种稍弱的同步机制在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。
2、volatile变量作用类似于同步变量读写操作:
从内存可见性的角度看,写入volatile变量相当于退出同步代码块,而读取volatile变量相当于进入同步代码块。
3、volatile不如synchronized安全:
在代码中如果过度依赖volatile变量来控制状态的可见性,通常会比使用锁的代码更脆弱,也更难以理解。仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它。一般来说,用同步机制会更安全些。
4、volatile无法同时保证内存可见性和原则性:
加锁机制(即同步机制)既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性,原因是声明为volatile的简单变量如果当前值与该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:“count++”、“count = count+1”。
当且仅当满足以下所有条件时,才应该使用volatile变量:
1、 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
2、该变量没有包含在具有其他变量的不变式中。
总结:在需要同步的时候,第一选择应该是synchronized关键字,这是最安全的方式,尝试其他任何方式都是有风险的。尤其在、jdK1.5之后,对synchronized同步机制做了很多优化,如:自适应的自旋锁、锁粗化、锁消除、轻量级锁等,使得它的性能明显有了很大的提升。