一个并发的问题

  • 现在有100张票,很多人来抢,可能就会出现超卖的问题

    1. public static int cnt = 100;
    2. public static void main(String[] args) throws InterruptedException {
    3. List<Thread> list = new ArrayList<>();
    4. for (int i=0;i<1000;i++){
    5. Thread thread = new Thread(){
    6. @Override
    7. public void run() {
    8. if(cnt>0){
    9. String name = Thread.currentThread().getName();
    10. System.out.println(name+"抢到了"+cnt+"张票");
    11. cnt--;
    12. }
    13. }
    14. };
    15. list.add(thread);
    16. }
    17. //启动
    18. for (int i=0;i<list.size();i++){
    19. list.get(i).start();
    20. }
    21. for (int i=0;i<list.size();i++){
    22. list.get(i).join();
    23. }
    24. System.out.println("剩下的票:"+cnt);
    25. }
  • 第一个问题:cnt>0作为条件,会导致并发的问题

  • 第二个问题:cnt在—的时候,也可能导致并发的问题
  • 使用锁进行解决

    1. public static int cnt = 100;
    2. //创建一把锁
    3. static Object lock = new Object();
    4. public static void main(String[] args) throws InterruptedException {
    5. List<Thread> list = new ArrayList<>();
    6. for (int i=0;i<1000;i++){
    7. Thread thread = new Thread(){
    8. @Override
    9. public void run() {
    10. //使用锁将可能出现并发的代码锁住
    11. synchronized (lock){
    12. if(cnt>0){
    13. String name = Thread.currentThread().getName();
    14. System.out.println(name+"抢到了"+cnt+"张票");
    15. cnt--;
    16. }
    17. }
    18. }
    19. };
    20. list.add(thread);
    21. }
    22. //启动
    23. for (int i=0;i<list.size();i++){
    24. list.get(i).start();
    25. }
    26. for (int i=0;i<list.size();i++){
    27. list.get(i).join();
    28. }
    29. System.out.println("剩下的票:"+cnt);
    30. }

    synchronized

  • 基本使用,锁对象

    • 所有的线程必须要使用同一把锁对象才可以有效果
    • 以下代码是无法锁住线程方法的, 如果是多线程,下面的代码,每个线程对象都会使用自己的独立锁 ```java public class TicketThread extends Thread{ public static int cnt = 100; Object lock = new Lock();

      @Override public void run() { //使用锁将可能出现并发的代码锁住 synchronized (lock){

      1. if(cnt>0){
      2. String name = Thread.currentThread().getName();
      3. System.out.println(name+"抢到了"+cnt+"张票");
      4. cnt--;
      5. }

      } } }

  1. - 可以将Object lock使用static修饰,或者从外部传入同一把锁
  2. - 修饰this对象
  3. - 注意必须是多个线程使用一个对象的时候才有效果,一般用于单例模式
  4. - 创建一个实现了Runnable的任务
  5. ```java
  6. public class TickTask implements Runnable {
  7. public int cnt = 100;
  8. @Override
  9. public void run() {
  10. //使用锁将可能出现并发的代码锁住
  11. synchronized (this){
  12. if(cnt>0){
  13. String name = Thread.currentThread().getName();
  14. System.out.println(name+"抢到了"+cnt+"张票");
  15. cnt--;
  16. }
  17. }
  18. }
  19. }
  • 通过线程池,让多个线程调用同一个task
    1. public static void main(String[] args) {
    2. //线程池+runnable组合
    3. ExecutorService service = Executors.newFixedThreadPool(10);
    4. TickTask task = new TickTask();
    5. for(int i=0;i<1000;i++){
    6. service.execute(task);
    7. }
    8. }
  • 修饰方法
    • 一般情况需要需要通过static配合使用才有效果,因为锁住是类的方法,而不是对象的方法
    • 如果是对象的方法,必须要保证是同一个对象才有效果
    • 一般情况,最好不要锁方法
  • 修饰类的类对象

    • 每个类被jvm加载之后,都会生产一个类的对象,而且是唯一的
    • 大多数的情况直接锁类对象,都是有效果的,因为类对象是唯一的
      1. synchronized(TicketThread.class){
      2. if(cnt>0){
      3. String name = Thread.currentThread().getName();
      4. System.out.println(name+"抢到了"+cnt+"张票");
      5. cnt--;
      6. }
      7. }
  • synchronized基本原理

    • 是利用synchronized去锁住一个对象,必然要判断这个对象是否是同一个对象,是通过monitor对锁进行监控,实现争抢锁的实现逻辑
    • 锁的信息是利用的对象Head信息进行比较,其中MarkWord用于存储对象自身的运行时数据
    • 每次有线程需要获取的锁的时候,都需要将锁的信息提供给monitor进行查询
    • 如果锁对象运行时的数据发生变化的时候,可能会导致锁对象在比较的时候发生不一致的情况
    • 结论不要修改锁存储的数据,以避免导致锁失效

      Lock手动锁的使用

  • synchronized是自动锁,什么时候加锁,什么时候释放锁,都是系统完成的,如果在使用不当,那么会出现死锁

    • 创建一个男孩线程去找女孩约会

      1. public class Boy extends Thread {
      2. @Override
      3. public void run() {
      4. try {
      5. date();
      6. } catch (InterruptedException e) {
      7. e.printStackTrace();
      8. }
      9. }
      10. public static void date() throws InterruptedException {
      11. //男孩只能去找一个女孩约会
      12. synchronized (Boy.class){
      13. System.out.println("男去找女孩约会了");
      14. Thread.sleep(10);
      15. //加上锁,防止女孩同和两个男孩约会
      16. synchronized(Girl.class){
      17. System.out.println("女孩在约会中...");
      18. }
      19. System.out.println("在一起约会了...");
      20. }
      21. }
      22. }
    • 创建一个女孩去找男孩约会

      1. public class Girl extends Thread{
      2. @Override
      3. public void run() {
      4. try {
      5. date();
      6. } catch (InterruptedException e) {
      7. e.printStackTrace();
      8. }
      9. }
      10. public void date() throws InterruptedException {
      11. //女孩只能去找一个男孩约会
      12. synchronized (Girl.class){
      13. System.out.println("女孩去找男孩约会了");
      14. //防止男孩和多个女孩约会
      15. synchronized (Boy.class){
      16. System.out.println("男孩在约会了");
      17. }
      18. System.out.println("在一起约会了");
      19. }
      20. }
      21. }
    • 开始约会

      1. public static void main(String[] args) throws InterruptedException {
      2. new Boy().start();
      3. new Girl().start();
      4. }
  • Lock是JDK提供的锁对象,可以手动控制锁的加载和释放

    • lock是一个接口

      1. public interface Lock {
      2. void lock(); //主动锁住代码,会阻塞代码
      3. void lockInterruptibly() throws InterruptedException;//可以中断的锁
      4. boolean tryLock();//尝试获取锁,非阻塞锁
      5. boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //尝试获取锁,指定一个等待时间
      6. void unlock();//释放锁
      7. Condition newCondition();//条件锁,可以分为读锁和写锁
      8. }
    • ReentrantLock的基本使用

      1. public class TestLock {
      2. static int cnt =10;
      3. public static void main(String[] args) {
      4. Lock lock = new ReentrantLock();
      5. //创建一个任务
      6. Runnable runnable = ()->{
      7. try{
      8. lock.lock();//当前线程主动获取锁,如果其他线程已经获取了锁,那么会阻塞
      9. if(cnt>0){
      10. System.out.println("抢到资源"+cnt);
      11. cnt--;
      12. if(cnt==5){
      13. throw new RuntimeException();
      14. }
      15. }
      16. }finally { //无论是否抛出异常,都释放锁
      17. lock.unlock();// 释放锁
      18. }
      19. };
      20. //创建线程池执行任务
      21. ExecutorService service = Executors.newFixedThreadPool(5);
      22. for (int i=0;i<100;i++){
      23. service.execute(runnable);
      24. }
      25. service.shutdown();
      26. }
      27. }
  • tryLock的使用

    • 尝试获取锁,如果成功则返回true,如果失败则返回false

      1. Lock lock = new ReentrantLock();
      2. //创建一个任务
      3. Runnable runnable = ()->{
      4. if(lock.tryLock()){//尝试获取锁,不会阻塞线程
      5. try{
      6. if(cnt>0){
      7. System.out.println("抢到资源"+cnt);
      8. cnt--;
      9. }
      10. } finally { //无论是否抛出异常,都释放锁
      11. lock.unlock();// 释放锁
      12. }
      13. }else{
      14. System.out.println("没有抢到资源");
      15. }
      16. };
    • 可以带时间参数的,在获取锁的时候,可以设置等待的时间

      1. //创建一个任务
      2. Runnable runnable = () -> {
      3. try {
      4. //第一个参数是等待多长时间
      5. //第二个参数是等待时间的单位
      6. if (lock.tryLock(1,TimeUnit.MICROSECONDS)) {
      7. try {
      8. if (cnt > 0) {
      9. System.out.println("抢到资源" + cnt);
      10. cnt--;
      11. }
      12. }finally {
      13. lock.unlock();
      14. }
      15. } else {
      16. System.out.println("没有抢到资源");
      17. }
      18. } catch (InterruptedException e) {
      19. e.printStackTrace();
      20. }
      21. };
  • lockInterruptibly:可以中断锁

    1. public static void main(String[] args) throws InterruptedException {
    2. Lock lock = new ReentrantLock();
    3. Runnable task = new Runnable() {
    4. @Override
    5. public void run() {
    6. try {
    7. lock.lockInterruptibly();//可以中断
    8. try {
    9. String name = Thread.currentThread().getName();
    10. System.out.println(name + "执行任务");
    11. Thread.sleep(3000);
    12. } catch (InterruptedException e) {
    13. e.printStackTrace();
    14. }finally {
    15. lock.unlock();
    16. }
    17. } catch (InterruptedException e) {
    18. // e.printStackTrace();
    19. System.out.println("不想再等待了");
    20. }
    21. }
    22. };
    23. Thread t1 = new Thread(task);
    24. Thread t2 = new Thread(task);
    25. t1.start();
    26. t2.start();
    27. Thread.sleep(1000);
    28. t2.interrupt(); //中断线程2的等待
    29. }

    锁的分类

  • 可重载锁:一个已经获得锁的线程,可以重复再加载锁

    • 案例

      1. class MyClass {
      2. public synchronized void method1() {
      3. method2();
      4. }
      5. public synchronized void method2() {
      6. }
      7. }
    • method1 和 methods都所有锁,一个线程加载了method1的锁之后,还可以继续加载method2的锁,如果不是可重载锁,则不能加载

  • 可中断锁:A线程获取了锁,B线程在等待锁的过程中,可以直接中断等待
  • 公平锁:使得多个线程在获取锁的机会上是比较公平
    Lock lock = new ReentrantLock(true);//true是公平,默认false是不公平
  • 读写锁:可以让数据的读取和修改的锁分开

    • 当一个读取数据的线程获取锁的时候,另一个线程如果是读取数据,不需要锁
    • 当一个读取数据的线程获取锁的时候,另一个线程如果是写如数据,需要锁
    • 当一个写入数据的线程获取锁的时候,另一个线程无论读取还是写,都需要锁
    • 基本规则
      • 写-写:互斥,阻塞。读-写:互斥,读阻塞写、写阻塞读。读-读:不互斥、不阻塞。
    • 读写分类

      1. public class MyData2 {
      2. int value = 10;
      3. //创建读写锁
      4. ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
      5. //分为写锁
      6. ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
      7. //读锁
      8. ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
      9. public int getValue() {
      10. readLock.lock();
      11. try{
      12. Thread.sleep(1000);
      13. return value;
      14. } catch (InterruptedException e) {
      15. e.printStackTrace();
      16. } finally {
      17. readLock.unlock();
      18. }
      19. return 0;
      20. }
      21. public void setValue(int value) {
      22. writeLock.lock();
      23. try{
      24. Thread.sleep(1000);
      25. this.value = value;
      26. } catch (InterruptedException e) {
      27. e.printStackTrace();
      28. } finally {
      29. writeLock.unlock();
      30. }
      31. }
      32. }

      线程通信

  • 在使用多线程的开发的时候,会发现线程运行顺序是无序的,无法对线程进行精确的控制,但是有些场景是需要对线程的顺序进行控制的,例如:生产者-消费-商品的问题

  • 创建一个生产者-消费-商品的场景

    • 创建商品

      1. public class Product {
      2. int id;
      3. String brand;//标签,例如北京,长沙
      4. String name;//名称,例如烤鸭,臭豆腐
      5. }
    • 创建生产者线程

      1. //生产者线程
      2. public class ProductThread extends Thread{
      3. Product product;//操作的商品
      4. public ProductThread(Product product) {
      5. this.product = product;
      6. }
      7. @Override
      8. public void run() {
      9. for (int i=1;i<=10;i++){
      10. product.setId(i);
      11. if(i%2==0){
      12. product.setBrand("长沙");
      13. try {
      14. Thread.sleep(100);
      15. } catch (InterruptedException e) {
      16. e.printStackTrace();
      17. }
      18. product.setName("臭豆腐");
      19. }else{
      20. product.setBrand("北京");
      21. try {
      22. Thread.sleep(100);
      23. } catch (InterruptedException e) {
      24. e.printStackTrace();
      25. }
      26. product.setName("烤鸭");
      27. }
      28. System.out.println("厨师加工了"+product.getId()+"-"+product.getBrand()+product.getName());
      29. }
      30. }
      31. }
    • 创建一个消费者线程

      1. public class ConsumerThread extends Thread{
      2. Product product;//操作的商品
      3. public ConsumerThread(Product product) {
      4. this.product = product;
      5. }
      6. @Override
      7. public void run() {
      8. for (int i=1;i<=10;i++){
      9. try {
      10. Thread.sleep(100);
      11. } catch (InterruptedException e) {
      12. e.printStackTrace();
      13. }
      14. System.out.println("顾客吃了:"+product.getId()+"-"+product.getBrand()+product.getName());
      15. }
      16. }
      17. }
  • 通过synchronized + notify/wait来控制线程的顺序

    • 给生产者添加 ```java public class ProductThread extends Thread{ Product product;//操作的商品

      public ProductThread(Product product) { this.product = product; }

      @Override public void run() { for (int i=1;i<=10;i++){

      1. synchronized (product){
      2. product.setId(i);
      3. if(i%2==0){
      4. product.setBrand("长沙");
      5. try {
      6. Thread.sleep(100);
      7. } catch (InterruptedException e) {
      8. e.printStackTrace();
      9. }
      10. product.setName("臭豆腐");
      11. }else{
      12. product.setBrand("北京");
      13. try {
      14. Thread.sleep(100);
      15. } catch (InterruptedException e) {
      16. e.printStackTrace();
      17. }
      18. product.setName("烤鸭");
      19. }
      20. System.out.println("厨师加工了"+product.getId()+"-"+product.getBrand()+product.getName());
      21. product.notify();
      22. try {
      23. //放弃锁,进入到等待的状态
      24. product.wait(); //阻塞状态
      25. } catch (InterruptedException e) {
      26. e.printStackTrace();
      27. }
      28. }

      } } }

  1. - 给消费者添加
  2. ```java
  3. Product product;//操作的商品
  4. public ConsumerThread(Product product) {
  5. this.product = product;
  6. }
  7. @Override
  8. public void run() {
  9. for (int i=1;i<=10;i++){
  10. synchronized (product){
  11. try {
  12. Thread.sleep(100);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. System.out.println("顾客吃了:"+product.getId()+"-"+product.getBrand()+product.getName());
  17. product.notify();//告诉厨师可以继续开始做了
  18. try {
  19. product.wait();//客户进入等待状态
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. }
  25. }
  26. }