线程安全问题

多个线程同时访问同一资源的时候有可能会出现信息不一致的情况,这是线程安全问题,下面是一个例子,

Account.class , 定义一个Account模型

 package threads.sync;

 public class Account {
private String accountNo;
private double balance;
public Account() {} 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;
} public double getBalance() {
return balance;
} public void setBalance(double balance) {
this.balance = balance;
} public int hashCode() {
return accountNo.hashCode();
} public boolean equals(Object obj) {
if (this == obj) return true;
if (obj != null && obj.getClass() == Account.class) {
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}

DrawThread.class ,定义一个取钱类,用来操作Account

 package threads.sync;

 public class DrawThread extends Thread {
private Account account;
private double drawAmount;
public DrawThread(String name, Account account, double drawAmount) {
super(name);
this.setAccount(account);
this.setDrawAmount(drawAmount);
}
public Account getAccount() {
return account;
}
public void setAccount(Account account) {
this.account = account;
}
public double getDrawAmount() {
return drawAmount;
}
public void setDrawAmount(double drawAmount) {
this.drawAmount = drawAmount;
} public void run() {
if (account.getBalance() >= drawAmount) {
System.out.println(getName()+" draw money: "+drawAmount);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.setBalance(account.getBalance() - drawAmount);
System.out.println(getName()+" balance : "+account.getBalance());
} else {
System.out.println("failed for insufficient balance ");
}
}
}

DrawTest.class , 写一个测试类

 package threads.sync;

 public class DrawTest {
public static void main(String[] args) {
Account acc = new Account("123456",1000);
new DrawThread("Thread-A",acc,800).start();
new DrawThread("Thread-B",acc,800).start();
}
}

执行结果,

 Thread-B draw money: 800.0
Thread-A draw money: 800.0
Thread-B balance : 200.0
Thread-A balance : -600.0

可见这里出现了逻辑错误,B线程取出800元后,账户里应该只剩下200元,但是接着A线程却也取出了800元,而且最终账户余额还成了负数,显然是不对的。

造成上面错误的过程如下,当B线程执行到DreadThread类的第28行时,已经成功取出了800元,然后进入了sleep状态,没有继续下面的扣除余额的动作;

此时JVM调度器将CPU切换到A线程执行,由于此时余额尚未扣除,A也能取出800元,之后A也进入sleep状态。

接着B线程从sleep状态经历了1毫秒之后,进入了就绪状态,接着获取了CPU进入了运行状态,进行了后面的动作,余额变成了200元。

最后A线程也醒来并获得继续运行机会,也做了一次扣款,结果余额变成了-600元(200-800)

以上便是一个典型的线程安全问题。

同步代码块

解决上面线程安全问题的一种办法是同步代码块,使得一块代码同一时间只能在一个线程中执行,也就是常说的同步监视器原理。同步代码格式如下,

synchronized(obj)

{

/*

* 需要同步的代码块

*/

}

这表示JVM使用obj对象作为同步监视器(通常使用被并发访问的对象),线程执行这段代码之前,必须先获取对同步监视器的锁定。

下面是一个用account对象作为同步监视器的例子,

其他类用前面例子不变,唯一需要修改的是DrawThread.class

 package threads.sync;

 public class DrawThread extends Thread {
private Account account;
private double drawAmount;
public DrawThread(String name, Account account, double drawAmount) {
super(name);
this.setAccount(account);
this.setDrawAmount(drawAmount);
}
public Account getAccount() {
return account;
}
public void setAccount(Account account) {
this.account = account;
}
public double getDrawAmount() {
return drawAmount;
}
public void setDrawAmount(double drawAmount) {
this.drawAmount = drawAmount;
} public void run() { synchronized(account) {
if (account.getBalance() >= drawAmount) {
System.out.println(getName()+" draw money: "+drawAmount);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.setBalance(account.getBalance() - drawAmount);
System.out.println(getName()+" balance : "+account.getBalance());
} else {
System.out.println("failed for insufficient balance ");
}
}
}
}

可以看到只是在线程执行体中加了synchronized(account) { }来将一块代码锁定,保证同一时间这段代码只能被一个线程执行。

执行结果,可以看到线程B去取款时已经没有足够余额了,所以失败,这与我们的设计初衷是相符的。

 Thread-A draw money: 800.0
Thread-A balance : 200.0
failed for insufficient balance

同步方法

同步方法与同步代码块非常相似,只不过同步方法是将整个方法修饰为安全的线程访问方法,注意不能修饰static方法。

同步方法的监视器是this,即调用该方法的对象。不需要显示地指定监视器。

下面的是同步方法的例子,在Accont.class中,我们新加入一个同步方法draw,用来替代原来DrawThread中取款的线程执行体,

 package threads.sync;

 public class Account {
private String accountNo;
private double balance;
public Account() {} 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;
} public double getBalance() {
return balance;
} public void setBalance(double balance) {
this.balance = balance;
} public int hashCode() {
return accountNo.hashCode();
} public boolean equals(Object obj) {
if (this == obj) return true;
if (obj != null && obj.getClass() == Account.class) {
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
} public synchronized void draw(double drawAmount) {
if ( balance >= drawAmount) {
System.out.println(Thread.currentThread().getName()+" draw money: "+drawAmount);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= drawAmount;
System.out.println(Thread.currentThread().getName()+" balance : "+balance);
} else {
System.out.println("failed for insufficient balance ");
}
} }

修改DrawThread.class,我们直接在线程执行体中调用Account.class中的同步方法,调用同步方法的对象account将成为同步监视器被加锁,

 package threads.sync;

 public class DrawThread extends Thread {
private Account account;
private double drawAmount;
public DrawThread(String name, Account account, double drawAmount) {
super(name);
this.setAccount(account);
this.setDrawAmount(drawAmount);
}
public Account getAccount() {
return account;
}
public void setAccount(Account account) {
this.account = account;
}
public double getDrawAmount() {
return drawAmount;
}
public void setDrawAmount(double drawAmount) {
this.drawAmount = drawAmount;
} public void run() {
// account对象将作为同步监视器被加锁
account.draw(drawAmount);
}
}

执行结果,

 Thread-A draw money: 800.0
Thread-A balance : 200.0
failed for insufficient balance

释放同步监视器的锁定

任何线程在进入同步代码块或同步方法之前,需要先获取同步监视器的锁定,最终会释放锁定(但不是显示地释放)。那么在什么情况下同步监视器锁定会被线程释放呢?

  • 当前线程的同步方法,同步代码块结束,当前线程释放同步监视器
  • 当前线程在同步方法,同步代码块中遇到break,return终止了执行的时候,当前线程会释放同步监视器
  • 当前线程在同步方法,同步代码块中出现了未处理的Error或Exception,导致无法继续执行下去,当前线程会释放同步监视器
  • 当前线程在执行同步方法,同步代码块时,程序执行了同步监视器对象的wait方法,则当前线程暂停,并释放同步监视器

以下情况线程不会释放同步监视器,

  • 线程在执行同步方法,同步代码块时,程序调用sleep(), yield()方法来暂停当前线程时,当前线程不会释放同步监视器
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器。

同步锁

同步锁可以显示地获取锁和释放锁,ReentrantLock是最常使用的同步锁。结合try .. finally {} 机制,可以确保同步锁在必要时得到释放。

JAVA8中提供了一个StampedLock类,可为读写操作提供不同模式,例如Reading, Writing, ReadingOptimitic...

下面是一个同步锁的例子,修改前面的Account.class,引入同步锁进行加锁和释放锁,其他类保持不变,

 package threads.sync;

 import java.util.concurrent.locks.ReentrantLock;

 public class Account {
private final ReentrantLock lock = new ReentrantLock();
private String accountNo;
private double balance;
public Account() {} 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;
} public double getBalance() {
return balance;
} public void setBalance(double balance) {
this.balance = balance;
} public int hashCode() {
return accountNo.hashCode();
} public boolean equals(Object obj) {
if (this == obj) return true;
if (obj != null && obj.getClass() == Account.class) {
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
} public void draw(double drawAmount) {
lock.lock();
try {
if ( balance >= drawAmount) {
System.out.println(Thread.currentThread().getName()+" draw money: "+drawAmount);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= drawAmount;
System.out.println(Thread.currentThread().getName()+" balance : "+balance);
} else {
System.out.println("failed for insufficient balance ");
}
} finally {
lock.unlock();
}
} }

执行结果,在DrawThread.class中,通过调用account的draw方法,使用ReentrantLock的对象对取款操作进行同步锁操作,

 Thread-A draw money: 800.0
Thread-A balance : 200.0
failed for insufficient balance

死锁

当两个线程互相等待对方释放同步监视器时就会发生死锁。

死锁很容易发生,尤其在有多个同步监视器的时候,下面就是一个例子,

A.class

 package threads.sync;

 public class A {
public synchronized void foo(B b) {
System.out.println(Thread.currentThread().getName()+": entered A.foo()");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+": trying to call B.last() ");
b.last();
} public synchronized void last() {
System.out.println("A.last() executing");
}
}

B.class

 package threads.sync;

 public class B {
public synchronized void bar(A a) {
System.out.println(Thread.currentThread().getName()+": entered B.bar()");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+": trying to call A.last() ");
a.last();
}
public synchronized void last() {
System.out.println("B.last() executing");
}
}

A和B两个类中的方法都是同步方法,通过线程执行体调用的话会对调用对象加锁(将调用对象作为同步监视器),例如下面这样,

DeadLock.class

 package threads.sync;

 public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public void init() {
Thread.currentThread().setName("main Thread");
a.foo(b);
System.out.println("after entering main Thread");
} @Override
public void run() {
Thread.currentThread().setName("sub Thread");
b.bar(a);
System.out.println("after entering sub Thread");
} public static void main(String[] args) {
DeadLock dl = new DeadLock();
new Thread(dl).start();
dl.init();
} }

执行结果,

 main Thread: entered A.foo()
sub Thread: entered B.bar()
sub Thread: trying to call A.last()
main Thread: trying to call B.last()

上面的执行结果到第4行的时候并没有结束,而是有两个线程处于阻塞状态,且这两个线程各自锁定一个同步监视器,同时又各自在请求对方的同步监视器,因此就陷入了死锁状态,具体过程如下,

  1. init()首先被执行(先后顺序随机),
  2. main 线程中调用a对象的foo方法,则main线程对a对象锁定,当main线程执行到foo方法中的第7行时,进入sleep状态(main线程不会释放同步监视器a),CPU切换到sub线程,
  3. sub线程中调用了b 对象的bar方法,于是sub 线程对b对象锁定,当sub线程执行到bar方法第7行时,也进入sleep状态(sub线程不会释放同步监视器b),
  4. main线程的由于先进入sleep所以会先醒来继续执行到foo方法第12行时,尝试调用b对象的同步方法last(),需要先锁定同步监视器b,
  5. 由于此时sub线程还处于sleep状态,并未释放同步监视器b,所以main线程将因此阻塞(依然不会释放同步监视器a),
  6. 当sub线程醒来之后,执行到bar方法第12行,尝试调用a对象的同步方法last(),需要先锁定同步监视器a,
  7. 由于此时main线程还处于阻塞状态并且锁定了同步监视器a,所以sub线程也会因此进入阻塞状态(依然不会释放同步监视器b),
  8. 至此,就形成了main线程持有同步监视器a,请求获取同步监视器b,而sub线程持有同步监视器b,请求获取同步监视器a的死锁局面

对于线程同步,出于性能方面考虑,有如下原则,(参考自阿里巴巴Java开发手册)

6. 【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能

锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。

7. 【强制】对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造

成死锁。

说明:线程一需要对表A、B、C依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序

也必须是A、B、C,否则可能出现死锁。

8. 【强制】并发修改同一记录时,避免更新丢失,要么在应用层加锁,要么在缓存加锁,要么在

数据库层使用乐观锁,使用version作为更新依据。

说明:如果每次访问冲突概率小于20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次

数不得小于3次。

JAVA基础知识之多线程——线程同步的更多相关文章

  1. JAVA基础知识之多线程——线程组和未处理异常

    线程组 Java中的ThreadGroup类表示线程组,在创建新线程时,可以通过构造函数Thread(group...)来指定线程组. 线程组具有以下特征 如果没有显式指定线程组,则新线程属于默认线程 ...

  2. JAVA基础知识之多线程——线程通信

    传统的线程通信 Object提供了三个方法wait(), notify(), notifyAll()在线程之间进行通信,以此来解决线程间执行顺序等问题. wait():释放当前线程的同步监视控制器,并 ...

  3. JAVA基础知识之多线程——线程池

    线程池概念 操作系统或者JVM创建一个线程以及销毁一个线程都需要消耗CPU资源,如果创建或者销毁线程的消耗源远远小于执行一个线程的消耗,则可以忽略不计,但是基本相等或者大于执行线程的消耗,而且需要创建 ...

  4. JAVA基础知识之多线程——线程的生命周期(状态)

    线程有五个状态,分别是新建(New).就绪(Runnable).运行(Running).阻塞(Blocked)和死亡(Dead). 新建和就绪 程序使用new会新建一个线程,new出的对象跟普通对象一 ...

  5. java基础知识总结--多线程

    1.扩展Java.lang.Thread类 1.1.进程和线程的区别: 进程:每个进程都有自己独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1~n个线程. 线程:同一类线 ...

  6. Java 基础【07】线程同步锁的选择

    在需要线程同步的时候如何选择合适的线程锁? 例:选择可以存入到常量池当中的对象,String对象等 public class SyncTest { private String name = &quo ...

  7. JAVA基础知识之多线程——控制线程

    join线程 在某个线程中调用其他线程的join()方法,就会使当前线程进入阻塞状态,直到被join线程执行完为止.join方法类似于wait, 通常会在主线程中调用别的线程的join方法,这样可以保 ...

  8. Java基础知识(多线程和线程池)

    新建状态: 一个新产生的线程从新状态开始了它的生命周期.它保持这个状态直到程序 start 这个线程. 运行状态:当一个新状态的线程被 start 以后,线程就变成可运行状态,一个线程在此状态下被认为 ...

  9. JAVA基础知识总结13(同步)

    好处:解决了线程安全问题. 弊端:相对降低性能,因为判断锁需要消耗资源,还容易产生了死锁. 定义同步是有前提的: 1,必须要有两个或者两个以上的线程,才需要同步. 2,多个线程必须保证使用的是同一个锁 ...

随机推荐

  1. HTML语言的一些元素(一)

    1)标题的六个等级:<h1>,<h2>,<h3>,<h4>,<h5>,<h6>.如果写了诸如:<h7>等,则作为正文 ...

  2. Swift语言实战晋级-第9章 游戏实战-跑酷熊猫-3 显示一个动态的熊猫

    一个静态的熊猫明显不能满足我们的欲望,接下来我们就让熊猫跑起来.序列帧动画的原理就是不停的切换不同的图片.当我们将一张一张的切换Panda类的跑动文理后,熊猫就跑起来了.那么首先我们需要一个数组常量来 ...

  3. Ways to access Oracle Database in PostgreSQL

    Today, organizations stores information(data) in different database systems. Each database system ha ...

  4. JavaScript----插入视频

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  5. vs2015社区版不支持installshield

    开始学习vs了 下一步就是在这个平台上提升技术咯. 写了老师上课讲的算法 生成了个VB程序结果不能在别的电脑上运行 .幸好我自己测试了下.要是直接拿去学校不是很~ 然后我就想到当初接触易语言时候的搞静 ...

  6. Mysql索引总结(一)

    数据库开发中索引的使用占了很重要的位置,好的索引会使数据库的读写效率加倍,烂的索引则会拖累整个系统甚至引发灾难. 索引分三类: index ----普通的索引,数据可以重复 unique ----唯一 ...

  7. 夺命雷公狗—angularjs—16—angularjs里面的缓存

    强大的angularjs也给我们预留了一套他的缓存机智,这样在某个程度上来说还是可以做到减轻一点服务器压力的.... <!DOCTYPE html> <html lang=" ...

  8. zw版【转发·台湾nvp系列Delphi例程】HALCON Cast 使用方式

    zw版[转发·台湾nvp系列Delphi例程]HALCON Cast 使用方式 procedure TForm1.Button1Click(Sender: TObject);var img, img1 ...

  9. NOIP200503采药

                          NOIP200503采药 [问题描述] 辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师.为此,他想拜附近最有威望的医师为师.医师为了判断他的资质, ...

  10. jsp页面的跳转取值

    <p >工单管理 >> <c:if test="${code eq 0}">全部工单>>详情页</c:if> <c ...