多线程编程很容易出现“错误情况”,这是由系统的线程调度具有一定的随机性造成的,不过即使程序偶然出现问题,那也是由于编程不当引起。使用多个线程访问同一个数据时很容易出现此类状况,因此我们需要通过线程安全来进行处理。

>线程安全问题

线程安全问题举例——银行取钱问题。银行取钱的基本流程上可以分为如下步骤:

  1. 用户输入账户、密码,系统判断用户的账户、密码是否正确。
  2. 用户输入取款金额
  3. 系统判断用户的余额是否大于取款金额
  4. 如果余额大于取款金额,则提取成功;如果余额小于取款金额则提取失败。

业务流程在单线程环境下没有问题,放在多线程并发的情况下,则可能出现问题。可能是指有几率出现,也许我们将程序运行一百万次也没有出现,但是不代表没有问题。

账户类:

public class Account {private String userNo;private double account;public Account(String userNo,double account){this.userNo = userNo;this.account = account;}public String getUserNo() {return userNo;}public void setUserNo(String userNo) {this.userNo = userNo;}public double getAccount() {return account;}public void setAccount(double account) {this.account = account;}
}

测试类:

public class ATMThread extends Thread {private Account account;private double money;public ATMThread(String atmNo, Account account, double money) {super(atmNo);this.account = account;this.money = money;}public void run() {if (this.account.getAccount() >= money) {System.out.println(this.getName() + "号ATM取钱成功!吐出钞票:" + this.money);// 假如此时该线程进入阻塞状态try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}// 修改金额this.account.setAccount(account.getAccount() - money);System.out.println("余额为:" + account.getAccount());}else{System.out.println("余额不足!");}}public static void main(String[] args) {Account account=new Account("12345", 1000);//同时对一个账户取钱操作new ATMThread("0001", account, 800).start();new ATMThread("0002", account, 800).start();}}

测试结果:

0001号ATM取钱成功!吐出钞票:800.0
0002号ATM取钱成功!吐出钞票:800.0
余额为:200.0
余额为:-600.0
0001号ATM取钱成功!吐出钞票:800.0
0002号ATM取钱成功!吐出钞票:800.0
余额为:200.0
余额为:200.0
0001号ATM取钱成功!吐出钞票:800.0
0002号ATM取钱成功!吐出钞票:800.0
余额为:-600.0
余额为:-600.0

可以看到,不仅成功的在1000元的账户中取出了1600元,由于线程调度的不确定性,最后的余额也呈现出不同的结果。

这并不是银行所期望的,是由于编程错误所引起的。

>同步代码块

如上例所示,之所以出现此种情况是因为run()方法的方法体不具有同步安全性——程序中有两个并发线程在修改Account对象,而系统恰好在一个线程进行休眠时切换到另一个线程进行执行修改Account对象,所以出现此问题。

为了解决此类问题,Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。同步代码块的语法格式如下:

synchronized(obj){

//此处的代码块为同步代码块 }

上面语法格式中的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。

虽然Java程序允许使用任何对象作为同步监视器,但是我们想一下同步监视器的目的是为了:阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。对于上面取钱模拟程序,应该考虑使用账户account作为同步监视器,把程序修改如下:

public class ATMThread extends Thread {private Account account;private double money;public ATMThread(String atmNo, Account account, double money) {super(atmNo);this.account = account;this.money = money;}public void run() {synchronized (account) {if (this.account.getAccount() >= money) {System.out.println(this.getName() + "号ATM取钱成功!吐出钞票:" + this.money);// 假如此时该线程进入阻塞状态try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}// 修改金额this.account.setAccount(account.getAccount() - money);System.out.println("余额为:" + account.getAccount());}else{System.out.println("余额不足!");}}   }public static void main(String[] args) {Account account=new Account("12345", 1000);//同时对一个账户取钱操作new ATMThread("0001", account, 800).start();new ATMThread("0002", account, 800).start();}}

运行结果:

0001号ATM取钱成功!吐出钞票:800.0
余额为:200.0
余额不足!

使用synchronized将run()方法里面的方法体修改为同步代码块,该同步代码块的同步监视器是account对象,这样的作法符合:

“加锁>>修改>>释放锁”的逻辑,任何线程在修改指定资源前,首先对该资源加锁,在加锁期间其他线程无法修改该资源,当该线程修改结束后,该线程释放对该资源的锁定。通过这种方式就可以保证并发线程在任一时刻只有一个线程可以进行修改共享资源的的代码区(也被称为临界区),所以同一时刻最多有一个线程处于临界区,从而保证了线程的安全。

>同步方法:

同步方法就是使用synchronized修饰的某个方法。对于同步方法而言,无需指定它的同步监视器,同步方法的同步监视器就是this,也就是调用该方法的对象。

通过使用同步方法可以非常方便地实现线程安全的类,线程安全的类具有如下特征:

  • 该类的对象可以被多个线程安全地访问。
  • 每个线程调用该对象的任意方法之后都将得到正确的结果。
  • 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。

前面介绍了可变类(可以改变实例内容)和不可变类,其中不可变类总是线程安全的,因为它的对象状态不可改变;但是可变对象需要额外的方法来保证其线程安全。例如上面的Accout就是一个可变类,它的userNo和account两个成员变量都可以被改变,当两个线程同时修改Account对象的account成员变量的值时,程序就出现了异常。下面将Account类的account的访问设置为线程安全的,那么只要把修改account的方法变成同步方法即可。

public class Account {private String userNo;private double account;public Account(String userNo,double account){this.userNo = userNo;this.account = account;}public synchronized void drawMoney(double money){if (this.account >= money) {System.out.println(this.userNo + "号ATM取钱成功!吐出钞票:" + money);// 假如此时该线程进入阻塞状态try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 修改金额this.account=this.account - money;System.out.println("余额为:" + account);}else{System.out.println("余额不足!");}}
}

测试类:

public class ATMThread extends Thread {private Account account;private double money;public ATMThread(String atmNo, Account account, double money) {super(atmNo);this.account = account;this.money = money;}public void run() {account.drawMoney(money);}public static void main(String[] args) {Account account = new Account("12345", 1000);// 同时对一个账户取钱操作new ATMThread("0001", account, 800).start();new ATMThread("0002", account, 800).start();}}

运行结果:

12345号ATM取钱成功!吐出钞票:800.0
余额为:200.0
余额不足!

面的TestThread类没有实现自己取钱的操作,而是直接调用Account类中的drawMoney方法来执行取钱操作,此方法使用synchronized关键字来修饰,同步方法的同步监视器为当前对象。当多个线程并发修改同一个account对象时,必须先对此对象加锁,同样符合了 加锁——修改——释放锁的逻辑。

注意:

  • synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、属性等。
  • 线程安全类是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响。
  • 不要对线程安全类的所有方法都进行同步,只对那些会改变共享资源的方法进行同步,例如Accout对象中的userNo属性就无须同步。
  • 如果可变类有两种运行环境,单线程和多线程环境,则应该提供此类的两个版本即:线程安全版本和线程不安全版本。

>释放同步监视器的锁定

任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?程序无法显示释放对同步监视器的锁定。

线程会在如下几种情况下释放对同步监视器的锁定:

  • 当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。
  • 当前线程的同步代码块、同步方法中遇到break、return终止该代码块、该方法的继续执行,当前线程将会释放同步监视器。
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或者Exception,导致了该代码块、该方法异常结束时,当期线程将会释放同步监视器。
  • 当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。

在如下情况下,线程不会释放同步监视器:

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

不释放同步监视器,如sleep:

public class TestThread extends Thread {private TestOne test;public TestThread(String name,TestOne test){super(name);this.test = test;}@Overridepublic void run() {this.test.show();}public static void main(String[] args) {TestOne one = new TestOne();TestThread thread = new TestThread("监视器的线程1",one);TestThread thread1 = new TestThread("监视器的线程2",one);thread1.start();thread.start();}
}class TestOne{public synchronized void show(){for(int i=0;i<20;i++){System.out.println(Thread.currentThread().getName()+"   "+i);try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}}}
}

>同步锁(Lock)

从Java5开始,Java提供了一种功能更强大的线程同步机制——通过显示定义同步锁对象来实现同步,在这种机制下,同步锁使用Lock对象来充当。

Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock可以实现更灵活的结构,可以具有差别很大的属性,并且支持多个相关的Condition对象。

Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象枷锁,线程开始访问共享资源之前应先获得Lock对象。

某些锁可能允许对共享资源的并发访问,例如ReadWriteLock读写锁就允许并发访问。Lock、ReadWriteLock是Java5提供的两个根接口,并为Lock提供了ReentrantLock实现类,为ReadWriteLock提供了ReentrantReadWriteLock实现类。

在实现线程安全的控制中,常用的是ReentrantLock,使用该Lock可以显示的加锁、释放锁。如下所示:

public class A {private final ReentrantLock lock = new ReentrantLock();public void method() {// 加锁lock.lock();try {// 需要保证安全的代码块} finally {// 释放锁lock.unlock();}}
}

使用ReentrantLock对象来进行同步,加锁和释放锁出现在不同的作用范围内时,通常建议使用finally块来确保在必要时释放锁。通过使用ReentrantLock对象,可以把Account类改为如下形式,它依然是线程安全的:

public class Account {private String userNo;private double account;public Account(String userNo,double account){this.userNo = userNo;this.account = account;}public synchronized void drawMoney(double money){ReentrantLock lock=new ReentrantLock();lock.lock();try {if (this.account >= money) {System.out.println(this.userNo + "号ATM取钱成功!吐出钞票:" + money);// 假如此时该线程进入阻塞状态try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 修改金额this.account=this.account - money;System.out.println("余额为:" + account);}else{System.out.println("余额不足!");}}finally{lock.unlock();}   }
}

同步方法或者同步代码块使用与竞争资源相关的、隐式的同步监视器,并且强制要求加锁和释放锁要出现在一个块结构中,而且当获取了多个锁时,他们必须以相反的顺序释放,且必须在与所有锁被获取时的相同的范围内释放所有锁。

虽然同步方法和同步代码块的范围机制使得多线程安全编程非常方便,而且还可以避免很多涉及锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。Lock提供了同步方法和同步代码块所没有的其他功能,包括用于非块结构的tryLock()方法,以及试图获取可中断锁的LockInterruptibly()方法,还有获取超时失效锁的tryLock(long time,TimeUnit unit)方法。

ReentrantLock锁具有可重入性,也就是说一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显示的调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。

public class TestThread extends Thread {private TestOne one;public TestThread(String name, TestOne one) {super(name);this.one = one;}@Overridepublic void run() {this.one.show1();}public static void main(String[] args) {TestOne one = new TestOne();TestThread th1 = new TestThread("线程1", one);TestThread th2 = new TestThread("线程2", one);th1.start();th2.start();}
}class TestOne {private final ReentrantLock lock = new ReentrantLock();public void show1() {lock.lock();try {for (int i = 0; i < 20; i++) {System.out.println(Thread.currentThread().getName() + "show1方法被执行" + i);if (i == 10) {this.show2();}}} finally {lock.unlock();}}private void show2() {lock.lock();try {for (int i = 0; i < 20; i++) {System.out.println(Thread.currentThread().getName() + "show2方法" + i);}} finally {lock.unlock();}}
}

运行结果:

线程2show1方法被执行0
线程2show1方法被执行1
线程2show1方法被执行2
线程2show1方法被执行3
线程2show1方法被执行4
线程2show1方法被执行5
线程2show1方法被执行6
线程2show1方法被执行7
线程2show1方法被执行8
线程2show1方法被执行9
线程2show1方法被执行10
线程2show2方法0
线程2show2方法1
线程2show2方法2
线程2show2方法3
线程2show2方法4
线程2show2方法5
线程2show2方法6
线程2show2方法7
线程2show2方法8
线程2show2方法9
线程2show2方法10
线程2show2方法11
线程2show2方法12
线程2show2方法13
线程2show2方法14
线程2show2方法15
线程2show2方法16
线程2show2方法17
线程2show2方法18
线程2show2方法19
线程2show1方法被执行11
线程2show1方法被执行12
线程2show1方法被执行13
线程2show1方法被执行14
线程2show1方法被执行15
线程2show1方法被执行16
线程2show1方法被执行17
线程2show1方法被执行18
线程2show1方法被执行19
线程1show1方法被执行0
线程1show1方法被执行1
线程1show1方法被执行2
线程1show1方法被执行3
线程1show1方法被执行4
线程1show1方法被执行5
线程1show1方法被执行6
线程1show1方法被执行7
线程1show1方法被执行8
线程1show1方法被执行9
线程1show1方法被执行10
线程1show2方法0
线程1show2方法1
线程1show2方法2
线程1show2方法3
线程1show2方法4
线程1show2方法5
线程1show2方法6
线程1show2方法7
线程1show2方法8
线程1show2方法9
线程1show2方法10
线程1show2方法11
线程1show2方法12
线程1show2方法13
线程1show2方法14
线程1show2方法15
线程1show2方法16
线程1show2方法17
线程1show2方法18
线程1show2方法19
线程1show1方法被执行11
线程1show1方法被执行12
线程1show1方法被执行13
线程1show1方法被执行14
线程1show1方法被执行15
线程1show1方法被执行16
线程1show1方法被执行17
线程1show1方法被执行18
线程1show1方法被执行19

可以看到:一个线程可以对已被加锁的ReentrantLock锁再次加锁,持有这个锁的线程先执行,并且两次锁都被释放后另一个线程才可以操作加锁的资源。

>死锁:

当两个线程互相等待对方释放同步监视器时就会发生死锁,Java虚拟机既没有监测,也没有采取措施来处理死锁情况,所以在进行多线程编程时应该采取一些措施来避免死锁现象的出现。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

死锁是很容易发生的,尤其在系统中出现多个同步监视器的情况下,如下程序将会出现死锁现象。

一般写死锁的例子,开启两个线程,A线程锁住A资源后就休眠,此时另一个B线程锁住B资源后休眠,这个时候A先休眠结束去锁B资源,因为B资源已经被B线程锁住只能等待,之后B结束休眠去锁A资源,因为A资源已经被A线程锁住也只能等待。这样死锁就产生了。

public class Dead implements Runnable{boolean flag;public static Object o1=new Object();//静态确保锁的是同一个对象public static Object o2=new Object();public Dead(boolean flag) {this.flag=flag;}@Overridepublic void run() {if (flag) {synchronized (o1) {System.out.println(Thread.currentThread().getName()+"锁上o1");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"持有o1请求锁上o2");synchronized (o2) {//在o1的临界区内,保证持有o1,去锁o2}}}else{synchronized (o2) {System.out.println(Thread.currentThread().getName()+"锁上o2");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"持有o2请求锁上o1");synchronized (o1) {  //在o2的临界区内,保证持有o2,去锁o1}}} }public static void main(String[] args) {Dead d=new Dead(false);Dead d2=new Dead(true);new Thread(d).start();new Thread(d2).start();}
}

执行结果:

Thread-1锁上o1
Thread-0锁上o2
Thread-0持有o2请求锁上o1
Thread-1持有o1请求锁上o2

另一个例子:

public class DeadThread implements Runnable {private A a = new A();private B b = new B();public void init(){Thread.currentThread().setName("主线程");a.a1(b);   }@Overridepublic void run() {Thread.currentThread().setName("副线程");b.b1(a);//1}public static void main(String[] args) {DeadThread dt = new DeadThread();new Thread(dt).start();dt.init();}
}class A{public synchronized void a1(B b){System.out.println("当前线程是:"+Thread.currentThread().getName()+"进入了A实例对象的a1方法");try {Thread.sleep(200);} catch (InterruptedException e) {e.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 b1(A a){System.out.println("当前线程是:"+Thread.currentThread().getName()+"进入了B实例对象的b1方法");try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("当前线程是:"+Thread.currentThread().getName()+"马上调用A实例对象的last方法");a.last();}public synchronized void last(){System.out.println("进入了B类的last方法内部!");}
}

执行结果:

当前线程是:主线程进入了A实例对象的a1方法
当前线程是:副线程进入了B实例对象的b1方法
当前线程是:主线程马上调用B实例对象的last方法
当前线程是:副线程马上调用A实例对象的last方法

主线程——调用a1方法,因此对a加锁——休眠——切换到副线程

副线程——调用b1方法,因此对b加锁——休眠——切换到主线程

主线程——需要调用b对象的last方法,需要对b对象加锁,而此时副线程对b对象的锁没有释放——阻塞

副线程——需要调用a对象的last方法,需要对a对象加锁,而此时主线程对a对象的锁没有释放——阻塞

因此出现死锁现象。

由于Thread类的suspend()方法非常容易导致死锁,所以Java不再推荐使用该方法,此方法已被废弃。

Java多线程(线程同步)相关推荐

  1. Java 多线程 线程同步

    线程同步 1.发生在多个线程操作同一个资源 2.并发:同一个对象被多个线程同时操作 3.于是,就需要线程同步.线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等 ...

  2. Java多线程——线程同步

    1.不安全的买票 //不安全的买票//线程不安全,有负数或者多人买到同一张票 public class UnsafeBuyTicket {public static void main(String[ ...

  3. Java多线程——线程的优先级和生命周期

    Java多线程--线程的优先级和生命周期 摘要:本文主要介绍了线程的优先级以及线程有哪些生命周期. 部分内容来自以下博客: https://www.cnblogs.com/sunddenly/p/41 ...

  4. JAVA中线程同步的方法(7种)汇总

    JAVA中线程同步的方法(7种)汇总 同步的方法: 一.同步方法 即有synchronized关键字修饰的方法. 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法. ...

  5. java多线程 线程安全_Java中的线程安全

    java多线程 线程安全 Thread Safety in Java is a very important topic. Java provides multi-threaded environme ...

  6. Java 多线程线程安全(面试概念解答二)

    Java 多线程线程安全 什么是线程安全? 为什么有线程安全问题? 线程安全解决办法? 同步代码块 同步函数 静态同步函数 多线程死锁 多线程的三大特性 原子性 可见性 有序性 Java内存模型 Vo ...

  7. JAVA中线程同步的几种实现方法

    JAVA中线程同步的几种实现方法 一.synchronized同步的方法: 1.synchronized同步方法 即有synchronized关键字修饰的方法. 由于java的每个对象都有一个内置锁, ...

  8. JAVA --- 多线程 -- 线程的创建

    JAVA - 多线程 – 线程的创建 线程的概念: 说起线程,先说程序和进程,多任务的概念. 程序(program):是指令和数据的有序集合,本身没有任何运行的含义,是一个静态的概念. 进程(proc ...

  9. java实验多线程机制_使用Java多线程的同步机制编写应用程序 PDF 下载

    使用Java多线程的同步机制编写应用程序 PDF 下载 本站整理下载: 相关截图: 主要内容: 一. 实验名称 使用Java多线程的同步机制编写应用程序 二. 实验目的及要求 1.理解并行/并发的概念 ...

  10. java多线程-线程的停止【interrupt】

    java多线程-线程的停止 文章目录 java多线程-线程的停止 线程停止的原理 如何正确停止线程 在普通情况下停止线程 代码展示 在阻塞情况下停止线程 代码展示 线程在每次迭代后都阻塞 代码展示 停 ...

最新文章

  1. listview的divider边距
  2. php多图片上传并回显,如何用input标签和jquery实现多图片的上传和回显功能
  3. c编程:输入一个数字n,则n代表n行,每行输入2个数字a,b计算每行的a+b问题。
  4. python获取窗口句柄_Python+selenium 获取浏览器窗口坐标、句柄的方法
  5. 蚂蚁的开放:想办法摸到10米的篮筐
  6. 带参数标签的取值方法
  7. js练习8(幻灯片切换效果)
  8. 在AIX 5.3+HACMP 5.4以上环境安装10gR2 10.2.0.1 RAC CRS Clusterware必须先运行Patch 6718715中的rootpre.sh...
  9. 前端培训Ajax-onreadystatechange 事件
  10. 21天学通java web 第二版pdf_21天学通JAVA WEB开发 pdf完全版_IT教程网
  11. Java递归求费数列和_java – 斐波纳契数列 – 递归求和
  12. ByteV打造智慧建筑可视化管理平台——IBMS智能化集成系统赋予楼宇“智慧大脑
  13. 抖音私聊不封号技术,教你怎么避免踩雷?
  14. iOS 16 中 SwiftUI 防止弹出的 sheet 视图被下滑关闭(dismiss)的新解决方案
  15. Swift网络请求 - RXSwift + PromiseKit + Moya
  16. HuffmanTree
  17. python中时间模块datetime总结
  18. 如何才能做到色彩平衡?
  19. Unity3d轻量渲染管线(LWRP)民间文档
  20. 基于STC89C52RC单片机的密码门锁

热门文章

  1. Lua table 拾珍
  2. linux升级openssl需要先卸载吗,在Linux系统上升级OpenSSL的方法
  3. idea高效找出全部未被使用的代码
  4. 16-修改文件内容 - vi
  5. IBM、Google、Oracle三巨头的公有云之殇(下)
  6. The Geometry has no Z values 解决办法
  7. [转]使用VS2010的Database 项目模板统一管理数据库对象
  8. mysql 错误收集和整理
  9. FR帧中继(点对点子接口)
  10. C#学习笔记---数据类型