欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

二、Java并发编程:Java并发机制的底层原理

程序员文章站 2022-05-05 11:06:08
...

一、线程安全问题

1. 一个典型的线程不安全的例子

  1. 多个线程同时操作同一份资源的(主要是进行读写操作)时候,就有可能会发生线程安全问题;比如两个人同时对同一个账户进行取款操作的时候,就有可能会出现余额为负数的结果。
  2. 示例:两个人同时操作一个账户
package concurrency.account;

/**
 * 账户类,主要记录账户余额,以及提供取款方法
 * @author lt
 * @date 2018年7月2日
 * @version v1.0
 */
public class Account {
	private String accountNo;
	private double balance;
	public Account(String accountNo, double balance){
		this.accountNo = accountNo;
		this.balance = balance;
	}
	public String getAccountNo() {
		return accountNo;
	}
	public void setAccountNo(String accountNo) {
		this.accountNo = accountNo;
	}
	//账户余额不允许随便修改,故只提供get方法
	public double getBalance() {
		return balance;
	}
	public void draw(double drawAmount){
		//取钱数不能超过余额数
		if(balance>=drawAmount){
			System.out.println(Thread.currentThread().getName()+"取钱成功!吐出钞票:"+drawAmount);
			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			//修改余额
			balance -= drawAmount;
			System.out.println("\t余额为:"+balance);
		} else {
			System.out.println("余额不足!取钱失败!");
		}
	}
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((accountNo == null) ? 0 : accountNo.hashCode());
		long temp;
		temp = Double.doubleToLongBits(balance);
		result = prime * result + (int) (temp ^ (temp >>> 32));
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Account other = (Account) obj;
		if (accountNo == null) {
			if (other.accountNo != null)
				return false;
		} else if (!accountNo.equals(other.accountNo))
			return false;
		if (Double.doubleToLongBits(balance) != Double
				.doubleToLongBits(other.balance))
			return false;
		return true;
	}
}

package concurrency.account;
/**
 * 取款操作的线程,继承Thread类
 * @author lt
 * @date 2018年7月2日
 * @version v1.0
 */
public class DrawThread extends Thread{

	private Account account;
	private double drawAmount;
	public DrawThread(String name, Account account, double drawAmount){
		super(name);
		this.account = account;
		this.drawAmount = drawAmount;
	}
	public void run(){
		account.draw(drawAmount);
	}
}

package concurrency.account;
/**
 * 测试类测试两个人同时操作同一个账户(取同一个账户的钱)
 * @author lt
 * @date 2018年7月2日
 * @version v1.0
 */
public class DrawTest {

	public static void main(String[] args) {
		for(int i=0; i<10; i++){
			Account account = new Account("0001", 1000);
			new DrawThread("甲", account, 800).start();
			new DrawThread("乙", account, 800).start();
		}
	}
}

/**
 * 输出结果
 */
乙取钱成功!吐出钞票:800.0
甲取钱成功!吐出钞票:800.0
	余额为:200.0
	余额为:-600.0

2. 解决方案:synchronized,lock

  1. synchronized修饰代码块
package concurrency.account;

/**
 * 线程同步:修饰代码块
 * @author lt
 * @date 2018年7月2日
 * @version v1.0
 */
public class Account {
	private String accountNo;
	private double balance;
	public Account(String accountNo, double balance){
		this.accountNo = accountNo;
		this.balance = balance;
	}
	public String getAccountNo() {
		return accountNo;
	}
	public void setAccountNo(String accountNo) {
		this.accountNo = accountNo;
	}
	//账户余额不允许随便修改,故只提供get方法
	public double getBalance() {
		return balance;
	}
	public void draw(double drawAmount){
		/**
		 * 一、synchronized加锁机制
		 * 1.synchronized关键字修饰代码块或者方法,同步监视器为this;
		 * 2.任何时刻,只能有一个线程获得同步监视器的锁,进而对资源进行操作;
		 * 二、synchronized释放锁
		 * 1.代码块正常终止或抛出异常;
		 * 2.调用同步监视器的wait()方法;
		 */
		synchronized(this){
			//取钱数不能超过余额数
			if(balance>=drawAmount){
				System.out.println(Thread.currentThread().getName()+"取钱成功!吐出钞票:"+drawAmount);
				try {
					Thread.sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				//修改余额
				balance -= drawAmount;
				System.out.println("\t余额为:"+balance);
			} else {
				System.out.println("余额不足!取钱失败!");
			}
		}
	}
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((accountNo == null) ? 0 : accountNo.hashCode());
		long temp;
		temp = Double.doubleToLongBits(balance);
		result = prime * result + (int) (temp ^ (temp >>> 32));
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Account other = (Account) obj;
		if (accountNo == null) {
			if (other.accountNo != null)
				return false;
		} else if (!accountNo.equals(other.accountNo))
			return false;
		if (Double.doubleToLongBits(balance) != Double
				.doubleToLongBits(other.balance))
			return false;
		return true;
	}
}

  1. synchronized修饰方法(不能修饰static方法)
package concurrency.account;

/**
 * 线程同步:修饰方法
 * @author lt
 * @date 2018年7月2日
 * @version v1.0
 */
public class Account {
	private String accountNo;
	private double balance;
	public Account(String accountNo, double balance){
		this.accountNo = accountNo;
		this.balance = balance;
	}
	public String getAccountNo() {
		return accountNo;
	}
	public void setAccountNo(String accountNo) {
		this.accountNo = accountNo;
	}
	//账户余额不允许随便修改,故只提供get方法
	public double getBalance() {
		return balance;
	}
	/**
	 * 一、synchronized加锁机制
	 * 1.synchronized关键字修饰代码块或者方法,同步监视器为this;
	 * 2.任何时刻,只能有一个线程获得同步监视器的锁,进而对资源进行操作;
	 * 二、synchronized释放锁
	 * 1.代码块正常终止或抛出异常;
	 * 2.调用同步监视器的wait()方法;
	 */
	public synchronized void draw(double drawAmount){
		//取钱数不能超过余额数
		if(balance>=drawAmount){
			System.out.println(Thread.currentThread().getName()+"取钱成功!吐出钞票:"+drawAmount);
			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			//修改余额
			balance -= drawAmount;
			System.out.println("\t余额为:"+balance);
		} else {
			System.out.println("余额不足!取钱失败!");
		}
	}
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((accountNo == null) ? 0 : accountNo.hashCode());
		long temp;
		temp = Double.doubleToLongBits(balance);
		result = prime * result + (int) (temp ^ (temp >>> 32));
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Account other = (Account) obj;
		if (accountNo == null) {
			if (other.accountNo != null)
				return false;
		} else if (!accountNo.equals(other.accountNo))
			return false;
		if (Double.doubleToLongBits(balance) != Double
				.doubleToLongBits(other.balance))
			return false;
		return true;
	}
}

  1. luck加锁
package concurrency.account;

import java.util.concurrent.locks.ReentrantLock;

/**
 * 线程同步
 * @author lt
 * @date 2018年7月2日
 * @version v1.0
 */
public class Account {
	private ReentrantLock lock = new ReentrantLock();
	private String accountNo;
	private double balance;
	public Account(String accountNo, double balance){
		this.accountNo = accountNo;
		this.balance = balance;
	}
	public String getAccountNo() {
		return accountNo;
	}
	public void setAccountNo(String accountNo) {
		this.accountNo = accountNo;
	}
	//账户余额不允许随便修改,故只提供get方法
	public double getBalance() {
		return balance;
	}
	/**
	 * 一、luck加锁机制
	 * 1.显示加锁,显示释放
	 */
	public void draw(double drawAmount){
		/**
		 * 加锁
		 */
		lock.lock();
		try{
			//取钱数不能超过余额数
			if(balance>=drawAmount){
				System.out.println(Thread.currentThread().getName()+"取钱成功!吐出钞票:"+drawAmount);
				try {
					Thread.sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				//修改余额
				balance -= drawAmount;
				System.out.println("\t余额为:"+balance);
			} else {
				System.out.println("余额不足!取钱失败!");
			}
		} finally {
			/**
			 * 释放
			 */
			lock.unlock();
		}
	}
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((accountNo == null) ? 0 : accountNo.hashCode());
		long temp;
		temp = Double.doubleToLongBits(balance);
		result = prime * result + (int) (temp ^ (temp >>> 32));
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Account other = (Account) obj;
		if (accountNo == null) {
			if (other.accountNo != null)
				return false;
		} else if (!accountNo.equals(other.accountNo))
			return false;
		if (Double.doubleToLongBits(balance) != Double
				.doubleToLongBits(other.balance))
			return false;
		return true;
	}
}

通过上边的案例,我们了解到,在使用多线程的时候,可能会发生线程安全的问题,加锁是处理线程安全问题的常见方式,接下来,就来深入了解一下Java并发机制的底层原理,这样做可以更好的使用并多线程来解决问题

二、volatile

用于保证共享变量在多个线程之间的可见性(当一个线程修改变量时,其他线程可以读取到修改的值),不会引起线程上下文的切换与调度,是轻量级的synchronized

1. volatile的定义与实现原理

定义:当一个变量被volatile修饰,Java线程内存模型保证任一线程对此变量的修改,其他线程均可读取到修改的值

原理:

三、synchronized

1. 简介

synchronized用于修饰代码块或者方法,被synchronized修饰的代码块或者方法,同一时间只能有一个线程在执行,其余线程只能等待该线程执行结束后才能继续执行;

2. 原理

由JVM规范可以了解,synchronized在JVM底层基于monitor对象的进入和退出来实现方法和代码块的同步;对于代码块同步使用的是monitorenter和monitorexit指令实现;monitorenter在代码编译后插入同步代码块的开始位置,monitorexit插入结束和异常位置;每一个对象都有一个monitor对象与之关联,当monitor对象被线程持有时,对象处于锁定状态

3. 作用

synchronized的作用主要有三个:

  1. 确保线程互斥的访问同步代码;
  2. 保证共享变量的修改能够及时可见;
  3. 有效解决重排序问题;

4. 用法

从语法上讲,Synchronized总共有三种用法:

  1. 修饰普通方法
  2. 修饰静态方法
  3. 修饰代码块

5. synchronized优化

使用监视器monitor来实现,而监视器monitor依赖于底层操作系统的Mutex Lock来实现。基于Mutex Lock进行线程切换时间较长,成本较高,所以称synchronized为重量级锁。为了提高性能,JDK1.6之后,引入了偏向锁,轻量级锁

6. 偏向锁

Java SE 1.6为了减少获得锁和释放锁时的资源消耗,引入了偏向锁和轻量锁,至此Java中的锁有四种状态,级别由低到高:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态;锁可以升级但是不能降级;锁的状态保存在对象头中,以32位JDK为例:

锁状态

25 bit

4bit

1bit 2bit
23bit 2bit 是否是偏向锁 锁标志位
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向互斥量(重量级锁)的指针 10
GC标记 11
偏向锁 线程ID Epoch 对象分代年龄 1 01
无锁 对象的hashCode 对象分代年龄 0 01

定义:偏向锁更像一种策略,用于降低多个线程在竞争获取锁的代价;它是通过在对象头和栈帧中记录偏向锁的线程ID,之后线程在进入和退出同步块时不需要CAS操作来加锁和解锁;当其他线程竞争锁的时候,偏向锁会撤销;Java 6和Java 7中默认启用偏向锁;可以通过-XX:BiasedLocking来禁用偏向锁;

7. 轻量级锁

8. 锁的优缺点对比

二、Java并发编程:Java并发机制的底层原理

四、原子操作的实现原理

原子操作是指不可中断的一个操作或者一系列操作

1. 处理器如何实现原子操作

32位IA-32处理器通过总线加锁缓存加锁的方式实现原子操作

1. 通过总线锁保证原子性

举个栗子:两个处理器执行同一条指令:i++,(i++指令可以拆分成三步:第一步,从内存中读取i的值;第二步,i+1;第三步,i赋值);两个处理器在同时执行时,有可能会发生这种情况:cpu1和cpu2并行执行第1,2,3步,执行完成后,内存中的i的值为2;多个处理器的情况下,这是有可能发生的;为了保证原子性操作,可以使用处理器提供的总线锁,在cpu1执行时,使用总线锁在总线上输出Lock#信号,其他处理器被阻塞,cpu1独占内存

2. 通过缓存锁保证原子性

通过总线锁的说明可知:总线锁锁住了其他cpu和内存之间的通信,开销巨大;缓存锁是指在修改缓存中的数据时,修改完成后,缓存回写到内存中,其他cpu重新从内存中读取

3. 不能使用缓存锁的情况
  1. 共享数据不在缓存中
  2. 不支持缓存的处理器

2. Java如何实现原子操作

1. 利用循环CAS实现原子操作

CAS(Compare and swap),即比较并交换;JVM的CAS利用的是处理器的CMPXCHG指令实现的;自旋CAS的核心操作即:循环进行CAS操作,直至成功为止;CAS也是实现我们平时所说的自旋锁或乐观锁的核心操作

示例

下面的例子展示了线程安全的计数器和非线程安全的计数器,其中线程安全的计数器是利用JUC中的Atomic包下的相关类来实现

package com.lt.thread04;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 1.验证Java利用循环CAS验证操作完成原子操作
 * @author lt
 * @date 2019年5月11日
 * @version v1.0
 */
public class Counter {

	private int m = 0;
	private AtomicInteger n = new AtomicInteger();
	//非线程安全的计数方法
	public void count(){
		m++;
	}
	//利用JUC的相关类实现线程安全的计数器(CAS)
	public void safeCount(){
		//循环进行CAS操作,直至成功为止
		while(true){
			int i = n.get();
			//如果当前值==期望值,则以原子方式将值设置为给定的更新值。相当于i=++i
			boolean flag = n.compareAndSet(i, ++i);
			//如果设置成功,则跳出循环,否则继续设置
			if(flag) break;
		}
	}
	public static void main(String[] args) throws Exception {
		Counter c = new Counter();
		List<Thread> ts = new ArrayList<>();
		for(int i=0; i<1000; i++){
			Thread t = new Thread(new Runnable() {
				@Override
				public void run() {
					c.count();
					c.safeCount();
				}
			}, "线程"+i);
			ts.add(t);
		}
		for(Thread t : ts){
			t.start();
		}
		//等待当前线程执行完毕
		for(Thread t : ts){
			t.join();
		}
		System.out.println(c.m);
		System.out.println(c.n);
	}
}
结果
996
1000
注意

使用CAS会存在两个问题

  1. ABA问题:一个变量初始值是A,变成了B,又变成了A;在CAS操作时,认为变量没有发生变化;解决方式是加版本号:1A->2B->3C;Java中提供了AtomicStampedReference类来解决ABA问题
  2. 循环时间长开销大:当设置值不成功时,会循环进行CAS操作,占用CPU,造成开销过大
2. 利用锁

Java中第二种原子操作的方式是利用锁:偏向锁,轻量级锁,互斥锁(重量级锁);其实除了偏向锁,轻量级锁和互斥锁的实现原理也是利用CAS操作,来获取锁和释放锁

五、死锁

  1. 死锁:当两个线程互相等待对方释放同步监视器时就会发生死锁
package concurrency.deadlock;
/**
 * 死锁验证
 * @author lt
 * @date 2018年7月3日
 * @version v1.0
 */
public class DeadLock {

	public static void main(String[] args) {
		final A a = new A();
		final B b = new B();
		new Thread(new Runnable() {
			@Override
			public void run() {
				a.invoke(b);
			}
		}, "线程1").start();;
		new Thread(new Runnable() {
			@Override
			public void run() {
				b.invoke(a);
			}
		}, "线程2").start();;
	}
}
class A{
	//① 线程一调用A的invoke()方法,并对a对象进行加锁
	public synchronized void invoke(B b){
		System.out.println(Thread.currentThread().getName()+"进入A的invlke()方法");
		//② 线程一休眠100毫秒,CPU切换执行线程二
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		//⑤ 线程一继续运行,调用B的print方法,但是b对象在第③步被加锁,没有释放锁,所以线程阻塞等待锁释放
		b.print();
	}
	public synchronized void print(){
		System.out.println("A的print()方法");
	}
}
class B{
	//③ 线程二调用B的invoke()方法,并对b对象进行加锁
	public synchronized void invoke(A a){
		System.out.println(Thread.currentThread().getName()+"进入B的invlke()方法");
		//④ 线程二休眠100毫秒,CPU切换执行线程一
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		//⑥ 线程二继续运行,调用A的print方法,但是a对象在第①步被加锁,没有释放锁,所以线程阻塞等待锁释放
		a.print();
	}
	public synchronized void print(){
		System.out.println("B的print()方法");
	}
}

参考资料

【1】Java总结篇系列:Java多线程(三)
【2】Java多线程系列目录(共43篇)
【3】Java并发编程的艺术
【4】Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)