当时用多线程访问同一个资源时,非常容易出现线程安全的问题,例如当多个线程同时对一个数据进行修改时,会导致某些线程对数据的修改丢失。因此需要采用同步机制来解决这种问题。

第一种 同步方法

第二种 同步代码块

第三种 使用特殊成员变量(volatile 成员变量)实现线程同步(前提是对成员变量的操作是原子操作)

第四种 使用Lock接口(java.util.concurrent.locks包)

第五种 使用线程局部变量(thread-local)解决多线程对同一变量的访问冲突,而不能实现同步(ThreadLocal类)

第六种 使用阻塞队列实现线程同步(java.util.concurrent包)

第七种 使用原子变量实现线程同步 (java.util.concurrent.atomic包)


第一种 同步方法

同步方法即使用 synchronized关键字修饰的方法。在Java语言中,每个对象都有一个内置的对象锁与之相关联,该锁会保护整个方法,即对象在任何时候只允许被一个线程所拥有,当一个线程调用对象的一段synchronized代码时,首先需要获得这个锁,然后去执行相应的代码,执行结束,释放锁。synchronized关键字也可以以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。

synchronized关键字主要有两种用法:synchronized方法和synchronized块。此外该关键字还可以作用于静态方法、类或某个实例,但这都对程序的效率有很大的影响。

给一个方法增加synchronized关键字之后就可以使它成为同步方法,这个方法可以是静态方法和非静态方法,但是不能是抽象类的抽象方法,也不能是接口中的抽象方法。

synchronized方法,在方法的声明前加入synchronized关键字。例如

  1. package com.test.multiThread;
  2.  
  3. public class Bank {
  4. private int account = 0;
  5.  
  6. public int getAccount(){
  7. return account;
  8. }
  9. // 同步方法
  10. public synchronized void save(int money){
  11. this.account += money;
  12. }
  13. }
  14.  
  15. =================================
  16.  
  17. package com.test.multiThread;
  18.  
  19. public class MyThread implements Runnable {
  20. private Bank bank;
  21. public MyThread(Bank bank){
  22. this.bank = bank;
  23. }
  24. @Override
  25. public void run() {
  26. bank.save(1);
  27. //bank.save01(1);
  28. //bank.save02(1);
  29. }
  30. }
  31.  
  32. =================================
  33.  
  34. package com.test.multiThread;
  35.  
  36. import java.util.ArrayList;
  37.  
  38. public class MultiThreadDemo {
  39. public static void main(String[] args) throws InterruptedException {
  40. Bank bank = new Bank();
  41. System.out.println(bank.getAccount());
  42. ArrayList<Thread> list = new ArrayList<>();
  43. for (int i = 0; i < 100000; i++){
  44. list.add(new Thread(new MyThread(bank)));
  45. }
  46. for (Thread thread: list){
  47. thread.start();
  48. }
  49. for (Thread thread: list){
  50. thread.join();
  51. }
  52. System.out.println(bank.getAccount());
  53. }
  54. }

只要把多线程访问的资源的操作放到multiThreadAccess方法中,就能够保证这个方法在同一时刻只能被一个线程访问,从而保证了多线程访问的安全性。然而当一个方法的方法体规模非常大时,把该方法声明为synchronized会大大影响程序的执行效率。为了提高程序的执行效率,Java语言提供了synchronized块。

第二种 同步代码块

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

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

可以把任意的代码块声明为synchronized,也可以制定上锁的对象,有非常高的灵活性。用法如下

  1. package com.test.multiThread;
  2.  
  3. public class Bank {
  4. private int account = 0;
  5.  
  6. public int getAccount(){
  7. return account;
  8. }
  9. // 同步代码块
  10. public void save(int money){
  11. synchronized (this){
  12. this.account += money;
  13. }
  14. }
  15. }
  16.  
  17. ===============================
  18.  
  19. package com.test.multiThread;
  20.  
  21. public class MyThread implements Runnable {
  22. private Bank bank;
  23. public MyThread(Bank bank){
  24. this.bank = bank;
  25. }
  26. @Override
  27. public void run() {
  28. bank.save(1);
  29. }
  30. }
  31.  
  32. ===============================
  33.  
  34. package com.test.multiThread;
  35.  
  36. import java.util.ArrayList;
  37.  
  38. public class MultiThreadDemo {
  39. public static void main(String[] args) throws InterruptedException {
  40. Bank bank = new Bank();
  41. System.out.println(bank.getAccount());
  42. ArrayList<Thread> list = new ArrayList<>();
  43. for (int i = 0; i < 100000; i++){
  44. list.add(new Thread(new MyThread(bank)));
  45. }
  46. for (Thread thread: list){
  47. thread.start();
  48. }
  49. for (Thread thread: list){
  50. thread.join();
  51. }
  52. System.out.println(bank.getAccount());
  53. }
  54. }

当使用synchronized来修饰某个共享资源的时候,如果线程Thread01在执行synchronized代码,另外一个线程Thread02也要同时执行同一对象的统一synchronized代码时,线程Thread02将要等到线程Thread01执行成后才能继续执行。在这种情况下,可以使用wait()方法和notify()方法。

在synchronized代码被执行期间,线程可以调用对象的wait()方法,释放对象锁,进入等待状态,并且可以调用notify()方法或者notifyAll()方法通知正在等待的而其他线程,notify()唤醒一个线程(等待队列中的第一个线程),并允许它去获得锁,而notifyAll()方法唤醒所有等待这个对象的线程,并允许它们去竞争获得锁。

第三种 使用特殊成员变量(volatile 成员变量)实现线程同步(前提是对成员变量的操作是原子操作)

volatile是一个类型修饰符,被设计用来修饰被不同线程访问和修饰的变量。当变量没有被volatile修饰时,线程读取数据时可能会从缓存中去读取,如果其他线程修改了该变量,则无法读取到修改后的数据。当变量被volatile修饰时,线程每次使用时都会直接到内存中提取,而不会利用缓存,从而保证了数据的同步。

volatile关键字主要目的是放置编译器对代码的优化,使得每次使用数据的时候都从内存里提取,而不是缓存,保证获得的数据是最新被修改的数据。但是volatile不能保证操作的原子性,一般不能替代synchronized代码块,除非对变量的操作是原子操作的情况下才可以使用volatile。

① volatile关键字为成员变量的访问提供了一种免锁机制,但要保证对成员变量的操作是原子操作的情况下才能使用

② volatile关键字相当于告诉虚拟机该成员变量可能会被其他线程修改

③ 每次使用被volatile修饰的成员变量都要从内存提取,重新计算,而不会使用寄存机器中的值

④ volatile不会提供任何原子操作,不能保证线程安全

⑤ volatile不能用来修饰final类型的变量

⑥ 使用volatile会降低程序的执行效率

Java中原子性保证:Java内存模型只保证了基本读取和复制是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock保证任一时刻只有一个线程执行该代码,那么自然就不存在原子性问题了,从而保证了原子性。

Java中可见性保证:synchronized和Lock、volatile三种,推荐使用synchronized方式,volatile有局限,适合某个特定场合。

第四种 使用Lock接口(java.util.concurrent.locks包)

JDK5新增了一个java.util.concurrent.locks包来支持同步。该包中提供了Lock接口以及它的一个实现类ReentrantLock(重入锁)

Lock接口也可以用来实现多线程的同步,其提供了如下方法来实现多线程的同步

  1. public abstract void lock() // 以阻塞方式来获得锁,即如果获得了锁就立即返回,如果其他线程持有锁,当前线程等待,直到获取锁后返回。当前线程会一直处于阻塞状态,且会忽略interrupt()方法
  2. public abstract boolean tryLock() // 以非阻塞的方式获得锁,即尝试性的去获取锁,如果获得锁就返回true,否则返回false
  3. public abstract boolean tryLock(long time, TimeUnit unit) // 如果在给定时间内获得锁,返回true,否则返回false
  4. public abstract void lockInterruptibly // 如果获得锁,则立即返回,如果没有获得锁,则当前线程会处于休眠状态,直到获得锁,或者当前线程被其他线程中断(会收到InterruptedException异常)。
  5. public abstract void unlock // 释放锁

ReentrantLock类的构造方法

  1. public ReentrantLock() // 创建一个ReentrantLock实例
  2. public ReentrantLock(boolean fair) // 创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用

使用Lock接口实现多线程同步的例子

  1. package com.test.multiThread;
  2.  
  3. import java.util.concurrent.locks.Lock;
  4. import java.util.concurrent.locks.ReentrantLock;
  5.  
  6. public class Bank {
  7. private int account = 0;
  8. private Lock lock = new ReentrantLock(); // 声明这个重入锁
  9.  
  10. public int getAccount(){
  11. return account;
  12. }
  13. public void save(int money){
  14. lock.lock(); // 以阻塞方式获得锁
  15. try {
  16. account += money;
  17. } finally {
  18. lock.unlock(); // 释放锁
  19. }
  20. }
  21. }
  22.  
  23. =============================
  24.  
  25. package com.test.multiThread;
  26.  
  27. public class MyThread implements Runnable {
  28. private Bank bank;
  29. public MyThread(Bank bank){
  30. this.bank = bank;
  31. }
  32. @Override
  33. public void run() {
  34. bank.save(1);
  35. }
  36. }
  37.  
  38. =============================
  39.  
  40. package com.test.multiThread;
  41.  
  42. import java.util.ArrayList;
  43.  
  44. public class MultiThreadDemo {
  45. public static void main(String[] args) throws InterruptedException {
  46. Bank bank = new Bank();
  47. System.out.println(bank.getAccount());
  48. ArrayList<Thread> list = new ArrayList<>();
  49. for (int i = 0; i < 100000; i++){
  50. list.add(new Thread(new MyThread(bank)));
  51. }
  52. for (Thread thread: list){
  53. thread.start();
  54. }
  55. for (Thread thread: list){
  56. thread.join();
  57. }
  58. System.out.println(bank.getAccount());
  59. }
  60. }

第五种 使用线程局部变量(thread-local)解决多线程对同一变量的访问冲突,而不能实现同步 (ThreadLocal类)

  1. public class ThreadLocal<T>
  2. extends Object

如果使用ThreadLocal来管理变量,则每一个使用该变量的线程都会获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。所以对于同线程对共享变量的操作互不影响。

  1. public class ThreadLocal<T>
  2. extends Object
  3. 常用方法
  4. public ThreadLocal() // 构造方法
  5. public T get() // 返回次线程局部变量的当前线程副本中的值
  6. public void set(T value) // 将次线程局部变量的当前线程副本中的值设置为value
  7. protected T initialValue() // 返回次线程局部变量的当前线程的初始值
  8. public void remove() //

Thread-local与同步机制的比较:

1)两者都是为了解决多线程中相同变量的访问冲突问题

2)Thread-local采用“空间换时间”方法,同步机制采用“时间换空间”的方式

使用Thread-local的例子

  1. package com.test.multiThread;
  2.  
  3. public class Bank {
  4. private static ThreadLocal<Integer> account = ThreadLocal.withInitial(() -> 0);
  5. public void save(int money){
  6. account.set(account.get() + money);
  7. }
  8. public int getAccount(){
  9. return account.get();
  10. }
  11. }
  12.  
  13. ============================
  14.  
  15. package com.test.multiThread;
  16.  
  17. public class MyThread implements Runnable {
  18. private Bank bank;
  19. public MyThread(Bank bank){
  20. this.bank = bank;
  21. }
  22. @Override
  23. public void run() {
  24. for (int i = 1; i < 10; i++){
  25. bank.save(i);
  26. }
  27. System.out.println("Thread-local中的值: " + bank.getAccount());
  28. }
  29. }
  30.  
  31. ============================
  32.  
  33. package com.test.multiThread;
  34.  
  35. import java.util.ArrayList;
  36.  
  37. public class MultiThreadDemo {
  38. public static void main(String[] args) throws InterruptedException {
  39. Bank bank = new Bank();
  40. System.out.println("原始值:" + bank.getAccount());
  41. ArrayList<Thread> list = new ArrayList<>();
  42. for (int i = 0; i < 10; i++){
  43. list.add(new Thread(new MyThread(bank)));
  44. }
  45. for (Thread thread: list){
  46. thread.start();
  47. }
  48. for (Thread thread: list){
  49. thread.join();
  50. }
  51. System.out.println("原始值:" + bank.getAccount());
  52. }
  53. }

结果:改变的只是线程中变量的值,线程结束后Thread-local变量就销毁了

第六种 使用阻塞队列实现线程同步(java.util.concurrent包)

在JDK5提供的java.util.concurrent包中的 Class LinkedBlockingQueue<E> 可以实现线程的同步。

LinkedBlockingQueue<E>是一个基于已连接节点的,范围任意的blocking queue。其常用方法如下:

  1. public LinkedBlockingQueue() //创建一个容量为Interger.MAX_VALUE的LinkedBlockingQueue
  2. public int size() // 返回队列中的元素个数
  3. public void put(E e) throws InterruptedException // 在队尾添加一个元素,如果队列满则阻塞
  4. public E take() throws InterruptedException // 返回并移除对首元素,如果队列空则阻塞

使用阻塞队列实现生产者-消费者。总的来说生产者的速度和消费者的速度相同,但是因为阻塞队列的缘故,不需要控制阻塞,当阻塞对列满的时候,生产者线程就会被阻塞,直到不再满;反之亦然,当消费者线程多于生产者线程时,消费者速度大于生产者速度,当队列为空时,就会阻塞消费者线程,直到队列非空。

  1. package com.test.multiThread;
  2.  
  3. import java.util.concurrent.BlockingQueue;
  4. import java.util.concurrent.LinkedBlockingQueue;
  5.  
  6. public class WorkDesk {
  7. private BlockingQueue<String> desk = new LinkedBlockingQueue<>(10);
  8. public void washDish() throws InterruptedException{
  9. desk.put("盘子");
  10. }
  11. public String useDish() throws InterruptedException{
  12. return desk.take();
  13. }
  14. }
  15.  
  16. =================================
  17.  
  18. package com.test.multiThread;
  19.  
  20. public class Producer implements Runnable {
  21. private String producerName;
  22. private WorkDesk workDesk;
  23.  
  24. public Producer(String producerName, WorkDesk workDesk){
  25. this.producerName = producerName;
  26. this.workDesk = workDesk;
  27. }
  28. @Override
  29. public void run() {
  30. try {
  31. while (true) {
  32. workDesk.washDish();
  33. System.out.println(producerName + "洗好一个盘子");
  34. Thread.sleep(1000);
  35. }
  36. } catch (Exception e){
  37. e.printStackTrace();
  38. }
  39. }
  40. }
  41.  
  42. =================================
  43.  
  44. package com.test.multiThread;
  45.  
  46. public class Consumer implements Runnable {
  47. private String consumerName;
  48. private WorkDesk workDesk;
  49.  
  50. public Consumer(String consumerName, WorkDesk workDesk){
  51. this.consumerName = consumerName;
  52. this.workDesk = workDesk;
  53. }
  54.  
  55. @Override
  56. public void run() {
  57. try {
  58. while (true) {
  59. workDesk.useDish();
  60. System.out.println(consumerName + "使用一个盘子");
  61. Thread.sleep(1000);
  62. }
  63. } catch (Exception e){
  64. e.printStackTrace();
  65. }
  66. }
  67. }
  68.  
  69. =================================
  70.  
  71. package com.test.multiThread;
  72.  
  73. import java.util.concurrent.ExecutorService;
  74. import java.util.concurrent.Executors;
  75.  
  76. public class TestBlockingQueue {
  77. public static void main(String[] args){
  78. WorkDesk workDesk = new WorkDesk();
  79.  
  80. ExecutorService service = Executors.newCachedThreadPool();
  81. Producer producer01 = new Producer("生产者-1-", workDesk);
  82. Producer producer02 = new Producer("生产者-2-", workDesk);
  83.  
  84. Consumer consumer01 = new Consumer("消费者-1-", workDesk);
  85. Consumer consumer02 = new Consumer("消费者-2-", workDesk);
  86.  
  87. service.submit(producer01);
  88. service.submit(producer02);
  89. service.submit(consumer01);
  90. service.submit(consumer02);
  91. }
  92. }

第七种 使用原子变量实现线程同步(java.util.concurrent.atomic包)

需要使用线程同步的根本原因在于对普通变量的操作不是原子的。

原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作,即这几步要么同时完成,要么都不完成。

在JDK5中提供的java.util.concurrent.atomic包中提供了创建原子类型变量的工具类,使用这些工具类能够简化线程同步。

  1. package com.test.multiThread;
  2.  
  3. import java.util.concurrent.atomic.AtomicInteger;
  4.  
  5. public class Bank {
  6. private AtomicInteger account = new AtomicInteger(0); // 创建具有给定初始值的新的AtomicInteger
  7.  
  8. public int getAccount(){
  9. return account.get(); // 获取当前值
  10. }
  11.  
  12. public void save(int money){
  13. account.addAndGet(money); // 以原子方式将给定值与当前值相加
  14. }
  15. }
  16.  
  17. ================================
  18.  
  19. package com.test.multiThread;
  20.  
  21. public class MyThread implements Runnable {
  22. private Bank bank;
  23. public MyThread(Bank bank){
  24. this.bank = bank;
  25. }
  26. @Override
  27. public void run() {
  28. bank.save(1);
  29. }
  30. }
  31.  
  32. ================================
  33.  
  34. package com.test.multiThread;
  35.  
  36. import java.util.ArrayList;
  37.  
  38. public class MultiThreadDemo {
  39. public static void main(String[] args) throws InterruptedException {
  40. Bank bank = new Bank();
  41. System.out.println("原始值:" + bank.getAccount());
  42. ArrayList<Thread> list = new ArrayList<>();
  43. for (int i = 0; i < 100000; i++){
  44. list.add(new Thread(new MyThread(bank)));
  45. }
  46. for (Thread thread: list){
  47. thread.start();
  48. }
  49. for (Thread thread: list){
  50. thread.join();
  51. }
  52. System.out.println("线程执行完后:" + bank.getAccount());
  53. }
  54. }

Java中多线程访问冲突的解决方式的更多相关文章

  1. Java进阶(四十二)Java中多线程使用匿名内部类的方式进行创建3种方式

    Java中多线程使用匿名内部类的方式进行创建3种方式 package cn.edu.ujn.demo; // 匿名内部类的格式: public class ThreadDemo { public st ...

  2. Java中多线程使用匿名内部类的方式进行创建3种方式

    /* * 匿名内部类的格式: */ public class ThreadDemo { public static void main(String[] args) { // 继承thread类实现多 ...

  3. Java:多线程概述与创建方式

    目录 Java:多线程概述与创建方式 进程和线程 并发与并行 多线程的优势 线程的创建和启动 继承Thread类 start()和run() 实现Runnable接口 实现Callable接口 创建方 ...

  4. Java中创建对象的几种方式

    Java中创建对象的五种方式: 作为java开发者,我们每天创建很多对象,但是我们通常使用依赖注入的方式管理系统,比如:Spring去创建对象,然而这里有很多创建对象的方法:使用New关键字.使用Cl ...

  5. Java中反射的三种常用方式

    Java中反射的三种常用方式 package com.xiaohao.test; public class Test{ public static void main(String[] args) t ...

  6. pycharm:terminal中显示乱码的解决方式

    pycharm:terminal中显示乱码的解决方式

  7. Java中创建对象的五种方式

    我们总是讨论没有对象就去new一个对象,创建对象的方式在我这里变成了根深蒂固的new方式创建,但是其实创建对象的方式还是有很多种的,不单单有new方式创建对象,还有使用反射机制创建对象,使用clone ...

  8. Java中的静态代理实现方式

    1.编写一个接口类 如:Subject package com.neusoft.pattern.staticProxy; /** * <p>Title:</p> * <p ...

  9. 2.1多线程(java学习笔记) java中多线程的实现(附静态代理模式)

    一.多线程 首先我们要清楚程序.进程.线程的关系. 首先进程从属于程序,线程从属于进程. 程序指计算机执行操作或任务的指令集合,是一个静态的概念. 但我们实际运行程序时,并发程序因为相互制约,具有“执 ...

随机推荐

  1. node.js fs、http使用

    学习node核心模块http.fs;的使用 首先在server.js文件中require两个模块http.fs; let fs = require('fs')let http = require (' ...

  2. Java的二分查找

    今天学习了二分查找,虽然代码简单,但还是要有必要,记录一下今天的学习的. public class TestBrinarySeach { public static void main(String[ ...

  3. StringBuild类

    每次拼接都会产生新的字符串对象,从而产生很多废弃的垃圾,拼的越多,垃圾越多,而利用StringBuilder来拼接字符串自始至终用的都是同一个StringBuilder容器 StringBuilder ...

  4. LINUX 设置 backspace为删除键

    描述 :在linux/unix平台上的 sqlplus中,如果输错了字符,要想删除,习惯性的按下backspace键后,发现非但没有删除想要删掉的字符,还多出了两个字符^H. 原因:由于终端默认ctr ...

  5. SQLServer 学习笔记 序

    邀月的博客 http://www.cnblogs.com/downmoon/archive/2011/03/10/1980172.html

  6. Solr的搭建

    Solr6.6.0下载地址 http://www.apache.org/dyn/closer.lua/lucene/solr/ 安装JRE 需要Java Runtime Environment(JRE ...

  7. 【Django】关于scss 的安装

    今天看视频教程的时候发现老师的样式文件改用了scss(然鹅我买的1块钱特价课程其实是节选出来的,所以前面没有看到过关于scss的介绍) 然后我本以为把原来的css改名字为scss就行,然鹅没有效果. ...

  8. 2019-3-10——生成对抗网络GAN---生成mnist手写数字图像

    """ 生成对抗网络(GAN,Generative Adversarial Networks)的基本原理很简单: 假设有两个网络,生成网络G和判别网络D.生成网络G接受一 ...

  9. linux之master和minion

    saltstack博客地址: https://www.cnblogs.com/pyyu/p/9465608.html在线yaml文件编写:http://www.bejson.com/validator ...

  10. PHP文件上传与下载

    一:上传文件与报错 $_FILES 超全局数组,包含了有关上传文件的所有信息! 而且,这个数组中只包含文件相关信息,其他数据依然在$_POST里面$_FILES是一个二维数组,每上传一个文件,都是数组 ...