Java多线程之线程的同步



实际开发中我们也经常提到说线程安全问题,那么什么是线程安全问题呢?

线程不安全就是说在多线程编程中出现了错误情况,由于系统的线程调度具有一定的随机性,当使用多个线程来访问同一个数据时,非常容易出现线程安全问题。具体原因如下:

  1,多个线程同时访问一个数据资源(该资源称为临界资源),形成数据发生不一致和不完整。

  2,数据的不一致往往是因为一个线程中的多个关联的操作(这几个操作合成原子操作)未全部完成。



关于线程安全问题,有一个经典的情景:银行取钱。代码如下:

/**
*
* @version 1L
* @author LinkinPark
* @since 2015-2-4
* @motto 梦似烟花心似水,同学少年不言情
* @desc ^ 账户类,封装账户编号、账户余额两个Field
*/
public class Account
{
private String accountNo;//账户编号
private double balance;//账户余额 public Account()
{
} public Account(String accountNo, double balance)
{
this.accountNo = accountNo;
this.balance = balance;
} public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
} public String getAccountNo()
{
return this.accountNo;
} public void setBalance(double balance)
{
this.balance = balance;
} public double getBalance()
{
return this.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;
} }
/**
*
* @version 1L
* @author LinkinPark
* @since 2015-2-4
* @motto 梦似烟花心似水,同学少年不言情
* @desc ^ 取钱的线程类
*/
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;
} // 当多条线程修改同一个共享数据时,将涉及数据安全问题。比如说:2条线程进来取钱了,一条取完钱了但是还没有修改余额,那么另外一条就又进来了,本来是钱不够了,但是他还以为钱够,所以逻辑上就有错了
public void run()
{
// 账户余额大于取钱数目
if (account.getBalance() >= drawAmount)
{
System.out.println(getName() + "取钱成功!取出的钱是:" + drawAmount);
try
{
Thread.sleep(1000);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t余额为: " + account.getBalance());
}
else
{
System.out.println(getName() + "取钱失败!余额不足!");
}
}
}
public class DrawTest
{
public static void main(String[] args)
{
// 创建一个账户
Account acct = new Account("15158117453", 1000);
// 模拟两个线程对同一个账户取钱
new DrawThread("LinkinPark", acct, 800).start();
new DrawThread("NightWish", acct, 800).start();
}
}

运行上面的程序,控制台结果如下:

  

很明显出错了,正常的应该是LinkinPark可以取钱,取完钱余额变成了200,NightWish就不能再次取钱了。。。



  既然如此,那么如何避免以上的问题呢?加锁。具体有2种方式:1,同步代码库   2,同步方法



使用第一种方式解决上面的问题,我们只需要修改取钱的那个线程类,给取钱的那个线程类中的那个账户添加一个同步监视器就可以了。

/**
*
* @version 1L
* @author LinkinPark
* @since 2015-2-4
* @motto 梦似烟花心似水,同学少年不言情
* @desc ^ 取钱的线程类:“加锁 → 修改 → 释放锁”
*/
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作为同步监视器,任何线程进入下面同步代码块之前,必须先获得对account账户的锁定——其他线程无法获得锁,也就无法修改它
synchronized (account)
{
// 账户余额大于取钱数目
if (account.getBalance() >= drawAmount)
{
// 吐出钞票
System.out.println(getName() + "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t余额为: " + account.getBalance());
}
else
{
System.out.println(getName() + "取钱失败!余额不足!");
}
}
//同步代码块结束,该线程释放同步锁
}
}

现在结果正确了。

 



使用第2种方式解决上面的问题,代码如下:

/**
*
* @version 1L
* @author LinkinPark
* @since 2015-2-4
* @motto 梦似烟花心似水,同学少年不言情
* @desc ^ 账户类,封装账户编号、账户余额两个Field,另外还有取钱的方法
*/
public class Account
{
// 封装账户编号、账户余额两个Field
private String accountNo;
private double balance; public Account()
{
} // 构造器
public Account(String accountNo, double balance)
{
this.accountNo = accountNo;
this.balance = balance;
} // accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
} public String getAccountNo()
{
return this.accountNo;
} // 因此账户余额不允许随便修改,所以只为balance提供getter方法,
public double getBalance()
{
return this.balance;
} // 提供一个线程安全draw()方法来完成取钱操作
public synchronized void draw(double drawAmount)
{
// 账户余额大于取钱数目
if (balance >= drawAmount)
{
// 吐出钞票
System.out.println(Thread.currentThread().getName() + "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
balance -= drawAmount;
System.out.println("\t余额为: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName() + "取钱失败!余额不足!");
}
} // 下面两个方法根据accountNo来重写hashCode()和equals()方法
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;
}
}
/**
*
* @version 1L
* @author LinkinPark
* @since 2015-2-4
* @motto 梦似烟花心似水,同学少年不言情
* @desc ^ 取钱的线程类:“加锁 → 修改 → 释放锁”
*/
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方法来执行取钱
// 同步方法的同步监视器是this,this代表调用draw()方法的对象。
// 也就是说:线程进入draw()方法之前,必须先对account对象的加锁。
account.draw(drawAmount);
}
}

关于上面这2种做法的总结:

第一种做法使用synchronized将run方法里的方法体修改成为同步代码块,去实现"加锁--修改完成--释放锁"的逻辑。这里有一个问题就是如何确定同步代码块中的那个同步监视器是那个对象呢?记住就可以了:这个对象就是可能被并发访问的共享资源。

第二种做法使用synchronized来修饰某个方法,使用这种方式不需要显式指定同步监视器,其实同步监视器也就是他自己了,也就是this。值得注意的是:synchronized关键字可以修饰方法,可以修饰代码块,但是不能修饰构造器和属性。加锁了以后效率肯定会受到影响的,所以我们要有选择的去针对那些操作存在竞争的资源的方法来加锁,不要随便加。

对比上面2种加锁的做法,我们不难发现,其实第2种的设计是比较好的,因为这种做法更符合面向对象设计规则。面向对象里面有一种流行的设计方式,叫做领域驱动设计(DDD),说白了就是说对每一个对象都实现良好的封装,关于操作这个对象的属性或者方法都应该写在这个对象里面,不应该写在别的类中。





  • 释放同步监视器的锁定

经过前面的整理,我们已经知道了,任何线程在进入同步代码块或者是同步方法之前,都会先获得对同步监视器的锁定,那么何时才会去释放这个锁定呢:

1,代码执行结束

2,代码遇见了retrue,break

3,代码中出现了未处理的Error和Exception

4,代码执行同步监视器的wait方法。

注意的是:程序调用Thread的sleep方法和yield方法,或者其他线程调用了该线程的suspend和resume方法都不会释放同步监视器的锁。

  • 最后介绍2个知识点,实际编码中使用到的情景并不多。

1,同步锁(Lock)

JDK1.5后,提供了一个Lock接口。Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。常用的实现类是ReentrantLock。API中是这么介绍ReentrantLock的:一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。

建议总是 立即实践,使用 lock 块来调用 try,在之前/之后的构造中,最典型的代码如下:

 class X {
private final ReentrantLock lock = new ReentrantLock();
// ... public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}

2,死锁

死锁就是说当2个线程互相等待对方来释放同步监视器。值得注意的是:死锁不报错的,整个程序不会异常,也不会给出任何的提示,只是所有的线程都处于阻塞状态,无法继续了。下面给出一个例子:

/**
*
* @version 1L
* @author LinkinPark
* @since 2015-2-4
* @motto 梦似烟花心似水,同学少年不言情
* @desc ^ 出现死锁的一个例子
*/
class A
{
public synchronized void foo(B b)
{
System.out.println("当前线程名: " + Thread.currentThread().getName() + " 进入了A实例的foo方法");
try
{
Thread.sleep(200);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用B实例的last方法");
b.last();
} public synchronized void last()
{
System.out.println("进入了A类的last方法内部");
}
} class B
{
public synchronized void bar(A a)
{
System.out.println("当前线程名: " + Thread.currentThread().getName() + " 进入了B实例的bar方法");
try
{
Thread.sleep(200);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用A实例的last方法");
a.last();
} public synchronized void last()
{
System.out.println("进入了B类的last方法内部");
}
} public class DeadLock implements Runnable
{
A a = new A();
B b = new B(); public void init()
{
Thread.currentThread().setName("主线程");
// 调用a对象的foo方法
a.foo(b);
System.out.println("进入了主线程之后");
} public void run()
{
Thread.currentThread().setName("副线程");
// 调用b对象的bar方法
b.bar(a);
System.out.println("进入了副线程之后");
} public static void main(String[] args)
{
DeadLock dl = new DeadLock();
// 以dl为target启动新线程
new Thread(dl).start();
// 调用init()方法
dl.init();
}
}

Java多线程之线程的同步的更多相关文章

  1. java多线程之线程的同步与锁定(转)

    一.同步问题提出 线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏. 例如:两个线程ThreadA.ThreadB都操作同一个对象Foo对象,并修改Foo对象上的数据. publicc ...

  2. 关于Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇高质量的博文)

    Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇质量高的博文) 前言:在学习多线程时,遇到了一些问题,这里我将这些问题都分享出来,同时也分享了几篇其他博客主的博客,并且将我个人的理解也分享 ...

  3. Java多线程02(线程安全、线程同步、等待唤醒机制)

    Java多线程2(线程安全.线程同步.等待唤醒机制.单例设计模式) 1.线程安全 如果有多个线程在同时运行,而这些线程可能会同时运行这段代码.程序每次运行结果和单线程运行的结果是一样的,而且其他的变量 ...

  4. Java多线程与线程同步

    六.多线程,线程,同步 ①概念: 并行:指两个或多个在时间同一时刻发生(同时发生) 并发:指两个或多个事件在同一时间段内发生 具体概念: 在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多 ...

  5. Java多线程之线程其他类

    Java多线程之线程其他类 实际编码中除了前面讲到的常用的类之外,还有几个其他类也有可能用得到,这里来统一整理一下: 1,Callable接口和Future接口 JDK1.5以后提供了上面这2个接口, ...

  6. Java多线程之线程的通信

    Java多线程之线程的通信 在总结多线程通信前先介绍一个概念:锁池.线程因为未拿到锁标记而发生的阻塞不同于前面五个基本状态中的阻塞,称为锁池.每个对象都有自己的锁池的空间,用于放置等待运行的线程.这些 ...

  7. Java多线程之线程的控制

    Java多线程之线程的控制 线程中的7 种非常重要的状态:  初始New.可运行Runnable.运行Running.阻塞Blocked.锁池lock_pool.等待队列wait_pool.结束Dea ...

  8. JAVA多线程之线程间的通信方式

    (转发) 收藏 记 周日,北京的天阳光明媚,9月,北京的秋格外肃穆透彻,望望窗外的湛蓝的天,心似透过栏杆,沐浴在这透亮清澈的蓝天里,那朵朵白云如同一朵棉絮,心意畅想....思绪外扬, 鱼和熊掌不可兼得 ...

  9. java多线程与线程间通信

    转自(http://blog.csdn.net/jerrying0203/article/details/45563947) 本文学习并总结java多线程与线程间通信的原理和方法,内容涉及java线程 ...

随机推荐

  1. Mac OSX下Sublime Text配置使用Ctags实现代码跳转

    1. 先用brew工具安装ctags,安装路径在/user/local/bin The default ctags executable in OSX does not support recursi ...

  2. Mac appium.dmg. Xcode Command Line Tools

    You need to install the command line tools as marked in your message: ✖ Xcode Command Line Tools are ...

  3. IntelliJ IDEA运行慢解决方法

    今天在用IntelliJ IDEA运行项目时速度奇慢,上网找了一些解决方法,记录一下以供参考. 修改配置文件 IntelliJ IDEA\bin下idea.exe.vmoptions -server ...

  4. PE文件详解(八)

    本文转载自小甲鱼PE文件详解系列教程原文传送门 当应用程序需要调用DLL中的函数时,会由系统将DLL中的函数映射到程序的虚拟内存中,dll中本身没有自己的栈,它是借用的应用程序的栈,这样当dll中出现 ...

  5. Centos7下配置Python3和Python2共存,以及对应版本Ipython安装配置

    1.查看是否已经安装Python Centos7默认安装了python2.7.5 因为一些命令要用它比如yum 它使用的是python2.7.5. 使用python -V命令查看一下是否安装Pytho ...

  6. ajax 处理请求回来的数据

    比如接口 /test, 请求方式get, 请求过来的数据要处理在container 里,如下代码 $.get("/test", {}, function(result){ $(&q ...

  7. 编写Qt Designer自定义控件

    一)流程概述 在使用Qt Designer设计窗体界面时,我们可以使用Widget Box里的窗体控件非常方便的绘制界面,比如拖进去一个按钮,一个文本编辑器等.虽然Qt Designer里的控件可以满 ...

  8. LVS集群之工作原理和调度算法(2)

      LVS的工作机制 LVS里Director本身不响应请求,只是接受转发请求到后方,Realservers才是后台真正响应请求. LVS 工作原理基本类似DNAT,又不完全相像,它是一种四层交换,默 ...

  9. CTF---Web入门第一题 what a fuck!这是什么鬼东西?

    what a fuck!这是什么鬼东西?分值:10 来源: DUTCTF 难度:易 参与人数:7942人 Get Flag:3358人 答题人数:3475人 解题通过率:97% what a fuck ...

  10. HDU 2296:Ring

    Problem Description For the hope of a forever love, Steven is planning to send a ring to Jane with a ...