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

一、线程安全问题

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

1bit2bit
23bit2bit是否是偏向锁锁标志位
轻量级锁指向栈中锁记录的指针00
重量级锁指向互斥量(重量级锁)的指针10
GC标记11
偏向锁线程IDEpoch对象分代年龄101
无锁对象的hashCode对象分代年龄001

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

7. 轻量级锁

8. 锁的优缺点对比

在这里插入图片描述

四、原子操作的实现原理

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

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底层优化(偏向锁、轻量级锁)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值