Java多线程 synchronized与ReentrantLock用法

使用 synchronized修饰,表示该方法是加锁的方法。使用相同this锁的方法,在任意时刻只有一个方法会被执行,在多线程中是竞争关系。除此之外多线程还存在依赖关系。例如,一个线程须等待另一个线程返回结果后,才能继续执行。Java中提供了相应的机制。

1、synchronized、wait、notify

考虑一个实际的流水线作业场景,一个线程负责生产产品,另一个线程负责在流水线上装配。由于生产时间不确定,为了不错过传送带上的产品,装配线程需要不断检查当前传送带上有无产品。为了简化逻辑,这里只有一个线程负责生产,一个线程负责装配。

import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;

public class ThreadDispatch {
    public static void main(String[] args) {
        var pack = new PackQueue();
        // 负责每隔1秒装入一次数据
        Thread t1 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    pack.addPack(Integer.toString(i));
                }
            }
        };

        // 每隔0.6秒钟取出数据
        Thread t2 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    try {
                        Thread.sleep(600);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    String s = pack.getPack();
                    System.out.println(s);
                }
            }
        };
        t1.start();
        t2.start();
    }
}

class PackQueue {
    private Queue<String> q = new LinkedList<>();

    public void addPack(String s) {
        this.q.add(s);
    }

    public String getPack() {
        if (this.q.isEmpty()) {
            return "empty";
        }
        return this.q.remove();
    }

}
//empty
//0
//empty
//1
//2
//empty
//3
//empty
//4
//5
//empty
//6
//empty
//7
//8
//empty
//9
//empty
//empty
//empty

以上逻辑是使用循环实现的。为了不错过传送带上的商品,装配线程t2需不断定时检查。这对设置检查的频率提出了要求。频率稍慢会错过产品,频率过快会浪费性能。最好的方式是,线程1生产好产品后,通知线程2装配,这样解决了“来不及”和“速度过快”的问题。

public class ThreadDispatch {
    public static void main(String[] args) {
        var pack = new PackQueue();
        // 负责每隔1秒装入一次数据
        Thread t1 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    pack.addPack(Integer.toString(i));
                }
            }
        };

        // 检查有无数据,没有就等待
        Thread t2 = new Thread() {
            @Override
            public void run() {
                try {
                    while(true) {
                        String s = pack.getPack();
                        System.out.println(s);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
            }
        };
        t1.start();
        t2.start();
    }
}

class PackQueue {
    private Queue<String> q = new LinkedList<>();

    public synchronized void addPack(String s) {
        this.q.add(s);
        this.notify();
    }

    public synchronized String getPack() throws InterruptedException {
        if (this.q.isEmpty()) {
        // return "empty";
            this.wait();
        }
        return this.q.remove();
    }
}
// 0
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9

优化之后,装配线程t2 不再“无节制”检查工作区,而是等待t1线程通知后工作。需要注意的是,这里只有一个装配线程t2,如果有多个线程,需要调用this.notifyAll取代this.notify,表示唤醒所有正在等待this锁的线程。唤醒多个线程,最终也只会有一个线程获取this锁,其余线程继续等待。另外,t2 线程在 t1 线程停止生产后永远也醒不过来了。考虑为 t2 线程指定一个wait超时时间,超时后会自动醒来。

2、 ReentrantLock、Condition

java5中引入了高级的处理并发的java.util.concurrent包,相比较synchronized机制,提供了尝试获取锁、超时等待等更多功能;不同于synchronized 在Java语言层面实现自动释放锁而不必考虑异常,ReentrantLock由Java代码实现,因此需要正确捕获异常和释放锁。使用 ReentrantLock 重写示例:

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

class PackQueue {
    private Queue<String> q = new LinkedList<>();
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();

    public void addPack(String s) {
        lock.lock();
        try {
            this.q.add(s);
            condition.signalAll();
        } finally {
            lock.unlock();
        }

    }

    public String getPack() throws InterruptedException {
        lock.lock();
        try {
            if (this.q.isEmpty()) {
                condition.await(2, TimeUnit.SECONDS);
            }
            return this.q.remove();
        } finally {
            lock.unlock();
        }

    }
}

值得注意的是,判断队列为空后,调用 condition.await(2, TimeUnit.SECONDS); 表示线程会自动超时醒来。由于此时队列为空,调用remove方法会报错;不过线程可以自动唤醒了。总结一下:

 

 
synchronized
ReentrantLock
加锁 通常使用 synchronized 修饰方法,表示使用this实例加锁
private final Lock lock = new ReentrantLock();
lock.lock()
释放锁 自动释放
lock.unlock();
线程等待
this.wait();
private final Condition condition = lock.newCondition();
condition.await();
线程唤醒
this.notify();
this.notifyAll();
condition. signal();
condition.signalAll();
锁类型 可重入锁 可重入锁
尝试获取锁 不支持
lock.tryLock(1, TimeUnit.SECONDS)

超时自动唤醒

不支持
condition.await(2, TimeUnit.SECONDS);

 

 
posted @ 2021-11-15 14:24  恩恩先生  阅读(79)  评论(0编辑  收藏  举报