五种Java多线程同步的方法

为什么要线程同步

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

一、不同步时的代码

Bank.java 

package threadTest; 

/**
* @author ww
*
*/
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

再看看运行结果:

余额不足 
账户余额:0

余额不足 
账户余额:0

1441790837380存进:100 
账户余额:100

1441790838380取出:100 
账户余额:0 
1441790838380存进:100 
账户余额:100

1441790839381取出:100 
账户余额:0

瞬间感觉可以理解了吧。

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

(2)同步代码块

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

Bank.java代码如下:

package threadTest; 

/**
* @author ww
*
*/
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)实现线程同步

a.volatile关键字为域变量的访问提供了一种免锁机制
b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新
c.因此每次使用该域就要重新计算,而不是使用寄存器中的值
d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量

Bank.java代码如下:

package threadTest; 

/**
* @author ww
*
*/
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 ww
*
*/
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 threadTest; 

/**
* @author ww
*
*/
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

余额不足 
账户余额:0

1441794247939存进:100 
账户余额:100

余额不足 
1441794248940存进:100 
账户余额:0

账户余额:200

余额不足 
账户余额:0

1441794249941存进:100 
账户余额:300

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

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

ThreadLocal与同步机制

a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题
b.前者采用以”空间换时间”的方法,后者采用以”时间换空间”的方式

现在都明白了吧。各有优劣,各有适用场景,希望本文可以对大家更深入的了解Java 多线程同步有所帮助。

(0)

相关推荐

  • 基于Java回顾之多线程同步的使用详解

    首先阐述什么是同步,不同步有什么问题,然后讨论可以采取哪些措施控制同步,接下来我们会仿照回顾网络通信时那样,构建一个服务器端的"线程池",JDK为我们提供了一个很大的concurrent工具包,最后我们会对里面的内容进行探索. 为什么要线程同步? 说到线程同步,大部分情况下, 我们是在针对"单对象多线程"的情况进行讨论,一般会将其分成两部分,一部分是关于"共享变量",一部分关于"执行步骤". 共享变量 当我们在线程对象(Run

  • java多线程实现服务器端与多客户端之间的通信

    用java语言构建一个网络服务器,实现客户端和服务器之间通信,实现客户端拥有独立线程,互不干扰. 应用多线程来实现服务器与多线程之间的通信的基本步骤 服务器端创建ServerSocket,循环调用accept()等待客户端链接 客户端创建一个Socket并请求和服务器端链接 服务器端接受客户端请求,创建socekt与该客户端建立专线链接 建立链接的socket在一个单独的线程上对话 服务器继续等待新的链接 服务器端Server.java package test.concurrent.socke

  • java 多线程-线程通信实例讲解

    线程通信的目标是使线程间能够互相发送信号.另一方面,线程通信使线程能够等待其他线程的信号. 通过共享对象通信 忙等待 wait(),notify()和 notifyAll() 丢失的信号 假唤醒 多线程等待相同信号 不要对常量字符串或全局对象调用 wait() 通过共享对象通信 线程间发送信号的一个简单方式是在共享对象的变量里设置信号值.线程 A 在一个同步块里设置 boolean 型成员变量 hasDataToProcess 为 true,线程 B 也在同步块里读取 hasDataToProc

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

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

  • 浅谈Java多线程实现及同步互斥通讯

    Java多线程深入理解本文主要从三个方面了解和掌握多线程: 1. 多线程的实现方式,通过继承Thread类和通过实现Runnable接口的方式以及异同点. 2. 多线程的同步与互斥中synchronized的使用方法. 3. 多线程的通讯中的notify(),notifyAll(),及wait(),的使用方法,以及简单的生成者和消费者的代码实现. 下面来具体的讲解Java中的多线程: 一:多线程的实现方式 通过继承Threa类来实现多线程主要分为以下三步: 第一步:继承 Thread,实现Thr

  • 深入理解JAVA多线程之线程间的通信方式

    一,介绍 本总结我对于JAVA多线程中线程之间的通信方式的理解,主要以代码结合文字的方式来讨论线程间的通信,故摘抄了书中的一些示例代码. 二,线程间的通信方式 ①同步 这里讲的同步是指多个线程通过synchronized关键字这种方式来实现线程间的通信. 参考示例: public class MyObject { synchronized public void methodA() { //do something.... } synchronized public void methodB()

  • Java多线程中线程间的通信实例详解

    Java多线程中线程间的通信 一.使用while方式来实现线程之间的通信 package com.ietree.multithread.sync; import java.util.ArrayList; import java.util.List; public class MyList { private volatile static List list = new ArrayList(); public void add() { list.add("apple"); } publ

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

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

  • 以银行取钱为例模拟Java多线程同步问题完整代码

    简单了解下在操作系统中进程和线程的区别: 进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程.(进程是资源分配的最小单位) 线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小.(线程是cpu调度的最小单位) 线程和进程一样分为五个阶段:创建.就绪.运行.阻塞.终止. 多进程是指操作系统能同时运行多个任务(程序). 多线程是指在同一程序中有多个顺序流在执行.首先存钱取钱的这个操作,应该是线程操作的

  • java多线程-同步块实例讲解

    java多线程-同步块 Java 同步块(synchronized block)用来标记方法或者代码块是同步的.Java 同步块用来避免竞争.本文介绍以下内容: Java 同步关键字(synchronzied) 实例方法同步 静态方法同步 实例方法中同步块 静态方法中同步块 Java 同步示例 Java 同步关键字(synchronized) Java 中的同步块用 synchronized 标记.同步块在 Java 中是同步在某个对象上.所有同步在一个对象上的同步块在同时只能被一个线程进入并执

  • Java多线程状态及方法实例解析

    这篇文章主要介绍了Java多线程状态及方法实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 首先介绍线程的五种状态: 新生态:New Thread() 就绪态:准备抢CPU时间片 运行态:抢到了CPU时间片 阻塞态:放弃已经抢到的CPU时间片,且暂时不参与争抢 死亡态:Run运行完了之后 接下来介绍三种方法:线程的阻塞,线程的优先级设置,线程的礼让 public class MutliThreadDemo4 { public static

  • ReentrantLock从源码解析Java多线程同步学习

    目录 前言 管程 管程模型 MESA模型 主要特点 AQS 共享变量 资源访问方式 主要方法 队列 node节点等待状态 ReentrantLock源码分析 实例化ReentrantLock 加锁 A线程加锁成功 B线程尝试加锁 释放锁 总结 前言 如今多线程编程已成为了现代软件开发中的重要部分,而并发编程中的线程同步问题更是一道难以逾越的坎.在Java语言中,synchronized是最基本的同步机制,但它也存在着许多问题,比如可重入性不足.死锁等等.为了解决这些问题,Java提供了更加高级的

  • JAVA多线程中join()方法的使用方法

    虽然关于讨论线程join()方法的博客已经非常极其特别多了,但是前几天我有一个困惑却没有能够得到详细解释,就是当系统中正在运行多个线程时,join()到底是暂停了哪些线程,大部分博客给的例子看起来都像是t.join()方法会使所有线程都暂停并等待t的执行完毕.当然,这也是因为我对多线程中的各种方法和同步的概念都理解的不是很透彻.通过看别人的分析和自己的实践之后终于想明白了,详细解释一下希望能帮助到和我有相同困惑的同学. 首先给出结论:t.join()方法只会使主线程(或者说调用t.join()的

  • Java多线程同步工具类CountDownLatch详解

    目录 简介 核心方法 CountDownLatch如何使用 CountDownLatch运行流程 运用场景 总结 简介 CountDownLatch是一个多线程同步工具类,在多线程环境中它允许多个线程处于等待状态,直到前面的线程执行结束.从类名上看CountDown既是数量递减的意思,我们可以把它理解为计数器. 核心方法 countDown():计数器递减方法. await():使调用此方法的线程进入等待状态,直到计数器计数为0时主线程才会被唤醒. await(long, TimeUnit):在

  • 五种JAVA GUI布局管理的方式

    1. 流式布局(FlowLayout) 定义: 通俗地说,流式布局就是根据窗口大小,自动改变窗口内组件的位置.例如:原窗口大小一行可以容纳10个BUTTON,但将窗口缩小后,每行仅能容纳5个BUTTON,此时原先的10个BUTTON中的五个就会自动排列到下一行. 示例:(省略panel的使用) Hashset package 布局管理; ​ import java.awt.*; import java.awt.event.WindowAdapter; import java.awt.event.

  • Python中五种实现字符串反转的方法

    目录 前言 方法1 方法2 方法3 方法4 方法5 前言 一道题目是实现一个反转字符串的函数,具体如下: 编写一个函数,其作用是将输入的字符串反转过来.输入字符串以字符数组 char[] 的形式给出. 不要给另外的数组分配额外的空间,你必须原地修改输入数组.使用 O(1) 的额外空间解决这一问题. 我们可以假设数组中的所有字符都是 ASCII 码表中的可打印字符. 示例 1: 输入:["h","e","l","l","

  • java多线程应用实现方法

    以前没有写笔记的习惯,现在慢慢的发现及时总结是多么的重要了,呵呵.虽然才大二,但是也快要毕业了,要加油了. 这一篇文章主要关于java多线程,主要还是以例子来驱动的.因为讲解多线程的书籍和文章已经很多了,所以我也不好意思多说,呵呵.大家可以去参考一些那些书籍.我这个文章主要关于实际的一些问题.同时也算是我以后复习的资料吧,.呵呵大家多多指教. 同时希望多结交一些技术上的朋友.谢谢. ---------------------------------------------------------

随机推荐