一、线程安全问题
1. 一个典型的线程不安全的例子
- 多个线程同时操作同一份资源的(主要是进行读写操作)时候,就有可能会发生线程安全问题;比如两个人同时对同一个账户进行取款操作的时候,就有可能会出现余额为负数的结果。
- 示例:两个人同时操作一个账户
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
- 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;
}
}
- 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;
}
}
- 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的作用主要有三个:
- 确保线程互斥的访问同步代码;
- 保证共享变量的修改能够及时可见;
- 有效解决重排序问题;
4. 用法
从语法上讲,Synchronized总共有三种用法:
- 修饰普通方法
- 修饰静态方法
- 修饰代码块
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. 锁的优缺点对比
四、原子操作的实现原理
原子操作是指不可中断的一个操作或者一系列操作
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. 不能使用缓存锁的情况
- 共享数据不在缓存中
- 不支持缓存的处理器
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会存在两个问题
- ABA问题:一个变量初始值是A,变成了B,又变成了A;在CAS操作时,认为变量没有发生变化;解决方式是加版本号:1A->2B->3C;Java中提供了AtomicStampedReference类来解决ABA问题
- 循环时间长开销大:当设置值不成功时,会循环进行CAS操作,占用CPU,造成开销过大
2. 利用锁
Java中第二种原子操作的方式是利用锁:偏向锁,轻量级锁,互斥锁(重量级锁);其实除了偏向锁,轻量级锁和互斥锁的实现原理也是利用CAS操作,来获取锁和释放锁
五、死锁
- 死锁:当两个线程互相等待对方释放同步监视器时就会发生死锁
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()方法");
}
}