[笔记]Java多线程基础——线程和线程安全

2017-09-10  本文已影响45人  蓝灰_q

因为处理器主频在硬件发展上的瓶颈,摩尔定律基本失效,现在真正起作用的是并行处理的Amdahl定律,毕竟,现在计算机的瓶颈在于存储和通信,而不是运算本身,并行运算可以更充分地发挥运算的能力,也是提升计算机性能。

线程及其实现

进程
进程是操作系统进行资源分配调度的最小单位,各进程有独立的系统资源(如内存、文件I/O等),互相之间不能直接访问,很多时候,一个进程就是一个应用,多个进程也许可以并发,但是进程本身没有并发的概念,需要借助线程实现并发。
线程
线程是处理器调度和分配的最小单位,线程没有自己的资源,线程既能共享所在进程的系统资源(线程私有的局部变量表来自于主内存,只为线程本身服务,输出的结果仍然要写回主内存),又能相对独立地执行调度。
线程因此可以通过并行处理提升效率,但也因此会互相干扰,如果线程崩溃,就会影响所在进程的所有线程。
状态
线程有5种状态
New,创建后未启动。
Runnable,Ready或Running,正等待CPU或正在执行。
Waiting/Timed Waiting,无限等待需要其他线程唤醒,限期等待会由系统自动唤醒
sleep是Native方法,由操作系统来阻塞线程,指定时间后恢复线程。
无限等待会调用wait,需要另一个线程调用notify()来通知恢复线程。
Blocked,阻塞,线程在等待锁被另一个线程释放出来
Terminated,线程已结束
这五种状态的转换关系如下:

线程状态转换关系
实现
Java用Thread实现线程,每个已经start的Thread都是一个线程,需要注意的是,Thread都是Native实现的,也就是通过操作系统,而不是JVM支持的。
在操作系统上,实现线程主要有三种方式:
1.内核线程
内核线程KLT是直接在操作系统内核上支持的,KLT和CPU之间只隔着一个调度器Thread Schedular,调度器把线程中的任务直接映射到各个CPU上。
程序一般会通过轻量级进程来使用内核线程,这是内核线程的高级接口,每个内核线程对应一个轻量级进程。
内核线程的所有操作都要在系统中调用,需要频繁在用户态和内核态切换,代价很高。
2.用户线程
用户线程完全在用户进程中创建,不需要切换内核态,所以开销很小,但是因为没有系统支持,进程之间的协调、阻塞、处理器映射等,都需要自己实现,所以非常复杂。
3.用户线程+轻量级进程
其实就是把前两种模式混合使用。

线程安全

线程安全问题其实就是并发的正确性问题,一个线程安全的行为,既不需要额外的同步和协调,也不用考虑在runtime中的调度和交替执行,一定能返回预期的结果。
五种线程安全场景
1.不变性
最简单最纯粹的场景就是不变性,一个不可变的对象一定是线程安全的,如final。
2.绝对线程安全
绝对线程安全是不切实际的,即使是线程安全的Vector容器,也只是在方法中用了synchronized修饰,方法调用时还是需要额外同步,否则,在多线程同时remove,仍然会有Index边界溢出的错误。
3.相对线程安全
一般意义上的线程安全就是相对线程安全,单独操作是线程安全的,但是在特定情况下,还需要在调用时增加额外的同步手段。Java提供的线程安全如Vector、HashTabe、Collections.synchronizedCollection()等,都是相对线程安全。
4.线程兼容
一般意义上的不是线程安全其实是线程兼容,指的是本身并不线程安全,可以在调用时增加同步手段,实现线程安全,常见的ArrayList和HashMap都是线程安全的。
5.线程对立
一些极端情况下,无论采用什么同步措施,都不能实现线程安全,就是线程对立,如Thread的suspend和resume,不能并行调用,很容易出现死锁。
实现线程安全,既与代码的编写有关,也与虚拟机的同步和锁有关,常见的三种线程安全实现方法为:
1.互斥同步
就是共享数据在并行运算中,同一时刻只能一个线程使用,synchronized和ReentrantLock都是互斥同步。
2.非阻塞同步
其实就是互斥同步的对立面,非阻塞同步相对乐观,认为并行不一定导致共享数据冲突,如果真的出现争用冲突,再做补偿即可(如重试操作,比如compareAndSet(current,next)就是不断尝试赋值,如果current和next的值和预期不一致,就说明数据被修改了,会再次循环尝试),sum.misc.Unsafe类就是非阻塞同步机制(ClassLoader才能直接使用,用户只能通过Java API间接使用,如AtomicInteger),非阻塞同步依赖于硬件指令集的发展和支持。
3.无同步方案
无同步方案不是不管线程安全,而是通过其他方式实现线程安全,不需要同步。
可重入代码
一个方向是通过代码实现无同步,就是可重入代码,可重入代码在执行过程中,随时可以中断,转而执行其他任务(包括递归该代码本身),然后重入继续执行,不会出现错误。
可重入代码也叫纯代码,容易令人想起纯函数(当然,不是同一维度),只要输入相同的数据,就能返回相同的结果。
线程本地存储
另一个方向是通过避免多线程的数据共享实现无同步,就是线程本地存储,也就是把共享数据控制在一个线程内,避免冲突。
大部分使用消费队列的模式都是线程本地存储,这种模式会尽量在一个线程内完成消费,Android中的Handler机制,就是通过ThreadLocal对象(实际上是一个HashMap,key为对象的hashcode,value为对象本身),让handler引用线程的Looper,Looper再依次处理自己MessageQueue中的Message,通过Message的target指向handler,实现在同一线程内处理消息队列。

锁的优化

多线程的重点是数据的高效共享,主要得解决竞争的问题,也就是对锁的优化。
自旋锁与自适应自旋
自旋针对的是线程的阻塞和恢复,因为线程的阻塞和恢复非常消耗资源,而等待的锁可能很快就会释放,所以在线程请求锁失败的时候,不立即阻塞线程,而是让它先执行一个忙循环(自旋)。
自旋也会消耗资源,适当的自旋次数效果才最好,自适应自旋会根据以往的自旋次数,动态调整自旋次数,基本策略就是自旋后能获得锁,下次就可以多自旋几次;如果自旋后没有获得锁,下次就会少自旋几次。
锁消除
在编译代码时,编译器认为某个锁完全没有必要,就会把锁消除。
锁粗化
如果虚拟机发现一串零碎的操作中,对同一个对象反复加锁解锁,就会把它们合并扩展为外侧的一个锁。
轻量级锁
绝大部分锁在同步周期是没有竞争的,加锁和解锁的操作虽然必须,但是消耗过重了,轻量级锁就是先用轻量级的锁来加锁解锁,如果同步期间没有发生竞争,就节省了资源;如果发生了竞争,就膨胀为常规的重量级的锁。
偏向锁
偏向锁和轻量级锁都是针对无竞争情况的优化,轻量级锁是在无竞争时消除互斥,而偏向锁是在无竞争时消除整个同步。

逃逸的优化

根据对象的作用域,可能发生逃逸行为,如果可以确认不会发生逃逸,也能进行优化。
方法逃逸
在方法中定义个一个对象,如果可能被外部方法引用,比如作为外部方法的参数,就是方法逃逸。
如果不会发生方法逃逸,就可以在线程的栈上分配内存,这样,在栈帧出栈时就可以回收内存,减轻GC压力。
线程逃逸
在线程中的一个对象,如果可能被外部线程引用,比如赋值给静态类变量或者其他线程的对象变量,就是线程逃逸。
如果不会发生线程逃逸,就可以消除同步,消除对变量的同步措施,因为在线程内部是天然有序的,不存在竞争问题。

引用

《深入理解Java虚拟机》
synchronized(this)与synchronized(class)
Java集合及concurrent并发包总结(转)
《深入理解Java虚拟机》读书笔记7:高效并发
深入理解java虚拟机 精华总结(面试)

上一篇下一篇

猜你喜欢

热点阅读