Java实现多线程同步五种方法详解

一、为什么要线程同步

因为当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。举个例子,如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块。假设账户原本有0块,如果取钱线程和存钱线程同时发生,会出现什么结果呢?取钱不成功,账户余额是100.取钱成功了,账户余额是0.那到底是哪个呢?很难说清楚。因此多线程同步就是要解决这个问题。

二、不同步时的代码

Bank.java

package threadTest; 

/**
 * @author lixiaoxi
 *
 */
public class Bank { 

  private int count =0;//账户余额 

  //存钱
  public void addMoney(int money){
    count +=money;
    System.out.println(System.currentTimeMillis()+"存进:"+money);
  } 

  //取钱
  public void subMoney(int money){
    if(count-money < 0){
      System.out.println("余额不足");
      return;
    }
    count -=money;
    System.out.println(+System.currentTimeMillis()+"取出:"+money);
  } 

  //查询
  public void lookMoney(){
    System.out.println("账户余额:"+count);
  }
}

SyncThreadTest.java

package threadTest; 

public class SyncThreadTest { 

  public static void main(String args[]){
    final Bank bank=new Bank(); 

    Thread tadd=new Thread(new Runnable() { 

      @Override
      public void run() {
        // TODO Auto-generated method stub
        while(true){
          try {
            Thread.sleep(1000);
          } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
          }
          bank.addMoney(100);
          bank.lookMoney();
          System.out.println("\n"); 

        }
      }
    }); 

    Thread tsub = new Thread(new Runnable() { 

      @Override
      public void run() {
        // TODO Auto-generated method stub
        while(true){
          bank.subMoney(100);
          bank.lookMoney();
          System.out.println("\n");
          try {
            Thread.sleep(1000);
          } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
          }
        }
      }
    });
    tsub.start(); 

    tadd.start();
  } 

}

代码很简单,我就不解释了,看看运行结果怎样呢?截取了其中的一部分,是不是很乱,有些看不懂。

余额不足
账户余额:0 

余额不足
账户余额:100 

1441790503354存进:100
账户余额:100 

1441790504354存进:100
账户余额:100 

1441790504354取出:100
账户余额:100 

1441790505355存进:100
账户余额:100 

1441790505355取出:100
账户余额:100

三、使用同步时的代码

1、同步方法

即有synchronized关键字修饰的方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

修改后的Bank.java

/**
 * @author lixiaoxi
 *
 */
public class Bank { 

  private int count =0;//账户余额 

  //存钱
  public synchronized void addMoney(int money){
    count +=money;
    System.out.println(System.currentTimeMillis()+"存进:"+money);
  } 

  //取钱
  public synchronized void subMoney(int money){
    if(count-money < 0){
      System.out.println("余额不足");
      return;
    }
    count -=money;
    System.out.println(+System.currentTimeMillis()+"取出:"+money);
  } 

  //查询
  public void lookMoney(){
    System.out.println("账户余额:"+count);
  }
}

再看看运行结果:

余额不足
账户余额:0 

余额不足
账户余额:0 

1441790837380存进:100
账户余额:100 

1441790838380取出:100
账户余额:0 

1441790838380存进:100
账户余额:100 

1441790839381取出:100
账户余额:0

瞬间感觉可以理解了吧。

注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。

2、同步代码块

即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。

Bank.java代码如下:

package threadTest; 

package threadTest; 

/**
 * @author lixiaoxi
 *
 */
public class Bank { 

  private int count =0;//账户余额 

  //存钱
  public void addMoney(int money){ 

    synchronized (this) {
      count +=money;
    }
    System.out.println(System.currentTimeMillis()+"存进:"+money);
  } 

  //取钱
  public void subMoney(int money){ 

    synchronized (this) {
      if(count-money < 0){
        System.out.println("余额不足");
        return;
      }
      count -=money;
    }
    System.out.println(+System.currentTimeMillis()+"取出:"+money);
  } 

  //查询
  public void lookMoney(){
    System.out.println("账户余额:"+count);
  }
}

运行结果如下:

余额不足
账户余额:0 

1441791806699存进:100
账户余额:100 

1441791806700取出:100
账户余额:0 

1441791807699存进:100
账户余额:100

效果和方法一差不多。

注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

3、使用特殊域变量(volatile)实现线程同步

(1)volatile关键字为域变量的访问提供了一种免锁机制;

(2)使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新;

(3)因此每次使用该域就要重新计算,而不是使用寄存器中的值;

(4)volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。

Bank.java代码如下:

package threadTest; 

/**
 * @author lixiaoxi
 *
 */
public class Bank { 

  private volatile int count = 0;// 账户余额 

  // 存钱
  public void addMoney(int money) { 

    count += money;
    System.out.println(System.currentTimeMillis() + "存进:" + money);
  } 

  // 取钱
  public void subMoney(int money) { 

    if (count - money < 0) {
      System.out.println("余额不足");
      return;
    }
    count -= money;
    System.out.println(+System.currentTimeMillis() + "取出:" + money);
  } 

  // 查询
  public void lookMoney() {
    System.out.println("账户余额:" + count);
  }
}

运行效果怎样呢?

余额不足
账户余额:0 

余额不足
账户余额:100 

1441792010959存进:100
账户余额:100 

1441792011960取出:100
账户余额:0 

1441792011961存进:100
账户余额:100

是不是又看不懂了,又乱了。这是为什么呢?就是因为volatile不能保证原子操作导致的,因此volatile不能代替synchronized。此外volatile会组织编译器对代码优化,因此能不使用它就不使用它吧。它的原理是每次要线程要访问volatile修饰的变量时都是从内存中读取,而不是从缓存当中读取,因此每个线程访问到的变量值都是一样的。这样就保证了同步。

4、使用重入锁实现线程同步

在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和块具有相同的基本行为和语义,并且扩展了其能力。

ReenreantLock类的常用方法有:

  • ReentrantLock() :创建一个ReentrantLock实例
  • lock() :获得锁
  • unlock() :释放锁

注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用。

Bank.java代码修改如下:

package threadTest; 

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; 

/**
 * @author lixiaoxi
 *
 */
public class Bank { 

  private int count = 0;// 账户余额 

  //需要声明这个锁
  private Lock lock = new ReentrantLock(); 

  // 存钱
  public void addMoney(int money) {
    lock.lock();//上锁
    try{
    count += money;
    System.out.println(System.currentTimeMillis() + "存进:" + money); 

    }finally{
      lock.unlock();//解锁
    }
  } 

  // 取钱
  public void subMoney(int money) {
    lock.lock();
    try{ 

    if (count - money < 0) {
      System.out.println("余额不足");
      return;
    }
    count -= money;
    System.out.println(+System.currentTimeMillis() + "取出:" + money);
    }finally{
      lock.unlock();
    }
  } 

  // 查询
  public void lookMoney() {
    System.out.println("账户余额:" + count);
  }
}

运行效果怎样呢?

余额不足
账户余额:0 

余额不足
账户余额:0 

1441792891934存进:100
账户余额:100 

1441792892935存进:100
账户余额:200 

1441792892954取出:100
账户余额:100

效果和前两种方法差不多。

如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码 。如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁。

5、使用局部变量实现线程同步

Bank.java代码如下:

package com.demo.test;

/**
 * @author lixiaoxi
 *
 */
public class Bank {

  private static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){ 

    @Override
    protected Integer initialValue() {
      // TODO Auto-generated method stub
      return 0;
    } 

  }; 

  // 存钱
  public void addMoney(int money) {
    count.set(count.get()+money);
    System.out.println(System.currentTimeMillis() + "存进:" + money); 

  } 

  // 取钱
  public void subMoney(int money) {
    if (count.get() - money < 0) {
      System.out.println("余额不足");
      return;
    }
    count.set(count.get()- money);
    System.out.println(+System.currentTimeMillis() + "取出:" + money);
  } 

  // 查询
  public void lookMoney() {
    System.out.println("账户余额:" + count.get());
  }
}

运行效果:

余额不足
账户余额:0

1511166594460存进:100
账户余额:200

余额不足
账户余额:0

1511166595460存进:100
账户余额:300

余额不足
账户余额:0

1511166596460存进:100
账户余额:400

看了运行效果,一开始一头雾水,怎么只让存,不让取啊?看看ThreadLocal的原理:

如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。现在明白了吧,原来每个线程运行的都是一个副本,也就是说存钱和取钱是两个账户,只是名字相同而已。所以就会发生上面的效果。

ThreadLocal与同步机制

a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题;

b.前者采用以”空间换时间”的方法,后者采用以”时间换空间”的方式。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 五种Java多线程同步的方法

    为什么要线程同步 因为当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常.举 个例子,如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块.假设账户原本有0块,如果取钱线程和存钱线程同时发生,会出现什么结果 呢?取钱不成功,账户余额是100.取钱成功了,账户余额是0.那到底是哪个呢?很难说清楚.因此多线程同步就是要解决这个问题. 一.不同步时的代码 Bank.java package threadTe

  • java多线程编程之Synchronized块同步方法

    文章分享了4个例子对synchronized的详细解释 1.是否加synchronized关键字的不同 public class ThreadTest { public static void main(String[] args) { Example example = new Example(); Thread t1 = new Thread1(example); Thread t2 = new Thread1(example); t1.start(); t2.start(); } } cl

  • 详解Java多线程编程中的线程同步方法

    1.多线程的同步: 1.1.同步机制: 在多线程中,可能有多个线程试图访问一个有限的资源,必须预防这种情况的发生.所以引入了同步机制:在线程使用一个资源时为其加锁,这样其他的线程便不能访问那个资源了,直到解锁后才可以访问. 1.2.共享成员变量的例子: 成员变量与局部变量: 成员变量: 如果一个变量是成员变量,那么多个线程对同一个对象的成员变量进行操作,这多个线程是共享一个成员变量的. 局部变量: 如果一个变量是局部变量,那么多个线程对同一个对象进行操作,每个线程都会有一个该局部变量的拷贝.他们

  • Java多线程synchronized同步方法详解

    1.synchronized 方法与锁对象 线程锁的是对象. 1)A线程先持有 object 对象的 Lock 锁, B线程可以以异步的方式调用 object 对象中的非 synchronized 类型的方法 2)A线程先持有 object 对象的 Lock 锁, B线程如果在这时调用 object 对象中的 synchronized 类型的方法,则需要等待,也就是同步. 2.脏读(DirtyRead) 示例: public class DirtyReadTest { public static

  • java多线程编程之使用Synchronized关键字同步类方法

    复制代码 代码如下: public synchronized void run(){     } 从上面的代码可以看出,只要在void和public之间加上synchronized关键字,就可以使run方法同步,也就是说,对于同一个Java类的对象实例,run方法同时只能被一个线程调用,并当前的run执行完后,才能被其他的线程调用.即使当前线程执行到了run方法中的yield方法,也只是暂停了一下.由于其他线程无法执行run方法,因此,最终还是会由当前的线程来继续执行.先看看下面的代码:sych

  • java多线程的同步方法实例代码

    java多线程的同步方法实例代码 先看一个段有关银行存钱的代码: class Bank { private int sum; public void add(int num){ sum = sum + num; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("total num is : " + sum); } } class Cu

  • java多线程编程之使用Synchronized块同步方法

    synchronized关键字有两种用法.第一种就是在<使用Synchronized关键字同步类方法>一文中所介绍的直接用在方法的定义中.另外一种就是synchronized块.我们不仅可以通过synchronized块来同步一个对象变量.也可以使用synchronized块来同步类中的静态方法和非静态方法.synchronized块的语法如下: 复制代码 代码如下: public void method(){    - -    synchronized(表达式)    {        -

  • java 多线程的同步几种方法

    java 多线程的同步几种方法 一.引言 前几天面试,被大师虐残了,好多基础知识必须得重新拿起来啊.闲话不多说,进入正题. 二.为什么要线程同步 因为当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常.举个例子,如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块.假设账户原本有0块,如果取钱线程和存钱线程同时发生,会出现什么结果呢?取钱不成功,账户余额是100.取钱成功了,账户余额是0.那到底是哪个

  • Java实现多线程同步五种方法详解

    一.为什么要线程同步 因为当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常.举个例子,如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块.假设账户原本有0块,如果取钱线程和存钱线程同时发生,会出现什么结果呢?取钱不成功,账户余额是100.取钱成功了,账户余额是0.那到底是哪个呢?很难说清楚.因此多线程同步就是要解决这个问题. 二.不同步时的代码 Bank.java package threadTe

  • Springboot配置返回日期格式化五种方法详解

    目录 格式化全局时间字段 1.前端时间格式化(不做无情人) 2.SimpleDateFormat格式化(不推荐) 3.DateTimeFormatter格式化(不推荐) 4.全局时间格式化(推荐) 实现原理分析 5.部分时间格式化(推荐) 总结 应急就这样 格式化全局时间字段 在yml中添加如下配置: spring.jackson.date-format=yyyy-MM-dd HH:mm:ss 或者 spring: jackson: ## 格式为yyyy-MM-dd HH:mm:ss date-

  • JavaScript生成UUID的五种方法详解

    目录 简介 1.第一种 2.第二种 3.第三种 4.第四种 5.第五种 简介 UUID(Universally Unique IDentifier) 全局唯一标识符. UUID是一种由算法生成的二进制长度为128位的数字标识符.UUID的格式为“xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx”,其中的 x 是 0-9 或 a-f范围内的一个32位十六进制数.在理想情况下,任何计算机和计算机集群都不会生成两个相同的UUID. 1.第一种 function guid() {

  • Redis实现分布式锁的五种方法详解

    目录 1. 单机数据一致性 2. 分布式数据一致性 3. Redis实现分布式锁 3.1 方式一 3.2 方式二(改进方式一) 3.3 方式三(改进方式二) 3.4 方式四(改进方式三) 3.5 方式五(改进方式四) 3.6 小结 在单体应用中,如果我们对共享数据不进行加锁操作,会出现数据一致性问题,我们的解决办法通常是加锁. 在分布式架构中,我们同样会遇到数据共享操作问题,本文章使用Redis来解决分布式架构中的数据一致性问题. 1. 单机数据一致性 单机数据一致性架构如下图所示:多个可客户访

  • JavaScript中数组去重常用的五种方法详解

    目录 1.对象属性(indexof) 2.new Set(数组) 3.new Map() 4.filter() + indexof 5.reduce() + includes 补充 原数组 const arr = [1, 1, '1', 17, true, true, false, false, 'true', 'a', {}, {}]; 1.对象属性(indexof) 利用对象属性key排除重复项 遍历数组,每次判断新数组中是否存在该属性,不存在就存储在新数组中 并把数组元素作为key,最后返

  • JS实现导出Excel的五种方法详解【附源码下载】

    本文实例讲述了JS实现导出Excel的五种方法.分享给大家供大家参考,具体如下: 这五种方法前四种方法只支持IE浏览器,最后一个方法支持当前主流的浏览器(火狐,IE,Chrome,Opera,Safari) <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>html 表格导出道</title> <sc

  • Java解析XML的四种方法详解

    XML现在已经成为一种通用的数据交换格式,它的平台无关性,语言无关性,系统无关性,给数据集成与交互带来了极大的方便.对于XML本身的语法知识与技术细节,需要阅读相关的技术文献,这里面包括的内容有DOM(Document Object Model),DTD(Document Type Definition),SAX(Simple API for XML),XSD(Xml Schema Definition),XSLT(Extensible Stylesheet Language Transform

  • 一文搞懂Java创建线程的五种方法

    目录 题目描述 解题思路 代码详解 第一种 继承Thread类创建线程 第二种:实现Runnable接口创建线程 第三种:实现Callable接口,通过FutureTask包装器来创建Thread线程 第四种:使用ExecutorService.Callable(或者Runnable).Future实现返回结果的线程 第五种:使用ComletetableFuture类创建异步线程,且是据有返回结果的线程 题目描述 Java创建线程的几种方式 Java使用Thread类代表线程,所有线程对象都必须

  • 使用Java构造和解析Json数据的两种方法(详解二)

    JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,采用完全独立于语言的文本格式,是理想的数据交换格式.同时,JSON是 JavaScript 原生格式,这意味着在 JavaScript 中处理 JSON数据不须要任何特殊的 API 或工具包. 在www.json.org上公布了很多JAVA下的json构造和解析工具,其中org.json和json-lib比较简单,两者使用上差不多但还是有些区别.下面接着介绍用org.json构造和解析Json数据的方法

  • 使用Java构造和解析Json数据的两种方法(详解一)

    JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,采用完全独立于语言的文本格式,是理想的数据交换格式.同时,JSON是 JavaScript 原生格式,这意味着在 JavaScript 中处理 JSON数据不须要任何特殊的 API 或工具包. 在www.json.org上公布了很多JAVA下的json构造和解析工具,其中org.json和json-lib比较简单,两者使用上差不多但还是有些区别.下面首先介绍用json-lib构造和解析Json数据的方法

随机推荐