• 线程同步基础


    多个执行线程共享一个资源的情景,是最常见的并发编程情景之一。在并发应用中常常遇到这样的情景:多个线程读或者写相同的数据,或者访问相同的文件或者数据库连接。

    为了防止这些共享资源可能出现的错误或数据不一致,我们必须实现一些机制来防止这些错误的发生。

    为了解决这些问题,人们引入了临界区概念。临界区是一个可用以访问共享资源的代码块,这个代码块在同一时间内只允许一个线程执行。

    为了帮助编程人员实行这个临界区,java提供了同步机制。当一个线程试图访问一个临界区时,它将使用一种同步机制来查看是不是已经有其他线程进入临界区。如果没有其他线程进入临界区,它就可以进入临界区;如果已经有线程进入了临界区,它就被同步机制挂起,直到进入的线程离开这个临界区。如果在等待进入临界区的线程不止一个,JVM会选择其中一个,其余的将继续等待。

    java语言提供了两种基本同步机制:

    1.synchronized关键字机制

    2.Lock接口及其实现机制

    一、synchronized关键字机制

    1.同步方法
    即有synchronized关键字修饰的方法。
    由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
    代码如:
    public synchronized void save(){}
    对于实例的同步方法,使用this即当前实例对象。
    对于静态的同步方法,使用当前类的字节码对象。
    注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。

    2.同步代码块
    即有synchronized关键字修饰的语句块。
    被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
    代码如:
    synchronized(object){}
    Java中任意的对象都可以作为一个监听器(monitor),监听器可以被上锁和解锁,在线程同步中称为同步锁,且同步锁在同一时间只能被一个线程所持有。上面的obj对象就是一个同步锁,分析一下上面代码的执行过程:
    1).一个线程执行到synchronized代码块,首先检查obj,如果obj为空,抛出NullPointerExpression异常;
    2).如果obj不为空,线程尝试给监听器上锁,如果监听器已经被锁,则线程不能获取到锁,线程就被阻塞;
    3).如果监听器没被锁,则线程将监听器上锁,并且持有该锁,然后执行代码块;
    4).代码块正常执行结束或者非正常结束,监听器都将自动解锁;

    线程同步锁对多个线程必须是互斥的,即多个线程需要使用同一个同步锁。代码中obj对象被多个线程共享,能够实现同步。
    注:同步是一种高开销的操作,因此应该尽量减少同步的内容。
    通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

    两者的区别主要体现在同步锁上面。对于实例的同步方法,因为只能使用this来作为同步锁,如果一个类中需要使用到多个锁,为了避免锁的冲突,必然需要使用不同的对象,这时候同步方法不能满足需求,只能使用同步代码块(同步代码块可以传入任意对象);或者多个类中需要使用到同一个锁,这时候多个类的实例this显然是不同的,也只能使用同步代码块,传入同一个对象。

    二、Lock接口及其实现机制

    Lock接口及实现类提供了更多的好处:

    1.支持更灵活的同步代码块结构。使用synchronized关键字时,只能在同一个synchronized块结构中获取和释放控制。Lock接口允许实现更复杂的临界区结构,即控制的获取和释放不出现在同一个块结构中。

    2.相比synchronized关键字,Lock接口提供了更多的功能。其中一个新功能是tryLock()方法的实现。这个方法试图获取锁,如果锁已被其他线程获取,它将返回false,并继续往下执行代码。使用synchronized关键字时,如果线程A试图执行一个同步代码块,而线程B已在执行这个同步代码块,则线程A就会被挂起直到线程B运行完这个同步代码块。使用锁tryLock()方法,通过返回值将得知是否有其他线程正在使用这个锁保护的代码块。

    3.Lock接口允许分离读和写操作,允许多个读线程和只有一个写线程。

    4.相比synchronized关键字,Lock接口具有更好的性能。

    锁的公平性
    ReentrantLock和ReentrantReadWriteLock类的构造器都含有一个布尔参数fair,它允许你控制着两个类的行为。默认fair值为false,它称为非公平模式。
    在非公平模式下,当有很多线程在等待锁时,锁将选择它们中的一个来访问临界区,这个选择是没有任何约束的。
    如果fair值为true,则称为公平模式。
    在公平模式下,当有很多线程在等待锁时,锁将选择它们中的一个来访问临界区,而且选择的是等待时间最长的。
    这两种模式只适用于lock()和unlock()方法。而Lock接口的tryLock()方法没有将线程置于休眠,fair属性并不影响这个方法。

    常用的线程类:

    1)、ReentrantLock类:是一个可重入的互斥锁,重入锁是一种递归无阻塞的同步机制。ReentrantLock由最近成功获取锁,还没有释放的线程所拥有,当锁被另一个线程拥有时,调用lock的线程可以成功获取锁。如果锁已经被当前线程拥有,当前线程会立即返回。
    a、防止重复执行(忽略重复触发)
    ReentrantLock lock = new ReentrantLock();
    public void getObject(){
        //如果已经被lock,则立即返回false不会等待,达到忽略操作的效果
        if (lock.tryLock()) {  
            try {  
                //操作  
            } finally {  
                lock.unlock();  
            }  
        }
    }    

    b、同步执行,类似synchronized
    ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁  
    //ReentrantLock lock = new ReentrantLock(true); //公平锁  
    public void getObject(){
        //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果  
        lock.lock();
        try {  
            //操作  
        } finally {  
            lock.unlock();  
        }
    }
    c、尝试等待执行
    ReentrantLock lock = new ReentrantLock(true); //公平锁  
    public void getObject(){
        try {  
            if (lock.tryLock(5, TimeUnit.SECONDS)) {      
                //如果已经被lock,尝试等待5s,看是否可以获得锁,如果5s后仍然无法获得锁则返回false继续执行  
                try {  
                    //操作  
                } finally {  
                    lock.unlock();  
                }  
            }  
        } catch (InterruptedException e) {  
            e.printStackTrace(); //当前线程被中断时(interrupt),会抛InterruptedException                   
        }
    }

    d、可中断锁的同步执行
    ReentrantLock lock = new ReentrantLock(true); //公平锁  
    public void getObject(){
        lock.lockInterruptibly();  
        try {  
            //操作  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        } finally {  
            lock.unlock();  
        }
    }

    ReadWriteLock类:读写锁,维护了一对相关的锁,一个用于只读操作,一个用于写入操作。只要没有writer,读取锁可以由多个reader线程同时保持。写入锁是独占的。
    ReentrantReadWriteLock类:可重入读写锁,会使用两把锁来解决问题,一个读锁,一个写锁。
    线程进入读锁的前提条件:
    1).没有其他线程的写锁,
    2).没有写请求或者有写请求,但调用线程和持有锁的线程是同一个
    线程进入写锁的前提条件:
    1).没有其他线程的读锁
    2).没有其他线程的写锁
    ReentrantReadWriteLock和ReentrantLock的区别,它和后者都是单独的实现,彼此之间没有继承或实现的关系:
    (a).重入方面其内部的WriteLock可以获取ReadLock,但是反过来ReadLock想要获得WriteLock则永远都不要想。
    (b).WriteLock可以降级为ReadLock,顺序是:先获得WriteLock再获得ReadLock,然后释放WriteLock,这时候线程将保持Readlock的持有。反过来ReadLock想要升级为WriteLock则不可能。
    (c).ReadLock可以被多个线程持有并且在作用时排斥任何的WriteLock,而WriteLock则是完全的互斥。这一特性最为重要,因为对于高读取频率而相对较低写入的数据结构,使用此类锁同步机制则可以提高并发量。
    (d).不管是ReadLock还是WriteLock都支持Interrupt,语义与ReentrantLock一致。
    (e).WriteLock支持Condition并且与ReentrantLock语义一致,而ReadLock则不能使用Condition,否则抛出UnsupportedOperationException异常。
    读写锁的例子:
    import java.util.Random;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    public class ReadWriteLockTest {
        public static void main(String[] args) {
            Queue3 q3 = new Queue3();
            for(int i=0;i<3;i++){
                new Thread(){
                    public void run(){
                        while(true){
                            q3.get();                        
                        }
                    }
                }.start();
            }
            for(int i=0;i<3;i++){        
                new Thread(){
                    public void run(){
                        while(true){
                            q3.put(new Random().nextInt(10000));
                        }
                    }            
                }.start();    
            }
        }
    }
     
    class Queue3{
        private Object data = null;//共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。
        private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
        public void get(){
            rwl.readLock().lock();//上读锁,其他线程只能读不能写
            System.out.println(Thread.currentThread().getName() + " be ready to read data!");
            try {
                Thread.sleep((long)(Math.random()*1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "have read data :" + data);        
            rwl.readLock().unlock(); //释放读锁,最好放在finnaly里面
        }
       
        public void put(Object data){
            rwl.writeLock().lock();//上写锁,不允许其他线程读也不允许写
            System.out.println(Thread.currentThread().getName() + " be ready to write data!");                    
            try {
                Thread.sleep((long)(Math.random()*1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.data = data;        
            System.out.println(Thread.currentThread().getName() + " have write data: " + data);                    
            rwl.writeLock().unlock();//释放写锁    
        }
    }

    在锁中使用多条件

    一个锁可能关联一个或多个条件,这些条件通过Condition接口声明。目的是允许线程获取锁并且查看等待的某一个条件是否满足,如果不满足就挂起直到某个线程唤醒它们。Condition接口提供了挂起线程和唤起线程的机制。

    Condition是在java1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition1的await()、signal()这种方式实现线程间协作更加安全和高效。
    Condition是个接口,基本的方法就是await()和signal()方法;
    Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
    调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
    Conditon中的await()对应Object的wait();
    Condition中的signal()对应Object的notify();
    Condition中的signalAll()对应Object的notifyAll()。
    代码示例:
    import java.util.concurrent.locks.Condition;  
    import java.util.concurrent.locks.ReentrantLock;  
     
     
    public class Main {  
        public static void main(String[] args) {  
            final ReentrantLock reentrantLock = new ReentrantLock();  
            final Condition condition = reentrantLock.newCondition();  
     
            new Thread(new Runnable() {  
                @Override  
                public void run() {  
                    reentrantLock.lock();  
                    System.out.println(Thread.currentThread().getName() + "拿到锁了");  
                    System.out.println(Thread.currentThread().getName() + "等待信号");  
                    try {  
                        condition.await();  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
     
                    System.out.println(Thread.currentThread().getName() + "拿到信号");  
     
                    reentrantLock.unlock();  
                }  
            }, "线程1").start();  
     
            new Thread(new Runnable() {  
                @Override  
                public void run() {  
                    reentrantLock.lock();  
                    System.out.println(Thread.currentThread().getName() + "拿到锁了");  
     
                    try {  
                        Thread.sleep(3000);  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
     
                    System.out.println(Thread.currentThread().getName() + "发出信号");  
                    condition.signalAll();  
     
                    reentrantLock.unlock();  
                }  
            }, "线程2").start();  
        }  

  • 相关阅读:
    C# 本质论 第三章 操作符和控制流
    LeetCode Online Judge 1. Two Sum
    C# 本质论 第二章 数据类型
    C# 本质论 第一章 C#概述
    TMDS代码
    `define `ifdef `elseif的巧妙使用
    加法器(行波与超前、全加与半加)
    DCI技术
    **标识符
    电容放置问题
  • 原文地址:https://www.cnblogs.com/web424/p/7880394.html
Copyright © 2020-2023  润新知