线程同步
定义:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时操作。
- 线程排队。
- 共享资源。
- 共享的资源是个变量。
线程同步实现方式
1.同步代码块
将synchronized(共用锁对象){}放在方法内;将需要同步的代码块放入{}中。实现Runnable接口创建线程时,同步对象可以使用this。线程安全。
public class Sychronized_test {
public static void main(String[] args) {
Ticket_Windows ticket_windows = new Ticket_Windows();
Thread thread1 = new Thread(ticket_windows);
Thread thread2 = new Thread(ticket_windows);
Thread thread3 = new Thread(ticket_windows);
thread1.start();
thread2.start();
thread3.start();
}
}
class Ticket_Windows implements Runnable{
private static int ticket = 100;
@Override
public void run() {
while (ticket > 0){
synchronized (this){
if (ticket == 0){
break;
}
System.out.println(Thread.currentThread() + ":" + ticket);
ticket--;
}
}
}
}
2.同步方法
实现Runnable接口创建线程时的情况下:将需要同步的代码块拿出来单独创建一个方法,然后在方法上加上关键字synchornized(加在方法返回类型前)。在run方法里调用此方法。线程安全。
public class Sychronized_test {
public static void main(String[] args) {
Ticket_Windows ticket_windows = new Ticket_Windows();
Thread thread1 = new Thread(ticket_windows);
Thread thread2 = new Thread(ticket_windows);
Thread thread3 = new Thread(ticket_windows);
thread1.start();
thread2.start();
thread3.start();
}
}
class Ticket_Windows implements Runnable{
private static int ticket = 100;
@Override
public void run() {
while (ticket > 0){
try {
sell();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void sell() throws InterruptedException {
if (ticket <= 0){
return;
}
System.out.println(Thread.currentThread() + ":" + ticket);
ticket--;
Thread.sleep(100);
}
}
3.Lock锁
JDK1.5开始,java提供了强大的的线程同步机制----通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock接口时控制多个线程对线程共享资源进行访问的工具。所提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReenTrantLock类实现了Lock,它拥有与synchornized相同的并发性和内存语义,可以显示加锁、释放锁。线程安全。
import java.util.concurrent.locks.ReentrantLock;
public class Sychronized_test {
public static void main(String[] args) {
Ticket_Windows ticket_windows = new Ticket_Windows();
Thread thread1 = new Thread(ticket_windows);
Thread thread2 = new Thread(ticket_windows);
Thread thread3 = new Thread(ticket_windows);
thread1.start();
thread2.start();
thread3.start();
}
}
class Ticket_Windows implements Runnable{
private static int ticket = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (ticket > 0){
lock.lock();
if (ticket <= 0){
return;
}
System.out.println(Thread.currentThread() + ":" + ticket);
ticket--;
lock.unlock();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4.Volatile
后面讲原理,没有用锁,只能修饰变量;是一个轻量级同步机制(低配版synchornized);线程不安全。
import java.util.concurrent.locks.ReentrantLock;
public class Sychronized_test {
public static void main(String[] args) {
Ticket_Windows ticket_windows = new Ticket_Windows();
Thread thread1 = new Thread(ticket_windows);
Thread thread2 = new Thread(ticket_windows);
Thread thread3 = new Thread(ticket_windows);
thread1.start();
thread2.start();
thread3.start();
}
}
class Ticket_Windows implements Runnable{
private volatile int ticket = 100;
@Override
public void run() {
while (ticket > 0){
if (ticket <= 0){
return;
}
System.out.println(Thread.currentThread() + ":" + ticket);
ticket--;
}
}
}
5.原子变量
在java.util.concurrent.atomic包中提供了创建原子类型变量的工具类,使用该类可以简化线程同步。 他是线程安全的。
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
public class Sychronized_test {
public static void main(String[] args) {
Ticket_Windows ticket_windows = new Ticket_Windows();
Thread thread1 = new Thread(ticket_windows);
Thread thread2 = new Thread(ticket_windows);
Thread thread3 = new Thread(ticket_windows);
thread1.start();
thread2.start();
thread3.start();
}
}
class Ticket_Windows implements Runnable{
private static int ticket = 100;
private static AtomicInteger atomicInteger = new AtomicInteger(ticket);
@Override
public void run() {
while(ticket > 0){
ticket= atomicInteger.getAndDecrement();
System.out.println(Thread.currentThread() + ":" + ticket);
}
}
}
JMM内存模型
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(栈空间);工作空间是每个线程的私有数据区域,而java内存模型中规定所有的变量都存储在主内存;主内存是共享内存区域,多有线程都可以访问,但线程对变量的操作(读取赋值)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问到对方的工作内存,线程间的通信(传值)必须通过主内存来完成,访问过程如下:
JMM :java内存模型(java Memory Model)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值刷新回主内存。
- 线程加锁前,必须读取主内存的最新值到自己的工作空间。
- 加锁解锁是同一把锁。
JMM三大特性: 可见性、原子性、有序性。
Volatile三大特性
1.保证变量内存可见性
当一个线程修改了Volatil共享变量,其他线程中的变量能立马知道该变量已经被修改了,并取这个新值到自己的本地内存中。
例如:有A、B两个线程,此时主内存中有个Volatil共享变量,值为5;A,B两个线程均获取到这个变量放到自己的本地内存中。当A线程在自己的本地内存中将这个值修改为2,B线程的本地内存能立马知道这个值被修改了,并且会将将自己本地内存中的5更新为2.。(正常情况下,各线程的本地内存相互独立,是不知道其他线程的操作的)
public class Volatile_test {
public volatile int age = 5;
public void setAge(int age) {
this.age = age;
}
public static void main(String[] args) {
Volatile_test volatile_test = new Volatile_test();
new Thread(new Runnable() {
@Override
public void run() {
volatile_test.setAge(60);
System.out.println(Thread.currentThread() + ":"+ volatile_test.age);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread() + ":" + volatile_test.age);
}
}).start();
System.out.println(Thread.currentThread() + ":" + volatile_test.age);
}
}
2.不保证原子性
原子性:不可分割、完整性。某个线程再执行业务时,中间不可以被加塞或分割。
public class Volatile_test {
public volatile int age = 5;
public void setAge() {
this.age = age + 1;
}
public static void main(String[] args) {
Volatile_test volatile_test = new Volatile_test();
for (int i = 0; i < 4; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//可以保证原子性
// synchronized (Volatile_test.class){
// for (int j = 0; j < 1000; j++) {
// volatile_test.setAge();
//
// }
// }
for (int j = 0; j < 1000; j++) {
volatile_test.setAge();
}
}
},"A"+i).start();
}
System.out.println(Thread.activeCount());
if (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread() + ":" + volatile_test.age);
}
}
不保证原子性的问题原因:出现了写覆盖。例如:A、B两个线程执行+1操作。当线程A完成+1,写入主内存这个期间;线程B被启动,并很快完成了自己的运算得到结果,此时运算的结果和A写进主内存的值一样,B不会重新获取值计算,而是将自己的值写入主内存。此时结果本应该+2,以为写覆盖,结果只+1。
JVM字节码
解决不保证原子性:synchornized、原子变量。
3.禁止指令重排
如何保证可见性、禁止指令重排
内存屏障(Memory Barrier):又称内存栅栏,是一个CPU指令,两个作用。
- 保证特定操作执行顺序。---会在指令中间插入一条Memory Barrier。通过插入内存屏障禁止在内存屏障前后的指令执行重排序。
- 保证某些变量内存的可见性。---强制刷出各种CPU缓存数据。
Volatile作用:
- 解决工作内存与主内存同步延迟现象导致的可见性问题。
- 解决对于指令重排导致的可见性问题和有序性问题。
原子变量与CAS (CompareAndSwap)
原子变量中的compareAndSet方法(CAS思想)。
import java.util.concurrent.atomic.AtomicInteger;
public class Atomic_test {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(20);
atomicInteger.compareAndSet(20,21);
System.out.println(atomicInteger.get());//21
atomicInteger.compareAndSet(20,22);
System.out.println(atomicInteger.get());//21
}
}
compareAndSet(期望值,更新值):如果我们当前值等于期望值,就将当前值改为更新值;如果不等,则当前值不变。-----比较并交换。
原子变量原理:unsafe+CAS
以AtomicInteger中getAndIncrement方法为例讲解: 这个方法解决了在多线程环境中自增1(i++)的线程安全问题。
1.先看看创建AtomicInteger对象时,它里面做了哪些变量的初始化。
Unsafe类:
是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地(native)方法来访问。unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。unsafe类存在于sun.misc包下,其内部方法可以向C指针一样直接操作内存,因为java内存中CAS操作的执行依赖于unsafe类的方法。(unsafe中的方法都是nativate修饰的,也就是说unsafe类中的方法都直接调用操作系统底层资源执行相应任务)
value:
被volatile修饰,保证了多线程之间的内存可见性。
valueoffset :
根据value的初始值,获取该值在内存中的初始地址,unsafe就是根据内存地址获取数据的。
2.调用 getAndIncrement方法。
3.调用getAndInt方法 (CAS)
var5 = this.getIntVolatile(var1, var2);相对于获取快照值;将主内存中的值拷贝至自己的本地内存中(方法栈)。
compareAndSwapInt(var1, var2, var5, var5 + var4) ;相当于将快照中的值与主内存中的值进行对比,看是否是同一个值。是就+1,不是继续循环获取新值。
4.compareAndSwapInt----CAS
CAS的缺陷
- 循环时间长,开销大(自旋锁)。
- 循环CAS只能保证一个共享变量的原子操作。
- ABA问题。---通过版本号解决,不仅仅比较内存(本地内存和主内存)中值是否相等,还要比较这个值的版本是否相等。
Lock锁(AQS+CAS)
1.AQS(AbstractQueueSychronizer)
用来构建锁或者其他同步器组件的重量级基础框架及整个JUC体系的基石;
通过内置的FIFO(CLH)的队列来完成资源获取线程的排队工作,将每个要去抢占资源的线程封装成一个Node节点来实现所得分配,使用一个volatile的int类型的成员变量(State)来表示同步状态,通过CAS完成对State值得修改(0表示资源可用;1表示资源不可用)。
Node节点: (封装的每一个用户线程)
是一个内部类;定义在AbstractQueueSychronizer类的内部,这个内部类里面申明了以下几个变量,这几个变量说明了这个队列是一个双向队列。
- private transient volatile Node head; 头指针
- private transient volatile Node tail; 尾指针
- volatile Node prev; 前指针
- volatile Node next; 后指针
同时还有两个变量:
- volatile Thread thread; 表示处于该节点的线程
- static final Node EXCLUSIVE = null; 表示当前线程的模式是否为排他模式(互斥)。
- volatile int waitStatus; 每个等待线程的状态,不同的值代表当前线程等待的状态。
- 值为0:当一个Node被初始化的时候的默认值。
- 值为1:表示线程获取锁的请求已经取消了。
- 值为-2:表示节点在等待队列中,节点线程等待被唤起。
- 值为-3:当前线程处于SHARED情况下,该字段才会使用。
- 值为-1:表示线程已经准备好,就等资源释放了。
CLH队列 :
小结:AQS=fifo+state
2.lock和AQS之间的关系
3.以 ReentrantLock为例剖析
业务案例代码:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLock_test {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
//带入一个银行办理业务的案例来模拟我们的AQS如何进行线程的管理和通知唤醒机制
//3个线程模拟3个来银行网点,受理窗口办理业务的顾客
//A顾客就是第一个顾客,此时受理窗口没有任何人,A可以直接去办理
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
System.out.println("-----A thread come in");
try {
Thread.sleep(2000);
}catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
},"A").start();
//第二个顾客,第二个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时B只能等待,
//进入候客区
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
System.out.println("-----B thread come in");
try {
Thread.sleep(2000);
}catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
},"B").start();
//第三个顾客,第三个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时C只能等待,
//进入候客区
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
System.out.println("-----C thread come in");
try {
Thread.sleep(2000);
}catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
},"C").start();
}
}
- new ReentrantLock():默认创建非公平锁。在构造方法中传入参数true启用公平锁。
- A 进来的时候,state=0,ReentrantLock的lock方法,调用的是Sync的实现类NonfairSync的lock方法。此时通过CAS的判断,A获得锁,state=1。A开始进行业务操作
- B进来的时候,state=1,调用的是Sync的实现类NonfairSync的lock方法。此时B无法通过CAS判断,调用下面的AQSacquire方法。
- acquire的第一轮判断,tryAcquire方法。实际调用的是Sync中nonfairTryAcquire方法。经过下面的分析,可以得知,nonfairTryAcquire方法返回false.
- acquire的第二轮判断,addWaiter方法 。实际调用的是AQS中的addWaiter方法。
此时B已进入AQS的队列进入等待状态。
-
调用acquireQueued方法
第一次自旋:判断第二个if中第一个条件,将哨兵节点状态改为-1,返回false。
第二次自旋,哨兵节点值为-1,返回true。走第二个条件。
自此B,C都被挂起,等A释放锁。
A.unlock释放锁:调用Syn中release方法,这个方法中又执行了两个方法。
tryRelease:
unparkSuccessor :
返回到上面acquireQueued方法中,B被唤醒,第二次自旋后B被阻塞了,接着往下走。再次进行自旋,此时再次尝试获取锁,因为A走了,可以获取到锁;B上位,进入到第一个if语句中,删除掉哨兵节点,让头节点指向B节点,将B节点中的信息都置为null,成为新的哨兵节点,结束自旋。
Synchonized原理
1.Synchonized保证可见性、原子性、有序性、可重入性。
2.案例代码
3.反汇编 java -p -v class路径
同步代码块:
main方法的反汇编
小结:
monitorenter:
monitorexit:
可以看到,反汇编中有两个 monitorexit指令,这是当同步代码块报异常时,也会释放锁。
同步方法
test方法的反汇编