一、引言

前几天面试,被大师虐残了,好多基础知识必须得重新拿起来啊。闲话不多说,进入正题。

二、为什么要线程同步

因为当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。举个例子,如果一个银行账户同时被两个线程操作,一个取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() {@Overridepublic void run() {// TODO Auto-generated method stubwhile(true){try {Thread.sleep(1000);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}bank.addMoney(100);bank.lookMoney();System.out.println("\n");}}});Thread tsub = new Thread(new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stubwhile(true){bank.subMoney(100);bank.lookMoney();System.out.println("\n");try {Thread.sleep(1000);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();} }}});tsub.start();tadd.start();}}

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

余额不足
账户余额:0余额不足
账户余额:1001441790503354存进:100
账户余额:1001441790504354存进:100
账户余额:1001441790504354取出:100
账户余额:1001441790505355存进:100
账户余额:1001441790505355取出:100
账户余额:100

四、使用同步时的代码

(1)同步方法:

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

修改后的Bank.java

package threadTest;/*** @author ww**/
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余额不足
账户余额:01441790837380存进:100
账户余额:1001441790838380取出:100
账户余额:0
1441790838380存进:100
账户余额:1001441790839381取出: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);}
}

运行结果如下:

余额不足
账户余额:01441791806699存进:100
账户余额:1001441791806700取出:100
账户余额:01441791807699存进: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余额不足
账户余额:1001441792010959存进:100
账户余额:1001441792011960取出:100
账户余额:01441792011961存进: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余额不足
账户余额:01441792891934存进:100
账户余额:1001441792892935存进:100
账户余额:2001441792892954取出:100
账户余额:100

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

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

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

Bank.java代码如下:

package threadTest;/*** @author ww**/
public class Bank {private static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){@Overrideprotected Integer initialValue() {// TODO Auto-generated method stubreturn 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余额不足
账户余额:01441794247939存进:100
账户余额:100余额不足
1441794248940存进:100
账户余额:0账户余额:200余额不足
账户余额:01441794249941存进:100
账户余额:300

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

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

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

现在都明白了吧。各有优劣,各有适用场景。手工,吃饭去了。

Java学习笔记---多线程同步的五种方法相关推荐

  1. 0037 Java学习笔记-多线程-同步代码块、同步方法、同步锁

    什么是同步 在上一篇0036 Java学习笔记-多线程-创建线程的三种方式示例代码中,实现Runnable创建多条线程,输出中的结果中会有错误,比如一张票卖了两次,有的票没卖的情况,因为线程对象被多条 ...

  2. java多线程同步的五种方法

    一.前几天去面试,被大师问道一些很基础的问题,感觉自己答的很不满意,闲话不多说,进入正题. 二.为什么要使用同步? 因为当我们有多个线程要同时访问同一个变量或对象时,如果这些线程中午既有读又有写操作时 ...

  3. Java学习笔记---多线程并发

    Java学习笔记---多线程并发 (一)认识线程和进程 (二)java中实现多线程的三种手段 [1]在java中实现多线程操作有三种手段: [2]为什么更推荐使用Runnable接口? [3][补充知 ...

  4. StackExchange.Redis学习笔记(二) Redis查询 五种数据类型的应用

    StackExchange.Redis学习笔记(二) Redis查询 五种数据类型的应用 原文: StackExchange.Redis学习笔记(二) Redis查询 五种数据类型的应用 Connec ...

  5. JAVA中创建线程池的五种方法及比较

    之前写过JAVA中创建线程的三种方法及比较.这次来说说线程池. JAVA中创建线程池主要有两类方法,一类是通过Executors工厂类提供的方法,该类提供了4种不同的线程池可供使用.另一类是通过Thr ...

  6. Java学习笔记 --- 多线程

    一.线程相关概念 程序 程序是为完成特定任务,用某种语言编写的一组指令的集合.简单的说就是我们写的代码 进程 1.进程是指运行中的程序,比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存 ...

  7. java多线程同步的四种方法_java中实现多线程的两种方法

    java多线程有几种实现方法,都是什么?同步有几种实java中多线程的实现方法有两种:1.直接继承thread类:2.实现runnable接口:同步的实现方法有五种:1.同步方法:2.同步代码块:3. ...

  8. Java实现线程同步的五种方法

    一.使用synchronized关键字 由于每个java对象都有一个内置锁,用synchronized修饰方法或者代码块时,内置锁会保护整个方法或代码块,要想执行这个方法或者代码块必须获得其内置锁,运 ...

  9. 实现Java线程同步的五种方法

    线程同步概念 Java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不明确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的 ...

最新文章

  1. 【系列索引】结合项目实例 回顾传统设计模式 打造属于自己的模式类系列
  2. php循环方法实现先序、中序、后序遍历二叉树
  3. Require.js
  4. 【笔记】js Function类型 内部方法callee
  5. Xcode终端常用的指令-终端输入
  6. 旷视孙剑团队提出Anchor DETR:基于Transformer的目标检测新网络
  7. css,css,javascript实现一个简单的计算器
  8. 并发译文翻译计划(二)
  9. 联系随笔2---在tomcat服务器上发布servlet应用程序
  10. java mvc中重复提交表单,spring mvc 防止重复提交表单的两种方法,推荐第二种
  11. Mysql优化(出自官方文档) - 第五篇
  12. Openproj 在64位操作系统报错errno=193
  13. 小猪的Python学习之旅 —— 7.Python并发之threading模块(1)
  14. win10中计算机是英文的,如何解决Win10启动和登陆界面语言中英文混合显示?
  15. 电脑版mc的服务器显示内存不足,我的世界内存溢出怎么办 我的世界内存不足解决方法_3DM单机...
  16. java基于ssm的在线装机DIY系统的分析与设计
  17. 上课作业(5)——#576. 饥饿的牛(hunger)
  18. 用VB创建一个对象数组
  19. python从字符串中提取指定的内容
  20. java项目设计与思路

热门文章

  1. ENC编码器之5G无线直播方案来袭
  2. 基于uniapp的旅游小程序APP源码
  3. CSGO地图制作:优化编译时间
  4. 2019年陕西单招计算机机测试题,2019年陕西单招数学试题答案.pdf
  5. Nunjucks使用
  6. nui-treeselect控件实现二级子节点互斥选择
  7. Java中三种常用布局方式
  8. linux系统下Qt应用程序重启,嵌入式Linux重启QT应用程序的简单办法(基于QT4.8 qws)...
  9. 使用matplotlib添加画布层面的外部图像
  10. 【Rust日报】 2019-06-04:「心得」如何改进一个棘手的并发接口