zoukankan      html  css  js  c++  java
  • 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关键字。例如

     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 
    35 package com.test.multiThread;
    36 
    37 import java.util.ArrayList;
    38 
    39 public class MultiThreadDemo {
    40     public static void main(String[] args) throws InterruptedException {
    41         Bank bank = new Bank();
    42         System.out.println(bank.getAccount());
    43         ArrayList<Thread> list = new ArrayList<>();
    44         for (int i = 0; i < 100000; i++){
    45             list.add(new Thread(new MyThread(bank)));
    46         }
    47         for (Thread thread: list){
    48             thread.start();
    49         }
    50         for (Thread thread: list){
    51             thread.join();
    52         }
    53         System.out.println(bank.getAccount());
    54     }
    55 }

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

    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的例子

     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 }
  • 相关阅读:
    敏捷开发读后感
    结对编程:电梯调度 (张恿、赵骞)
    软件工程 Individual Project
    [转]SQLServer 2008以上误操作数据库恢复方法——日志尾部备份
    下拉菜单demo---参考阿里云首页顶部下拉菜单
    走进spring之springmvc实战篇(二)
    剖析javascript全局变量和局部变量
    走进spring之springmvc实战篇(一)
    日常总结——JSP篇(补)
    我的程序员之路
  • 原文地址:https://www.cnblogs.com/0820LL/p/9633787.html
Copyright © 2011-2022 走看看