Java并发编程系列文章《一》多线程基础——Java线程与进程的基本概念《二》多线程基础——Java线程入门类和接口《三》多线程基础——Java线程组和线程优先级《四》多线程基础——Java线程生命周期及转换《五》多线程基础——Java线程间的通信(互斥与协作)《六》实际应用——如何优雅的关闭线程《七》实际应用——生产者与消费者模型
并发编程(多线程)一直以来都是程序员头疼的难题。曾经听别人总结过并发编程的第一原则,那就是不要写并发程序,哈哈哈。后来发现,这样能够显著提高程序响应和吞吐量的利器,哪还能忍得住不会用呢?
整理出《Java并发编程系列文章》,共计7篇,与君共勉。
《五》多线程基础——Java线程间的通信(互斥与协作)
- 1、锁与同步
- 2、等待和通知
- 3、信号量
- 4、管道
- 5、其他互斥与协作方法
- 5.1、sleep()
- 5.2、join()
- 5.3、interrupt()
- 5.4、ThreadLocal类
1、锁与同步
在Java中,锁的概念都是基于对象的,所以我们⼜经常称它为对象锁。线程同步听着很高大上,cao,被吓了一跳,其实就是让线程之间按照⼀定的顺序执⾏。怎么实现呢?我们可以使⽤锁来实现它。
public class ObjectLock {
private static Object lock = new Object();
static class ThreadA implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 100; i++) {
System.out.println("Thread A " + i);
}
}
}
}
static class ThreadB implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 100; i++) {
System.out.println("Thread B " + i);
}
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
Thread.sleep(10);
new Thread(new ThreadB()).start();
}
}
这⾥声明了⼀个名字为 lock 的对象锁。我们在 ThreadA 和ThreadB 内需要同步的代码块⾥,都是⽤ synchronized 关键字加上了同⼀个对象锁 lock 。
根据线程和锁的关系,同⼀时间只有⼀个线程持有⼀个锁,那么线程B就会等线程A执⾏完成后释放 lock ,线程B才能获得锁 lock 。
这⾥在主线程⾥使⽤sleep⽅法睡眠了10毫秒,是为了防⽌线程B先得到锁。因为如果同时start,线程A和线程B都是出于就绪状态,操作系统可能会先让B运⾏。这样就会先输出B的内容,然后B执⾏完成之后⾃动释放锁,线程A再执⾏。
更多内容,请查看《八》线程安全·数据共享——Synchronized与锁
2、等待和通知
上⾯⼀种基于“锁”的方式,获取CPU
时间片的线程需要不断地去尝试获得锁,如果失败了,当再次获取CPU
时间片后继续尝试。这可能会耗费服务器资源。⽽等待/通知机制
是另⼀种⽅式。
Java多线程的等待/通知机制
是基于 Object 类的 wait()
和 notify()
,notifyAll()
来实现的。notify()
会随机叫醒⼀个正在等待的线程,⽽notifyAll()
会叫醒所有正在等待的线程。
例如:线程A持有了⼀个锁 lock 并开始执⾏,它可以使⽤lock.wait()
让⾃⼰进⼊等待状态。这个时候, lock 这个锁是被释放了的。线程B获得了 lock 这个锁并开始执⾏,它可以在某⼀时刻,使⽤ lock.notify()
,叫醒之前释放 lock 锁并进⼊等待状态的线程A,注意,此时锁依然在线程B手中,只有当线程B执行完了,线程A才能竞争lock 锁。
public class WaitAndNotify {
private static Object lock = new Object();
static class ThreadA implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
try {
System.out.println("ThreadA: " + i);
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
}
}
static class ThreadB implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
try {
System.out.println("ThreadB: " + i);
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
Thread.sleep(1000);
new Thread(new ThreadB()).start();
}
}
// 输出:
ThreadA: 0
ThreadB: 0
ThreadA: 1
ThreadB: 1
ThreadA: 2
ThreadB: 2
ThreadA: 3
ThreadB: 3
ThreadA: 4
ThreadB: 4
在这个Demo⾥,线程A和线程B⾸先打印出⾃⼰需要的东⻄,然后使⽤ notify() ⽅法叫醒另⼀个正在等待的线程,然后自己使⽤ wait() ⽅法陷⼊等待并释放 lock 锁。需要注意的是等待/通知机制使⽤的是使⽤同⼀个对象锁,如果你两个线程使 ⽤的是不同的对象锁,那它们之间是不能⽤等待/通知机制通信的。
多线程的休息室WaitSet的详细介绍:
(1)所有对象都可以作为锁,也都有WaitSet这么一个概念,用来存放调用该对象的wait()方法之后进入阻塞状态的线程;
(2)WaitSet中的线程被notify()之后,不会立即执行,因为执行notify()的线程正在持有对象锁。
(3)线程从WaitSet中被唤醒的顺序,不一定是进入的顺序。
(4)线程被唤醒后,必须重新获取锁。
3、信号量
等待后续补充
4、管道
等待后续补充
5、其他互斥与协作方法
5.1、sleep()
- sleep是Thread的方法,wait是Object的方法。
- wait释放cpu资源,同时释放锁;sleep释放cpu资源,但是不释放锁,所以易死锁。
- wait可以指定时间,也可以不指定;⽽sleep必须指定时间。
- wait必须放在同步块或同步⽅法中,⽽sleep可以再任意位置。
5.2、join()
- 执行join语句的当前线程,会等被join的线程执行完,再执行。
- 例如:start httpServer,启动的线程会随着主线程结束后自动挂掉,所以通过。
5.3、interrupt()
- 线程自己不能interrupt本身,但可以中断别的线程。被调用interrupt方法进行中断的线程如果正处于wait,sleep,join状态,那么会报InterruptedException。
- 特别注意interrupt正处于join的线程时,interrupt的不是被调用join的线程,而是调用join的线程。
- interrupt和interrupted的区别:interrupt()用于继承Thread的线程,Thread.interrupted()用于实现Runnable的线程
5.4、ThreadLocal类
- 如果开发者希望将类的某个静态变量(user ID或者transaction ID)与线程状态关联,则可以考虑使⽤ThreadLocal。
- 最常⻅的ThreadLocal使⽤场景为⽤来解决数据库连接、Session管理等。数据库连接和Session管理涉及多个复杂对象的初始化和关闭。
- 如果在每个线程中声明⼀些私有变量来进⾏操作,那这个线程就变得不那么“轻量”了,需要频繁的创建和关闭连接。