Java中多线程访问冲突的解决方式

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

第一种 同步方法

第二种 同步代码块

第三种 使用特殊成员变量(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关键字。例如

package com.test.multiThread;public class Bank {private int account = 0;public int getAccount(){return account;}// 同步方法public synchronized void save(int money){this.account += money;}
}=================================package com.test.multiThread;public class MyThread implements Runnable {private Bank bank;public MyThread(Bank bank){this.bank = bank;}@Overridepublic void run() {bank.save(1);//bank.save01(1);//bank.save02(1);}
}=================================package com.test.multiThread;import java.util.ArrayList;public class MultiThreadDemo {public static void main(String[] args) throws InterruptedException {Bank bank = new Bank();System.out.println(bank.getAccount());ArrayList<Thread> list = new ArrayList<>();for (int i = 0; i < 100000; i++){list.add(new Thread(new MyThread(bank)));}for (Thread thread: list){thread.start();}for (Thread thread: list){thread.join();}System.out.println(bank.getAccount());}
}

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

第二种 同步代码块

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

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

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

package com.test.multiThread;public class Bank {private int account = 0;public int getAccount(){return account;}// 同步代码块public void save(int money){synchronized (this){this.account += money;}}
}===============================package com.test.multiThread;public class MyThread implements Runnable {private Bank bank;public MyThread(Bank bank){this.bank = bank;}@Overridepublic void run() {bank.save(1);}
}===============================package com.test.multiThread;import java.util.ArrayList;public class MultiThreadDemo {public static void main(String[] args) throws InterruptedException {Bank bank = new Bank();System.out.println(bank.getAccount());ArrayList<Thread> list = new ArrayList<>();for (int i = 0; i < 100000; i++){list.add(new Thread(new MyThread(bank)));}for (Thread thread: list){thread.start();}for (Thread thread: list){thread.join();}System.out.println(bank.getAccount());}
}

当使用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接口也可以用来实现多线程的同步,其提供了如下方法来实现多线程的同步

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

ReentrantLock类的构造方法

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

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

package com.test.multiThread;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class Bank {private int account = 0;private Lock lock = new ReentrantLock();  // 声明这个重入锁public int getAccount(){return account;}public void save(int money){lock.lock();  // 以阻塞方式获得锁try {account += money;} finally {lock.unlock();  // 释放锁}}
}=============================package com.test.multiThread;public class MyThread implements Runnable {private Bank bank;public MyThread(Bank bank){this.bank = bank;}@Overridepublic void run() {bank.save(1);}
}=============================package com.test.multiThread;import java.util.ArrayList;public class MultiThreadDemo {public static void main(String[] args) throws InterruptedException {Bank bank = new Bank();System.out.println(bank.getAccount());ArrayList<Thread> list = new ArrayList<>();for (int i = 0; i < 100000; i++){list.add(new Thread(new MyThread(bank)));}for (Thread thread: list){thread.start();}for (Thread thread: list){thread.join();}System.out.println(bank.getAccount());}
}

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

public class ThreadLocal<T> extends Object

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

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

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

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

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

使用Thread-local的例子

package com.test.multiThread;public class Bank {private static ThreadLocal<Integer> account = ThreadLocal.withInitial(() -> 0);public void save(int money){account.set(account.get() + money);}public int getAccount(){return account.get();}
}============================package com.test.multiThread;public class MyThread implements Runnable {private Bank bank;public MyThread(Bank bank){this.bank = bank;}@Overridepublic void run() {for (int i = 1; i < 10; i++){bank.save(i);}System.out.println("Thread-local中的值: " + bank.getAccount());}
}============================package com.test.multiThread;import java.util.ArrayList;public class MultiThreadDemo {public static void main(String[] args) throws InterruptedException {Bank bank = new Bank();System.out.println("原始值:" + bank.getAccount());ArrayList<Thread> list = new ArrayList<>();for (int i = 0; i < 10; i++){list.add(new Thread(new MyThread(bank)));}for (Thread thread: list){thread.start();}for (Thread thread: list){thread.join();}System.out.println("原始值:" + bank.getAccount());}
}

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

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

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

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

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

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

package com.test.multiThread;import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;public class WorkDesk {private BlockingQueue<String> desk = new LinkedBlockingQueue<>(10);public void washDish() throws InterruptedException{desk.put("盘子");}public String useDish() throws InterruptedException{return desk.take();}
}=================================package com.test.multiThread;public class Producer implements Runnable {private String producerName;private WorkDesk workDesk;public Producer(String producerName, WorkDesk workDesk){this.producerName = producerName;this.workDesk = workDesk;}@Overridepublic void run() {try {while (true) {workDesk.washDish();System.out.println(producerName + "洗好一个盘子");Thread.sleep(1000);}} catch (Exception e){e.printStackTrace();}}
}=================================package com.test.multiThread;public class Consumer implements Runnable {private String consumerName;private WorkDesk workDesk;public Consumer(String consumerName, WorkDesk workDesk){this.consumerName = consumerName;this.workDesk = workDesk;}@Overridepublic void run() {try {while (true) {workDesk.useDish();System.out.println(consumerName + "使用一个盘子");Thread.sleep(1000);}} catch (Exception e){e.printStackTrace();}}
}=================================package com.test.multiThread;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class TestBlockingQueue {public static void main(String[] args){WorkDesk workDesk = new WorkDesk();ExecutorService service = Executors.newCachedThreadPool();Producer producer01 = new Producer("生产者-1-", workDesk);Producer producer02 = new Producer("生产者-2-", workDesk);Consumer consumer01 = new Consumer("消费者-1-", workDesk);Consumer consumer02 = new Consumer("消费者-2-", workDesk);service.submit(producer01);service.submit(producer02);service.submit(consumer01);service.submit(consumer02);}
}

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

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

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

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

package com.test.multiThread;import java.util.concurrent.atomic.AtomicInteger;public class Bank {private AtomicInteger account = new AtomicInteger(0);  // 创建具有给定初始值的新的AtomicIntegerpublic int getAccount(){return account.get();  // 获取当前值}public void save(int money){account.addAndGet(money);  // 以原子方式将给定值与当前值相加}
}================================package com.test.multiThread;public class MyThread implements Runnable {private Bank bank;public MyThread(Bank bank){this.bank = bank;}@Overridepublic void run() {bank.save(1);}
}================================package com.test.multiThread;import java.util.ArrayList;public class MultiThreadDemo {public static void main(String[] args) throws InterruptedException {Bank bank = new Bank();System.out.println("原始值:" + bank.getAccount());ArrayList<Thread> list = new ArrayList<>();for (int i = 0; i < 100000; i++){list.add(new Thread(new MyThread(bank)));}for (Thread thread: list){thread.start();}for (Thread thread: list){thread.join();}System.out.println("线程执行完后:" + bank.getAccount());}
}

转自:https://www.cnblogs.com/0820LL/p/9633787.html

Java中多线程访问冲突的解决方式相关推荐

  1. (面经总结)一篇文章带你整理面试过程中关于Java 中多线程的创建方式的最全整理

    文章目录 一.Java线程的创建方式 二.继承Thread类 三.实现 Runnable 接口 四.通过ExecutorService和`Callable`实现有返回值的线程 五.基于线程池 六.面试 ...

  2. 12月18日云栖精选夜读 | Java 中创建对象的 5 种方式!...

    作为Java开发者,我们每天创建很多对象,但我们通常使用依赖管理系统,比如Spring去创建对象.然而这里有很多创建对象的方法,我们会在这篇文章中学到. Java中有5种创建对象的方式,下面给出它们的 ...

  3. JAVA实现多线程的三种方式

    在Java中可通过三种方式来实现多线程: 1.继承Thread类,重写run( )方法 2.实现Runnable接口,重写run( )方法 3.实现Callable接口,重写call( )方法并使用F ...

  4. Java 实现多线程的四种方式 超详细

    Java 实现多线程的四种方式 文章目录 Java 实现多线程的四种方式 一.继承 Thread 类 二.实现 Runnable 接口 三.实现 Callable 接口 四.线程池 1,Executo ...

  5. java创建多线程的四种方式

    java多线程的创建方式是面试经常会被问到的一个问题,因此在这里我对java创建多线程的四种方式做一个简单的归纳与总结,便于复习. 一.继承Thread类创建多线程 ① 创建一个继承于Thread类的 ...

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

    Java中创建对象的几种方式 1.使用new创建对象,在堆上创建. 2.克隆 3.反序列化 4.反射创建对象 5.NIO中可以使用本地方法直接分配堆外内存. 转载于:https://www.cnblo ...

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

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

  8. Java中创建对象的四种方式

    为什么80%的码农都做不了架构师?>>>    Java中创建对象的四种方式 (1) 用new语句创建对象,这是最常见的创建对象的方法.    (2) 运用反射手段,调用java.l ...

  9. Java中多线程的性能比较

    Java中有多种用于多线程的技术. 可以通过同步关键字,锁或原子变量来并行化Java中的一段代码. 这篇文章将比较使用synced关键字ReentrantLock,getAndIncrement()以 ...

最新文章

  1. 使用 rocketmq-spring-boot-starter 来配置、发送和消费 RocketMQ 消息
  2. 360数科 CTO 王继平:金融 IT 变革浪潮下,360数科的技术破局
  3. Java笔记-java web实现验证码
  4. 【Day08】请简述虚拟 DOM 中 Key 的作用和好处
  5. iap php,PHP语言之华为应用内支付IAP验签
  6. TypeScript极速完全进阶指南-2中级篇
  7. 【ASP.NET Web API教程】2.3.4 创建Admin视图
  8. 3.看板方法---一种成功秘诀
  9. MongoDB 快速入门--高级
  10. (executor 1 exited caused by one of the running tasks) Reason: Executor heartbeat timed out after
  11. Zerg虫族的传说[官方资料]
  12. Python文本彩色图像去污
  13. ElasticSearch(6.3.0)的配置和使用全过程
  14. uni-app 自定义table-demo 左右列固定冻结
  15. Word中批量更新域的两个小方法
  16. 数据处理(10):SHP与JSON格式文件相互转换
  17. 程序员的诗和唐寅的诗
  18. 端口汇聚和端口聚合的区别
  19. Hive运行任务报错:Ended Job = job_1685266933359_0001 with errors Error during job, obtaining debugging info
  20. pandas 05-变形

热门文章

  1. Storm-源码分析-Topology Submit-Client
  2. Delphi 与 DirectX 之 DelphiX(25): TDIB.Blur();
  3. android支付平台,android移动支付
  4. mysql的extra,MySQL SQL优化-重点是 extra
  5. [蓝桥杯][2018年第九届真题]全球变暖
  6. vue——缓存路由组件
  7. shell菜鸟学习之echo命令
  8. 新仓库无线AP手持连接故障
  9. ConcurrentHashMap源码跟踪记录
  10. C++雾中风景4:多态引出的困惑,对象的拷贝?