多线程---详解各种锁和AQS原理_finecoarselock.class-程序员宅基地

技术标签: 多线程  多线程与高并发  并发编程  

1. synchronized

详细信息参考下方文章

https://blog.csdn.net/A_Java_Dog/article/details/118679431

  • synchronized(Object)

    Object不能是String , Integer, Long , 因为字符串最终都是基于一个字符串常量池中的数据。

  • 线程同步

    synchronized

    • 锁的是对象, 是不是代码

    • synchronized void methodName 锁定的是this(当前对象)
      synchronized static void m静态方法锁定的是XXX.class

      public class T {
              
      
          private int count = 10;
      
          public synchronized void m() {
               //等同于在方法的代码执行时要synchronized(this)
              count--;
              System.out.println(Thread.currentThread().getName() + " count = " + count);
          }
      }
      
      private static int count = 10;
      
      //因为这个m是静态方法, 所以这里等同于synchronized(T.class)
      public synchronized static void m() {
               
          count--;
          System.out.println(Thread.currentThread().getName() + " count = " + count);
      }
      
    • 锁定方法和非锁定方法同步执行

        public static void main(String[] args) {
              
            T t = new T();
            //m1是同步方法, m2是非同步方法, 如果m1和m2不能同时调用, 则肯定会在m1执行完成后,
            //也就是输出m1 end后, 才会执行m2
            //结果显示: m1开始执行后, m2立刻开始执行, 也就是两者执行不受影响
            new Thread(t::m1, "t1").start();
            new Thread(t::m2, "t2").start();
        }
      
        public synchronized void m1() {
              
            System.out.println(Thread.currentThread().getName() + " m1 start...");
            try {
              
                Thread.sleep(10000);
            } catch (InterruptedException e) {
              
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " m1 end");
        }
      
        public void m2() {
              
            System.out.println(Thread.currentThread().getName() + " m2 start...");
            try {
              
                Thread.sleep(5000);
            } catch (InterruptedException e) {
              
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " m2 end");
        }
      
    • 锁升级- 偏向锁, 自旋锁, 重量级锁

    • 使用条件:1. 执行时间短(加锁代码),线程数少,用自旋

      ​ 2. 执行时间长,线程数多,用重量级锁(系统锁)(系统锁有等待队列, 不占用CPU资源)

      ​ 3. 操作消耗时间长->使用重量级锁

可重入锁, 非公平锁, 悲观锁

可重入

同一个线程

synchronized代码中 , 同一个线程可再次调用其他synchronized修饰的方法,如下例所示

public class T01_ReentrantLock1 {
    
   synchronized void m1() {
    
      for(int i=0; i<10; i++) {
    
         try {
    
            TimeUnit.SECONDS.sleep(1);
         } catch (InterruptedException e) {
    
            e.printStackTrace();
         }
         System.out.println(i);
         if(i == 2) {
    
             //如果synchronized锁不可重入, 则这里将会阻塞住, 因为当前线程执行的m1方法持有该对象的锁,
            this.m2();
             //这里输出m2的结果, 证明同一个线程下synchronized锁可重入
         }
      }
      
   }
   
   synchronized void m2() {
    
      System.out.println("m2 ...");
   }
   
   public static void main(String[] args) {
    
      T01_ReentrantLock1 rl = new T01_ReentrantLock1();
      new Thread(rl::m1).start();
      try {
    
         TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
    
         e.printStackTrace();
      }
   }
}

执行结果

在这里插入图片描述

不同线程

/**
 * 本例中由于m1锁定this,只有m1执行完毕的时候,m2才能执行
 * @author cyc
 */
package com.cyc.juc.c_020_lock;

import java.util.concurrent.TimeUnit;

public class T01_ReentrantLock1 {
    
   synchronized void m1() {
    
      for(int i=0; i<10; i++) {
    
         try {
    
            TimeUnit.SECONDS.sleep(1);
         } catch (InterruptedException e) {
    
            e.printStackTrace();
         }
         System.out.println(i);
      }
      
   }
   
   synchronized void m2() {
    
      System.out.println("m2 ...");
   }
   
   public static void main(String[] args) {
    
      T01_ReentrantLock1 rl = new T01_ReentrantLock1();
      new Thread(rl::m1).start();
      try {
    
         TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
    
         e.printStackTrace();
      }
       //由于m1锁定this,只有m1执行完毕的时候,m2才能执行
      new Thread(rl::m2).start();
   }
}

执行结果

在这里插入图片描述

2. volatile

保证线程可见性

  • MESI
  • 缓存一致性协议

见上方 线程三大特性-可见性解析

禁止指令重排序

使用以下单例模式, 懒汉式的双重检查为例

package com.cyc.design.singleton;

/**
 * @author chenyunchang
 * @version 1.0
 * lazy loading
 * 也称懒汉式
 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
 * 可以通过synchronized解决,但也带来效率下降
 */
public class Singleton06 {
    

    //volatile关键字禁止指令重排
    private static volatile Singleton06 INSTANCE;


    public void c() {
    
        System.out.println("C");
    }

    /**
     * 构造方法为私有,只能在当前类中new,外部类无法new出来
     */
    private Singleton06() {
    
    }

    public static Singleton06 getInstance() {
    
        if (INSTANCE == null) {
    
            //双重检查
            synchronized (Singleton06.class) {
    
                if (INSTANCE == null) {
    
                    try {
    
                        //这里让进入此代码块的线程睡一毫秒
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
    
                        e.printStackTrace();
                    }
                    INSTANCE = new Singleton06();
                }
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
    
        for (int i = 0; i < 100; i++) {
    
            new Thread(() ->
                    System.out.println(Singleton06.getInstance().hashCode())
            ).start();
        }
    }

}

查看结果

在这里插入图片描述

为什么要加volatile?

为了防止指令重排。这里涉及到类的加载过程。

首先,第一个线程进来了, 加上锁之后,进入到INSTANCE = new Singleton06();代码,在初始化进行到一半的时候,也就是在preparation阶段,已经给Singleton06申请完内存,里面的成员变量已经赋过默认值,比如0,此时INSTANCE 已经指向这个分配的内存, 已经不再是null,此时另外一个线程进来了,由于此时INSTANCE 已经进行了半初始化状态,所以在if (INSTANCE == null)为false,此时另一个线程会拿到这个INSTANCE中的成员变量进行操作, 这样显然是不满足要求的。

想要解析这个问题, 需要查看其字节码文件

例如下面这个测试类T, 使用idea插件查看其字节码文件

在这里插入图片描述

在0 new #2 <com/cyc/jvm/c0_basic/T>之后,已经申请过内存。

4 invokespecial #3 <com/cyc/jvm/c0_basic/T.> 这个给类中的静态变量赋初始值

在调用完4之后,才会把这块内存赋值给t,但是由于指令可能会重排的原因, 如果先执行的是7 astore_1, 相当于先把这个地址扔到内存中, 然后在进行的T初始化, 这种情况下,在双重检查懒汉式单例中,就会出现有别的线程读取到半初始化的单例。

  • DCL单例
  • Double Check Lock

3. 锁优化

锁细化

 /**
 * synchronized优化
 * 同步代码块中的语句越少越好
 * 比较m1和m2
 * @author cyc
 */
package com.cyc.juc.c_016_LockOptimization;
import java.util.concurrent.TimeUnit;

public class FineCoarseLock {
    
   
   int count = 0;

   synchronized void m1() {
    
      //do sth need not sync
      try {
    
         TimeUnit.SECONDS.sleep(2);
      } catch (InterruptedException e) {
    
         e.printStackTrace();
      }
      //业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
      count ++;
      
      //do sth need not sync
      try {
    
         TimeUnit.SECONDS.sleep(2);
      } catch (InterruptedException e) {
    
         e.printStackTrace();
      }
   }
   
   void m2() {
    
      //do sth need not sync
      try {
    
         TimeUnit.SECONDS.sleep(2);
      } catch (InterruptedException e) {
    
         e.printStackTrace();
      }
      //业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
      //采用细粒度的锁,可以使线程争用时间变短,从而提高效率
      synchronized(this) {
    
         count ++;
      }
      //do sth need not sync
      try {
    
         TimeUnit.SECONDS.sleep(2);
      } catch (InterruptedException e) {
    
         e.printStackTrace();
      }
   }

   

}

锁粗化

当一个方法中有很多代码块都加锁时 , 不如直接将锁加在方法上

锁对象

/**
 * 锁定某对象o,如果o的属性发生改变,不影响锁的使用
 * 但是如果o变成另外一个对象,则锁定的对象发生改变
 * 应该避免将锁定对象的引用变成另外的对象或者将对象设为final类型
 * @author cyc
 */
package com.cyc.juc.c_017_MoreAboutSync;

import java.util.concurrent.TimeUnit;


public class SyncSameObject {
    

   /*final*/ Object o = new Object();

   void m() {
    
      synchronized(o) {
    
         while(true) {
    
            try {
    
               TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
    
               e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName());
            
            
         }
      }
   }
   
   public static void main(String[] args) {
    
      SyncSameObject t = new SyncSameObject();
      //启动第一个线程
      new Thread(t::m, "t1").start();
      
      try {
    
         TimeUnit.SECONDS.sleep(3);
      } catch (InterruptedException e) {
    
         e.printStackTrace();
      }
      //创建第二个线程
      Thread t2 = new Thread(t::m, "t2");
      
      t.o = new Object(); //锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会
      
      t2.start();
      
   }

   

}

4. CAS(无锁优化, 自旋)

在java.util.concurrent包下, Atomic开头的都是通过CAS来保证线程安全的类

https://blog.csdn.net/A_Java_Dog/article/details/118679431

cas(期望值,更新值)

m=0

m++

expected = read m;

cas(0,1){
for(;//如果当前m值==0 m=1

}

5. JUC同步锁

0. 前置知识(AQS)

AQS维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,保证线程之间的可见性和有序性。state的访问方式有三种:

  • getState()
  • setState()
  • compareAndSetState()

AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

1. ReentrantLock(可重入锁, 排他锁)

源码解析

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

acquire(int)

此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。

  1. tryAcquire()尝试直接去获取资源,如果成功则直接返回(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加塞一次,而CLH队列中可能还有别的线程在等待);
  2. addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
  3. acquireQueued()使线程阻塞在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

ReentrantLock属于JUC自带的锁, CAS类型,需要自己手动解锁,加锁一定要写在try finally中,保证在最终一定能解锁, 而synchronized属于自动上锁,自动释放锁。

synchronized代码块字节码展示:

在这里插入图片描述

ReentrantLock代码块字节码展示:

在这里插入图片描述

同一个线程

/**
 * reentrantlock用于替代synchronized
 * 由于m1锁定this,只有m1执行完毕的时候,m2才能执行
 * 使用reentrantlock可以完成同样的功能
 * 需要注意的是,必须要必须要必须要手动释放锁(重要的事情说三遍)
 * 使用syn锁定的话如果遇到异常,jvm会自动释放锁,但是lock必须手动释放锁,因此经常在finally中进行锁的释放
 * @author cyc
 */
public class T02_ReentrantLock2 {
    
    Lock lock = new ReentrantLock();

    void m1() {
    
        try {
    
            lock.lock(); //synchronized(this)
            for (int i = 0; i < 10; i++) {
    
                TimeUnit.SECONDS.sleep(1);

                System.out.println(i);
                if (i == 2) {
    
                   this.m2();
                }
            }
        } catch (InterruptedException e) {
    
            e.printStackTrace();
        } finally {
    
            lock.unlock();
        }
    }

    void m2() {
    
        try {
    
            lock.lock();
            System.out.println("m2 ...");
        } finally {
    
            lock.unlock();
        }

    }

    public static void main(String[] args) {
    
        T02_ReentrantLock2 rl = new T02_ReentrantLock2();
        new Thread(rl::m1).start();
        try {
    
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
    
            e.printStackTrace();
        }
    }
}

不同线程

public class T02_ReentrantLock2 {
    
    Lock lock = new ReentrantLock();

    void m1() {
    
        try {
    
            lock.lock(); //synchronized(this)
            for (int i = 0; i < 10; i++) {
    
                TimeUnit.SECONDS.sleep(1);
                System.out.println(i);
            }
        } catch (InterruptedException e) {
    
            e.printStackTrace();
        } finally {
    
            lock.unlock();
        }
    }

    void m2() {
    
        try {
    
            lock.lock();
            System.out.println("m2 ...");
        } finally {
    
            lock.unlock();
        }

    }

    public static void main(String[] args) {
    
        T02_ReentrantLock2 rl = new T02_ReentrantLock2();
        new Thread(rl::m1).start();
        try {
    
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
    
            e.printStackTrace();
        }
        new Thread(rl::m2).start();
    }
}

tryLock

m1执行10秒钟, 在m1执行期间, m2去尝试在5秒钟获取锁,如果获取不到则继续执行下面的方法,输出m2 …false。

/**
 * 使用reentrantlock可以进行“尝试锁定”tryLock,这样无法锁定,或者在指定时间内无法锁定,线程可以决定是否继续等待
 * @author cyc
 */
package com.cyc.juc.c_020_lock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class T03_ReentrantLock3 {
    
   Lock lock = new ReentrantLock();

   void m1() {
    
      try {
    
         lock.lock();
         for (int i = 0; i < 10; i++) {
    
            TimeUnit.SECONDS.sleep(1);

            System.out.println(i);
         }
      } catch (InterruptedException e) {
    
         e.printStackTrace();
      } finally {
    
         lock.unlock();
      }
   }

   /**
    * 使用tryLock进行尝试锁定,不管锁定与否,方法都将继续执行
    * 可以根据tryLock的返回值来判定是否锁定
    * 也可以指定tryLock的时间,由于tryLock(time)抛出异常,所以要注意unclock的处理,必须放到finally中
    */
   void m2() {
    
      /*
      boolean locked = lock.tryLock();
      System.out.println("m2 ..." + locked);
      if(locked) lock.unlock();
      */
      
      boolean locked = false;
      
      try {
    
         // 在5秒内尝试获取锁, 如果获取不到, 则放弃去获取
         locked = lock.tryLock(5, TimeUnit.SECONDS);
         System.out.println("m2 ..." + locked);
      } catch (InterruptedException e) {
    
         e.printStackTrace();
      } finally {
    
         if(locked) lock.unlock();
      }
      
   }

   public static void main(String[] args) {
    
      T03_ReentrantLock3 rl = new T03_ReentrantLock3();
      new Thread(rl::m1).start();
      try {
    
         TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
    
         e.printStackTrace();
      }
      new Thread(rl::m2).start();
   }
}

执行结果

在这里插入图片描述

将m1方法改为休眠3秒钟,

   for (int i = 0; i < 3; i++) {
    
            TimeUnit.SECONDS.sleep(1);

            System.out.println(i);
         }

输出结果m2 …true

lockInterruptibly()

/**
 * 使用ReentrantLock还可以调用lockInterruptibly方法,可以对线程interrupt方法做出响应,
 * 在一个线程等待锁的过程中,可以被打断
 * @author cyc
 */
public class T04_ReentrantLock4 {
    
		
	public static void main(String[] args) {
    
		Lock lock = new ReentrantLock();
		
		
		Thread t1 = new Thread(()->{
    
			try {
    
				lock.lock();
				System.out.println("t1 start");
				TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
				System.out.println("t1 end");
			} catch (InterruptedException e) {
    
				System.out.println("interrupted!");
			} finally {
    
				lock.unlock();
			}
		});
		t1.start();
		
		Thread t2 = new Thread(()->{
    
			try {
    
				//lock.lock();
				lock.lockInterruptibly(); //可以对interrupt()方法做出响应
				System.out.println("t2 start");
				TimeUnit.SECONDS.sleep(5);
				System.out.println("t2 end");
			} catch (InterruptedException e) {
    
				System.out.println("t2 interrupted!");
			} finally {
    
				lock.unlock();
			}
		});
		t2.start();
		
		try {
    
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
    
			e.printStackTrace();
		}
		t2.interrupt(); //打断线程2的等待
		
	}
}

  • 输出结果

在这里插入图片描述

公平锁

new ReentrantLock(true)为新建一个公平锁,所谓公平锁, 即每个线程获取锁之前, 都要去锁的等待队列里去查看有无正在等待锁的线程, 如果有,则加入等待队列,按照先后顺序去获取锁。如果没有,则直接去拿锁。

new ReentrantLock()默认为非公平锁,忽视等待队列, 直接去争抢锁。

注: synchronized属于非公平锁

/**
 *
 * ReentrantLock还可以指定为公平锁
 *
 * @author cyc
 */
package com.cyc.juc.c_020_lock;

import java.util.concurrent.locks.ReentrantLock;

public class T05_ReentrantLock5 extends Thread {
    

    private static ReentrantLock lock = new ReentrantLock(true); //参数为true表示为公平锁(默认为false),请对比输出结果

    @Override
    public void run() {
    
        for (int i = 0; i < 100; i++) {
    
            lock.lock();
            try {
    
                System.out.println(Thread.currentThread().getName() + "获得锁");
            } finally {
    
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
    
        T05_ReentrantLock5 rl = new T05_ReentrantLock5();
        Thread th1 = new Thread(rl);
        Thread th2 = new Thread(rl);
        th1.start();
        th2.start();
    }
}
  • 输出结果

在这里插入图片描述

2. ReadWriteLock(读写锁)

/**
 * 读写锁(共享锁+排他锁), 读的时候是共享, 写的时候是排他
 * @author fei
 */
public class T10_TestReadWriteLock {
    
    static Lock lock = new ReentrantLock();
    private static int value;

    static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    static Lock readLock = readWriteLock.readLock();
    static Lock writeLock = readWriteLock.writeLock();

    public static void read(Lock lock) {
    
        try {
    
            lock.lock();
            Thread.sleep(1000);
            System.out.println("read over!");
            //模拟读取操作
        } catch (InterruptedException e) {
    
            e.printStackTrace();
        } finally {
    
            lock.unlock();
        }
    }

    public static void write(Lock lock, int v) {
    
        try {
    
            lock.lock();
            Thread.sleep(1000);
            value = v;
            System.out.println("write over!");
            //模拟写操作
        } catch (InterruptedException e) {
    
            e.printStackTrace();
        } finally {
    
            lock.unlock();
        }
    }



    public static void main(String[] args) {
    
        //排它锁
//        Runnable readR = ()-> read(lock);
        //读写锁-读, 使用读写锁, 多个线程可同时读取同一个数据, 无需等待
        Runnable readR = ()-> read(readLock);

//        Runnable writeR = ()->write(lock, new Random().nextInt());
        //读写锁-写, 使用读写锁, 写的时候, 其他线程需要等待该线程之后完毕之后, 才可以进行读写
        Runnable writeR = ()->write(writeLock, new Random().nextInt());

        for(int i=0; i<18; i++) new Thread(readR).start();
        for(int i=0; i<2; i++) new Thread(writeR).start();


    }
}

3. LangAdder(分段锁)

/**
 * 几种锁的执行效率测试
 * count1,count2, count3分别使用不同的方式来实现递增
 */
public class T02_AtomicVsSyncVsLongAdder {
    
    static long count2 = 0L;
    static AtomicLong count1 = new AtomicLong(0L);
    //LongAdder内部做了一种类似分段锁(分段锁也是CAS操作)的操作, 例如本例中, 1000个线程可能会分为多个组,
    // 每组单独计算, 计算出的值最后累加一下, 得出总计值, LongAdder在线程特别多的时候, 优势比较明显
    static LongAdder count3 = new LongAdder();

    public static void main(String[] args) throws Exception {
    
        Thread[] threads = new Thread[1000];

        for(int i=0; i<threads.length; i++) {
    
            threads[i] =
                    new Thread(()-> {
    
                        for(int k=0; k<100000; k++) {
    
                            count1.incrementAndGet();
                        }
                    });
        }

        long start = System.currentTimeMillis();

        for(Thread t : threads ) {
    
            t.start();
        }

        for (Thread t : threads) {
    
            t.join();
        }

        long end = System.currentTimeMillis();

        //TimeUnit.SECONDS.sleep(10);

        System.out.println("Atomic: " + count1.get() + " time " + (end-start));
        //-----------------------------------------------------------
        Object lock = new Object();

        for(int i=0; i<threads.length; i++) {
    
            threads[i] =
                new Thread(() -> {
    
                    for (int k = 0; k < 100000; k++) {
    
                        synchronized (lock) {
    
                            count2++;
                        }
                    }
                });
        }

        start = System.currentTimeMillis();

        for(Thread t : threads ) {
    
            t.start();
        }

        for (Thread t : threads) {
    
            t.join();
        }

        end = System.currentTimeMillis();


        System.out.println("Sync: " + count2 + " time " + (end-start));


        //----------------------------------
        for(int i=0; i<threads.length; i++) {
    
            threads[i] =
                    new Thread(()-> {
    
                        for(int k=0; k<100000; k++) {
    
                            count3.increment();
                        }
                    });
        }

        start = System.currentTimeMillis();

        for(Thread t : threads ) {
    
            t.start();
        }

        for (Thread t : threads) {
    
            t.join();
        }

        end = System.currentTimeMillis();

        //TimeUnit.SECONDS.sleep(10);

        System.out.println("LongAdder: " + count1.longValue() + " time " + (end-start));
    }
}

4. CountDownLatch(倒数门栓)

倒数门栓

/**
 * CountDown 倒数, Latch门栓, CountDownLatch倒数几个数打开门栓
 * 常用在控制线程上, 可以控制线程执行结束后, 才开始执行下面的代码
 *
 * @author cyc
 * @date 2021-08-07 16:03:42
 */
public class T06_TestCountDownLatch {
    
    public static void main(String[] args) {
    
        usingJoin();
        //使用CountDownLatch比join更加灵活
        usingCountDownLatch();
    }

    private static void usingCountDownLatch() {
    
        Thread[] threads = new Thread[100];
        CountDownLatch latch = new CountDownLatch(threads.length);

        for(int i=0; i<threads.length; i++) {
    
            threads[i] = new Thread(()->{
    
                //下面的threads[i].start()执行过后才开始执行以下代码,
                //每执行一次, latch.countDown()会相应减1, 当减为0时,
                // latch.await(),才会执行
                int result = 0;
                for(int j=0; j<10000; j++) {
    
                    result += j;
                }
                latch.countDown();
            });
        }

        for (int i = 0; i < threads.length; i++) {
    
            threads[i].start();
        }

        try {
    
            System.out.println("latch await 等待执行");
            //这里会阻塞住主线程, 只有当latch减为0的时候, 才会继续执行下面的代码
            latch.await();
        } catch (InterruptedException e) {
    
            e.printStackTrace();
        }

        System.out.println("end latch");
    }

    private static void usingJoin() {
    
        Thread[] threads = new Thread[100];

        for(int i=0; i<threads.length; i++) {
    
            threads[i] = new Thread(()->{
    
                int result = 0;
                for(int j=0; j<10000; j++) {
    
                    result += j;
                }
            });
        }

        for (int i = 0; i < threads.length; i++) {
    
            threads[i].start();
        }

        for (int i = 0; i < threads.length; i++) {
    
            try {
    
                //每一个线程都合并到主线程上, 主线程要等所有线程执行结束之后,
                // 才开始继续执行下面的代码(准备发车, 坐满就走)
                threads[i].join();
            } catch (InterruptedException e) {
    
                e.printStackTrace();
            }
        }

        System.out.println("end join");
    }
}

  • 执行结果

在这里插入图片描述

5. CyclicBarrier(循环栅栏)

循环栅栏

package com.cyc.juc.c_020_lock;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

/**
 * Cyclic循环的, Barrier栅栏, CyclicBarrier循环的栅栏
 * @author cyc
 * @date 2021-08-07 16:35:30
 */
public class T07_TestCyclicBarrier {
    
    public static void main(String[] args) {
    
        //CyclicBarrier barrier = new CyclicBarrier(20);	

        CyclicBarrier barrier = new CyclicBarrier(20, () -> System.out.println("满人"));

        for(int i=0; i<100; i++) {
    

                new Thread(()->{
    
                    try {
    
                        barrier.await();
                    } catch (InterruptedException e) {
    
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
    
                        e.printStackTrace();
                    }
                }).start();
            
        }
    }
}
  • 执行结果

在这里插入图片描述

  • 观察源码

在这里插入图片描述
在这里插入图片描述

点击barrier.await()方法进入发现,当执行的线程数量达到new CyclicBarrier(n)中的n时, 重新生成一次新的循环栅栏, 通过观察结果可知, n值是20, 执行的线程数量为100, 所以输出了5个CyclicBarrier中run方法的执行结果

应用场景

  • 复杂操作和并发执行

    ​ 例如有三个线程A,B,C , 分别去执行访问数据库, 访问网络和读取文件, 此时 , 就可以使用CyclicBarrier, 他可以在所有线程都执行完毕之后, 才去执行主流程.

6. CountDownLatch和CyclicBarrier的区别

CountDownLatch是设定一个值, 每次countDown以下, 直到等于0, 但是并不一定要在每一个线程都countDown一下, 在一个线程里CountDown也可以

就比如这样写, 依然可以

        Thread t = new Thread(() -> {
    
            for (int i = 0; i < threads.length; i++) {
    
                latch.countDown();
            }
        });
        t.start();

而CyclicBarrier就不一样了, 他必须在每个线程里调用await()方法, 才可以

测试一下

for (int i = 0; i < 100; i++) {
    
    try {
    
        System.out.println("等待中");
        barrier.await();
    } catch (InterruptedException e) {
    
        e.printStackTrace();
    } catch (BrokenBarrierException e) {
    
        e.printStackTrace();
    }
}

只会输出一次 等待中, 然后就堵塞在那里了。

7. Semaphore

常用在限流上,

例如车道和收费站 , 收费站只有两个, 车道可能有多个

package com.cyc.juc.c_020_lock;

import java.util.concurrent.Semaphore;

/**
 * Semaphore 信号灯, Semaphore(n)最多允许n个线程同时运行
 * 常用在限流上
 * @author fei
 */
public class T11_TestSemaphore {
    
    public static void main(String[] args) {
    
        //允许两个线程同时执行, 没有先后顺序
        Semaphore s = new Semaphore(2);
        //允许两个线程同时执行, 公平锁,内部有等待队列,并按照先后顺序执行
//        Semaphore s = new Semaphore(2, true);
        //允许一个线程同时执行
        //Semaphore s = new Semaphore(1);

        new Thread(()->{
    
            try {
    
                //阻塞方法, 如果取不到 , 则阻塞在这里
                //这里取一下, new Semaphore(1)中的1就会变成0
                s.acquire();

                System.out.println("T1 running...");
                Thread.sleep(200);
                System.out.println("T1 running...");

            } catch (InterruptedException e) {
    
                e.printStackTrace();
            } finally {
    
                //将1加回去,即将线程加回去
                s.release();
            }
        }).start();

        new Thread(()->{
    
            try {
    
                s.acquire();

                System.out.println("T2 running...");
                Thread.sleep(200);
                System.out.println("T2 running...");

                s.release();
            } catch (InterruptedException e) {
    
                e.printStackTrace();
            }
        }).start();
    }
}

8. LockSupport

/**
 * LockSupport 可以叫醒指定线程, unPark可以先与park之前调用
 */
public class T13_TestLockSupport {
    
    public static void main(String[] args) {
    
        Thread t = new Thread(()->{
    
            for (int i = 0; i < 10; i++) {
    
                System.out.println(i);
                if(i == 5) {
    
                    //阻塞当前线程, 需要手动唤醒
                    LockSupport.park();
                }
                try {
    
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
    
                    e.printStackTrace();
                }
            }
        });

        t.start();

        try {
    
            TimeUnit.SECONDS.sleep(8);
        } catch (InterruptedException e) {
    
            e.printStackTrace();
        }
        System.out.println("after 8 senconds!");

        LockSupport.unpark(t);
    }
}

  • 执行结果

在这里插入图片描述

6. 测试题

线程同步

实现一个容器,提供两个方法,add,size
写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束

方法一 普通判断

线程不同步
public class T01_WithoutVolatile {
    

	List lists = new ArrayList();

	public void add(Object o) {
    
		lists.add(o);
	}

	public int size() {
    
		return lists.size();
	}
	
	public static void main(String[] args) {
    
		T01_WithoutVolatile c = new T01_WithoutVolatile();

		new Thread(() -> {
    
			for(int i=0; i<10; i++) {
    
				c.add(new Object());
				System.out.println("add " + i);
				
				try {
    
					TimeUnit.SECONDS.sleep(1);
				} catch (InterruptedException e) {
    
					e.printStackTrace();
				}
			}
		}, "t1").start();
		
		new Thread(() -> {
    
			while(true) {
    
				if(c.size() == 5) {
    
					break;
				}
			}
			System.out.println("t2 结束");
		}, "t2").start();
	}
}

  • 输出结果
add 0
add 1
add 2
add 3
add 4
add 5
add 6
add 7
add 8
add 9

由于线程1给list加的值,并不同立即同步给线程2 , 所以线程2会无法结束

使用线程同步
/**
 * 给lists添加volatile之后,t2能够接到通知,但是,t2线程的死循环很浪费cpu,如果不用死循环,
 * 而且,如果在if 和 break之间被别的线程打断,得到的结果也不精确,
 * @author cyc
 */
public class T02_WithVolatile {
    

   //添加volatile,使t2能够得到通知
   //volatile List lists = new LinkedList();
   //同步容器
   volatile List lists = Collections.synchronizedList(new LinkedList<>());

   public void add(Object o) {
    
      lists.add(o);
   }

   public int size() {
    
      return lists.size();
   }

   public static void main(String[] args) {
    

      T02_WithVolatile c = new T02_WithVolatile();
      new Thread(() -> {
    
         for(int i=0; i<10; i++) {
    
            c.add(new Object());
            System.out.println("add " + i);
  
            try {
    
               TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
    
               e.printStackTrace();
            }
         }
      }, "t1").start();
      
      new Thread(() -> {
    
         while(true) {
    
            if(c.size() == 5) {
    
               break;
            }
         }
         System.out.println("t2 结束");
      }, "t2").start();
   }
}
  • 输出结果

add 0
add 1
add 2
add 3
add 4
t2 结束
add 5
add 6
add 7
add 8
add 9

方法二 wait()和notify()

反例
/**
 * 这里使用wait和notify做到,wait会释放锁,而notify不会释放锁
 * 需要注意的是,运用这种方法,必须要保证t2先执行,也就是首先让t2监听才可以
 * @author cyc
 */
package com.cyc.juc.c_020_01_Interview;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;


public class T03_NotifyHoldingLock {
     //wait notify

   //添加volatile,使t2能够得到通知
   volatile List lists = new ArrayList();

   public void add(Object o) {
    
      lists.add(o);
   }

   public int size() {
    
      return lists.size();
   }
   
   public static void main(String[] args) {
    
      T03_NotifyHoldingLock c = new T03_NotifyHoldingLock();
      
      final Object lock = new Object();
      
      new Thread(() -> {
    
         synchronized(lock) {
    
            System.out.println("t2启动");
            if(c.size() != 5) {
    
               try {
    
                  lock.wait();
               } catch (InterruptedException e) {
    
                  e.printStackTrace();
               }
            }
            System.out.println("t2 结束");
         }
         
      }, "t2").start();
      
      try {
    
         TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e1) {
    
         e1.printStackTrace();
      }

      new Thread(() -> {
    
         System.out.println("t1启动");
         synchronized(lock) {
    
            for(int i=0; i<10; i++) {
    
               c.add(new Object());
               System.out.println("add " + i);
               
               if(c.size() == 5) {
    
                  //notify()并不释放锁, 所以t1会等t2执行完之后才会执行
                  lock.notify();
               }
               
               try {
    
                  TimeUnit.SECONDS.sleep(1);
               } catch (InterruptedException e) {
    
                  e.printStackTrace();
               }
            }
         }
      }, "t1").start();
       
   }
}
优化方法
/**
 * notify之后,t1必须释放锁,t2退出后,也必须notify,通知t1继续执行
 * 整个通信过程比较繁琐
 * @author cyc
 */
package com.cyc.juc.c_020_01_Interview;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;


public class T04_NotifyFreeLock {
    

   //添加volatile,使t2能够得到通知
   volatile List lists = new ArrayList();

   public void add(Object o) {
    
      lists.add(o);
   }

   public int size() {
    
      return lists.size();
   }
   
   public static void main(String[] args) {
    
      T04_NotifyFreeLock c = new T04_NotifyFreeLock();
      
      final Object lock = new Object();
      
      new Thread(() -> {
    
         synchronized(lock) {
    
            System.out.println("t2启动");
            if(c.size() != 5) {
    
               try {
    
                  lock.wait();
               } catch (InterruptedException e) {
    
                  e.printStackTrace();
               }
            }
            System.out.println("t2 结束");
            //通知t1继续执行
            lock.notify();
         }
         
      }, "t2").start();
      
      try {
    
         TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e1) {
    
         e1.printStackTrace();
      }

      Thread t = new Thread(() -> {
    
         System.out.println("测试");
      });

      new Thread(() -> {
    
         System.out.println("t1启动");
         synchronized(lock) {
    
            for(int i=0; i<10; i++) {
    
               c.add(new Object());
               System.out.println("add " + i);
               
               if(c.size() == 5) {
    
                  lock.notify();
                  //释放锁,让t2得以执行
                  try {
    
                     lock.wait();
                  } catch (InterruptedException e) {
    
                     e.printStackTrace();
                  }
               }
               
               try {
    
                  TimeUnit.SECONDS.sleep(1);
               } catch (InterruptedException e) {
    
                  e.printStackTrace();
               }
            }
         }
      }, "t1").start();
      
      
   }
}

方法三 CountDownLatch()

一个countDownLatch
/**
 * @author cyc
 */
public class T05_CountDownLatch {
    

   // 添加volatile,使t2能够得到通知
   volatile List lists = new ArrayList();

   public void add(Object o) {
    
      lists.add(o);
   }

   public int size() {
    
      return lists.size();
   }

   public static void main(String[] args) {
    
      T05_CountDownLatch c = new T05_CountDownLatch();

      CountDownLatch latch = new CountDownLatch(1);

      new Thread(() -> {
    
         System.out.println("t2启动");
         if (c.size() != 5) {
    
            try {
    
               latch.await();
               
               //也可以指定等待时间
               //latch.await(5000, TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
    
               e.printStackTrace();
            }
         }
         System.out.println("t2 结束");

      }, "t2").start();

      try {
    
         TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e1) {
    
         e1.printStackTrace();
      }

      new Thread(() -> {
    
         System.out.println("t1启动");
         for (int i = 0; i < 10; i++) {
    
            c.add(new Object());
            System.out.println("add " + i);

            if (c.size() == 5) {
    
               // 打开门闩,让t2得以执行
               latch.countDown();
            }
         }

      }, "t1").start();

   }
}

执行结果

t2启动
t1启动
add 0
add 1
add 2
add 3
add 4
add 5
add 6
t2 结束
add 7
add 8
add 9

可以看到, 正常来说,应该在打印完add 4之后打印 t2结束,但是由于t1的latch.await();执行之后,打印t2 结束执行结束之前, t1的线程又执行了两遍,导致“t2技术”在i=6时才输出。

优化CountDownLatch

使用两个CountDownLatch

/**
 * countDownLatch优化
 * @author cyc
 */
package com.cyc.juc.c_020_01_Interview;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class T05_CountDownLatch_optimize {
    

	// 添加volatile,使t2能够得到通知
	volatile List lists = new ArrayList();

	public void add(Object o) {
    
		lists.add(o);
	}

	public int size() {
    
		return lists.size();
	}

	public static void main(String[] args) {
    
		T05_CountDownLatch_optimize c = new T05_CountDownLatch_optimize();

		CountDownLatch latch1 = new CountDownLatch(1);
		CountDownLatch latch2 = new CountDownLatch(1);

		new Thread(() -> {
    
			System.out.println("t2启动");
			if (c.size() != 5) {
    
				try {
    
					latch1.await();
					//也可以指定等待时间
					//latch1.await(5000, TimeUnit.MILLISECONDS);
				} catch (InterruptedException e) {
    
					e.printStackTrace();
				}
			}
			System.out.println("t2 结束");
            //在t2执行结束后, 再放开门栓
			latch2.countDown();

		}, "t2").start();

		try {
    
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e1) {
    
			e1.printStackTrace();
		}

		new Thread(() -> {
    
			System.out.println("t1启动");
			for (int i = 0; i < 10; i++) {
    
				c.add(new Object());
				System.out.println("add " + i);

				if (c.size() == 5) {
    
					// 打开门闩,让t2得以执行
					latch1.countDown();
					try {
    
                        //latch1.countDown();执行后, 继续执行latch2.await();等待latch2归零
						latch2.await();
					} catch (InterruptedException e) {
    
						e.printStackTrace();
					}
				}

				/*try {
					TimeUnit.SECONDS.sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}*/
			}

		}, "t1").start();

	}
}

方法四 LockSupport

使用lockSupport和上面的CountDownLatch会有同样的问题, 下面开始演示

普通方案
/**
 * @author cyc
 */
public class T06_LockSupport {
    

	// 添加volatile,使t2能够得到通知
	volatile List lists = new ArrayList();

	public void add(Object o) {
    
		lists.add(o);
	}

	public int size() {
    
		return lists.size();
	}

	public static void main(String[] args) {
    
		T06_LockSupport c = new T06_LockSupport();

		CountDownLatch latch = new CountDownLatch(1);

		Thread t2 = new Thread(() -> {
    
			System.out.println("t2启动");
			if (c.size() != 5) {
    

				LockSupport.park();

			}
			System.out.println("t2 结束");


		}, "t2");

		t2.start();

		try {
    
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e1) {
    
			e1.printStackTrace();
		}

		new Thread(() -> {
    
			System.out.println("t1启动");
			for (int i = 0; i < 10; i++) {
    
				c.add(new Object());
				System.out.println("add " + i);

				if (c.size() == 5) {
    
					LockSupport.unpark(t2);
				}
			}

		}, "t1").start();

	}
}

同样会出现, t2结束的输出语句不一定在t1输出 add 4之后, 可能是在 add5, add6,…之后

优化方案 LockSupport
/**
 * @author cyc
 */
package com.cyc.juc.c_020_01_Interview;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.LockSupport;

public class T07_LockSupport_WithoutSleep {
    

	// 添加volatile,使t2能够得到通知
	volatile List lists = new ArrayList();

	public void add(Object o) {
    
		lists.add(o);
	}

	public int size() {
    
		return lists.size();
	}

	static Thread t1 = null, t2 = null;

	public static void main(String[] args) {
    
		T07_LockSupport_WithoutSleep c = new T07_LockSupport_WithoutSleep();

		t1 = new Thread(() -> {
    
			System.out.println("t1启动");
			for (int i = 0; i < 10; i++) {
    
				c.add(new Object());
				System.out.println("add " + i);

				if (c.size() == 5) {
    
					LockSupport.unpark(t2);
					LockSupport.park();
				}
			}
		}, "t1");

		t2 = new Thread(() -> {
    
			System.out.println("t2启动");
			LockSupport.park();
			System.out.println("t2 结束");
			LockSupport.unpark(t1);


		}, "t2");

		t2.start();
		t1.start();

	}
}

方法五 Semaphore

public class T08_Semaphore {
    
    // 添加volatile,使t2能够得到通知
    volatile List lists = new ArrayList();

    public void add(Object o) {
    
        lists.add(o);
    }

    public int size() {
    
        return lists.size();
    }

    static Thread t1 = null, t2 = null;

    public static void main(String[] args) {
    
        T08_Semaphore c = new T08_Semaphore();
        Semaphore s = new Semaphore(1);

        t1 = new Thread(() -> {
    
            try {
    
                s.acquire();
                for (int i = 0; i < 5; i++) {
    
                    c.add(new Object());
                    System.out.println("add " + i);
                }
                s.release();
            } catch (InterruptedException e) {
    
                e.printStackTrace();
            }

            try {
    
                t2.start();
                //等t2执行完了之后, t1再继续执行
                t2.join();
            } catch (InterruptedException e) {
    
                e.printStackTrace();
            }

            try {
    
                s.acquire();
                for (int i = 5; i < 10; i++) {
    
                    System.out.println("add " + i);
                }
                s.release();
            } catch (InterruptedException e) {
    
                e.printStackTrace();
            }

        }, "t1");

        t2 = new Thread(() -> {
    
            try {
    
                s.acquire();
                System.out.println("t2 结束");
                s.release();
            } catch (InterruptedException e) {
    
                e.printStackTrace();
            }
        }, "t2");
        t1.start();
    }
}

容器- 生产者消费者

* 面试题:写一个固定容量同步容器,拥有put和get方法,以及getCount方法,
* 能够支持2个生产者线程以及10个消费者线程的阻塞调用

1. 使用wait和notify/notifyAll来实现

/**
 * 写一个固定容量同步容器,拥有put和get方法,以及getCount方法,
 * 能够支持2个生产者线程以及10个消费者线程的阻塞调用
 * 
 * 使用wait和notify/notifyAll来实现
 * 
 * @author cyc
 */
public class MyContainer1<T> {
    
   final private LinkedList<T> lists = new LinkedList<>();
   final private int MAX = 10; //最多10个元素
   private int count = 0;


   /**
    * 生产
    * @param t
    */
   public synchronized void put(T t) {
    
      while(lists.size() == MAX) {
     //想想为什么用while而不是用if?,如果这里使用if,
         // 执行完this.wait()后, 就不会继续判断lists.size() == MAX,而是往下执行了。
         try {
    
            this.wait(); //effective java
         } catch (InterruptedException e) {
    
            e.printStackTrace();
         }
      }

      lists.add(t);
      ++count;
      this.notifyAll(); //通知消费者线程进行消费
   }

   /**
    * 消费
    * @return
    */
   public synchronized T get() {
    
      T t = null;
      while(lists.size() == 0) {
    
         try {
    
            this.wait();
         } catch (InterruptedException e) {
    
            e.printStackTrace();
         }
      }
      t = lists.removeFirst();
      count --;
      this.notifyAll(); //通知生产者进行生产
      return t;
   }

   public static void main(String[] args) {
    
      MyContainer1<String> c = new MyContainer1<>();
      //启动消费者线程
      for(int i=0; i<10; i++) {
    
         new Thread(()->{
    
            for(int j=0; j<5; j++) {
    
               System.out.println(c.get()+", count: "+ c.count);
            }
         }, "c" + i).start();
      }

      try {
    
         TimeUnit.SECONDS.sleep(2);
      } catch (InterruptedException e) {
    
         e.printStackTrace();
      }

      //启动生产者线程
      for(int i=0; i<2; i++) {
    
         new Thread(()->{
    
            for(int j=0; j<25; j++) {
    
               c.put(Thread.currentThread().getName() + " " + j);
            }
         }, "p" + i).start();
      }
   }
}

输出结果

p1 0, count: 9
p1 5, count: 4
p1 6, count: 3
p1 7, count: 2
p1 8, count: 1
p1 4, count: 10
p1 9, count: 9
p0 0, count: 8
p0 1, count: 7

2. 使用lock.newCondition()

上面的代码有个问题, 那就是使用notifyAll时, 会通知所有线程, 而不是仅仅通知消费者线程, 接下来使用lock.newCondition()来进行优化,newCondition()即新建一个条件, 这个条件便是等待队列, 每次的通知也只是通知对应等待队列的线程

/**
 * 写一个固定容量同步容器,拥有put和get方法,以及getCount方法,
 * 能够支持2个生产者线程以及10个消费者线程的阻塞调用
 * 使用Lock和Condition来实现
 * 对比两种方式,Condition的方式可以更加精确的指定哪些线程被唤醒
 * 
 * @author cyc
 */
package com.cyc.juc.c_021_01_interview;

import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyContainer2<T> {
    
	final private LinkedList<T> lists = new LinkedList<>();
	final private int MAX = 10; //最多10个元素
	private int count = 0;
	
	private Lock lock = new ReentrantLock();
	//Condition本质上属于等待队列, new Condition()相当于新建了一个等待队列
	private Condition producer = lock.newCondition();
	private Condition consumer = lock.newCondition();
	
	public void put(T t) {
    
		try {
    
			lock.lock();
			while(lists.size() == MAX) {
     //想想为什么用while而不是用if?
				producer.await();
			}
			
			lists.add(t);
			++count;
			consumer.signalAll(); //通知消费者线程进行消费
		} catch (InterruptedException e) {
    
			e.printStackTrace();
		} finally {
    
			lock.unlock();
		}
	}
	
	public T get() {
    
		T t = null;
		try {
    
			lock.lock();
			while(lists.size() == 0) {
    
				consumer.await();
			}
			t = lists.removeFirst();
			count --;
			producer.signalAll(); //通知生产者进行生产
		} catch (InterruptedException e) {
    
			e.printStackTrace();
		} finally {
    
			lock.unlock();
		}
		return t;
	}
	
	public static void main(String[] args) {
    
		MyContainer2<String> c = new MyContainer2<>();
		//启动消费者线程
		for(int i=0; i<10; i++) {
    
			new Thread(()->{
    
				for(int j=0; j<5; j++) System.out.println(c.get());
			}, "c" + i).start();
		}
		
		try {
    
			TimeUnit.SECONDS.sleep(2);
		} catch (InterruptedException e) {
    
			e.printStackTrace();
		}
		
		//启动生产者线程
		for(int i=0; i<2; i++) {
    
			new Thread(()->{
    
				for(int j=0; j<25; j++) c.put(Thread.currentThread().getName() + " " + j);
			}, "p" + i).start();
		}
	}
}

总结

AQS原理

Abstract : 因为它并不知道到怎么上锁。参照模板方法设计模式即可,暴露出上锁逻辑

Queue:线程阻塞队列

Synchronizer: 同步

AQS便是使用CAS+state完成多线程抢锁逻辑,以及使用Queue完成抢不到锁的线程排队

AQS核心代码

获取锁的代码

	public final void acquire(int arg) {
    
    if (!tryAcquire(arg) && //子类判定获取锁失败返回false, 那么同理,取反,则表示为true
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //获取失败后添加到阻塞队列中
        selfInterrupt();
	}

	//模板方法, 需要子类去实现,子类去实现获取锁的逻辑, AQS并不知道你怎么使用这个statue来上锁
	//因为是子类需要去实现的, 所以这里使用protected修饰
	protected boolean tryAcquire(int arg) {
    
  	 throw new UnsupportedOperationException();
	}

释放锁的代码

    public final boolean release(int arg) {
    
        //子类判断释放锁成功后
        if (tryRelease(arg)) {
    
            //检查阻塞队列唤醒即可
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

	//模板方法, 需要子类去实现,子类去实现获取锁的逻辑, AQS并不知道你怎么使用这个statue来释放锁	
    protected boolean tryRelease(int arg) {
    
        throw new UnsupportedOperationException();
    }

**总结: **

子类只需要实现自己的获取锁逻辑和释放锁逻辑即可, 至于排队阻塞队列等待、唤醒机制均由AQS来完成

ReentranLock原理

概念

基于AQS实现的可重入锁实现类

核心变量和构造器

public class ReentrantLock implements Lock, java.io.Serializable {
    
    private final Sync sync;

    
    abstract static class Sync extends AbstractQueuedSynchronizer {
    

        abstract void lock();

        //非公平锁获取锁方法
        final boolean nonfairTryAcquire(int acquires) {
    
            final Thread current = Thread.currentThread();
            int c = getState();
            //当执行到这里, 正好持有锁的线程释放了锁,那么可以尝试抢锁
            if (c == 0) {
    
                //继续抢锁, 不看有没有线程排队
                if (compareAndSetState(0, acquires)) {
    
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 当前线程就是持有锁的线程, 表明锁重入
            else if (current == getExclusiveOwnerThread()) {
    
                // 利用state整型变量进行次数记录
                int nextc = c + acquires;
                //如果超过了int表示范围, 表明符号溢出, 所以抛出异常(int范围 正:2的32次方-1,负:2的32次方)
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                  (nextc);
                return true;
            }
            return false;
        }

        protected final boolean tryRelease(int releases) {
    
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
    
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

        final ConditionObject newCondition() {
    
            return new ConditionObject();
        }

        final boolean isLocked() {
    
            return getState() != 0;
        }
    }
    
    //公平锁获取锁的方法
    static final class FairSync extends Sync {
    
        private static final long serialVersionUID = -3000897897090466540L;
		// 由ReentrantLock调用
        final void lock() {
    
            // 没有尝试抢锁, 直接进入AQS标准获取锁流程
            acquire(1);
        }
		// AQS调用, 自己自己实现获取锁的流程
        protected final boolean tryAcquire(int acquires) {
    
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
    
                //注意: 这里和非公平锁的区别在于: hasQueuedPredecessors看看队列中是否有线程正在排队, 没有的话再通过CAS抢锁。
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
    
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 当前线程就是获取锁的线程, 那么这里是锁重入
            else if (current == getExclusiveOwnerThread()) {
    
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }
    

    public ReentrantLock() {
    
        sync = new NonfairSync();
    }


    public ReentrantLock(boolean fair) {
    
        sync = fair ? new FairSync() : new NonfairSync();
    }
    
    static final class NonfairSync extends Sync {
    
       //由ReentrantLock调用获取锁
        final void lock() {
    
            //非公平锁, 直接抢锁, 不管有没有线程排队
            if (compareAndSetState(0, 1))
                //上锁成功, 那么标识当前线程为获取锁的线程 
                setExclusiveOwnerThread(Thread.currentThread());
            else
                //抢锁失败, 进入AQS的标准获取锁流程
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
    
            return nonfairTryAcquire(acquires);
        }
    }
    
}

这里首先理解一下公平锁和非公平锁

公平锁与非公平锁

在这里插入图片描述

为什么非公平锁的性能高于公平锁

因为公平锁需要排队, 排队需要时间, 唤醒阻塞的线程也需要时间, 就如上图公平锁图中的B加入到运行队列,期间进行线程切换需要时间。

非公平锁性能高于公平锁的原因:线程上下文切换+调度延迟时间

核心方法

获取锁操作

public void lock() {
    
    //直接通过sync同步器进行上锁
    sync.lock();
}

而在ReentrantLock中sync有公平锁和非公平锁, 如下所示

在这里插入图片描述

如何自己来实现一把锁?

  1. 如何表示锁状态:无锁?有锁?

    boolean status;//true 有锁 false 无锁 只能表示两种状态

    为了实现锁重入, 那么我需要记录锁重入次数: int times;

    两个变量有点冗余了, 所以我们直接用int state; 来表示锁状态和重入次数: 0无锁, 大于0重入次数 特别地: 1为重入一次, 也即只加锁一次

  2. 如何保证多线程抢锁线程安全

    CAS

  3. 如何处理获取不到锁的线程?

    自旋 阻塞 自旋+阻塞

  4. 如何释放锁?

    自旋: 自己抢锁

    阻塞: 唤醒

自旋锁

自旋锁缺点:CPU占用不干事,导致性能障碍,简称占着茅坑不拉屎。

自旋锁优点:适用于执行步骤较少且快的操作,自旋一会马上就能获取锁,这样不会消耗太多CPU资源。

注意:当CPU个数增加的情况下,优点会退化成自旋锁的缺点

使用场景:争用较少且代码量小的 临界区

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/A_Java_Dog/article/details/119519119

智能推荐

使用nginx解决浏览器跨域问题_nginx不停的xhr-程序员宅基地

文章浏览阅读1k次。通过使用ajax方法跨域请求是浏览器所不允许的,浏览器出于安全考虑是禁止的。警告信息如下:不过jQuery对跨域问题也有解决方案,使用jsonp的方式解决,方法如下:$.ajax({ async:false, url: 'http://www.mysite.com/demo.do', // 跨域URL ty..._nginx不停的xhr

在 Oracle 中配置 extproc 以访问 ST_Geometry-程序员宅基地

文章浏览阅读2k次。关于在 Oracle 中配置 extproc 以访问 ST_Geometry,也就是我们所说的 使用空间SQL 的方法,官方文档链接如下。http://desktop.arcgis.com/zh-cn/arcmap/latest/manage-data/gdbs-in-oracle/configure-oracle-extproc.htm其实简单总结一下,主要就分为以下几个步骤。..._extproc

Linux C++ gbk转为utf-8_linux c++ gbk->utf8-程序员宅基地

文章浏览阅读1.5w次。linux下没有上面的两个函数,需要使用函数 mbstowcs和wcstombsmbstowcs将多字节编码转换为宽字节编码wcstombs将宽字节编码转换为多字节编码这两个函数,转换过程中受到系统编码类型的影响,需要通过设置来设定转换前和转换后的编码类型。通过函数setlocale进行系统编码的设置。linux下输入命名locale -a查看系统支持的编码_linux c++ gbk->utf8

IMP-00009: 导出文件异常结束-程序员宅基地

文章浏览阅读750次。今天准备从生产库向测试库进行数据导入,结果在imp导入的时候遇到“ IMP-00009:导出文件异常结束” 错误,google一下,发现可能有如下原因导致imp的数据太大,没有写buffer和commit两个数据库字符集不同从低版本exp的dmp文件,向高版本imp导出的dmp文件出错传输dmp文件时,文件损坏解决办法:imp时指定..._imp-00009导出文件异常结束

python程序员需要深入掌握的技能_Python用数据说明程序员需要掌握的技能-程序员宅基地

文章浏览阅读143次。当下是一个大数据的时代,各个行业都离不开数据的支持。因此,网络爬虫就应运而生。网络爬虫当下最为火热的是Python,Python开发爬虫相对简单,而且功能库相当完善,力压众多开发语言。本次教程我们爬取前程无忧的招聘信息来分析Python程序员需要掌握那些编程技术。首先在谷歌浏览器打开前程无忧的首页,按F12打开浏览器的开发者工具。浏览器开发者工具是用于捕捉网站的请求信息,通过分析请求信息可以了解请..._初级python程序员能力要求

Spring @Service生成bean名称的规则(当类的名字是以两个或以上的大写字母开头的话,bean的名字会与类名保持一致)_@service beanname-程序员宅基地

文章浏览阅读7.6k次,点赞2次,收藏6次。@Service标注的bean,类名:ABDemoService查看源码后发现,原来是经过一个特殊处理:当类的名字是以两个或以上的大写字母开头的话,bean的名字会与类名保持一致public class AnnotationBeanNameGenerator implements BeanNameGenerator { private static final String C..._@service beanname

随便推点

二叉树的各种创建方法_二叉树的建立-程序员宅基地

文章浏览阅读6.9w次,点赞73次,收藏463次。1.前序创建#include&lt;stdio.h&gt;#include&lt;string.h&gt;#include&lt;stdlib.h&gt;#include&lt;malloc.h&gt;#include&lt;iostream&gt;#include&lt;stack&gt;#include&lt;queue&gt;using namespace std;typed_二叉树的建立

解决asp.net导出excel时中文文件名乱码_asp.net utf8 导出中文字符乱码-程序员宅基地

文章浏览阅读7.1k次。在Asp.net上使用Excel导出功能,如果文件名出现中文,便会以乱码视之。 解决方法: fileName = HttpUtility.UrlEncode(fileName, System.Text.Encoding.UTF8);_asp.net utf8 导出中文字符乱码

笔记-编译原理-实验一-词法分析器设计_对pl/0作以下修改扩充。增加单词-程序员宅基地

文章浏览阅读2.1k次,点赞4次,收藏23次。第一次实验 词法分析实验报告设计思想词法分析的主要任务是根据文法的词汇表以及对应约定的编码进行一定的识别,找出文件中所有的合法的单词,并给出一定的信息作为最后的结果,用于后续语法分析程序的使用;本实验针对 PL/0 语言 的文法、词汇表编写一个词法分析程序,对于每个单词根据词汇表输出: (单词种类, 单词的值) 二元对。词汇表:种别编码单词符号助记符0beginb..._对pl/0作以下修改扩充。增加单词

android adb shell 权限,android adb shell权限被拒绝-程序员宅基地

文章浏览阅读773次。我在使用adb.exe时遇到了麻烦.我想使用与bash相同的adb.exe shell提示符,所以我决定更改默认的bash二进制文件(当然二进制文件是交叉编译的,一切都很完美)更改bash二进制文件遵循以下顺序> adb remount> adb push bash / system / bin /> adb shell> cd / system / bin> chm..._adb shell mv 权限

投影仪-相机标定_相机-投影仪标定-程序员宅基地

文章浏览阅读6.8k次,点赞12次,收藏125次。1. 单目相机标定引言相机标定已经研究多年,标定的算法可以分为基于摄影测量的标定和自标定。其中,应用最为广泛的还是张正友标定法。这是一种简单灵活、高鲁棒性、低成本的相机标定算法。仅需要一台相机和一块平面标定板构建相机标定系统,在标定过程中,相机拍摄多个角度下(至少两个角度,推荐10~20个角度)的标定板图像(相机和标定板都可以移动),即可对相机的内外参数进行标定。下面介绍张氏标定法(以下也这么称呼)的原理。原理相机模型和单应矩阵相机标定,就是对相机的内外参数进行计算的过程,从而得到物体到图像的投影_相机-投影仪标定

Wayland架构、渲染、硬件支持-程序员宅基地

文章浏览阅读2.2k次。文章目录Wayland 架构Wayland 渲染Wayland的 硬件支持简 述: 翻译一篇关于和 wayland 有关的技术文章, 其英文标题为Wayland Architecture .Wayland 架构若是想要更好的理解 Wayland 架构及其与 X (X11 or X Window System) 结构;一种很好的方法是将事件从输入设备就开始跟踪, 查看期间所有的屏幕上出现的变化。这就是我们现在对 X 的理解。 内核是从一个输入设备中获取一个事件,并通过 evdev 输入_wayland

推荐文章

热门文章

相关标签