一 同步代码块
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()方法

线程同步 synchronized 同步代码块 同步方法 同步锁的更多相关文章

  1. JAVA之旅(十三)——线程的安全性,synchronized关键字,多线程同步代码块,同步函数,同步函数的锁是this

    JAVA之旅(十三)--线程的安全性,synchronized关键字,多线程同步代码块,同步函数,同步函数的锁是this 我们继续上个篇幅接着讲线程的知识点 一.线程的安全性 当我们开启四个窗口(线程 ...

  2. About 静态代码块,普通代码块,同步代码块,构造代码块和构造函数的纳闷

    构造函数用于给对象进行初始化,是给与之对应的对象进行初始化,它具有针对性,函数中的一种.特点:1:该函数的名称和所在类的名称相同.2:不需要定义返回值类型.3:该函数没有具体的返回值.记住:所有对象创 ...

  3. 线程的同步机制:同步代码块&同步方法

    解决存在的线程安全问题:打印车票时出现重票,错票 使用同步代码块的解决方案 TestWindow2 package com.aff.thread; /* 使用实现Runnable接口的方式,售票 存在 ...

  4. java的同步方法和同步代码块,对象锁,类锁区别

    /** * @author admin * @date 2018/1/12 9:48 * 作用在同一个实例对象上讨论 * synchronized同步方法的测试 * 两个线程,一个线程调用synchr ...

  5. 深入理解Java中的同步静态方法和synchronized(class)代码块的类锁

    一.回顾学习内容 在前面几篇博客中我我们已经理解了synchronized对象锁.对象锁的重入.synchronized方法块.synchronized非本对象的代码块, 链接:https://www ...

  6. 2.2.9静态同步synchronized方法与synchronized(class)代码块

    关键字synchronized还可以应用在static静态方法上,这样写那是对当前的*.java文件对应的class类进行持锁, 测试如下 package com.cky.bean; /** * Cr ...

  7. 牛客网Java刷题知识点之什么是代码块、普通代码块、静态代码块、同步代码块、构造代码块以及执行顺序

    不多说,直接上干货! 这种形式的程序段我们将其称之为代码块,所谓代码块就是用大括号({})将多行代码封装在一起,形成一个独立的数据体,用于实现特定的算法.一般来说代码块是不能单独运行的,它必须要有运行 ...

  8. JAVA_四大代码块_普通代码块、构造代码块、静态代码块、同步代码块。

    普通代码块 在方法或语句中出现的{}里面的内容就被称为普通代码块,普通代码块和一般的语句执行顺序一样,由他们在代码中出现的次序决定,即--"先出现先执行". 但是不同的普通代码块即 ...

  9. 静态同步synchronized方法和synchronized(class)代码块

    关键字synchronized还可以应用在static静态方法上,如果这样写,那是对当前的*.java文件对应的Class类进行持锁. package synStaticMethod; /** * C ...

随机推荐

  1. c# winform textbox与combox让用户不能输入

    textbox的ReadOnly属性设置为true combox的Enable属性设置为false 运行后效果如下 点击第一个和第二个,会把按钮text赋值给文本框和combox 并且用户不能输入

  2. docker无法连接进程

    simon@simon-X550LD:~$ docker run hello-world docker: Cannot connect to the Docker daemon. Is the doc ...

  3. JavaScript模块化开发库之SeaJS

    SeaJS是一个很好的前端模块化开发库,源码不到1500行,压缩后才4k,质量极高.

  4. 在windows下的mysql使用

    具体可参照http://jingyan.baidu.com/article/3aed632e19b5e8701080918f.html 1.安装mysql. 直接百度搜索mysql下载.

  5. 第 14 章 迭代器模式【Iterator Pattern】

    以下内容出自:<<24种设计模式介绍与6大设计原则>> 周五下午,我正在看技术网站,第六感官发觉有人在身后,扭头一看,我C,老大站在背后,赶忙站起来, “王经理,你找我?” 我 ...

  6. 第 8 章 适配器模式【Adapter Pattern】

    以下内容出自:<<24种设计模式介绍与6大设计原则>> 好,请安静,后排聊天的同学别吵醒前排睡觉的同学了,大家要相互理解嘛.今天讲适配器模式,这个模式也很简单,你笔记本上的那个 ...

  7. DataGrid点击上下一页loading效果

    js添加显示loading和取消loading方法 function showtbloading() { var target = $("#GridView1"); if (tar ...

  8. shell 基础 $(cd `dirname $0`;pwd)

    $ cd `dirname $0` 和PWD%/* shell变量的一些特殊用法 在命令行状态下单纯执行 $ cd `dirname $0` 是毫无意义的.因为他返回当前路径的"." ...

  9. jsp EL表达式 字符串的比较

    jsp EL表达式 字符串的比较 跟JavaScript一样,直接使用两个等于号即可:== 代码如下: <c:if test="${highLight == 'visa'}" ...

  10. 【网络流24题】 No.2 太空飞行计划问题 (最大闭合权图 最大流 )

    原题:         W教授正在为国家航天中心计划一系列的太空飞行.每次太空飞行可进行一系列商业性实验而获取利润.现已确定了一个可供选择的实验集合 E={E1,E2,...,Em},和进行这些实验需 ...