Java多线程
作者:Grey
原文地址:
线程和进程的关系
线程就是轻量级进程,是程序执行的最小单位。
多进程的方式也可以实现并发,为什么我们要使用多线程?
- 共享资源在线程间的通信比较容易。
- 线程开销更小。
进程和线程的区别?
- 进程是一个独立的运行环境,而线程是在进程中执行的一个任务。他们两个本质的区别是是否单独占有内存地址空间及其它系统资源(比如I/O):
- 进程单独占有一定的内存地址空间,所以进程间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。
- 进程单独占有一定的内存地址空间,一个进程出现问题不会影响其他进程,不影响主程序的稳定性,可靠性高;一个线程崩溃可能影响整个程序的稳定性,可靠性较低。
- 进程单独占有一定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大;线程只需要保存寄存器和栈信息,开销较小。
- 进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即CPU分配时间的单位 。
创建线程
有三种方式
- 继承Thread类,重写run方法
- 实现Runnable接口,实现run方法
- 通过线程池创建
- 通过Callable/Future创建(需要返回值的时候)
线程基本操作
sleep
当前线程睡一段时间
yield
这是一个静态方法,一旦执行,它会使当前线程让出一下CPU。但要注意,让出CPU并不表示当前线程不执行了。当前线程在让出CPU后,还会进行CPU资源的争夺,但是是否能够再次被分配到就不一定了。
join
等待另外一个线程的结束,当前线程才会运行
public class ThreadJoin {
volatile static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
for (i = 0; i < 10000; i++) {
}
});
thread.start();
// join 方法表示主线程愿意等待子线程执行完毕后才继续执行
// 如果不使用join方法,那么i输出的可能是一个很小的值,因为还没等子线程
// 执行完毕后,主线程就已经执行了打印i的操作
thread.join();
System.out.println(i);
}
}
线程状态切换
CAS
比较与交换的意思
举个例子:
内存有个值是3,如果用Java通过多线程去访问这个数,每个线程都要把这个值+1,之前是需要加锁,即synchronized关键字来控制,但是JUC的包出现后,有了CAS操作,可以不需要加锁来处理,流程是:
第一个线程:把3拿过来,线程本地区域做计算+1,然后把4写回去,
第二个线程:也把3这个数拿过来,线程本地区域做计算+1后,在回写回去的时候,会做一次比较,如果原来的值还是3,那么说明这个值之前没有被打扰过,就可以把4写回去,如果这个值变了,假设变为了4,那么说明这个值已经被其他线程修改过了,那么第二个线程需要重新执行一次,即把最新的4拿过来继续计算,回写回去的时候,继续做比较,如果内存中的值依然是4,说明没有其他线程处理过,第二个线程就可以把5回写回去了。
流程图如下:
ABA问题
CAS会出现一个ABA的问题,即在一个线程回写值的时候,其他线程其实动过那个原始值,只不过其他线程操作后这个值依然是原始值。
如何来解决ABA问题呢?
我们可以通过版本号或者时间戳来控制,比如数据原始的版本是1.0,处理后,我们把这个数据的版本改成变成2.0版本, 时间戳来控制也一样,
以Java为例,AtomicStampedReference这个类,它内部不仅维护了对象值,还维护了一个时间戳。
当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳。
当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值,写入才会成功。
因此,即使对象值被反复读写,写回原值,只要时间戳发生变化,就能防止不恰当的写入。
Note:
CAS的底层实现
Unsafe.cpp-->Atom::cmpxchg--Atomic_linux_x86_inline.hpp-->调用了汇编的LOCK_IF_MP方法
Multiple_processor
lock cmpxchg
虽然cmpxchg指令不是原子的,但是加了lock指令后,则cmpxhg被上锁,不允许被打断。在多核CPU中,必须加lock
使用CAS好处
jdk早期是重量级别锁 ,通过0x80中断 进行用户态和内核态转换,所以效率比较低,有了CAS操作,大大提升了效率。
对象的内存布局
这里说到的对象内存布局和具体的虚拟机实现是有关系的,我们讨论的是Hotspot的实现。
参考资料
[多线程与高并发-马士兵]