zoukankan      html  css  js  c++  java
  • Java中的synchronized以及读写锁

    Java中的synchronized以及读写锁

    什么是同步?

    在一个多线程的环境中,多个线程同时访问相同的资源的情况是存在的。例如,两个线程试图写入同一个文本文件。它们之间没有任何的同步,当两个或多个线程对同一文件具有写访问权时,写入该文件的数据可能会损坏。
    同理,在JVM中,每个线程在各自的栈上存储了一份变量的副本。某些其他线程可能会更改这些变量的实际值。但是更改后的值可能不会刷新到其他线程的本地副本中。
    这可能导致程序执行错误和非确定性行为。

    为了避免这种问题,Java给我们提供了synchronized这有助于实现线程之间的通信,使得只有一个线程访问同步资源,而其他线程等待资源变为空闲。

    synchronized关键字可以被用在下面一些不同的方式中,比如一个同步块:

     

    synchronized(someobject){
            //thread-safe code here
    }

    对方法进行同步:

     

    public synchronized void someMethod(){
            //thread-safe code here
    }

    3.在JVM中synchronized是如何实现的

    当一个线程试图进入一个同步块或者同步方法中的时候,它必须先获得一个同步对象上的锁。一次只可以有一个线程获取锁,并且执行块中的代码。

    如果其他线程尝试访问该同步块,则必须等待,直到当前线程执行完同步块的代码。当前线程退出后,锁将被自动释放,其它线程可以获取锁并进入同步代码块。

    • 对于一个synchronized块来说,在synchronized关键字后的括号中指定的对象上获取锁;
    • 对于一个synchronized static方法,锁是在.class对象上获取的;
    • 对于synchronized实例方法来说,锁定是在该类的当前实例上获得的,即该实例(this);

    4.同步方法

    定义同步方法就像在返回类型之前简单地包含关键字一样简单。我们定义一个顺序打印数字1-5之间的方法。会有两个线程来访问这个方法,所以让我们来看看在没有使用synchronized关键字它们的运行情况, 和我们使用关键字来锁住共享对象会发生什么:

     

    public class NonSynchronizedMethod {
    
        public void printNumbers() {
            System.out.println("Starting to print Numbers for " + Thread.currentThread().getName());
    
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
    
            System.out.println("Completed printing Numbers for " + Thread.currentThread().getName());
        }
    }

    现在,让我们实现两个访问该对象并希望运行 printNumbers() 方法的自定义线程:

     

    class ThreadOne extends Thread {
    
        NonSynchronizedMethod nonSynchronizedMethod;
    
        public ThreadOne(NonSynchronizedMethod nonSynchronizedMethod) {
            this.nonSynchronizedMethod = nonSynchronizedMethod;
        }
    
        @Override
        public void run() {
            nonSynchronizedMethod.printNumbers();
        }
    }
    
    class ThreadTwo extends Thread {
    
        NonSynchronizedMethod nonSynchronizedMethod;
    
        public ThreadTwo(NonSynchronizedMethod nonSynchronizedMethod) {
            this.nonSynchronizedMethod = nonSynchronizedMethod;
        }
    
        @Override
        public void run() {
            nonSynchronizedMethod.printNumbers();
        }
    }

    这些线程共享一个相同的对象NonSynchronizedMethod,它们会在这个对象上同时去调用非同步的方法printNumbers()。

    为了测试这个,写一个main方法来做测试:

     

    public class TestSynchronization {
        public static void main(String[] args) {
    
            NonSynchronizedMethod nonSynchronizedMethod = new NonSynchronizedMethod();
    
            ThreadOne threadOne = new ThreadOne(nonSynchronizedMethod);
            threadOne.setName("ThreadOne");
    
            ThreadTwo threadTwo = new ThreadTwo(nonSynchronizedMethod);
            threadTwo.setName("ThreadTwo");
    
            threadOne.start();
            threadTwo.start();
    
        }
    }

    运行上面的代码,我们会得到下面的结果:

    Starting to print Numbers for ThreadOne  
    
    Starting to print Numbers for ThreadTwo  
    
    ThreadTwo 0  
    
    ThreadTwo 1  
    
    ThreadTwo 2  
    
    ThreadTwo 3  
    
    ThreadTwo 4  
    
    Completed printing Numbers for ThreadTwo  
    
    ThreadOne 0  
    
    ThreadOne 1  
    
    ThreadOne 2  
    
    ThreadOne 3  
    
    ThreadOne 4  
    
    Completed printing Numbers for ThreadOne
    

    虽然ThreadOne先开始执行的,但是ThreadTwo先结束的。

    当我们再次运行上面的程序的时候,我们会得到一个不同的结果:

    Starting to print Numbers for ThreadOne  
    
    Starting to print Numbers for ThreadTwo  
    
    ThreadOne 0  
    
    ThreadTwo 0  
    
    ThreadOne 1  
    
    ThreadTwo 1  
    
    ThreadOne 2  
    
    ThreadTwo 2  
    
    ThreadOne 3  
    
    ThreadOne 4  
    
    ThreadTwo 3  
    
    Completed printing Numbers for ThreadOne  
    
    ThreadTwo 4  
    
    Completed printing Numbers for ThreadTwo
    

    这些输出完全是偶然的,完全不可预测。每次运行都会给我们一个不同的输出。因为可以有更多的线程,我们可能会遇到问题。在实际场景中,在访问某种类型的共享资源(如文件或其他类型的IO)时,这一点尤为重要,而不是仅仅打印到控制台。

    下面我们采用同步的方法,使用synchronized关键字:

     

    public synchronized void printNumbers() {
            System.out.println("Starting to print Numbers for " + Thread.currentThread().getName());
    
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
    
            System.out.println("Completed printing Numbers for " + Thread.currentThread().getName());
    }

    代码中只是给方法添加了一个synchronized关键字,没有其它的改动。现在我们运行上面的代码,得到如下所示的结果:

    Starting to print Numbers for ThreadOne  
    
    ThreadOne 0  
    
    ThreadOne 1  
    
    ThreadOne 2  
    
    ThreadOne 3  
    
    ThreadOne 4  
    
    Completed printing Numbers for ThreadOne  
    
    Starting to print Numbers for ThreadTwo  
    
    ThreadTwo 0  
    
    ThreadTwo 1  
    
    ThreadTwo 2  
    
    ThreadTwo 3  
    
    ThreadTwo 4  
    
    Completed printing Numbers for ThreadTwo
    

    在这里,我们看到即使两个线程同时运行,只有一个线程一次进入synchronized方法,在这种情况下是ThreadOne。一旦完成执行,ThreadTwo就可以执行printNumbers()方法

    5.同步块

    多线程的主要目的是尽可能并行地执行任意数量的任务。但是,同步限制了必须执行同步方法或块的线程的并行性。

    但是,我们可以尝试通过在同步范围内保留尽可能少的代码来减少以同步方式执行的代码量。可能有许多场景,不是在整个方法上同步,而是可以在方法中同步几行代码。

    我们可以使用synchronized块来包含代码的那部分而不是整个方法。也就是说对于需要同步的代码块进行同步,而不是对整个方法进行同步。

    由于在同步块内部执行的代码量较少,因此每个线程都会更快地释放锁定。结果,其他线程花费更少的时间等待锁定并且代码吞吐量大大增加。

    让我们修改前面的例子,只同步for循环打印数字序列,实际上,它是我们示例中应该同步的唯一代码部分:

     

    public class SynchronizedBlockExample {
        public void printNumbers() {
            System.out.println("Starting to print Numbers for " + Thread.currentThread().getName());
            synchronized (this) {
                for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName() + " " + i);
                }
            }
            System.out.println("Completed printing Numbers for " + Thread.currentThread().getName());
        }
    }

    运行结果:

    Starting to print Numbers for ThreadOne  
    
    Starting to print Numbers for ThreadTwo  
    
    ThreadOne 0  
    
    ThreadOne 1  
    
    ThreadOne 2  
    
    ThreadOne 3  
    
    ThreadOne 4  
    
    Completed printing Numbers for ThreadOne  
    
    ThreadTwo 0  
    
    ThreadTwo 1  
    
    ThreadTwo 2  
    
    ThreadTwo 3  
    
    ThreadTwo 4  
    
    Completed printing Numbers for ThreadTwo
    

    尽管ThreadTwo在ThreadOne完成其任务之前"开始"打印数字似乎令人担忧,这只是因为我们在停止ThreadTwo锁之前,允许线程通过System.out.println("Completed printing Numbers for " + Thread.currentThread().getName())语句。

    这很好,因为我们只想同步每个线程中的数字序列。我们可以清楚地看到两个线程只是通过同步for循环以正确的顺序打印数字。

    6.结论

    在这个例子中,我们看到了如何在Java中使用synchronized关键字来实现多个线程之间的同步。我们还通过例子了解了何时可以使用synchronized方法和块。

    Java中的读写锁

    synchronized基本上都是排它锁,意味着这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,在写线程访问的时候其他的读线程和写线程都会被阻塞。读写锁维护一对锁(读锁和写锁),通过锁的分离,使得并发性提高。

    读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

    ReadWriteLock仅定义了获取读锁和写锁的两个方法,即readLock()方法和writeLock()方法

    public class Cache {
        static Map<String, Object> map = new HashMap<String, Object>();
        static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
        static Lock r = rwl.readLock();
        static Lock w = rwl.writeLock();
        // 获取一个key对应的value
        public static final Object get(String key) {
            r.lock();
            try {
                return map.get(key);
            } finally {
                r.unlock();
            }
        }
        // 设置key对应的value,并返回旧的value
        public static final Object put(String key, Object value) {
            w.lock();
            try {
                return map.put(key, value);
            } finally {
                w.unlock();
            }
        }
        // 清空所有的内容
        public static final void clear() {
            w.lock();
            try {
                map.clear();
            } finally {
                w.unlock();
            }
        }
    }

    上述示例中,Cache组合一个非线程安全的HashMap作为缓存的实现,同时使用读写锁的读锁和写锁来保证Cache是线程安全的。

    在读操作get(String key)方法中,需要获取读锁,这使得并发访问该方法时不会被阻塞。

    写操作put(String key,Object value)方法和clear()方法,在更新HashMap时必须提前获取写锁,当获取写锁后,其他线程对于读锁和写锁的获取均被阻塞,而只有写锁被释放之后,其他读写操作才能继续。

    Cache使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性,同时简化了编程方式。

  • 相关阅读:
    Centos/Docker/Nginx/Node/Jenkins 操作
    MyBatis 流式查询
    127.0.0.1
    Spring中的@Bean注解
    工厂模式
    webservice
    vs每次拉下一个控件都必选设置为绝对位置才可以移动,怎样解决啊
    ASP.NET AJAX 概述
    AJAX介绍
    时间控件
  • 原文地址:https://www.cnblogs.com/kexinxin/p/11537875.html
Copyright © 2011-2022 走看看