Java多线程基础——线程和线程安全

Java多线程基础——线程和线程安全

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

线程及其实现

进程

进程是操作系统进行资源分配调度的最小单位,各进程有独立的系统资源(如内存、文件I/O等),互相之间不能直接访问,很多时候,一个进程就是一个应用,多个进程也许可以并发,但是进程本身没有并发的概念,需要借助线程实现并发。

线程

线程是处理器调度和分配的最小单位,线程没有自己的资源,线程既能共享所在进程的系统资源(线程私有的局部变量表来自于主内存,只为线程本身服务,输出的结果仍然要写回主内存),又能相对独立地执行调度。

线程因此可以通过并行处理提升效率,但也因此会互相干扰,如果线程崩溃,就会影响所在进程的所有线程。

状态

线程有5种状态

New,创建后未启动。

Runnable,Ready或Running,正等待CPU或正在执行。

Waiting/Timed Waiting,无限等待需要其他线程唤醒,限期等待会由系统自动唤醒

sleep是Native方法,由操作系统来阻塞线程,指定时间后恢复线程。

无限等待会调用wait,需要另一个线程调用notify()来通知恢复线程。

Blocked,阻塞,线程在等待锁被另一个线程释放出来

Terminated,线程已结束

这五种状态的转换关系如下:

Java多线程基础——线程和线程安全

实现

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多线程基础——线程和线程安全

学习过程中遇到什么问题或者想获取学习资源的话,欢迎加入Java学习交流群346942462,我们一起学Java!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值