初学多线程

title: Java_JUC
categories:
  - 后端
  - Java
tags:
  - 并发
  - 多线程
author:
  - Jungle
date: 2021-08-10 20:42:30

# Thread

火车票多线程抢票

 

1. 初始构思

思路:

  • static修饰ticketNum,使其唯一;

  • 并设置volatile,保证变量在多个线程之间的可见性。

 package com.Jungle;
 ​
 /*
 * 遇到问题:
 * 1. 如何解决用户同时抢一张票的问题?
 * */
 public class myThread implements Runnable {
 ​
     // 有多少张火车票
     private volatile static int ticketNum = 10;
 ​
     @Override
     public void run() {
         while (true) {
             if (ticketNum <= 0) return;
             try {
                 Thread.sleep(1);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println(Thread.currentThread() +
                     " 拿到了第" + ticketNum-- + "张票!");
         }
     }
 ​
     public static void main(String[] args) {
         myThread t1 = new myThread();
         myThread t2 = new myThread();
         myThread t3 = new myThread();
 ​
         new Thread(t1, "小明").start();
         new Thread(t2, "老师").start();
         new Thread(t3, "黄牛党").start();
     }
 ​
 }

失败了!

 // 输出
 Thread[小明,5,main] 拿到了第10张票!
 Thread[老师,5,main] 拿到了第8张票!
 Thread[黄牛党,5,main] 拿到了第9张票!
 Thread[黄牛党,5,main] 拿到了第7张票!
 Thread[小明,5,main] 拿到了第5张票!
 Thread[老师,5,main] 拿到了第6张票!
 Thread[黄牛党,5,main] 拿到了第4张票!
 Thread[老师,5,main] 拿到了第2张票!
 Thread[小明,5,main] 拿到了第3张票!
 Thread[黄牛党,5,main] 拿到了第1张票!
 Thread[小明,5,main] 拿到了第-1张票!
 Thread[老师,5,main] 拿到了第0张票!
 ​
 Process finished with exit code 0

2. 使用外部停止方式

思想:flag == false时停止!

 // 改名 myThread --> BuyTicket, 放在新建的UnsafeBuyTicket类里
 ​
 package com.Jungle;
 ​
 public class UnsafeBuyTicket {
     public static void main(String[] args) {
         //在同一个车站窗口买票
         BuyTicket station = new BuyTicket();
 ​
         new Thread(station, "小明").start();
         new Thread(station, "老师").start();
         new Thread(station, "黄牛党").start();
     }
 ​
 ​
 }
 ​
 class BuyTicket implements Runnable {
 ​
     // 有多少张火车票
     private volatile static int ticketNum = 10;
 ​
     boolean flag = true; // 外部停止方式
 ​
     private void buy() throws InterruptedException {
         // 判断是否有票(无票立即结束)
         if (ticketNum <= 0) {
             flag = false;
             return;
         }
         // 模拟延时
         Thread.sleep(100);
         // 买票
         System.out.println(Thread.currentThread() + " 拿到了第" + ticketNum-- + "张票!");
     }
 ​
     // 买票
     @Override
     public void run() {
         // 买票
         while (flag) {
             try {
                 buy();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
     }
 }

输出: 出现负数线程不安全!

 Thread[老师,5,main] 拿到了第10张票!
 Thread[小明,5,main] 拿到了第9张票!
 Thread[黄牛党,5,main] 拿到了第8张票!
 Thread[小明,5,main] 拿到了第7张票!
 Thread[老师,5,main] 拿到了第6张票!
 Thread[黄牛党,5,main] 拿到了第5张票!
 Thread[小明,5,main] 拿到了第4张票!
 Thread[老师,5,main] 拿到了第3张票!
 Thread[黄牛党,5,main] 拿到了第2张票!
 Thread[小明,5,main] 拿到了第1张票!
 Thread[老师,5,main] 拿到了第-1张票!
 Thread[黄牛党,5,main] 拿到了第0张票!
 ​
 Process finished with exit code 0

为什么出现会出现负数?

每个线程在自己的工作内存交互,内存控制不当会造成数据不一致.

同时看到最后一张票, 判断通过(此时票数为1), 其中一人取票后变为负数, 其余人输出负数.

线程不安全的集合:

  • ArrayList

  • LinkedList

  • HashMap

3. 怎么解决线程不安全?

synchronize: 底层原理是同步+锁

案例分析:

  • 对于火车票抢票, 多个人在同一个窗口进行, 可以直接在buy()方法里加synchronized关键字, 会对本类对象(BuyTicket station = new BuyTicket();)进行锁定和排队.

  • 银行家案例, 见下分析

 

 

 package com.Jungle;
 ​
 public class UnsafeBuyTicket {
     public static void main(String[] args) {
         //在同一个车站窗口买票
         BuyTicket station = new BuyTicket();
 ​
         new Thread(station, "小明").start();
         new Thread(station, "老师").start();
         new Thread(station, "黄牛党").start();
     }
 ​
 ​
 }
 ​
 class BuyTicket implements Runnable {
 ​
     // 有多少张火车票
     private volatile static int ticketNum = 10;
 ​
     boolean flag = true; // 外部停止方式
 ​
     // 同步方法
     private synchronized void buy() throws InterruptedException {
         // 判断是否有票(无票立即结束)
         if (ticketNum <= 0) {
             flag = false;
             return;
         }
         // 模拟延时
         Thread.sleep(100);
         // 买票
         System.out.println(Thread.currentThread() + " 拿到了第" + ticketNum-- + "张票!");
     }
 ​
     // 买票
     @Override
     public void run() {
         // 买票
         while (flag) {
             try {
                 buy();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
     }
 }
 ​

输出:

 Thread[小明,5,main] 拿到了第10张票!
 Thread[小明,5,main] 拿到了第9张票!
 Thread[黄牛党,5,main] 拿到了第8张票!
 Thread[老师,5,main] 拿到了第7张票!
 Thread[老师,5,main] 拿到了第6张票!
 Thread[黄牛党,5,main] 拿到了第5张票!
 Thread[黄牛党,5,main] 拿到了第4张票!
 Thread[黄牛党,5,main] 拿到了第3张票!
 Thread[黄牛党,5,main] 拿到了第2张票!
 Thread[小明,5,main] 拿到了第1张票!
 ​
 Process finished with exit code 0

若不是本类对象的同步,则需要使用共享资源作为同步监视器

案例分析:

  • 对于双方对同一账户取钱来说, 启动线程的是双方, 共两个线程, 共享资源是银行账户, 所以需要用到同步代码块, 即锁定银行账户.

 

 

 package com.Jungle;
 ​
 import java.nio.charset.StandardCharsets;
 ​
 public class UnsafeBank {
     public static void main(String[] args) {
         Account account = new Account(100, "结婚基金");
 ​
         UserDrawing you = new UserDrawing(account, 50, "你");
         UserDrawing yourGF = new UserDrawing(account, 100, "你的女朋友");
 ​
         you.start();
         yourGF.start();
     }
 }
 ​
 // 银行账户
 class Account {
     int balance; // 余额
     String cardName; // 卡名
     public Account(int balance, String cardName) {
         this.balance = balance;
         this.cardName = cardName;
     }
 }
 ​
 // 用户取钱
 class UserDrawing extends Thread {
     Account account; // 账户
     int drawingCount; // 取多少钱(单位:万)
     int cash; // 取钱后的手里的钱
 ​
     public UserDrawing(Account account, int drawingCount, String userName) {
         super(userName);
         this.account = account;
         this.drawingCount = drawingCount;
     }
 ​
     // 取钱
     @Override
     public void run() {
         // 锁账户
         synchronized (account) {
             if (account.balance < drawingCount) {
                 System.out.println(account.cardName + "余额不足!");
                 return;
             }
             try {
                 Thread.sleep(1000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             account.balance -= drawingCount; // 账户里的钱减少
             cash += drawingCount; // 手里的钱增多
 ​
             System.out.println(new StringBuilder().
                     append(account.cardName).append("余额为: ").append(account.balance));
             System.out.println(new StringBuilder().
                     append(this.getName()).append("手里的钱: ").append(cash));
         }
     }
 }

输出:

 结婚基金余额为: 50
 你手里的钱: 50
 结婚基金余额不足!
 ​
 Process finished with exit code 0

案例3: 不安全的集合

package com.Jungle;

import java.util.ArrayList;
import java.util.List;

// 线程不安全的集合
public class UnsafeList {

    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();

        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                synchronized (list) {
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(list.size());
    }
}

输出:

10000

Process finished with exit code 0

CopyOnWriteArrayList

Copy On Write 也是一种重要的思想,在写少读多的场景下,为了保证集合的线程安全性,我们完全可以在当前线程中得到原始数据的一份拷贝,然后进行操作。JDK集合框架中为我们提供了 ArrayList 的这样一个实现:CopyOnWriteArrayList。但是如果不是写少读多的场景,使用 CopyOnWriteArrayList 开销比较大,因为每次对其更新操作(add/set/remove)都会做一次数组拷贝。

package com.Jungle;

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

public class JUCTest {
    public static void main(String[] args) {
        List<String> list = new CopyOnWriteArrayList<String>();

        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(list.size());
    }
}

涉及到的源码

上文对应的思路应该是: 先复制一份数据, 然后add, 最后再新的数组引用赋值回去.

所以, 针对本例不太适合!

适合: 写少读多

public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}
final void setArray(Object[] a) {
    array = a;
}

final transient ReentrantLock lock = new ReentrantLock();

/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;

final Object[] getArray() {
    return array;
}

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

// ReentrantLock:
public void lock() {
    sync.lock();
}

private final Sync sync;

public void unlock() {
    sync.release(1);
}

Collections.synchronizedList()

package com.Jungle;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class JUCTest {
    public static void main(String[] args) {
//        List<String> list = new CopyOnWriteArrayList<String>();
        List<String> list = Collections.synchronizedList(new ArrayList<String>());

        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(list.size());
    }
}

输出:

10000

Process finished with exit code 0

涉及到的源码

public static <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list) :
            new SynchronizedList<>(list));
}

static class SynchronizedRandomAccessList<E>
    extends SynchronizedList<E>
    implements RandomAccess {

    SynchronizedRandomAccessList(List<E> list) {
        super(list);
    }
}

static class SynchronizedList<E>
    extends SynchronizedCollection<E>
    implements List<E> {
    private static final long serialVersionUID = -7754090372962971524L;

    final List<E> list;

    SynchronizedList(List<E> list) {
        super(list);
        this.list = list;
    }
}

static class SynchronizedCollection<E> implements Collection<E>, Serializable {
    private static final long serialVersionUID = 3053995032091335093L;

    final Collection<E> c;  // Backing Collection
    final Object mutex;     // Object on which to synchronize     ---> 加锁的对象            

    SynchronizedCollection(Collection<E> c) {
        this.c = Objects.requireNonNull(c);
        mutex = this;
    }

    SynchronizedCollection(Collection<E> c, Object mutex) {
        this.c = Objects.requireNonNull(c);
        this.mutex = Objects.requireNonNull(mutex);
    }

    public boolean add(E e) {
        synchronized (mutex) {return c.add(e);}
    }
    
    public boolean remove(Object o) {
        synchronized (mutex) {return c.remove(o);}
    }
}

ThreadPoolExecutor

最终采用线程池方式实现!

使用阿里巴巴推荐的创建线程池的方式

package com.Jungle;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class JUCTest {

    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;

    public static void main(String[] args) {
//        List<String> list = new CopyOnWriteArrayList<String>();
        List<String> list = Collections.synchronizedList(new ArrayList<String>());

        //使用阿里巴巴推荐的创建线程池的方式
        //通过ThreadPoolExecutor构造函数自定义参数创建
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,         // 核心线程数为 5
                MAX_POOL_SIZE,          // 最大线程数 10
                KEEP_ALIVE_TIME,        // 大于核心线程数时,核心线程外的线程等待时间: 1L
                TimeUnit.SECONDS,       // 等待时间的单位为 TimeUnit.SECONDS
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),   // workQueue: 任务队列为 ArrayBlockingQueue,并且容量为 100
                new ThreadPoolExecutor.CallerRunsPolicy()); // handler: 饱和策略为 CallerRunsPolicy

        for (int i = 0; i < 10000; i++) {
            Runnable worker = ()->{
                list.add(Thread.currentThread().getName());
            };
            //执行Runnable
            executor.execute(worker);
        }
        //终止线程池
        executor.shutdown();
        while (!executor.isTerminated()) {
        }
        System.out.println("Finished all threads");
        System.out.println(list.size());
    }
}

输出

Finished all threads
10000

Process finished with exit code 0


目录

火车票多线程抢票

1. 初始构思

2. 使用外部停止方式

3. 怎么解决线程不安全?

CopyOnWriteArrayList

Collections.synchronizedList()

ThreadPoolExecutor


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值