Java程序员:如果把线程当作一个人来对待,所有问题都简单易懂了

多线程的问题都曾经困扰过每个开发人员,今天将从全新视角来解说,希望读者都能明白。

问题究竟出在哪里?

事情起因:线程可以独立自主的运行,可以认为它具有主观能动性。

造成结果:对它的掌控能力变弱了,而且又不能直接把它“干掉”。

解决方案:凡事商量着来,互相配合着把事情完成。

作者观点:其实就是把线程当作人来对待。

场景一,停止

 “大胖,大胖,12点了,该去吃饭了,别写了”

 “好的,好的,稍等片刻,把这几行代码写完就走”

要点:把停止的信号传达给别人,别人处理完手头的事情就自己主动停止了。

static void stopByFlag() {

 ARunnable ar = new ARunnable();

new Thread(ar).start();

 ar.tellToStop();

}

 static class ARunnable implements Runnable {

 volatile boolean stop;

 void tellToStop() {

 stop = true;

}

 @Override

 public void run() {

println("进入不可停止区域 1。。。");

doingLongTime(5);

 println("退出不可停止区域 1。。。");

println("检测标志stop = %s", String.valueOf(stop));

if (stop) {

println("停止执行");

return;

}

 println("进入不可停止区域 2。。。");

doingLongTime(5);

 println("退出不可停止区域 2。。。");

 }

 }

 解说:线程在预设的地点检测flag,来决定是否停止。

场景二,暂停/恢复

“大胖,大胖,先别发请求了,对方服务器快挂了”

 “好的,好的,等这个执行完就不发了”

 过了一会

 “大胖,大胖,可以重新发请求了”

 “好的,好的”

要点:把暂停的信号传达给别人,别人处理完手头的事情就自己主动暂停了。但是恢复是无法自主进行的,只能由操作系统来恢复线程的执行。

 static void pauseByFlag() {

BRunnable br = new BRunnable();

 new Thread(br).start();

br.tellToPause();

 sleep(8);

br.tellToResume();

 }

 static class BRunnable implements Runnable {

volatile boolean pause;

void tellToPause() {

pause = true;

 }

void tellToResume() {

synchronized (this) {

 this.notify();

 }

 }

@Override public void run() {

 println("进入不可暂停区域 1。。。");

doingLongTime(5);

 println("退出不可暂停区域 1。。。");

 println("检测标志pause = %s", String.valueOf(pause));

if (pause) {

 println("暂停执行");

try { synchronized (this) {

 this.wait();

}

} catch (InterruptedException e) {

e.printStackTrace();

}

println("恢复执行");

}

 println("进入不可暂停区域 2。。。");

doingLongTime(5);

 println("退出不可暂停区域 2。。。");

}

 }

解说:还是在预设的地点检测flag。然后就是wait/notify配合使用。

场景三,插队

“大胖,大胖,让我站到你前面,不想排队了”

“好吧”

要点:别人插队到你前面,必须等他完事后才轮到你。

static void jqByJoin() {

CRunnable cr = new CRunnable();

Thread t = new Thread(cr);

t.start();

sleep(1);

 try {

 t.join();

 } catch (InterruptedException e) {

 e.printStackTrace();

 } println("终于轮到我了");

}

 static class CRunnable implements Runnable {

@Override

public void run() {

 println("进入不可暂停区域 1。。。");

 doingLongTime(5);

 println("退出不可暂停区域 1。。。");

 }

}

解说:join方法可以让某个线程插到自己前面,等它执行完,自己才会继续执行。

场景四,叫醒

“大胖,大胖,醒醒,醒醒,看谁来了”

“谁啊,我去”

要点:要把别人从睡梦中叫醒,一定要采取稍微暴力一点的手段。

static void stopByInterrupt() {

DRunnable dr = new DRunnable();

Thread t = new Thread(dr);

 t.start();

sleep(2);

 t.interrupt();

 }

static class DRunnable implements Runnable {

 @Override

public void run() {

println("进入暂停。。。");

 try {

 sleep2(5);

 } catch (InterruptedException e) {

println("收到中断异常。。。");

 println("做一些相关处理。。。");

}

 println("继续执行或选择退出。。。");

 }

 }

解说:线程在sleep或wait时,是处于无法交互的状态的,此时只能使用interrupt方法中断它,线程会被激活并收到中断异常。

常见的协作配合

上面那些场景,其实都是对一个线程的操作,下面来看多线程间的一些配合。

事件一,考试

假设今天考试,20个学生,1个监考老师。规定学生可以提前交卷,即把卷子留下,直接走人就行了。但老师必须等到所有的学生都走后,才可以收卷子,然后装订打包。如果把学生和老师都看作线程,就是1个线程和20个线程的配合问题,即等20个线程都结束了,这1个线程才开始。

比如20个线程分别在计算数据,等它们都结束后得到20个中间结果,最后这1个线程再进行后续汇总、处理等。

 static final int COUNT = 20;

 static CountDownLatch cdl = new CountDownLatch(COUNT);

public static void main(String[] args) throws Exception {

 new Thread(new Teacher(cdl)).start();

sleep(1);

for (int i = 0; i < COUNT; i++) {

new Thread(new Student(i, cdl)).start();

 }

synchronized (ThreadCo1.class) {

ThreadCo1.class.wait();

}

}

 static class Teacher implements Runnable {

CountDownLatch cdl;

Teacher(CountDownLatch cdl) {

this.cdl = cdl;

}

@Override

public void run() { println("老师发卷子。。。");

 try {

cdl.await();

 } catch (InterruptedException e) {

e.printStackTrace();

}

 println("老师收卷子。。。");

}

 }

 static class Student implements Runnable {

CountDownLatch cdl;

 int num;

Student(int num, CountDownLatch cdl) {

 this.num = num;

 this.cdl = cdl;

}

 @Override

 public void run() {

 println("学生(%d)写卷子。。。", num);

doingLongTime();

 println("学生(%d)交卷子。。。", num);

 cdl.countDown();

 }

}

解说:每完成一个线程,计数器减1,当减到0时,被阻塞的线程自动执行。

事件二,劳动

大胖和小白去了创业公司,公司为了节约开支,没有请专门的保洁人员。让员工自己扫地和擦桌。

大胖觉得擦桌轻松,就让小白去扫地。可小白觉得扫地太累,也想擦桌。

为了公平起见,于是决定,每人先干一半,然后交换工具,再接着干对方剩下的那一个半。

static Exchangerex = new Exchanger<>();

 public static void main(String[] args) throws Exception {

new Thread(new Staff("大胖", new Tool("笤帚", "扫地"), ex)).start();

new Thread(new Staff("小白", new Tool("抹布", "擦桌"), ex)).start();

 synchronized (ThreadCo3.class) {

ThreadCo3.class.wait();

 }

 }

 static class Staff implements Runnable {

String name;

Tool tool;

Exchangerex;

Staff(String name, Tool tool, Exchanger ex) {

this.name = name;

this.tool = tool;

 this.ex = ex;

 }

@Override

 public void run() {

println("%s拿的工具是[%s],他开始[%s]。。。", name, tool.name, tool.work);

doingLongTime();

println("%s开始交换工具。。。", name);

try {

tool = ex.exchange(tool);

 } catch (Exception e) {

e.printStackTrace();

}

 println("%s的工具变为[%s],他开始[%s]。。。", name, tool.name, tool.work);

 }

 }

 static class Tool {

String name;

String work;

Tool(String name, String work) {

 this.name = name; this.work = work;

}

 }

解说:两个线程在预设点交换变量,先到达的等待对方。

生产与销售的问题

 工厂生产出来的产品会先放到仓库存储,销售人员签了单子后,会从仓库把产品发给客户。

如果生产的过快,仓库里产品越堆越多,直到把仓库堆满,那就必须停止生产,因为没地方放了。 此时只能让销售人员赶紧出去签单子,把产品发出去,仓库就有了空间,可以恢复生产了。

如果销售的过快,仓库里产品越来越少,直到把仓库清空,那就必须停止销售,因为没产品了。 此时只能让生产人员赶紧生产产品,把产品放到仓库里,仓库里就有了产品,可以恢复销售了。 可能会有人问,为什么不让生产和销售直接挂钩呢,把仓库这个环节去掉?

这样会造成两种不好的情况:

 一是突然来了很多单子,生产人员累成死Dog也生产不出来。

 二是很长时间没有单子,生产人员闲成废Dog也无事可做。 仓库就是一个缓冲区,能有效的吸收波动,很大程度上减少波动的传递,起到一种解耦作用,由强耦合变成一种松散耦合。 这其实就对应计算机里经典的生产者和消费者问题。

经典的生产者和消费者

一到多个线程充当生产者,生产元素。一到多个线程充当消费者,消费元素。 在两者之间插入一个队列(Queue)充当缓冲区,建立起生产者和消费者的松散耦合。

正常情况下,即生产元素的速度和消费元素的速度差不多时,生产者和消费者其实是不需要去关注对方的。 生产者可以一直生产,因为队列里总是有空间。消费者可以一直消费,因为队列里总是有元素。即达到一个动态的平衡。 但在特殊情况下,比如生产元素的速度很快,队列里没有了空间,此时生产者必须自我“ba工”,开始“睡大觉”。

 一旦消费者消费了元素之后,队列里才会有空间,生产者才可以重启生产,所以,消费者在消费完元素后有义务去叫醒生产者复工。 更准确的说法应该是,只有在生产者“睡大觉”时,消费者消费完元素后才需要去叫醒生产者。否则,其实可以不用叫醒,因为人家本来就没睡。 反之,如果消费元素的速度很快,队列里没有了元素,只需把上述情况颠倒过来即可。 但这样的话就会引入一个新的问题,就是要能够准备的判断出对方有没有在睡大觉,为此就必须定义一个状态变量,在自己即将开始睡大觉时,自己设置下这个变量。

对方通过检测这个变量,来决定是否进行叫醒操作。当自己被叫醒后,首先要做的就是清除一下这个变量,表明我已经醒来复工了。 这样就需要多维护一个变量和多了一部分判断逻辑。可能有些人会觉得可以通过判断队列的“空”或“满”(即队列中的元素数目)来决定是否进行叫醒操作。 在高并发下,可能刚刚判断队列不为空,瞬间之后队列可能已经变为空的了,这样会导致逻辑出错。线程可能永远无法被叫醒。 因此,综合所有,生产者每生产一个元素后,都会通知消费者,“现在有元素的,你可以消费”。

 同样,消费者每消费一个元素后,也会通知生产者,“现在有空间的,你可以生产”。 很明显,这些通知很多时候(即对方没有睡大觉时)是没有真正意义的,不过无所谓,只要忽略它们就行了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • 一、基础知识:1、JVM、JRE和JDK的区别:JVM(Java Virtual Machine):java虚拟机...
    杀小贼阅读 2,357评论 0 4
  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,426评论 1 15
  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 2,941评论 1 18
  • 设置与获取线程名称 package test; /** * 设置与获取线程名称 * @author gaofuzh...
    自由主义者阅读 253评论 0 0
  • 【 线程间的通讯 wait()在对象上等待,等待通知(在等待过程中释放对象锁、等待必须在同步块内、这个对象就是同步...
    征程_Journey阅读 690评论 0 0