• 多线程-synchronized、lock


    1、什么时候会出现线程安全问题?

      在多线程编程中,可能出现多个线程同时访问同一个资源,可以是:变量、对象、文件、数据库表等。此时就存在一个问题:

      每个线程执行过程是不可控的,可能导致最终结果与实际期望结果不一致或者直接导致程序出错。

      如我们在第一篇博客中出现的count--的问题。这是一个典型的非线程安全问题。这一被多个线程访问的资源count变量被称为:临界资源(共享资源)。但当多个线程执行一个方法,方法内部的局部变量并不是临界资源,因为方法在栈上执行,java栈是线程私有的,因此不会产生线程安全问题


     2、如何解决线程安全问题?

      基本上所有的并发模式解决线程安全问题,都采用序列化临界资源的方案,即同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。在访问临界资源的代码前加锁,当访问完临界资源后释放锁,让其他线程继续访问。java中提供了两种方式来实现同步互斥访问:synchronized和lock


     3、synchronized关键字详解

      synchronized的三种应用方式包括:

      a:修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁

      b:修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

      c:修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁

      代码演示:实例对象锁就是synchronized修饰实例对象中的实例方法,实例方法不包括静态方法

    public class ThreadDemo2 {
        public static void main(String[] args) throws InterruptedException {
            Test1 test1 = new Test1();
            Thread t1 = new Thread(test1);
            Thread t2 = new Thread(test1);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
        }
    }
    class Test1 implements Runnable{
        //临界资源
        static int i=0;
    
        /**
         * synchronized 修饰实例方法
         */
        public synchronized void increase(){
            i++;
        }
        @Override
        public void run() {
            for(int j=0;j<100000;j++){
                increase();
                System.out.println(i);
            }
        }
    }

      当synchronized修饰increase()方法后,i值的操作便是线程安全的。输出结果是200000,如果不加synchronized,结果可能小于这个值。当一个线程正在访问一个对象的synchronized实例方法,其他线程就不能访问该对象其他的synchronized方法。一个对象只有一把锁。当一个线程获取该对象的锁后,其他线程无法获取该对象的锁。但可以访问该对象的非synchronized方法。有一种特殊的情况,当两个线程访问的实例对象不同,则锁是不同的,当这两个线程操作数据并非共享的,线程安全是有保障的,但当操作数据是共享的,那么线程安全无法保证,演示 如下代码:

    public class ThreadDemo2 {
        public static void main(String[] args) throws InterruptedException {
            Test1 test1 = new Test1();
            Thread t1 = new Thread(test1);
            Test1 test2 = new Test1();
            Thread t2 = new Thread(test2);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
        }
    }
    class Test1 implements Runnable{
        //临界资源
        static int i=0;
    
        /**
         * synchronized 修饰实例方法 锁对象是实例对象
         */
        public synchronized void increase(){
            i++;
        }
        @Override
        public void run() {
            for(int j=0;j<100000;j++){
                increase();
                System.out.println(i);
            }
        }
    }

      此demo运行结果也会出现值小于20000,我们创建了两个Test1的实例,启动两个不同的线程对共享变量i进行操作,虽然我们队increase方法添加了同步锁,但却new了两个不同实例,此时就存在两个实例对象锁,因此t1和t2都会进入各自的对象锁,因此无法保证线程安全。解决这种错误地方式就是将synchronized作用于静态的increase方法,这样的话,对象锁就是当前类对象,无论创建多少个实例对象,类对象只有一个,对象锁是唯一的。

    public class ThreadDemo2 {
        public static void main(String[] args) throws InterruptedException {
            Test1 test1 = new Test1();
            Thread t1 = new Thread(test1);
            Test1 test2 = new Test1();
            Thread t2 = new Thread(test2);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
        }
    }
    class Test1 implements Runnable{
        //临界资源
        static int i=0;
    
        /**
         * synchronized 修饰静态方法,锁对象是类的class对象
         */
        public static synchronized void increase(){
            i++;
        }
        @Override
        public void run() {
            for(int j=0;j<100000;j++){
                increase();
                System.out.println(i);
            }
        }
    }

      synchronized作用于静态方法时,锁是当前类的class对象锁。静态成员是类成员。通过class对象锁可以控制静态成员的并发操作。需要注意的是:如果一个线程调用一个实例对象的非static synchronized方法,而另一个线程调用这个实例对象所属类的静态synchronized方法,是允许的,不会发生互斥现象。因为访问静态synchronized方法占用的锁是当前类的class对象,而非访问静态synchronized方法占用的当前实例对象锁。锁对象不同,但我们需要意识到这种情况下可能发生线程安全问题,因为操作了共享资源。

      一些情况下,我们编写的方法体过大,同时存在一些比较耗时的操作。而需要同步的代码只有一小部分,我们可以通过synchronized代码块来对需要同步的代码进行包裹:

    public class ThreadDemo2 implements Runnable{
        static ThreadDemo2 test1 = new ThreadDemo2();
        //临界资源
        static int i=0;
        @Override
        public void run() {
            //省略其他耗时操作....
            //使用同步代码块对变量i进行同步操作,锁对象为test1
            synchronized(test1){
                for(int j=0;j<1000000;j++){
                    i++;
                }
            }
        }
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(test1);
            Thread t2 = new Thread(test1);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(i);
        }
    }

      当前情况下,将synchronized作用于一个给定的实例对象test1,即当前实例对象就是锁对象,除了test1作为对象外,我们还可以使用this对象(synchronized(this)代表当前实例)或当前类的class对象作为锁(synchronized(ThreadDemo2.class))。


     synchronized的一些特性:

      在java中synchronized是基于原子性的内部锁机制,是可重入的,在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性

      线程中断与synchronized:对于synchronized来说,如果一个线程在等待锁,结果只有两种。要么它获得锁继续执行,要么保存等待,即使调用中断线程的方法,也不会有效。

      等待唤醒机制与synchronized:这里主要指notify/notify/wait方法。使用这三个方法必须在synchronized代码块或synchronized方法中,否则就会抛出IllegalMonitorStateException异常。因为调用这几个方法必须拿到当前对象的monitor对象。monitor存在于引用指针中,而synchronized关键字可以获取monitor。与sleep不同的是wait方法调用完后,线程将被暂停,但wait方法会释放掉当前持有的锁。直到线程调用notify/notifyAll方法后才继续执行。sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后,不会马上释放锁,而是在相应的synchronized代码块或synchronized方法执行结束后才自动释放锁。


     synchronized实现原理:

      synchronized同步块:  

        synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

      synchronized同步方法:

        synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。


     4、Lock详解

      在上面synchronized的详解中,我们可以了解到当一个代码块被synchronized修饰,一个线程获取了对应的锁并执行该代码块。其他线程只能一直等待,等待获取锁的线程释放锁。这里获取锁的线程是释放锁的可能有两种:

      1)获取锁的线程执行完该代码块,线程释放锁

      2)线程执行发生异常,jvm会让线程自动释放锁

      如果这个获取锁的线程由于等待IO或其他原因被阻塞且没有释放锁,其他线程便只能等待。这种情况下synchronized就有了一些缺陷。通过Lock我们可以弥补这些缺陷。

      Lock的使用:

      在lock接口中,有四个方法来获取锁。lock()、tryLock()、tryLock(long time,TimeUnit unit) 和lockInterruptibly()。使用unLock()来释放锁。由于lock不会主动释放锁,发生异常时,不会自动释放锁。一般使用Lock必须在try{}catch{}块中进行。并将释放锁的操作放在finally中,保证锁一定被释放,防止死锁的发生。

      Lock():获取锁,如果锁已被其他线程获取,则进行等待

    Lock lock = ...;
    lock.lock();
    try{
        //处理任务
    }catch(Exception ex){
    
    }finally{
      //释放锁  
      lock.unlock();      
    }

      tryLock():尝试获取锁,如果获取成功,则返回true,如果获取失败则返回false。该方法无论如何都会立即返回,拿不到锁时不会一直等待。

      tryLock(long time,TimeUnit unit):与tryLock()方法类似,不同的是该方法拿不到锁时会等待一定时间,在时间期限内还拿不到锁就返回false。

    Lock lock = ...;
    if(lock.tryLock()){
        try{
            //处理任务
        }catch(Exception ex){
        }finally{
            //释放锁
           lock.unlock();            
        }  
    }else{
        //如果不能获取锁,执行其他任务
    }

      lockInterruptibly():获取锁时,如果线程正在等待获取锁,那该线程能响应中断,即中断等待状态。当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,若A线程获取了锁,而B线程只能等待。那么对B线程调用threadB.interrupt()方法能够中断B线程的等待过程。该方法的声明中抛出了异常,使用时必须放在try块中或在调用方法外声明抛出InterruptedException。

    public void method() throws InterruptedException {
        lock.lockInterruptibly();
        try {  
         //.....
        }
        finally {
            lock.unlock();
        }  
    }

      注意:持有锁的线程,是不会被Interrupt()方法中断的,它只能中断阻塞过程中的线程。当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,才可以响应中断。


     5、Lock接口的实现类ReentranLock

      ReentrantLock是唯一实现了Lock接口的类,并提供了更多的方法。

      Lock的正确使用

    public class Test {
        private ArrayList<Integer> arrayList = new ArrayList<Integer>();
        //此处声明lock为全局变量
        private Lock lock = new ReentrantLock();
        public static void main(String[] args) {
            final Test test = new Test();
            new Thread() {
                public void run() {
                    test.insert(Thread.currentThread());
                };
            }.start();
            new Thread() {
                public void run() {
                    test.insert(Thread.currentThread());
                };
            }.start();
        }
        
        public void insert(Thread thread) {
            lock.lock();
            try {
                System.out.println("当前是线程:"+thread.getName()+"获得了锁");
                for (int i = 0; i < 5; i++) {
                    arrayList.add(i);
                }
            } catch (Exception e) {
                
            } finally {
                System.out.println("线程:"+thread.getName()+"释放了锁");
                lock.unlock();
            }
        }
    }

    此处注意,特意在声明Lock的时候注释了是全局变量。因为当lock在方法里创建成局部变量的时候。每个线程执行到lock.lock()获取到的是不同的锁。不会发生冲突。一般使用时将Lock声明为全局变量即可。

    在这段代码里的insert()方法使用tryLock()方法,可以知道线程有没有获取到锁并输出结果。

    public void insert(Thread thread) {
            if(lock.tryLock()) {
                try {
                    System.out.println("当前是线程:"+thread.getName()+"获得了锁");
                    for (int i = 0; i < 5; i++) {
                        arrayList.add(i);
                    }
                } catch (Exception e) {
                    
                } finally {
                    System.out.println("线程:"+thread.getName()+"释放了锁");
                    lock.unlock();
                }
            }else {
                System.out.println("线程:"+thread.getName()+"获取锁失败");
            }
        }

    6、ReadWriteLock

      ReadWriteLock定义了两个方法,一个用来获取读锁,一个用来获取写锁。将文件的读写操作分开,分成两个锁来分配给线程。使得多个线程可以同时进行读操作。

      实现类:ReentrantReadWriteLock。主要两个方法readLock()和writeLock()用来获取读锁和写锁。

      一个实例:多个线程同时进行读操作。使用synchronized

    public class Test {
        private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
        public static void main(String[] args) {
            final Test test = new Test();
            new Thread() {
                public void run() {
                    test.get(Thread.currentThread());
                };
            }.start();;
            new Thread() {
                public void run() {
                    test.get(Thread.currentThread());
                };
            }.start();;
        }
        
        public synchronized void get(Thread thread) {
            long start = System.currentTimeMillis();
            while(System.currentTimeMillis() - start <= 1) {
                System.out.println("线程:"+thread.getName()+"正在进行读操作");
            }
            System.out.println("线程:"+thread.getName()+"读操作完毕");
        }
    }

    这样的输出结果可以发现,一个时间段内只有一个线程在执行读操作。一个线程执行完读操作,另一个线程才有机会执行。

    改为读写锁,实现多个线程同时读操作

    public  void get(Thread thread) {
            rwl.readLock().lock();
            try {
                long start = System.currentTimeMillis();
                while(System.currentTimeMillis() - start <= 1) {
                    System.out.println("线程:"+thread.getName()+"正在进行读操作");
                }
                System.out.println("线程:"+thread.getName()+"读操作完毕");
            } catch (Exception e) {
                // TODO: handle exception
            } finally {
                rwl.readLock().unlock();
            }
        }

    这段代码的输出结果可以看出,同时两个线程都在执行读操作。这样的话效率大大提升。不过要注意,如果一个线程占用了读锁,此时其他线程要申请写锁,那申请写锁的线程会一直等待释放读锁。如果一个线程占用了写锁,此时其他线程要申请写锁或读锁,则申请的线程会一直等待释放写锁。从性能上看,竞争资源不激烈,lock跟synchronized性能差不多,当竞争资源激烈时,lock的性能要远远优于synchronized。


    Synchronized和lock区别:
      1、Synchronized是java语言内置的特性,而lock是一个接口
      2、Synchronized不需要用户手动释放锁,当synchronized方法或代码块执行完后,自动释放锁,而lock需要用户手动释放锁,如果没有手动释放,可能产生死锁
      3、Synchronized修饰时,等待的线程会一直等待不能响应中断,lock可以让等待锁的线程响应中断。
      4、Lock可以知道有没有成功获取锁(tryLock方法),而synchronized不可以
      5、Lock可以提高多个线程进行读操作的效率

     7、锁的概念:

      可重入锁:从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功。该分配机制是基于线程而非基于方法的调用。

    class MyClass{
        synchronized void method1(){
            method2();
        }
        synchronized void method2(){
        } 

      synchronized是可重入锁。当一个线程执行method1时,已经获取到了对象的锁。调用method2就无需重新申请锁。不具备重入性时,线程持有该对象的锁,又去申请该对象的锁。将会使线程一直等待永远获取不到锁。synchronized和Lock都具备可重入性。

      可中断锁:可以响应中断的锁。即可以使在等待中的线程自己中断或者在别的线程中中断它。Lock是可响应中断的,synchronized不是。

      公平锁:尽量以请求锁的顺序来获取锁。多个线程同时等待一个锁,当此锁被释放时,等待最久的线程优先获得该锁。

      非公平锁:无法保证锁的获取是按照请求锁的顺序进行的。这样可能导致某个或一些线程永远获取不到锁,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。对于ReentranLock和ReentrantReadWriteLock,它默认是公平锁,也可设置为非公平锁

      读写锁:该锁将一个资源的访问分成了两个锁,读锁和写锁。保证了多个线程之间的读操作不发生冲突。ReadWriteLock是读写锁,它是一个接口。ReentrantReadWriteLock实现了这个接口。可以通过readLock()获取读锁,通过writeLock()获取写锁。

      

      

      

  • 相关阅读:
    python机器学习基础教程-鸢尾花分类
    LaTeX实战经验:如何写算法
    Latex公式最好的资料
    BibTex (.bib) 文件的注释
    Latex中参考文献排序
    LATEX双栏最后一页如何平衡两栏内容
    Latex强制图片位置
    Endnote输出Bibtex格式
    redis学习
    20180717
  • 原文地址:https://www.cnblogs.com/zhangbLearn/p/9790173.html
Copyright © 2020-2023  润新知