那些年的开源项目,你跑起来了吗?
742 2023-04-03 03:40:48
在多线程系统中,彼此之间的通信协作非常重要,下面来聊聊线程间通信的几种方式。
回到顶部想像一个场景,A、B两个线程操作一个共享List对象,A对List进行add操作,B线程等待List的size=500时就打印记录日志,这要怎么处理呢?
一个办法就是,B线程while (true) { if(List.size == 500) {打印日志} },这样两个线程之间就有了通信,B线程不断通过轮训来检测 List.size == 500 这个条件。
这样可以实现我们的需求,但是也带来了问题:CPU把资源浪费了B线程的轮询操作上,因为while操作并不释放CPU资源,导致了CPU会一直在这个线程中做判断操作。
这要非常浪费CPU资源,所以就需要有一种机制来实现减少CPU的资源浪费,而且还可以实现在多个线程间通信,它就是“wait/notify”机制。
定义两个线程类:
?1234567891011121314151617181920212223public
class
MyThread1_1
extends
Thread {
private
Object lock;
public
MyThread1_1(Object lock) {
this
.lock = lock;
}
public
void
run() {
try
{
synchronized
(lock) {
System.out.println(Thread.currentThread().getName() +
"开始------wait time = "
+ System.currentTimeMillis());
lock.wait();
System.out.println(Thread.currentThread().getName() +
"开始------sleep time = "
+ System.currentTimeMillis());
Thread.sleep(
2000
);
System.out.println(Thread.currentThread().getName() +
"结束------sleep time = "
+ System.currentTimeMillis());
System.out.println(Thread.currentThread().getName() +
"结束------wait time = "
+ System.currentTimeMillis());
}
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
}
?1234567891011121314151617181920212223
public
class
MyThread1_2
extends
Thread {
private
Object lock;
public
MyThread1_2(Object lock) {
this
.lock = lock;
}
public
void
run() {
try
{
synchronized
(lock) {
System.out.println(Thread.currentThread().getName() +
"开始------notify time = "
+ System.currentTimeMillis());
lock.notify();
System.out.println(Thread.currentThread().getName() +
"开始------sleep time = "
+ System.currentTimeMillis());
Thread.sleep(
2000
);
System.out.println(Thread.currentThread().getName() +
"结束------sleep time = "
+ System.currentTimeMillis());
System.out.println(Thread.currentThread().getName() +
"结束------notify time = "
+ System.currentTimeMillis());
}
}
catch
(Exception e) {
e.printStackTrace();
}
}
}
测试方法,myThread1先执行,然后sleep 一秒后,myThread2再执行
?12345678910111213@Test
public
void
test1()
throws
InterruptedException {
Object object =
new
Object();
MyThread1_1 myThread1_1 =
new
MyThread1_1(object);
MyThread1_2 myThread1_2 =
new
MyThread1_2(object);
myThread1_1.start();
Thread.sleep(
1000
);
myThread1_2.start();
myThread1_1.join();
myThread1_2.join();
}
执行结果:
?12345678Thread-
0
开始------wait time =
1639464183921
Thread-
1
开始------notify time =
1639464184925
Thread-
1
开始------sleep time =
1639464184925
Thread-
1
结束------sleep time =
1639464186928
Thread-
1
结束------notify time =
1639464186928
Thread-
0
开始------sleep time =
1639464186928
Thread-
0
结束------sleep time =
1639464188931
Thread-
0
结束------wait time =
1639464188931
可以看到第一行和第二行 开始执行之间只间隔了1s,说明wait方法确实进入等待,
而且没有继续执行wait后面的sleep 2秒,而是执行了notify方法,说明wait方法可以使调用该方法的线程释放共享资源的锁,然后从运行状态退出,进入等待队列,直到被再次唤醒。
第二行和第五行间隔2秒钟,说明notify方法不会释放共享资源的锁。
第6行 说明notify执行完后,唤醒了刚才wait的线程,从而继续执行后面的sleep方法。
说明notify方法可以随机唤醒等待队列中等待同一共享资源的“一个”线程,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅通知“一个”线程。
另外还有notifyAll()方法可以使所有正在等待队列中等待同一共享资源的“全部”线程从等待状态退出,进入可运行状态。
此时,优先级最高的那个线程最先执行,但也有可能是随机执行,因为这要取决于JVM虚拟机的实现。
回到顶部
前面的测试方法中几乎都使用了join方法,那么这个方法到底起到什么作用呢?
在很多情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时运算,主线程往往将早于子线程结束之前结束,
所以在主线程中使用join方法的作用就是让主线程等待子线程线程对象销毁。
?123456789101112131415161718192021222324252627282930313233343536373839404142434445
/**
* Waits at most {@code millis} milliseconds for this thread to
* die. A timeout of {@code 0} means to wait forever.
*
* <p> This implementation uses a loop of {@code this.wait} calls
* conditioned on {@code this.isAlive}. As a thread terminates the
* {@code this.notifyAll} method is invoked. It is recommended that
* applications not use {@code wait}, {@code notify}, or
* {@code notifyAll} on {@code Thread} instances.
*
* @param millis
* the time to wait in milliseconds
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public
final
synchronized
void
join(
long
millis)
throws
InterruptedException {
long
base = System.currentTimeMillis();
long
now =
0
;
if
(millis <
0
) {
throw
new
IllegalArgumentException(
"timeout value is negative"
);
}
if
(millis ==
0
) {
while
(isAlive()) {
wait(
0
);
}
}
else
{
while
(isAlive()) {
long
delay = millis - now;
if
(delay <=
0
) {
break
;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
看下jdk API的源码可以看到,其实join内部使用的还是wait方法进行等待,
join(long millis)方法的一个重点是要区分出和sleep(long millis)方法的区别:
sleep(long millis)不释放锁,join(long millis)释放锁,因为join方法内部使用的是wait(),因此会释放锁。join()其实就是join(0)而已。
回到顶部
ThreadLocal不是用来解决共享对象的多线程访问问题的,而是实现每一个线程都维护自己的共享变量,起到线程隔离的作用。
关于ThreadLocal源码分析可以参考这篇文章:https://www.cnblogs.com/xrq730/p/4854813.html。
下面看个ThreadLocal的例子:
?12345public
class
Tools {
public
static
ThreadLocal<Object> tl =
new
ThreadLocal<Object>();
}
两个线程类,分别向ThreadLocal里设置值
?123456789101112131415public
class
MyThread1_1
extends
Thread {
@Override
public
void
run() {
try
{
for
(
int
i =
0
; i <
10
; i++) {
Tools.tl.set(
"ThreadA"
+ (i +
1
));
System.out.println(
"ThreadA get Value="
+ Tools.tl.get());
Thread.sleep(
200
);
}
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
}
?123456789101112131415public
class
MyThread1_2
extends
Thread {
@Override
public
void
run() {
try
{
for
(
int
i =
0
; i <
10
; i++) {
Tools.tl.set(
"ThreadB"
+ (i +
1
));
System.out.println(
"ThreadB get Value="
+ Tools.tl.get());
Thread.sleep(
200
);
}
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
}
?12345678910111213
@Test
public
void
test1() {
try
{
MyThread1_1 a =
new
MyThread1_1();
MyThread1_2 b =
new
MyThread1_2();
a.start();
b.start();
a.join();
b.join();
}
catch
(Exception e) {
e.printStackTrace();
}
}
执行结果:
?1234567891011121314151617181920ThreadB get Value=ThreadB1
ThreadA get Value=ThreadA1
ThreadA get Value=ThreadA2
ThreadB get Value=ThreadB2
ThreadA get Value=ThreadA3
ThreadB get Value=ThreadB3
ThreadA get Value=ThreadA4
ThreadB get Value=ThreadB4
ThreadB get Value=ThreadB5
ThreadA get Value=ThreadA5
ThreadB get Value=ThreadB6
ThreadA get Value=ThreadA6
ThreadB get Value=ThreadB7
ThreadA get Value=ThreadA7
ThreadB get Value=ThreadB8
ThreadA get Value=ThreadA8
ThreadA get Value=ThreadA9
ThreadB get Value=ThreadB9
ThreadB get Value=ThreadB10
ThreadA get Value=ThreadA10
可以看到两个线程取出的值没有重复也没有互相影响,其实它内部变化的只是线程本身的 ThreadLocalMap。
感兴趣的还可以去看看 InheritableThreadLocal,它可以在子线程中取得父线程继承下来的值。
回到顶部
1:《Java并发编程的艺术》
2:《Java多线程编程核心技术》