zoukankan      html  css  js  c++  java
  • Java的锁

    线程同步

    Java使用synchronized关键字对一个对象进行加锁,synchronized保证了代码块在任意时刻最多只有一个线程能执行

    使用synchronized:

    1.找出修改共享变量的线程代码块

    2.选择一个共享实例作为锁;

    3.使用synchronized(lockObject){}

    在使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁

    JVM规范定义了几种原子操作:

    基本类型(long和double除外)赋值      引用类型赋值,List<String> list = anotherList

     1 public class Main{
     2   public static void main(String[] args) throws Exception{
     3         var add = new AddThread();
     4         var dec = new DecThread();
     5         add.start();
     6         dec.start();
     7         add.join();
     8         dec.join();
     9         System.out.println(Counter.count);
    10     }  
    11 }
    12 
    13 class Counter{
    14     public static final Object lock = new Object();
    15     public static int count = 0;      
    16 }
    17 
    18 class AddThread extends Thread{
    19     public void run(){
    20         for (int i=0; i<10000; i++){
    21             synchronized(Counter.lock){
    22                 Counter.count += 1
    23             }
    24     }
    25     }
    26 }
    27 
    28 class DecThread extends Thread{
    29     public void run(){
    30         for (int i=0; i<10000; i++){
    31             synchronized(Counter.lock){
    32                 Counter.count -= 1;
    33             }
    34             }
    35     }
    36 }

    同步方法

        public void add(int n) {
            synchronized(this) {
                count += n;
            }
        }
    
    

    线程调用add()dec()方法时,它不必关心同步逻辑,因为synchronized代码块在add()dec()方法内部。并且,我们注意到,synchronized锁住的对象是this,即当前实例,这又使得创建多个Counter实例的时候,它们之间互不影响,可以并发执行

    如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的

    Java标准库的java.lang.StringBuffer也是线程安全的。

    还有一些不变类,例如StringIntegerLocalDate,它们的所有成员变量都是final,多线程同时访问时只能读不能写,这些不变类也是线程安全的。

    最后,类似Math这些只提供静态方法,没有成员变量的类,也是线程安全的。

    除了上述几种少数情况,大部分类,例如ArrayList,都是非线程安全的类,我们不能在多线程中修改它们。但是,如果所有线程都只读取,不写入,那么ArrayList是可以安全地在线程间共享的。

    public void add(int n) {
        synchronized(this) { // 锁住this
            count += n;
        } // 解锁
    }
    
    等价
    public synchronized void add(int n) { // 锁住this
        count += n;
    } // 解锁

    用synchronized修饰的方法就是同步方法,它表示整个方法都必须用this实例加锁。

    如果对一个静态方法添加synchronized修饰符,它锁住的是该类的class实例

    因为对于static方法,是没有this实例的,因为static方法是针对类而不是实例。但是任何一个类都有一个由JVM自动创建的Class实例,因此,对static方法添加synchronized,锁住的是该类的class实例。

    Java的线程锁是可重入的锁:

    JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。由于Java的线程锁是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出synchronized块,记录-1,减到0的时候,才会真正释放锁。

    使用wait和notify

    synchronized解决了多线程竞争的问题,但是并没有解决多线程协调的问题

    多线程协调运行的原则就是:

    当条件不满足时,线程进入等待状态;当条件满足时,线程被唤醒,继续执行任务

    public synchronized String getTask(){
      while (queue.isEmpty()){
            this.wait();
        }      
        return queue.remove();
    }
    
    #wait方法必须在当前获取的锁对象上调用,这里获取的是this锁,因此调用this.wait()
    #wait方法的执行机制非常复杂。他是定义在Object类的一个native方法,也就是由JVM的C代码实现的。其次,必须在synchronized块中才能调用wait()方法,因为wait()方法调用时,会释放线程获得的锁,wait()方法返回后,线程又会重新试图获得锁。
    #当一个线程在this.wait()等待时,它就会释放this锁,从而使得其他线程能够在其他方法获得this锁。
    #在相同的锁对象上调用notify方法,就可以让等待的线程被重新唤醒,这个方法会唤醒一个正在this锁等待的线程,从而使得等待线程从this.wait()方法返回。
    #notifyAll()更安全,有些时候,如果代码逻辑考虑不周,用notify()会导致只唤醒了一个线程,而其他线程可能永远等待下去。
    #wait()方法返回时需要重新获得this锁
    #要始终在while循环中wait(),并且每次被唤醒后拿到this锁就必须再次判断

    ReentrantLock 可重入锁

    高级的处理并发的java.util.concurrent包,它提供了大量更高级的并发功能,能大大简化多线程程序的编写。

    synchronized缺点:重;获取时必须一直等待,没有额外的尝试机制

    synchronized是Java语言层面提供的语法,所以不需要考虑异常,而ReentrantLock是Java代码实现的锁,就必须先获取锁,然后在finally中正确释放锁。

    优势:可以尝试获取锁,设置等到时间,超时未获取到锁,则返回False,程序就可以做一些额外处理,而不是无限等待下去。

    使用ReentrantLock比直接使用synchronized更安全,线程在tryLock()失败的时候不会导致死锁。

    if (lock.tryLock(1, TimeUnit.SECONDS)) {
        try {
            ...
        } finally {
            lock.unlock();
        }
    }

    使用Condition

    用可重入锁时候怎么编写wait和notify的功能呢?使用condition对象来实现

    Condition:

    await()会释放当前锁,进入等待状态

    signal()会唤醒某个等待线程

    signalAll()会唤醒所有等待线程

    唤醒线程从await()返回后需要重新获得锁

    此外,和tryLock类似,await()可以在等待指定时间后,如果还没有被其他线程通过signal()或signalAll()唤醒,可以自己醒来

    ReadWriteLock

    只允许一个线程写入(其他线程既不能写入也不能读取)

    没有写入时,多个线程允许同时读(提高性能)

    StampedLock

    ReadWriteLock有个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁

    新的读写锁StampedLock

    StampedLock和ReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后写入。这样一来,读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。

    乐观锁的意思就是乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。反过来,悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。

    StampedLock能进一步提升并发效率

    但是是有代价的:一是代码更加复杂,二是它是不可重入锁,不能再一个线程中反复获取同一个锁

    使用Concurrent集合

    BlockingQueue

    java.util.concurrent包提供的线程安全的集合:ArrayBlockingQueue

    Atomic

    线程池:Java语言虽然内置了多线程支持,启动一个新线程非常方便,但是,创建线程需要操作系统资源(线程资源,栈空间等),频繁创建和销毁大量线程需要消耗大量时间。

    能够接收大量小任务并进行分发处理的就是线程池。简单地说,线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。

    ExecutorService接口表示线程池

    ExecutorService executor = Executors.newFixedThreadPool(3);

    提供的几个常用实现类:

    FixedThreadPool:线程数固定的线程池

    CachedThreadPool:线程数根据任务动态调整的线程池

    SingleThreadExecutor:仅单线程执行的线程池

    如何在一个线程内传递状态?ThreadLocal

  • 相关阅读:
    tree
    mkdir
    touch
    rename
    dirname
    vue之导入Bootstrap以及jQuery的两种方式
    Vue组件系统
    报错:ERROR! The server quit without updating PID file (/usr/local/var/mysql/chenyuntekiMacBook-Air.local.pid).
    Linux命令
    Vue知识点小总结1
  • 原文地址:https://www.cnblogs.com/liushoudong/p/12634192.html
Copyright © 2011-2022 走看看