一 同步代码块
1.为了解决并发操作可能造成的异常,java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。其语法如下:
synchronized(obj){
//同步代码块
}
其中obj就是同步监视器,它的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。虽然java程序允许使用任何对象作为同步监视器,但 是同步监视器的目的就是为了阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。
2.小例子
Account.java
public class Account {
private String accountNo;
private double balance;
public Account(String accountNo,double balance){
this.accountNo=accountNo;
this.balance=balance;
} public double getBalance() {
return balance;
} public void setBalance(double balance) {
this.balance = balance;
} public String getAccountNo() {
return accountNo;
} public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
} @Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; Account account = (Account) o; return accountNo.equals(account.accountNo); } @Override
public int hashCode() {
return accountNo.hashCode();
}
}
DrawThread.java
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(){
synchronized (account){
if(account.getBalance()>=drawAmount){
System.out.println(getName() + "取钱成功,吐出钞票: " + drawAmount);
try{
Thread.sleep(1);
}catch(InterruptedException ex){
ex.getStackTrace();
}
account.setBalance(account.getBalance()-drawAmount);
System.out.println("\t余额为:"+account.getBalance());
}else{
System.out.println(getName()+"取钱失败,余额不足");
}
}
}
}
DrawTest.java
public class DrawTest {
public static void main(String[] args){
Account acct=new Account("1234567",1000);
new DrawThread("甲",acct,800).start();
new DrawThread("乙",acct,800).start();
}
}
甲取钱成功,吐出钞票: 800.0
    余额为:200.0
乙取钱失败,余额不足
3.如果将DrawThread的同步去掉:
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(){
// synchronized (account){
if(account.getBalance()>=drawAmount){
System.out.println(getName() + "取钱成功,吐出钞票: " + drawAmount);
try{
Thread.sleep(1);
}catch(InterruptedException ex){
ex.getStackTrace();
}
account.setBalance(account.getBalance()-drawAmount);
System.out.println("\t余额为:"+account.getBalance());
}else{
System.out.println(getName()+"取钱失败,余额不足");
}
// }
}
}

会出现这些情况的结果:

乙取钱成功,吐出钞票: 800.0
甲取钱成功,吐出钞票: 800.0
    余额为:200.0
    余额为:-600.0
 
甲取钱成功,吐出钞票: 800.0
乙取钱成功,吐出钞票: 800.0
    余额为:200.0
    余额为:200.0
程序使用synchronized将run()方法里的方法体修改成同步代码块,同步监视器就是account对象,这样的做法符合“加锁-修改-释放锁”的逻辑,这样就可以保证并发线程在任一时刻只有一个线程进入修改共享资源的代码区。多次运行,结果只有一个。
 
二 同步方法
1.同步方法就是使用synchronized关键字修饰某个方法,这个方法就是同步方法。这个同步方法(非static方法)无须显式指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。通过同步方法可以非常方便的实现线程安全的类,线程安全的类有如下特征:
该类的对象可以方便的被多个线程安全的访问;
每个线程调用该对象的任意方法之后都能得到正确的结果;
每个线程调用该对象的任意方法之后,该对象状态依然能保持合理状态。
2.不可变类总是线程安全的,因为它的对象状态不可改变可变类需要额外的方法来保证其线程安全,在Account类中我们只需要把balance的方法变成同步方法即可。
Account.java
public class Account {
private String accountNo;
private double balance;
public Account(String accountNo,double balance){
this.accountNo=accountNo;
this.balance=balance;
} //因为账户余额不可以随便更改,所以只为balance提供getter方法
public double getBalance() {
return balance;
} public String getAccountNo() {
return accountNo;
} public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
} @Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; Account account = (Account) o; return accountNo.equals(account.accountNo); } @Override
public int hashCode() {
return accountNo.hashCode();
} //提供一个线程安全的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()+"取钱失败,余额不足");
}
}
}
DrawThread.java
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);
}
}
DrawTest.java
public class DrawTest {
public static void main(String[] args){
Account acct=new Account("1234567",1000);
new DrawThread("甲",acct,800).start();
new DrawThread("乙",acct,800).start();
}
}
注意,synchronized可以修饰方法,修饰代码块,但是不能修饰构造器、成员变量等。在Account类中定义draw()方法,而不是直接在 run()方法中实现取钱逻辑,这种做法更符合面向对象规则。DDD设计方式,即Domain Driven Design(领域驱动设计),认为每个类都应该是完备的领域对象,Account代表用户账户,就应该提供用户账户的相关方法。通过draw()方法来执行取钱操作,而不是直接将setBalance()方法暴露出来任人操作。
但是,可变类的线程安全是以降低程序的运行效率为代价的,不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(共享资源)的方法进行同步。同时,可变类有两种运行环境:单线程环境和多线程环境, 则应该为可变类提供两种版本,即线程安全版本和线程不安全版本。如JDK提供的StringBuilder在单线程环境下保证更好的性能,StringBuffer可以保证多线程安全。
 
三 释放同步监视器的锁定
1.任何线程进入同步代码块,同步方法之前,必须先获得对同步监视器的锁定,那么如何释放对同步监视器的锁定呢,线程会在一下几种情况下释放同步监视器:
当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器;
当前线程在同步代码块、同步方法中遇到break,return终止了该代码块、方法的继续执行;
当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块、方法的异常结束;
当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器;
2.以下几种情况,线程不会释放同步监视器:
线程执行同步代码块或同步方法时,程序调用Thread.sleep(),Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器;
线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器,当然,程序应尽量避免使用suspend()和resume()方法来控制线程。
 
四 同步锁:
1.Java5开始,Java提供了一种功能更加强大的线程同步机制——通过显式定义同步锁对象来实现同步,这里的同步锁由Lock对象充当。
Lock 对象提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock是控制多个线程对共享资源进行访问的工具。通常, 锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应该先获得Lock对象。
某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁),Lock,ReadWriteLock是Java5提供的两个根接口,并为 Lock提供了ReentrantLock实现类,为ReadWriteLock提供了ReentrantReadWriteLock实现类。在 Java8中提供了新型的StampLock类,在大多数场景下它可以替代传统的ReentrantReadWriteLock。 ReentrantReadWriteLock为读写操作提供了三种锁模式:Writing,ReadingOptimistic,Reading。
2.在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁)。主要的代码格式如下:
class X{
//定义锁对象
private final ReentrantLock lock=new ReentrantLock();
//定义需要保证线程安全的方法
public void m(){
//加锁
lock.lock();
try{
//...method body
}
//使用finally块来保证释放锁
finally{
lock.unlock();
}
}
}
将Account.java修改为:
public class Account {
private final ReentrantLock lock=new ReentrantLock();
private String accountNo;
private double balance;
public Account(String accountNo,double balance){
this.accountNo=accountNo;
this.balance=balance;
} //因为账户余额不可以随便更改,所以只为balance提供getter方法
public double getBalance() {
return balance;
} public String getAccountNo() {
return accountNo;
} public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
} @Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; Account account = (Account) o; return accountNo.equals(account.accountNo); } @Override
public int hashCode() {
return accountNo.hashCode();
} //提供一个线程安全的draw()方法来完成取钱操作
public void draw(double drawAmount){
//加锁
lock.lock();
try {
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() + "取钱失败,余额不足");
}
}finally {
lock.unlock();
}
}
}
使用Lock与使用同步代码有点相似,只是使用Lock时可以显式使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器。使用 Lock时每个Lock对象对应一个Account对象,一样可以保证对于同一个Account对象,同一个时刻只能有一个线程进入临界区。Lock提供 了同步方法和同步代码块所没有的其他功能,包括使用非块结构的tryLock()方法,以及试图获取可中断锁的lockInterruptibly()方法,还有获取超时失效锁的tryLock(long,TimeUnit)方法。
ReentrantLock可重入锁的意思是,一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显式调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。
 
五 死锁
当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有检测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁出现。一旦出现死锁,程序既不会发生任何异常,也不会给出任何提示,只是所有线程都处于阻塞状态,无法继续。
如DeadLock.java
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(b);
System.out.println("进入了主线程之后...");
}
public void run(){
Thread.currentThread().setName("副线程");
b.bar(a);
System.out.println("进入了副线程之后...");
}
public static void main(String[] args){
DeadLock d1=new DeadLock();
new Thread(d1).start();
d1.init();
}
}

结果:

当前线程名为:主线程进入了A实例的foo()方法
当前线程名为:副线程进入了B实例的bar()方法
当前线程名为:主线程试图调用B实例的last()方法
当前线程名为:副线程试图调用A实例的last()方法

Java同步锁全息详解的更多相关文章

  1. Java中String做为synchronized同步锁使用详解

    Java中使用String作同步锁 在Java中String是一种特殊的类型存在,在jdk中String在创建后是共享常量池的,即使在jdk1.8之后实现有所不同,但是功能还是差不多的. 借助这个特点 ...

  2. Java分布式锁实现详解

    在进行大型网站技术架构设计以及业务实现的过程中,多少都会遇到需要使用分布式锁的情况.那么问题也就接踵而至,哪种分布式锁更适合我们的项目? 下面就这个问题,我做了一些分析: 分布式锁现状: 目前几乎很多 ...

  3. “全栈2019”Java多线程第十六章:同步synchronized关键字详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  4. Java精通并发-自旋对于synchronized关键字的底层意义与价值分析以及互斥锁属性详解与Monitor对象特性解说【纯理论】

    自旋对于synchronized关键字的底层意义与价值分析: 对于synchronized关键字的底层意义和价值分析,下面用纯理论的方式来对它进行阐述,自旋这个概念就会应运而生,还是很重要的,下面阐述 ...

  5. 《手把手教你》系列技巧篇(七十一)-java+ selenium自动化测试-自定义类解决元素同步问题(详解教程)

    1.简介 前面宏哥介绍了几种关于时间等待的方法,也提到了,在实际自动化测试脚本开发过程,百分之90的报错是和元素因为时间不同步而发生报错.本文介绍如何新建一个自定义的类库来解决这个元素同步问题.这样, ...

  6. java.lang.Thread类详解

    java.lang.Thread类详解 一.前言 位于java.lang包下的Thread类是非常重要的线程类,它实现了Runnable接口,今天我们来学习一下Thread类,在学习Thread类之前 ...

  7. Java并发关键字Volatile 详解

    Java并发关键字Volatile 详解 问题引出: 1.Volatile是什么? 2.Volatile有哪些特性? 3.Volatile每个特性的底层实现原理是什么? 相关内容补充: 缓存一致性协议 ...

  8. java之StringBuffer类详解

    StringBuffer 线程安全的可变字符序列. StringBuffer源码分析(JDK1.6): public final class StringBuffer extends Abstract ...

  9. Java内存模型(JMM)详解

    在Java JVM系列文章中有朋友问为什么要JVM,Java虚拟机不是已经帮我们处理好了么?同样,学习Java内存模型也有同样的问题,为什么要学习Java内存模型.它们的答案是一致的:能够让我们更好的 ...

随机推荐

  1. apache2.4 的安装

    Apache2.4 安装包下载地址 http://httpd.apache.org/docs/current/platform/windows.html#down 选择ApacheHaus 进入后 这 ...

  2. 转:关于Android机型适配这件小事儿

    http://www.umindex.com/devices/android_resolutions 大家都知道Android机型分裂严重,在开发Android App的时候永远都面临适配N多机型的问 ...

  3. JMeter 一:Elements of a Test Plan

    参考:http://jmeter.apache.org/usermanual/test_plan.html 最小测试集包括:Test Plan,一个Thread Group,以及一个或多个Sample ...

  4. ASP.NET WEB API处理流程

    前言:大图请看 http://www.asp.net/posters/web-api/ASP.NET-Web-API-Poster.pdf Web Api Hosting 我们不仅可以通过Web应用程 ...

  5. python——关于Python Profilers性能分析器

    1. 介绍性能分析器 profiler是一个程序,用来描述运行时的程序性能,并且从不同方面提供统计数据加以表述.Python中含有3个模块提供这样的功能,分别是cProfile, profile和ps ...

  6. js math 对数和指数处理 expm1 log1p

    1.Math.expm1() Math.expm1(x)返回 ex - 1,即Math.exp(x) - 1. Math.expm1(-1) // -0.6321205588285577 Math.e ...

  7. CentOS安装glibc-2.14(转)

    到http://ftp.gnu.org/gnu/glibc/下载glibc-2.14.tar.xz tar glibc-2.14.tar.gz cd glibc-2.14 mkdir build cd ...

  8. Unity Inspector 给组件自动关联引用(二)

    通过声明的变量名称,主动关联引用. 使用这个关联引用两种方式1.  给你组件继承  MonoAutoQuote 点击组件inspector 按钮执行2.  给你组件类添加[AAutoQuote] 特性 ...

  9. JavaScript-常用正则函数(适合忘记时看)

    test:测试string是否包含有匹配结果,包含返回true,不包含返回false. <script type="text/javascript"> var str ...

  10. CentOS7关闭默认防火墙启用iptables防火墙

    CentOS 7.0默认使用的是firewall作为防火墙,这里改为iptables防火墙步骤. 1.关闭firewall: systemctl stop firewalld.service #停止f ...