• java多线程(线程同步)


    一、同步方法

    对共享资源进行访问的方法定义中加上synchronized关键字修饰,使得此方法称为同步方法。可以简单理解成对此方法进行了加锁,其锁对象为当前方法所在的对象自身。多线程环境下,当执行此方法时,首先都要获得此同步锁(且同时最多只有一个线程能够获得),只有当线程执行完此同步方法后,才会释放锁对象,其他的线程才有可能获取此同步锁,以此类推...

    public synchronized void run() {
           
        // ....
     
    }
    View Code

    二、同步代码块

    正如上面所分析的那样,解决线程安全问题其实只需限制对共享资源访问的不确定性即可。使用同步方法时,使得整个方法体都成为了同步执行状态,会使得可能出现同步范围过大的情况,于是,针对需要同步的代码可以直接另一种同步方式——同步代码块来解决。

    同步代码块的格式为:

    synchronized (obj) {
    
                  //... 
    
    }
    View Code

    其中,obj为锁对象,因此,选择哪一个对象作为锁是至关重要的。一般情况下,都是选择此共享资源对象作为锁对象。

    如上例中,最好选用account对象作为锁对象。(当然,选用this也是可以的,那是因为创建线程使用了runnable方式,如果是直接继承Thread方式创建的线程,使用this对象作为同步锁会其实没有起到任何作用,因为是不同的对象了。因此,选择同步锁时需要格外小心...)

    三、Lock对象同步锁

    使用Lock对象同步锁可以方便的解决此问题,唯一需要注意的一点是Lock对象需要与资源对象同样具有一对一的关系。Lock对象同步锁一般格式为:

    class X{ 
      
        priavte final Lock lock = ReentrantLock();
        lock.lock();
        try{
            //处理任务
        }catch(Exception ex){
         
        }finally{
            lock.unlock();   //释放锁
        }
    
    }
    View Code

    首先lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

    3.1ReentrantLock(重入锁)

    重入锁可以完全替代synchronized关键字,在jdk5早期版本中重入锁的性能远远好于synchronized,但从JDK6开始JDK在synchronized中做了大量的优化,是的两者的性能差距不大。

    ReentrantLock具有公平和非公平两种模式,也各有优缺点:
    公平锁是严格的以FIFO的方式进行锁的竞争,但是非公平锁是无序的锁竞争,刚释放锁的线程很大程度上能比较快的获取到锁,队列中的线程只能等待,所以非公平锁可能会有“饥饿”的问题。但是重复的锁获取能减小线程之间的切换,而公平锁则是严格的线程切换,这样对操作系统的影响是比较大的,所以非公平锁的吞吐量是大于公平锁的,这也是为什么JDK将非公平锁作为默认的实现。

    公平锁

    大多数情况下锁的申请都是非公平的。如一个线程1先请求了锁A,然后线程2页也请求了锁A,那么当锁A可用时,是线程1可以获得锁还是线程2是不一定的,系统只是会从这个锁的等待队列中随机挑选一个。

    重入锁允许我们对其公平性进行设置。公平锁的一大特点是:它不会产生饥饿现象。只要排队,最终你就可以获得资源。可以使用如下构造函数创建公平锁:

    public ReentrantLock(boolean fair)
    View Code

    当参数fair为true,表示锁的公平的,当然由于公平所需要维护有序队列,因此公平锁的实现成本比较高,性能相对也底下,所以默认都是非公平锁。

    public class Test {
        public static ReentrantLock lock = new ReentrantLock(true);
        public static void main(String[] args) throws Exception {
            Thread t1 = new TestThread();
            Thread t2 = new TestThread();
            t1.start();
            t2.start();
        }
    }
    class TestThread extends Thread {
        @Override
        public void run() {
            while(true)
            try {
                Test.lock.lock();
                System.out.println(Thread.currentThread().getName()+"获得锁");
            } finally{
                Test.lock.unlock();
            }
        }
    }
    View Code

    可以看到如上代码制定公平锁之后,两个线程交替获得锁

    Thread-1获得锁
    Thread-0获得锁
    Thread-1获得锁
    Thread-0获得锁
    Thread-1获得锁
    Thread-0获得锁
    Thread-1获得锁
    Thread-0获得锁
    Thread-1获得锁
    Thread-0获得锁
    Thread-1获得锁
    Thread-0获得锁
    Thread-1获得锁
    ...
    View Code

    3.2ReentrantReadWriteLock(读写锁)

    ReadWriteLock是JDK5开始提供的读写分离锁。读写分离开有效的帮助减少锁的竞争,以提升系统性能。用锁分离的机制避免多个读操作线程之间的等待。

    读写锁的访问约束:

    • 读-读不互斥:读读之间不阻塞
    • 读-写互斥:读堵塞写,写也阻塞读
    • 写-写互斥:写写阻塞

    如果在一个系统中读的操作次数远远大于写操作,那么读写锁就可以发挥明显的作用,提升系统性能

    public class Test {
        private static Lock lock = new ReentrantLock();
        private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        private static Lock readLock = reentrantReadWriteLock.readLock();
        private static Lock writeLock = reentrantReadWriteLock.writeLock();
        private static int value;
    
        public static Object handleRead(Lock lock) throws InterruptedException {
            try {
                lock.lock();
                Thread.sleep(1000);// 模拟读操作
                System.out.println("读操作:" + value);
                return value;
            } finally {
                lock.unlock();
            }
        }
    
        public static void handleWrite(Lock lock, int index)
                throws InterruptedException {
            try {
                lock.lock();
                Thread.sleep(1000);// 模拟写操作
                System.out.println("写操作:" + value);
                value = index;
            } finally {
                lock.unlock();
            }
        }
    
        public static void main(String[] args) throws Exception {
            TestReadThread testReadThread = new TestReadThread();
            TestWriteThread testWriteThread = new TestWriteThread();
            for (int i = 0; i < 18; i++) {
                new Thread(testReadThread).start();
            }
            for (int i = 18; i < 20; i++) {
                new Thread(testWriteThread).start();
            }
    
        }
        
        private static class TestReadThread extends Thread {
            @Override
            public void run() {
                try {
                    //Test.handleRead(lock);
                    Test.handleRead(readLock);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        private static class TestWriteThread extends Thread {
            @Override
            public void run() {
                try {
                    //Test.handleWrite(lock,new Random().nextInt(100));
                    Test.handleWrite(writeLock,new Random().nextInt(100));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    View Code

    参考https://www.cnblogs.com/whatadiors/p/8013086.html

    四、lock与synchronized的区别

    1、ReentrantLock (可重复锁)拥有Synchronized相同的并发性和内存语义,此外还多了 锁投票,定时锁等候和中断锁等。
    线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定,
    如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断
    如果 使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情

    ReentrantLock获取锁定与三种方式:

    • lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁
    • tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;
    • tryLock(long timeout,TimeUnit unit), 如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;
    • lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断


    2、在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;

    3、Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;

    4、Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

    五、原子操作CAS

    5.0的多线程任务包对于同步的性能方面有了很大的改进,在原有synchronized关键字的基础上,又增加了ReentrantLock,以及各种Atomic类。了解其性能的优劣程度,有助与我们在特定的情形下做出正确的选择。

    synchronized(同步关键字): 
    在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好,不管用没用过5.0多线程包的程序员都能理解。

    ReentrantLock(可重复锁):
    ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。

    Atomic(原子类):
    和上面的类似,不激烈情况下,性能比synchronized略逊,而激烈的时候,也能维持常态。激烈的时候,Atomic的性能会优于ReentrantLock一倍左右。但是其有一个缺点,就是只能同步一个值,一段代码中只能出现一个Atomic的变量,多于一个同步无效。因为他不能在多个Atomic之间同步。

    AtomicInteger用法

    AtomicInteger提供原子操作来进行Integer的使用,因此十分适合高并发情况下的使用。

    public class Sample2 {
    
        private static AtomicInteger count = new AtomicInteger(0);
    
        public static void increment() {
            count.getAndIncrement();
        }
    
    }
    View Code

    以上两段代码,在使用Integer的时候,必须加上synchronized保证不会出现并发线程同时访问的情况,而在AtomicInteger中却不用加上synchronized,在这里AtomicInteger是提供原子操作的,下面就对这进行相应的介绍。

    CAS原子操作说明:一个旧值A,一个内存地址V,一个预期值B,把i值存到A当中,i++存到B当中,当且仅当V=A,才将B更新到V当中。

    原子操作参考https://blog.csdn.net/u010904188/article/details/87712060

    参考https://www.cnblogs.com/zhaoyan001/p/8885360.html

    参考https://www.cnblogs.com/nsw2018/p/5821738.html

    六、wait()/notify()/notifyAll()线程通信

    wait():导致当前线程等待并使其进入到等待阻塞状态。直到其他线程调用该同步锁对象的notify()或notifyAll()方法来唤醒此线程。

    notify():唤醒在此同步锁对象上等待的单个线程,如果有多个线程都在此同步锁对象上等待,则会任意选择其中某个线程进行唤醒操作,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。

    notifyAll():唤醒在此同步锁对象上等待的所有线程,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。

    我们需要注意如下几点:

    1.wait()方法执行后,当前线程立即进入到等待阻塞状态,其后面的代码不会执行;

    2.notify()/notifyAll()方法执行后,将唤醒此同步锁对象上的(任意一个-notify()/所有-notifyAll())线程对象,但是,此时还并没有释放同步锁对象,也就是说,如果notify()/notifyAll()后面还有代码,还会继续进行,知道当前线程执行完毕才会释放同步锁对象;

    3.notify()/notifyAll()执行后,如果右面有sleep()方法,则会使当前线程进入到阻塞状态,但是同步对象锁没有释放,依然自己保留,那么一定时候后还是会继续执行此线程,接下来同2;

    4.wait()/notify()/nitifyAll()完成线程间的通信或协作都是基于不同对象锁的,因此,如果是不同的同步对象锁将失去意义,同时,同步对象锁最好是与共享资源对象保持一一对应关系;

    5.当wait线程唤醒后并执行时,是接着上次执行到的wait()方法代码后面继续往下执行的。

    在Condition中,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll(),

    参考https://www.cnblogs.com/lwbqqyumidi/p/3821389.html

  • 相关阅读:
    mysql查询版本
    mysql导出bug备注
    linux查找文件
    linux 编译式安装nginx
    linux文件校验
    linux 编译式安装apache
    网络层
    数据链路层
    物理层
    servlet过滤器
  • 原文地址:https://www.cnblogs.com/yanmingyuan/p/10566207.html
Copyright © 2020-2023  润新知