闭关修炼(三)多线程之间的通讯


多线程之间通讯

什么是多线程之间的通讯?

目的是避免多个线程在操作同一个资源(共享资源)线程安全问题,其中多个线程操作的动作存在不同。

生产者与消费者例子

生产者线程对共享资源做写操作,消费者线程读共享资源,生产者表示发布资源,消费者表示读取资源,他们都同时对共享资源进行操作,但是操作的动作不同。

共享资源线程不安全演示代码:

package duoxiancheng;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

/**
 * 共享资源实体类
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
class Res {

    private String userName;
    private String sex;
}


/**
 * 生成者线程
 */
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
@AllArgsConstructor
class Producer extends Thread {
    private Res res;

    public void run() {
        int count = 0;
        while (true) {
            if (count == 0) {
                res.setUserName("小红");
                res.setSex("女");
            } else {
                res.setUserName("小明");
                res.setSex("男");
            }
            // 每次循环count依次取:0 1 0 1
            count = (count + 1) % 2;
        }
    }
}

/**
 * 消费者
 */
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
class Consumer extends Thread {
    private Res res;

    @Override
    public void run() {
        while (true){
            System.out.println(res.toString());
        }
    }
}

public class ShengchanXiaofei {
    public static void main(String[] args) {
        // 共享资源res
        Res res = new Res();
        Producer producer = new Producer(res);
        Consumer consumer = new Consumer(res);
        producer.start();
        consumer.start();
    }
}

这段代码会产生线程安全问题,小红性别出现了男,出现的这样结果是因为消费者线程读共享资源的时候,读到一半刚好被暂停了,生产者线程又修改了资源,就导致了数据出错的问题。
在这里插入图片描述

如何解决生成者和消费者问题?

生产者线程和消费者线程之间要进行同步!
是两个线程都要进行同步,这样才能保证两个线程之间的数据的原子性问题。

如何添加同步?

用synchronized代码块,并且两个线程使用同一把锁,故不能使用this锁,因为这个两个线程是不同类。 他们共享Res类资源,故可以用res对象作为锁。

/**
 * 生成者线程
 */
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
@AllArgsConstructor
class Producer extends Thread {
    private Res res;

    public void run() {
        int count = 0;
        while (true) {
            synchronized (res){
                if (count == 0) {
                    res.setUserName("小红");
                    res.setSex("女");
                } else {
                    res.setUserName("小明");
                    res.setSex("男");
                }
                // 每次循环count依次取:0 1 0 1
                count = (count + 1) % 2;
            }

        }
    }
}

/**
 * 消费者
 */
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
class Consumer extends Thread {
    private Res res;

    @Override
    public void run() {
        while (true){
            synchronized (res){
                System.out.println(res.toString());
            }
        }
    }
}

但是这个代码还是有缺陷,和实际的生成应用还是有差别,消费者拿数据应该是一男一女交替,而不是一片连在一起。

wait和notify

对上面提到的问题的解决办法?

写完一个,读一个,保证这样的同步。生产者没有任何生产,消费者不能读。消费者没有消费完生产者不能生产。某程度上等于没有缓冲区的生产情况。

使用wait和notify方法可以解决。

wait和notify的作用?

wait让当前线程从运行状态变为休眠状态,让出cpu执行权,释放当前锁的资源,把资源给阻塞中的线程。

notify让当前线程从休眠状态变为运行状态

wait和notify的使用条件?

  1. 要在同步中才能使用wait和notify,并且是锁对象(共享资源)调用wait和notify。
  2. 使用同一把锁,即都在synchronized代码块中,使用锁对象相同。
  3. wait和notify是成对出现的

使用代码例子

package duoxiancheng;

import lombok.*;

/**
 * 共享资源实体类
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
class Res {

    private String userName;
    private String sex;
    // false时生产者运行,true时消费者运行
    private Boolean flag = false;
}


/**
 * 生成者线程
 */
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
@AllArgsConstructor
class Producer extends Thread {
    private Res res;

    @SneakyThrows
    public void run() {
        int count = 0;
        while (true) {
            synchronized (res) {
                if (res.getFlag()) {
                    // flag为true时变成休眠状态,并且释放锁的资源
                    res.wait();
                }
                if (count == 0) {
                    res.setUserName("小红");
                    res.setSex("女");
                } else {
                    res.setUserName("小明");
                    res.setSex("男");
                }
                // 每次循环count依次取:0 1 0 1
                count = (count + 1) % 2;
                // 写操作完成,更改flag值,再进行生产者线程时将变为休眠状态
                res.setFlag(true);
                // 唤醒当前被wait的线程
                res.notify();
            }

        }
    }
}

/**
 * 消费者
 */
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
class Consumer extends Thread {
    private Res res;

    @SneakyThrows
    @Override
    public void run() {
        while (true) {
            synchronized (res) {
                // flag为true时消费者运行,如果flag为false时线程应休眠,释放对象锁。
                if(!res.getFlag()){
                    res.wait();
                }
                System.out.println(res.toString());
                // 消费者执行完毕,更改flag值
                res.setFlag(false);
                // 唤醒当前被wait的线程
                res.notify();
            }
        }
    }
}

public class ShengchanXiaofei {
    public static void main(String[] args) {
        // 共享资源res
        Res res = new Res();
        Producer producer = new Producer(res);
        Consumer consumer = new Consumer(res);
        producer.start();
        consumer.start();
    }
}

两点重要的点
一是wait让线程让出cpu执行权,释放当前锁
二是notify唤醒使用wait的线程,即通知正在等待控制权的线程可以继续运行

notifyAll

唤醒所有等待

wait和sleep异同点在哪里?

wait可以释放锁的资源
sleep不会释放锁的资源

都是做休眠

wait需要notify才能从休眠变为运行状态
sleep只要时间到了就变为运行状态

小试牛刀题

Object类中有哪些方法?
wait,notify,notifyAll,equals,hashCode,getClass,clone,toString,finalize

synchronized代码块什么时候开始上锁?什么时候开始释放锁?
代码开始和代码结束

synchronized有什么缺点?
效率低,扩展性不高,不能自定义

你了解哪些java并发包?
Atomic中的原子类如AtomicInteger,AtomicIntegerArray,AtomicReference等,Lock锁

多线程并发和网页并发的区别?
多线程并发是多个Thread线程同时操作同一个资源,网页并发是多个请求同时访问一台服务器。

JDK1.5-Lock

什么是Lock接口?

用来实现锁的功能,Lock接口提供了与synchronized关键字类似的同步功能,但需要在使用时手动获取锁和释放锁。

Lock锁写法

lock锁一般定义在共享资源中,多个线程使用同一个lock对象

Lock lock = new ReentrantLock();
lock.lock();
try{
    
}
finally {
	lock.unlock();
}

演示代码

wait和notify不能在这里使用,只能在synchronized代码块中使用!

package duoxiancheng;

import com.sun.org.apache.xml.internal.security.signature.reference.ReferenceNodeSetData;
import lombok.*;

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

/**
 * 共享资源实体类
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
class Res {

    private String userName;
    private String sex;
    // false时生产者运行,true时消费者运行
    private Boolean flag = false;
    private Lock lock = new ReentrantLock();
}


/**
 * 生成者线程
 */
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
@AllArgsConstructor
class Producer2 extends Thread {
    private Res res;

    @SneakyThrows
    public void run() {
        int count = 0;
        while (true) {

            try {
                // 开始上锁
                res.getLock().lock();
                if (count == 0) {
                    res.setUserName("小红");
                    res.setSex("女");
                } else {
                    res.setUserName("小明");
                    res.setSex("男");
                }
                // 每次循环count依次取:0 1 0 1
                count = (count + 1) % 2;
                res.setFlag(true);
            } catch (Exception e) {
                e.printStackTrace();

            } finally {
                // 释放锁
                res.getLock().unlock();
            }

        }

    }
}


/**
 * 消费者
 */
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
class Consumer2 extends Thread {
    private Res res;

    @SneakyThrows
    @Override
    public void run() {
        while (true) {
            try {
                res.getLock().lock();
                System.out.println(res.toString());
                res.setFlag(false);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 释放锁
                res.getLock().unlock();
            }

        }
    }
}

public class LockDemo {
    public static void main(String[] args) {

        // 共享资源res
        Res res = new Res();
        Producer2 producer = new Producer2(res);
        Consumer2 consumer = new Consumer2(res);
        producer.start();
        consumer.start();
    }
}

什么是ReentrantLock锁?

日后其他博客里再写

Condition类解决wait和notify无法使用问题

Condition一般和Lock锁一起使用。

Condition的功能类似于传统技术中的Object.wait和notify。

在线程中实例化Condition

Condition condition = res.getLock().newCondition();

使用await和signal方法等待和唤醒

示例代码:

package duoxiancheng;

import lombok.*;

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

@Data
@AllArgsConstructor
@NoArgsConstructor
class Res {
    public String userName;
    public String sex;
    public Boolean flag = false;
    Lock lock = new ReentrantLock();
}

class InputThread extends Thread {
    private Res res;
    Condition newCondition;

    public InputThread(Res res, Condition newCondition) {
        this.res = res;
        this.newCondition = newCondition;
    }

    @Override
    public void run() {
        int count = 0;
        while (true) {
            try {
                res.lock.lock();
                if (res.flag) {
                    try {
                        newCondition.await();
                    } catch (Exception e) {
                        // TODO: handle exception
                    }
                }
                if (count == 0) {
                    res.userName = "小明";
                    res.sex = "男";
                } else {
                    res.userName = "小红";
                    res.sex = "女";
                }
                count = (count + 1) % 2;
                res.flag = true;
                newCondition.signal();
            } catch (Exception e) {
                // TODO: handle exception
            } finally {
                res.lock.unlock();
            }
        }

    }
}

class OutThrad extends Thread {
    private Res res;
    private Condition newCondition;

    public OutThrad(Res res, Condition newCondition) {
        this.res = res;
        this.newCondition = newCondition;
    }

    @Override
    public void run() {
        while (true) {
            try {
                res.lock.lock();
                if (!res.flag) {
                    try {
                        newCondition.await();
                    } catch (Exception e) {
                        // TODO: handle exception
                    }
                }
                System.out.println(res.userName + "," + res.sex);
                res.flag = false;
                newCondition.signal();
            } catch (Exception e) {
                // TODO: handle exception
            } finally {
                res.lock.unlock();
            }

        }

    }
}

public class LockDemo {

    public static void main(String[] args) {
        Res res = new Res();
        Condition newCondition = res.lock.newCondition();
        InputThread inputThread = new InputThread(res, newCondition);
        OutThrad outThrad = new OutThrad(res, newCondition);
        inputThread.start();
        outThrad.start();
    }

}

小试牛刀题

说说lock锁和synchronized(同步)的区别?

  1. Lock接口可以尝试非阻塞地获取锁,当前线程尝试获取锁,如果这一时刻如果没有被其他线程获取到,则成功获取并持有锁。
  2. Lock锁能被中断地获取锁,synchronized是代码块执行完毕或者报异常才释放锁
  3. synchronized自动上锁和释放锁,而lock锁需要自己上锁和释放锁,lock锁灵活性更强。
  4. lock锁运用比synchronized要多

如何停止线程

用stop方法为什么不好?

程序执行到一半的时候,直接把程序stop,程序中断,后面一半程序没有执行完,先前执行的代码也不会回滚,即程序不会恢复,不安全。

说说设计停止线程的思路?

根据stop的缺点进行优化,代码执行完毕再停止。无非让循环停止。

代码设计:

package ch3;


import lombok.SneakyThrows;

class MyThread extends Thread {
    private volatile Boolean flag = true;

    @Override
    public void run() {
        System.out.println("start loop");
        while (flag) {
        }
        System.out.println("stop loop");
    }

    public void stopThread() {
        this.flag = false;
    }
}

public class StopDemo {
    @SneakyThrows
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();

        for (int i = 0; i < 2; i++) {
            System.out.println("main thread, waiting");
            Thread.sleep(1000);
        }
        myThread.stopThread();
        for (int i = 0; i < 5; i++) {
            System.out.println("main thread, waiting");
            Thread.sleep(1000);

        }

    }
}

但是这样的设计还有什么问题呢?如果while循环中存在wait等待,则不会到while判断为false退出循环,直到线程被notify唤醒,然而这样不是我们希望的,因此正确的用法是使用interrupt方法,interrupt方法会抛出异常,在异常处理中处理停止线程。

package ch3;


import lombok.SneakyThrows;

class MyThread extends Thread {
    private volatile Boolean flag = true;

    @Override
    public synchronized void run() {
        try{
            System.out.println("start loop");
            while (flag) {
                wait();
            }
        }catch (Exception e){
            stopThread();
            System.out.println("stop loop");
        }

    }

    public void stopThread() {
        this.flag = false;
    }
}

public class StopDemo {
    @SneakyThrows
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();

        for (int i = 0; i < 2; i++) {
            System.out.println("main thread, waiting");
            Thread.sleep(1000);
        }
        myThread.interrupt();
        for (int i = 0; i < 5; i++) {
            System.out.println("main thread, waiting");
            Thread.sleep(1000);

        }

    }
}

ThreadLocal类

谈谈什么是ThreadLocal?

是本地线程, 为每一个线程提供一个局部变量。

线程t1、t2共享主内存中的数据,使用ThreadLoacl后,在本地线程提供局部变量,将共享的数据变成本地的变量。

ThreadLocal例子

package ch3;

import lombok.AllArgsConstructor;

class ResNumber {
    public int count = 0;
    public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            // threadLocal起始值赋值为0
            return 0;
        }
    };

    public String getNumber() {
        count = threadLocal.get() + 1;
        threadLocal.set(count);
        return String.valueOf(count);
    }
}

@AllArgsConstructor
class LocalThread extends Thread {
    public ResNumber resNumber;

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + ":" + resNumber.getNumber());
        }
    }
}

public class ThreadLoca {

    public static void main(String[] args) {
        ResNumber resNumber = new ResNumber();
        LocalThread localThread1 = new LocalThread(resNumber);
        LocalThread localThread2 = new LocalThread(resNumber);
        LocalThread localThread3 = new LocalThread(resNumber);
        localThread1.start();
        localThread2.start();
        localThread3.start();

    }
}

ThreadLocal的原理?

Map<Thread,Object>集合,Thread为当前线程,Object为要local的数据

底层代码

set方法:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

get方法:

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值