回顾一下多线程-贰

分割线

线程同步-synchronized

  • 多个线程操作同一资源时会有问题出现,用 synchronized 同步.

    线程同步的形成条件: 队列+锁

    实现形式有 同步方法 和 同步代码块


  1. 同步方法, 锁的是方法所属对象 this

    如下买票例子中 buy 方法锁的是 Ticket ticket 这个对象

    public synchronized void buy() {}

  2. 同步代码块, 锁 obj

    synchronized (obj) {
    // 操作
    }

买票

public class Ticket implements Runnable {
private int ticketNums = 10; //票数
boolean flag = true; //外部停止方法

@Override
public void run() {
while (flag) {
buy();
//模拟延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public synchronized void buy() {
//判断是否有票
if (ticketNums <= 0) {
flag = false;
return;
}
System.out.println(Thread.currentThread().getName() + "-->得到倒数第" + ticketNums-- + "票");
}

public static void main(String[] args) {
Ticket ticket = new Ticket();

new Thread(ticket, "小明").start();
new Thread(ticket, "老师").start();
new Thread(ticket, "黄牛").start();
}
}
  • 结果

    不加锁: 有错误

    小明-->得到倒数第10票
    老师-->得到倒数第9票
    黄牛-->得到倒数第8票
    老师-->得到倒数第7票
    小明-->得到倒数第6票
    黄牛-->得到倒数第5票
    老师-->得到倒数第4票
    小明-->得到倒数第3票
    黄牛-->得到倒数第3票
    老师-->得到倒数第2票
    小明-->得到倒数第1票
    黄牛-->得到倒数第1票

    加锁: 无误

    小明-->得到倒数第10票
    黄牛-->得到倒数第9票
    老师-->得到倒数第8票
    小明-->得到倒数第7票
    黄牛-->得到倒数第6票
    老师-->得到倒数第5票
    黄牛-->得到倒数第4票
    小明-->得到倒数第3票
    老师-->得到倒数第2票
    黄牛-->得到倒数第1票

分割线

银行取款

import java.math.BigDecimal;

public class Bank {
public static void main(String[] args) {
Account account = new Account(new BigDecimal(100), "我的账户");

DrawingChannel a = new DrawingChannel(account, new BigDecimal(50), "A");
DrawingChannel b = new DrawingChannel(account, new BigDecimal(100), "B");

a.start();
b.start();
}
}

//账户
class Account {
BigDecimal balance;//余额
final String name; //卡名

public Account(BigDecimal balance, String name) {
this.balance = balance;
this.name = name;
}
}

/**
* 银行:模拟取款
* * 这里之所以没用实现Runnable接口的方式是为了调用Thread类中一些方法
*/
class DrawingChannel extends Thread {
final Account account; //账户
BigDecimal drawingMoney; //取了多少钱
BigDecimal nowMoney; //现在手里有多少钱

public DrawingChannel(Account account, BigDecimal drawingMoney, String name) {
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
this.nowMoney = new BigDecimal(0);
}

@Override
public void run() {
synchronized (account) {
//判断有没有钱
if (account.balance.compareTo(drawingMoney) < 0) {
System.out.println(account.name + "钱不够" + drawingMoney + "," + this.getName() + "无法取走");
return;
}

// 放大错误
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
draw();
}
}

private void draw() {
//卡内余额 = 余额 - 取的钱
account.balance = account.balance.subtract(drawingMoney);
System.out.println(this.getName() + "取走" + drawingMoney);

//手里的钱
nowMoney = nowMoney.add(drawingMoney);

System.out.println(account.name + "余额为:" + account.balance);
System.out.println(this.getName() + "手里的钱:" + nowMoney);
}
}
  • 预期结果

    A取走50
    我的账户余额为:50
    A手里的钱:50
    我的账户钱不够100,B无法取走

关于试错技巧

  • 熟练使用.sleep()试错

    • 试错前

      我的账户钱不够100,B无法取走
      我的账户余额为:50
      A手里的钱:50

    • 试错后

      B 取走 100
      A 取走 50
      我的账户余额为:50
      我的账户余额为:50
      A 手里的钱:50
      B 手里的钱:100

分割线

集合与线程安全

多个线程同时操作集合对象时可能会存在覆写(线程不安全)

非线程同步

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

for (int i = 0; i < 10000; i++) {
new Thread(() -> {
list.add(Thread.currentThread().getName());
}).start();
}
Thread.sleep(3000); // 延时,为了等上面执行完毕
System.out.println(list.size());//输出:9992
}
}

线程同步

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

for (int i = 0; i < 10000; i++) {
new Thread(() -> {
synchronized (list) {
list.add(Thread.currentThread().getName());
}
}).start();
}
Thread.sleep(3000); // 延时,为了等上面执行完毕
System.out.println(list.size());//输出:10000
}
}

线程安全集合

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

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

Thread.sleep(3000);
System.out.println(list.size());//输出:10000
}
}

分割线

死锁

两线程在各自拥有一个对象的锁时都等待对方线程释放对象的锁 ; 也有可能很多线程产生环形/网状死锁.

  • 如下例子就会产生死锁

    //死锁:多个线程互相抱着对方需要的资源
    public class DeadLock {
    public static void main(String[] args) {
    Makeup m1 = new Makeup(0, "小黑");
    Makeup m2 = new Makeup(1, "小白");

    m1.start();
    m2.start();
    }
    }

    //口红
    class Lipstick {
    }

    //镜子
    class Mirror {
    }

    //化妆
    class Makeup extends Thread {
    //需要的资源只有一份,用static来保证只有一份
    static final Lipstick lipstick = new Lipstick();
    static final Mirror mirror = new Mirror();

    int choice; //选择
    String name; //使用化妆品的人

    public Makeup(int choice, String name) {
    this.choice = choice;
    this.name = name;
    }

    @Override
    public void run() {
    //化妆
    try {
    makeup();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }

    //化妆,互相持有对方的锁,就是需要拿到对方的资源
    private void makeup() throws InterruptedException {
    if (choice == 0) {
    synchronized (lipstick) { //获得口红的锁
    System.out.println(this.name + "获得口红的锁");
    Thread.sleep(1000);

    synchronized (mirror) { //一秒钟后想获得镜子的锁
    System.out.println(this.name + "获得镜子的锁");
    }
    }
    } else {
    synchronized (mirror) { //获得镜子的锁
    System.out.println(this.name + "获得镜子的锁");
    Thread.sleep(2000);
    synchronized (lipstick) { //两秒钟后想获得口红的锁
    System.out.println(this.name + "获得口红的锁");
    }
    }
    }
    }
    }

产生条件

  • 四个必要条件:

    1. 互斥条件:一个资源每次只能被一个进程使用。
    2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    3. 不剥夺条件:进程已获得的资源,在未使用完之前不能强行剥夺。
    4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

解决方案

  • 使用完同步对象后立即释放

    • 比如上面的例子中使用完口红或者镜子后未释放,再去获取另一个对象的锁,就会产生死锁了
    • 修改: 把 makeup 改为如下
    private void makeup() throws InterruptedException {
    if (choice == 0) {
    synchronized (lipstick) { // 获得口红的锁
    System.out.println(this.name + "获得口红的锁");
    Thread.sleep(1000);
    }
    synchronized (mirror) { // 一秒钟后想获得镜子的锁
    System.out.println(this.name + "获得镜子的锁");
    }
    } else {
    synchronized (mirror) { // 获得镜子的锁
    System.out.println(this.name + "获得镜子的锁");
    Thread.sleep(2000);
    }
    synchronized (lipstick) { // 两秒钟后想获得口红的锁
    System.out.println(this.name + "获得口红的锁");
    }
    }
    }

分割线

可重入锁-ReentrantLock

  • ReentrantLock (也叫 RT-Lock) 类实现了 java.util.concurrent.locks.Lock 接口

    与 synchronized 区别:

    1. ReentrantLock 是显式加解锁,它只能锁代码块

    2. 性能比 synchronized 好

  • 使用优先度: ReentrantLock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)

  • 注意点: 加解锁最好要在 try-finally 里

    try {
    while (true) {
    lock.lock();
    try {
    if ( ) {
    // ...
    } else {
    // ...
    }
    } catch (InterruptedException ignored) {
    } finally {
    lock.unlock();
    }
    }
  • 例子

    public class TestLock implements Runnable {
    int ticketNums = 10;
    private final ReentrantLock lock = new ReentrantLock(); // 定义Lock锁

    @Override
    public void run() {
    while (true) {
    try {
    lock.lock(); // 加锁
    if (ticketNums > 0) {
    System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "票");
    } else {
    break;
    }
    } finally {
    lock.unlock(); // 解锁
    try {
    Thread.sleep(500);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    }

    public static void main(String[] args) {
    TestLock testLock = new TestLock();

    new Thread(testLock, "a").start();
    new Thread(testLock, "b").start();
    new Thread(testLock, "c").start();
    }
    }
  • 结果

    a-->拿到了第10
    c-->拿到了第9
    b-->拿到了第8
    b-->拿到了第7
    c-->拿到了第6
    a-->拿到了第5
    b-->拿到了第4
    a-->拿到了第3
    c-->拿到了第2
    c-->拿到了第1

分割线

多线程与循环控制

public class TestLock implements Runnable {
int ticketNums = 10;
private final ReentrantLock lock = new ReentrantLock(); // 定义Lock锁

@Override
public void run() {
while (ticketNums > 0) {
try {
lock.lock(); // 加锁
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "票");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 解锁
}
}
}

public static void main(String[] args) {
TestLock testLock = new TestLock();

new Thread(testLock, "a").start();
new Thread(testLock, "b").start();
new Thread(testLock, "c").start();
}
}
  • 上面代码 while 循环会存在判断失误

    c-->拿到了第10
    c-->拿到了第9
    c-->拿到了第8
    c-->拿到了第7
    c-->拿到了第6
    c-->拿到了第5
    c-->拿到了第4
    c-->拿到了第3
    c-->拿到了第2
    c-->拿到了第1
    b-->拿到了第0
    a-->拿到了第-1
  • ticketNums 在判断之后被多次修改

    上面 10~1 次都是 c 线程执行的,它执行后轮到 b 和 a

    但是 b 与 a 线程实际上是在ticketNums=10时进入的循环,所以会导致-1出现

    所以建议遇到多线程循环控制时,直接while(true),然后在内部用if

    (反过来想: 是因为 lock 不能在循环之外加)

分割线

延迟对多线程的影响

下面三个例子数据都没错,关键看并发数量和执行时间

  • 瞬间执行完,资源被单一线程全部抢占 (并非不合理,只不过是处理器没分配到 B,C)

    public class TestLock implements Runnable {
    int ticketNums = 10;
    private final ReentrantLock lock = new ReentrantLock(); // 定义Lock锁

    @Override
    public void run() {
    while (true) {
    try {
    lock.lock(); // 加锁
    if (ticketNums > 0) {
    System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "票");
    } else {
    break;
    }
    } finally {
    lock.unlock(); // 解锁
    // try {
    // Thread.sleep(500);
    // } catch (InterruptedException e) {
    // e.printStackTrace();
    // }
    }
    }
    }

    public static void main(String[] args) {
    TestLock testLock = new TestLock();

    new Thread(testLock, "a").start();
    new Thread(testLock, "b").start();
    new Thread(testLock, "c").start();
    }
    }
    c-->拿到了第9
    c-->拿到了第8
    c-->拿到了第7
    c-->拿到了第6
    c-->拿到了第5
    c-->拿到了第4
    c-->拿到了第3
    c-->拿到了第2
    c-->拿到了第1

  • 给他加个延迟试试: 三线程并行,资源分配合理

    public void run() {
    while (true) {
    try {
    lock.lock(); // 加锁
    if (ticketNums > 0) {
    System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "票");
    } else {
    break;
    }
    } finally {
    lock.unlock(); // 解锁
    try {
    Thread.sleep(500);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    }
    b-->拿到了第10
    a-->拿到了第9
    c-->拿到了第8
    b-->拿到了第7
    a-->拿到了第6
    c-->拿到了第5
    c-->拿到了第4
    a-->拿到了第3
    b-->拿到了第2
    b-->拿到了第1

  • 再试试延迟之后解开同步锁: 单线执行,资源分配不平衡

    public void run() {
    while (true) {
    try {
    lock.lock(); // 加锁
    if (ticketNums > 0) {
    System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "票");
    } else {
    break;
    }
    } finally {
    try {
    Thread.sleep(500);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    lock.unlock(); // 解锁
    }
    }
    }
    a-->拿到了第10
    a-->拿到了第9
    c-->拿到了第8
    c-->拿到了第7
    c-->拿到了第6
    c-->拿到了第5
    c-->拿到了第4
    c-->拿到了第3
    c-->拿到了第2
    b-->拿到了第1

分割线

线程通信-wait-notify

class Clerk {
public int productNumber; // 售货员手中的商品数
}

public class ProducterAndCustomer {
public static void main(String[] args) {
Clerk clerk = new Clerk();

new Thread(() -> {
while (true) { //一直生产
synchronized (clerk) {
try {
if (clerk.productNumber == 0) {
System.out.println("产品为0, 开始生产");
while (clerk.productNumber < 5) {
System.out.println("库存: " + clerk.productNumber++);
Thread.sleep(500);
}
clerk.notifyAll();
//商品数不为0时让clerk等待
} else {
// clerk.wait();
clerk.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "生产者").start();

new Thread(() -> {
while (true) { //一直消费
synchronized (clerk) {
try {
if (clerk.productNumber == 5) {
System.out.println("产品为5,开始消费");
while (clerk.productNumber > 0) {
System.out.println("库存: " + clerk.productNumber--);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
clerk.notifyAll();
} else {
// clerk.wait();
clerk.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "消费者").start();
}
}
  • 结果

    产品为0, 开始生产
    库存: 0
    库存: 1
    库存: 2
    库存: 3
    库存: 4
    产品为5,开始消费
    库存: 5
    库存: 4
    库存: 3
    库存: 2
    库存: 1
    产品为0, 开始生产
    库存: 0

分割线

线程池

  • 线程池的出现是为了方便大量的线程创建,回收和管理

  • 需要了解 ExecutorService 线程池接口;以及 Executors 线程池工具类.

    • corePoolSize: 核心池的大小

    • maximumPoolSize:最大线程数

    • keepAliveTime: 线程没有任务时最多保持多长时间后会终止

    • void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执 Runnable

    • Futuresubmit(Callabletask) :执行任务,有返回值,一般用来执行 Callable

  • 例子

    public class TestThreadPool {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    // 1.创建服务,创建线程池
    ExecutorService service = Executors.newFixedThreadPool(10);
    Runnable runnable = () -> {
    System.out.println(Thread.currentThread().getName() + " runnable");
    };
    Callable<String> callable = () -> {
    return Thread.currentThread().getName() + " callable";
    };

    // 2. 执行
    for (int i = 0; i < 5; i++) {
    service.execute(runnable);
    System.out.println(service.submit(callable).get());
    }

    // 3.关闭连接
    service.shutdown();
    }
    }
    /*
    pool-1-thread-1 runnable
    pool-1-thread-2 callable
    pool-1-thread-3 runnable
    pool-1-thread-4 callable
    pool-1-thread-5 runnable
    pool-1-thread-6 callable
    pool-1-thread-7 runnable
    pool-1-thread-8 callable
    pool-1-thread-9 runnable
    pool-1-thread-10 callable
    */