Java多线程
type
status
date
slug
summary
tags
category
icon
password
进程和线程
- 进程:
每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1~N个线程。(进程是资源分配的最小单位)
- 线程:
同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)
三态模型

- 引起进程状态转换的具体原因如下:
- 运行态→等待态:等待使用资源;如等待外设传输;等待人工干预。
- 等待态→就绪态:资源得到满足;如外设传输结束;人工干预完成。
- 运行态→就绪态:运行时间片到;出现有更高优先权进程。
- 就绪态→运行态:CPU 空闲时选择一个就绪进程。
五态模型

- 引起进程状态转换的具体原因如下:
- NULL→新建态:执行一个程序,创建一个子进程。
- 新建态→就绪态:当操作系统完成了进程创建的必要操作,并且当前系统的性能和虚拟内存的容量均允许。
- 运行态→终止态:当一个进程到达了自然结束点,或是出现了无法克服的错误,或是被操作系统所终结,或是被其他有终止权的进程所终结。
- 运行态→就绪态:运行时间片到;出现有更高优先权进程。
- 运行态→等待态:等待使用资源;如等待外设传输;等待人工干预。
- 就绪态→终止态:未在状态转换图中显示,但某些操作系统允许父进程终结子进程。
- 等待态→终止态:未在状态转换图中显示,但某些操作系统允许父进程终结子进程。
- 终止态→NULL:完成善后操作。
JVM中线程的六态模型

Java实现多线程的方式
- extend Thread
- implements Runnable
- implements Callable
- ExecutorService
synchronized
的三种应用方式
- 修饰实例方法
作用于当前实例加锁,进入同步代码前要获得当前实例的锁
- 修饰方法
作用于当前类对象加锁,进入同步代码前要获得当前对象的锁
- 修饰代码块
指定加锁对象,对给定对象加锁,进入同步代码前要获得给定对象的锁
当要执行6~7行时需要去堆内存中去申请object对象的锁,不是栈里的object,而是栈内存中object指向堆内存中Object对象的锁。
- 互斥锁
线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时会自动释放锁,当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。互斥锁其实提供了一种原子操作,让所有线程以串行的方式执行同步代码块。
当要执行6~7行时需要new一个Demo然后指向自己自身锁定自身后在继续执行同步代码执行完会释放对自身的锁定。
- 还可以这样写
- ⚠️
synchronized
锁定的是一个对象,不是锁住代码块。
- ⚠️
synchronized
修饰的同步代码块中的语句尽量少,程序效率会得到提高因为synchronized
的锁比较重。
- ⚠️
synchronized
锁定的对象指向堆内存中的引用对象发生改变时锁会失效,因为synchronized
锁定的是堆内存里的对象,而不是栈内存里的引用。
- ⚠️
synchronized
不要锁定字符串常量不然会出现一些诡异的问题Jetty
之前就出现过这样的BUG
这样的BUG
往往比较隐蔽不易被发现。如下代码所示
synchronized
修饰静态方法
synchronized修饰静态方法相当于锁定了这个类的Class对象就是java.lang里的Class对象,一个类的静态方法和静态属性,属于类本身。
想一想上面的代码会有什么问题,当5个线程同时对count--并打印的时候会出现线程的重入的问题,就是第一个线程执行代码时执行count--还没来的及打印,第二个线程进来执行count--,同理第三四五个线程。也会出现问题。出现问题的可能具有随机性。
- 可重入
若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。
- Fixed
把run方法加上synchronized进行修饰,就是在在执行run方法里的代码时,先锁住栈内存中demo指向堆内存中的Demo对象,然后对变量count进行自减操作。所以synchronized修饰的方法具有原子性,在执行时不会对其他线程打断,就是要么执行完要么就不执行。
一个synchronized修饰的方法在运行中,一个非synchronized方法是可以运行的,假如方法one是一个人他去卫生间进去后他把门插上了,而方法two就像是在卫生间里打扫卫生的老大爷,两个方法互不影响。就是只有方法one在执行的时候才会申请者那把锁。
- 输出
在这段代码中对写加了锁,对读没有加锁,也就是说在主线程调用加锁的set方法时正在处理一些业务还没处理完时,一个不加锁的get方法执行时获取到的数据是set方法中还没完成的数据。就会出现脏读的问题。
- 脏读
脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
- 不可重复读
是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两 次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不 可重复读。例如,一个编辑人员两次读取同一文档,但在两次读取之间,作者重写了该文档。当编辑人员第二次读取文档时,文档已更改。原始读取不可重复。如果 只有在作者全部完成编写后编辑人员才可以读取文档,则可以避免该问题。
- 幻读
是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。 同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象 发生了幻觉一样。例如,一个编辑人员更改作者提交的文档,但当生产部门将其更改内容合并到该文档的主复本时,发现作者已将未编辑的新材料添加到该文档中。 如果在编辑人员和生产部门完成对原始文档的处理之前,任何人都不能将新材料添加到文档中,则可以避免该问题。
一个同步方法可以调用另外一个同步方法,一个线程已经拥有已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁。也就是说synchronized获得的锁是可重入的。
模拟死锁
- 死锁
- 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
- 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
- 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
- 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
在子类中调用父类的同步方法
在子类中调用父类的同步方法实际上锁住的是同一个对象都是子类ChildDemo这个对象。
多线程与异常
程序在执行过程中,如果出现异常,默认情况锁会被释放,所以在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。比如,在一个WebApplication处理过程中,多个Servlet线程共同访问同一个资源,这时如果异常处理不当,在第一个线程抛出异常,其他线程就会进出代码同步区,有可能会访问到异常产生时的数据。因此一定要非常小心的处理同步业务逻辑中的异常。
volatile 关键字
volatile 关键字,使一个变量在多个间线程可见A,B线程都用到一个变量,Java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道。使用volatile关键字,会让所有线程都会读到变量的修改值。
- 上面程序不加
volatile
关键字

当P1执行时会从内存中读取running的值到CPU的缓存中,然后执行P1的业务,当P2执行时同样先从内存中读取running的值对running的值进行修改,想要终止P1的业务但是由于running没有加volatile关键字进行修饰所以线程间running这个变量值是不可见的,也就是说P2在修改running值时P1是不知道的,所以上面代码中的循环会一直执行不会终止。
- 上面程序加了
volatile
关键字

当给变量running加上volatile关键字时,当P2修改running的值后会通知所有线程告诉running的值修改了,你线程内存中的值过期了请重新到内存中去读。所以P1就可以终止了。
volatile并不能保证多个线程共同修改running变量时所带来的不一致的问题,也就是说valatile不能代替synchronized比如下面这段程序变量count的初始值为0创建10个线程每个线程让count自增10000结果应该是十万,但是最后的值是不正确的,因为volatile可以保证的是线程间的可见性,但光可见行是不足以让最后的值是正确的,还是要靠synchronized来锁住demo方法来保证方法的原子性。synchronized和volatile的区别就是,volatile只保证线程间的可见性,不保证原子性。synchronized既保证线程间的的可见性也可以保证线程间的原子性。synchronized要比volatile的并发性能低不少
对volatile变量的单次读/写操作可以保证原子性的,如long和double类型变量,但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。
volatile原理
可见性实现
线程本身并不直接与主内存进行数据的交互,而是通过线程的工作内存来完成相应的操作。这也是导致线程间数据不可见的本质原因。因此要实现volatile变量的可见性,直接从这方面入手即可。对volatile变量的写操作与普通变量的主要区别有两点:
- 修改volatile变量时会强制将修改后的值刷新的主内存中。
- 修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。
有序性实现
Java中的happen-before规则,JSR 133中对Happen-before的定义如下:
Two actions can be ordered by a happens-before relationship.If one action happens before another, then the first is visible to and ordered before the second.
通俗一点说就是如果a happen-before b,则a所做的任何操作对b是可见的。(这一点大家务必记住,因为happen-before这个词容易被误解为是时间的前后)。再来看看JSR 133中定义了哪些happen-before规则:
Each action in a thread happens before every subsequent action in that thread.An unlock on a monitor happens before every subsequent lock on that monitor.A write to a volatile field happens before every subsequent read of that volatile.A call to start() on a thread happens before any actions in the started thread.All actions in a thread happen before any other thread successfully returns from a join() on that thread.If an action a happens before an action b, and b happens before an action c, then a happens before c.
翻译过来为:
- 同一个线程中的,前面的操作 happen-before 后续的操作。(即单线程内按代码顺序执行。但是,在不影响在单线程环境执行结果的前提下,编译器和处理器可以进行重排序,这是合法的。换句话说,这一是规则无法保证编译重排和指令重排)。
- 监视器上的解锁操作 happen-before 其后续的加锁操作。(Synchronized 规则)
- 对volatile变量的写操作 happen-before 后续的读操作。(volatile 规则)
- 线程的start() 方法 happen-before 该线程所有的后续操作。(线程启动规则)
- 线程所有的操作 happen-before 其他线程在该线程上调用 join 返回成功后的操作。
- 如果 a happen-before b,b happen-before c,则a happen-before c(传递性)。
使用Atomic类来保证原子性
Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
所以,所谓原子类说简单点就是具有原子/原子操作特征的类。
并发包
java.util.concurrent
的原子类都存放在java.util.concurrent.atomic下,如下图所示。
Atomic
类适合完成一些简单的同步操作,效率会比synchronized
高好多,因为Atomic
利用的是CPU指令来进行操作的Java底层并没有能力去操作CPU底层指令的能力所以Java的Unsafe
类调用C++
来完成对CPU的指令操作。如下代码中的incrementAndGet()
就并不是依靠synchronized
来保持同步的,依靠的是底层CPU指令。Atomic底层原理
在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是"原子操作",因为中断只能发生于指令之间(因为线程的调度需要通过中断完成)。这也是某些CPU指令系统中引入了test_and_set、test_and_clear等指令用于临界资源互斥的原因。在对称多处理器(Symmetric Multi-Processor)结构中就不同了,由于系统中有多个处理器在独立地运行,即使能在单条指令中完成的操作也有可能受到干扰。
在x86 平台上,CPU提供了在指令执行期间对总线加锁的手段。CPU芯片上有一条引线#HLOCK pin,如果汇编语言的程序中在一条指令前面加上前缀"LOCK",经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。 当然,并不是所有的指令前面都可以加lock前缀的,只有ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG,DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, 和 XCHG指令前面可以加"LOCK"指令,实现原子操作。
Atomic的核心操作就是CAS(compare and set,利用CMPXCHG指令实现,它是一个原子指令),该指令有三个操作数,变量的内存值V(value的缩写),变量的当前预期值E(exception的缩写),变量想要更新的值U(update的缩写),当内存值和当前预期值相同时,将变量的更新值覆盖内存值,执行伪代码如下。
现在我们就用CAS操作来解决上述问题。B线程将内存中的变量i读取一个临时变量中(假设此时读取的值为0),然后再将i的值读取到core1的算数运算单元中,接下来进行加1操作,比较临时变量中的值和i当前的值是否相同,如果相同用运算单元中的结果(即i+1)的值覆盖内存中i的值(注意这一部分就是CAS操作,它是个原子操作,不能被中断且其它线程中的CAS操作不能同时执行,否则指令执行失败。如果指令失败,说明A线程已经将i的值加1。由此可知如果两个线程一开始读取的i的值为都为0,那么必然只有一个线程的CAS操作能够成功,因为CAS操作不能并发执行。对于CAS操作执行失败的线程,只要循环执行CAS操作,那么一定能够成功。可以看到并没有线程阻塞,这和synchronize的原理有着本质的不同。
Atomic包简介及源码分析
- Atomic包中的类按照操作的数据类型可以分成4组
- 线程安全的基本类型的原子性操作
- AtomicBoolean,AtomicInteger,AtomicLong
- 线程安全的数组类型的原子性操作,它操作的不是整个数组,而是数组中的单个元素
- AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
- 基于反射原理对象中的基本类型(长整型、整型和引用类型)进行线程安全的操作
- AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
- 线程安全的引用类型及防止ABA问题的引用类型的原子操作
- AtomicReference ,AtomicMarkableReference,AtomicStampedReference
- 有参构造函数
从构造函数函数可以看出,数值存放在成员变量value中
成员变量value声明为volatile类型,说明了多线程下的可见性,即任何一个线程的修改,在其它线程中都会被立刻看到
- compareAndSet方法
value的值通过内部this和valueOffset传递
- getAndSet方法
这个方法就是最核心的CAS操作, 在该方法中调用了
compareAndSet
方法如果在执行if (compareAndSet(current, newValue) 之前其它线程更改了value的值,那么导致 value 的值必定和current的值不同,compareAndSet执行失败,只能重新获取value的值,然后继续比较,直到成功。
- i++实现
- ++i实现
AtomicInteger例子
下面的程序,利用AtomicInteger模拟卖票程序,运行结果中不会出现两个程序卖了同一张票,也不会卖到票为负数
ABA问题
可以发现,CAS实现的过程是先取出内存中某时刻的数据,在下一时刻比较并替换,那么在这个时间差会导致数据的变化,此时就会导致出现“ABA”问题。
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。
尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
Loading...