线程间通信的模型有两种:

  • 共享内存
  • 消息传递


通信模型模拟场景

两个线程,一个线程对当前数值加 1,另一个线程对当前数值减1,要求用线程间通信

synchronized方案

/**
 * synchronized方案
 */
public class CommunicatBySync {
    //加减对象 volatile关键字实现线程交替加减
    private volatile  int number = 0;
    /**
     * 加 1
     */
    public synchronized void increment() {
        try {
            while (number != 0) {
                this.wait();
            }
            number++;
            System.out.println("--------" + Thread.currentThread().getName() + "加一成功----------, 值为:" + number);
            notifyAll();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 减一
     */
    public synchronized void decrement() {
        try {
            while (number == 0) {
                this.wait();
            }
            number--;
            System.out.println("--------" + Thread.currentThread().getName() + "减一成功----------, 值为:" + number);
            notifyAll();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Lock方案

/**
 * Lock方案
 */
public class CommunicatByLock {
     //加减对象
    private int number = 0;
    //声明锁
    private Lock lock = new ReentrantLock();
    //声明钥匙
    private Condition condition = lock.newCondition();

    /**
     * 加 1
     */
    public void increment() {
        lock.lock();
        try {
            while (number != 0) {
                condition.await();
            }

            number++;
            System.out.println("--------" + Thread.currentThread().getName() + "加一成功----------, 值为:" + number);
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 减一
     */
    public void decrement() {
        lock.lock();
        try {
            while (number == 0) {
                condition.await();
            }
            number--;
            System.out.println("--------" + Thread.currentThread().getName() + "减一成功----------, 值为:" + number);
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
/**
 * 测试
 */
public class CommunicatTest {
    /**
     * 交替加减
     */
    public static void main(String[] args) {
//        CommunicatBySync test = new CommunicatBySync();
        CommunicatByLock test = new CommunicatByLock();
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                test.increment();
            }
        }, "线程A").start();

        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                test.decrement();
            }
        }, "线程B").start();
    }
}



//返回结果
--------线程A加一成功----------, 值为:1
--------线程B减一成功----------, 值为:0
--------线程A加一成功----------, 值为:1
--------线程B减一成功----------, 值为:0
--------线程A加一成功----------, 值为:1
--------线程B减一成功----------, 值为:0
--------线程A加一成功----------, 值为:1
--------线程B减一成功----------, 值为:0
--------线程A加一成功----------, 值为:1
--------线程B减一成功----------, 值为:0

wait()、notify、notifyAll()方法

wait()、notify()、notifyAll()是三个定义在Object类里的方法,可以用来控制线程的状态。这三个方法最终调用的都是jvm级的native方法。随着jvm运行平台的不同可能有些许差异。

对象调用wait方法(等待):使持有该对象的线程把该对象的控制权交出去,然后处于等待状态。对象调用notify方法(运行):通知某个正在等待这个对象控制权的线程可以继续运行。对象调用notifyAll方法(全部运行):通知所有等待这个对象控制权的线程继续运行。注意:一定要在线程同步中使用,并且是同一个锁的资源

wait与sleep区别

sleep()方法属于Thread类中的静态方法。sleep()方法使程序暂停,在执行指定的时间中,让出cpu给其他线程,但依然保持监控状态,当指定的时间到了,便自动恢复运行状态。在调用sleep()方法的过程中线程不会释放对象锁

wait()方法属于Object类中的方法。当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的锁定池,只有针对此对象调用notify()方法后,该线程才进入对象锁定池准备获取对象锁进入运行状态。

它两均会被interrupted方法中断。


Lock

在 JDK1.5 之后,并发包(java.util.concurrent,简称JUC)中新增了 Lock 接口。Lock锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。Lock提供了比synchronized更多的功能。


Lock 与 synchronized 关键字的区别

  1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
  2. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
  3. Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
  4. 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
  5. Lock可以提高多个线程进行读操作的效率。

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。

Lock接口

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}
lock()方法

lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:

Lock lock = ...;
lock.lock();
try{
	//处理任务
}catch(Exception e){
    
}finally{
	lock.unlock(); //释放锁
}
Condition

关键字synchronized与wait()/notify()这两个方法一起使用可以实现等待/通知模式, Lock锁的newContition()方法返回Condition对象,Condition类也可以实现等待/通知模式。用notify()通知时,JVM会随机唤醒某个等待的线程, 使用Condition类可以进行选择性通知

Condition比较常用的两个方法:

  • await()会使当前线程等待,同时会释放锁,
  • signal()时,线程会重新获得锁并继续执行。用于唤醒一个等待的线程。

注意:在调用Condition的await()/signal()方法前,也需要线程持有相关的Lock锁,调用await()后线程会释放这个锁,在singal()调用后会从当前Condition对象的等待队列中,唤醒 一个线程,唤醒的线程尝试获得锁, 一旦获得锁成功就继续执行。

public class LockConditionTest {

    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        lock.lock();
        try {
            Condition condition = lock.newCondition();
            //先持有锁
            condition.await();//线程等待
            //释放锁
            condition.signal();//线程唤醒
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

ReentrantLock

ReentrantLock,意思是“可重入锁”,是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。

ReadWriteLock

ReadWriteLock也是一个接口,在它里面只定义了两个方法:

public interface ReadWriteLock {
    /**
    Returns the lock used for reading.
    @return the lock used for reading.
    */
    Lock readLock();

    /**
    Returns the lock used for writing.
    @return the lock used for writing.
    */
    Lock writeLock();
}

一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成 2 个锁来分配给线程,从而使得多个线程可以同时进行读操作。ReentrantReadWriteLock 实现了ReadWriteLock接口。ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁。

涉及到写的操作都会阻塞等待:无论是写在前还是在后,读读则不会

  • 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
  • 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

停止线程

停止线程思路

  1. 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
  2. 使用stop方法强行终止线程(这个方法不推荐使用,因为stop和suspend、resume一样,也可能发生不可预料的结果)。
  3. 使用interrupt方法中断线程。