网络上对于线程的解析总是天花龙凤的,给你灌输一大堆概念,考研、本科的操作系统必修课尤甚,让你就算仔细看完一大堆文章都不知道干什么。
下面不取网站复制粘贴,在讲解自己的Java线程并发、互斥与同步之前先给大家解构操作系统书中那些给出书者为了出书者而写的废话到底是什么意思。
大神们如果只想看程序,可以自行跳过,反正我的文章从来新手向,不喜勿看。
一、线程的基本概念
其实线程的概念非常简单,多一个线程,就多了一个做事情的人。
比如搬东西,搬10箱货物,这就是所谓的一个进程,一个人,就是所谓的一个线程,把10箱货物从一头搬到另一头,也就是所谓的完成一个进程,当然比不上多个人把10箱货物从一头搬到另一头的效率,也就是所谓的多线程完成一个进程比单个线程完成一个进程的效率高。哪需要像某些书籍用大量的篇幅,某些老师用大量的课时去解释什么是线程与进程?
现在的多线程操作系统,如Windows,它的优势就在于多个人同时完成一件事,这样就快,提高效率。
当然,这样多个人搬东西也引发了一些问题,比如在搬东西的时候,有那么的一个时间,两个人同时搬完自己的货物,同时开始搬下一件货物,那么,当两个人见到同一件货物的时候,都想搬这件货物的时候怎么协商。当然,这谁先搬谁后搬没有任何关系,只是你两个人不要同时在搬一个货物就行了,也就是我这个管理者,必须保证所有参与搬运的人,一个货物对于一个人,这样搬运的效率才最大化,搬运的规矩确立了搬东西的人才不会争吵。这就是所谓了线程互斥问题。如果出现上述的情况,我必须设置一个搬东西的缓冲区。搬东西之前必须排队,大家轮流拿东西,规矩,这样就不会争吵了。
如果前方有人没有拿好东西,后面要搬东西的人,无论你手脚多么快,你都要给我等!那个拿东西的人拿东西之前先立flag,拿完东西就收起flag。所有人搬东西之前给我好好看看有没有flag,没有flag才给我拿东西。这就是所谓的“进程中访问临界资源的那段代码称为临界区”,临界资源就是东西,你要拿东西就是访问临界资源,临界区就是这个等待拿东西的队列。
最后还有一个关于线程同步的概念,所谓的同步,就是如果在这个搬东西的进程之后,有一个需要运走东西的人,就必须等待所有东西搬完,他才能开始运东西,搬东西是一个进程,运东西是一个进程,那么这两个进程之间就形成了同步。进程也就是线程嘛,本质是相同的嘛,只是进程是大量线程的集合,也就是说进程是一件大事,线程是一件小事。所谓的大事小事对于不同人来说是不同的,比如对于有些人来说,回宿舍打游戏就是大事、进程,开电脑就是小事、线程,而对于一些人来说,拿到名牌大学的录取通知书是大事、线程,每天好好学习就是小事、线程。因此进程与线程也是相对的。
二、Java中实现线程并发、互斥与同步
Java其实只比那复杂的Mfc、C++简单一点而已,类名、方法的运用比较清晰而已,但也好不了多少,因为它所包含的东西太多了,但这也同时也是它的好处,就像汇编语言那样,你越难写,越难学的东西,反而是最有用的东西,它能够控制的东西多,Java中是可以开多线程的,某些语言估计不让这样搞。
1、基本目标
假设我们要写这样的一个程序:有四个黄牛党,名字是分别是线程1、线程2、线程3、线程4,他们的任务就是抢光票站里面的20张票,售票员的卖走一张票需要1s,线程1的速率是每1s买走1张票,线程2则是2s/票,线程3s/票,线程4则是0.5s/票,然后主线程main命令他们开始抢票之后,等待票站的所有票被卖走之后统计各个黄牛的得票情况。
整个程序跑出来的结果如上面所示,当然处理速率也没有这么快,我做gif的时候,等待的时间被剪掉,不用大家看得这么慢,文件也不用这么大。
这里就是一个多线程并发的例子,如果1个线程再不停地刷票当然没有秩序问题,不会出现在某一时间多个线程对一张票提出请求,也就不用出现缓冲区的概念。然后最后做统计的的主线程也不用等待,必须顺序结构的程序,语句与语句之前默认是同步的关系,这里要考虑好到底是等待那个进程完成才开始做自己的事情。
如果我们改变每一个黄牛的速率,得到的结果是不同的:
通过这两个程序的对比,我们也可以发现,速度快的黄牛,无论程序怎么跑,最后得到的票肯定多,这符合自然规律。
如果某只黄牛手脚过慢,那就会出现长期处于缓冲区的情况,最后得到的票就少,甚至没有,出现所谓的“饿死”的概念,也就是长期得到资源。
其实这个社会也是,如果你太废,做事的效率低,手脚慢,你也不用想得到社会的资源。如果你想功成名就,得到大量的社会资源,那么从现在开始,请提高你的做事效率,努力戒除拖延症哦!
2、制作过程
(1)主函数
这个程序在程序头无需引入任何Java包,JDK自带
请注意,你创建这个Java程序,请不要命名为Thread,这样你是作死的,因为Thread是Java程序中的一个关键字
public class Threadtest {
public static void main(String[] args) throws Exception {
MyThread my = new MyThread();
new Thread(my, "线程1").start();
new Thread(my, "线程2").start();
new Thread(my, "线程3").start();
new Thread(my, "线程4").start();
while (my.semaphoretomain == 0) {
System.out.println(Thread.currentThread().getName() + "于缓冲区");
try {
Thread.currentThread().sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("^_^票已卖光,明天请早,都各自散吧!(杀死所有进程)");
System.out.println("=========得票统计=========");
System.out.println("线程1:" + my.count1 + "张");
System.out.println("线程2:" + my.count2 + "张");
System.out.println("线程3:" + my.count3 + "张");
System.out.println("线程4:" + my.count4 + "张");
}
}
线程需要抛出异常,
开线程的方法怎么开,如上所示,MyThread是一会儿我要写的一个进程,创建一个新的MyThread进程其名为my,而线程1、线程2、……分明是这个my进程的线程。
至于以下的这段代码:
while (my.semaphoretomain == 0) {
System.out.println(Thread.currentThread().getName() + "于缓冲区");
try {
Thread.currentThread().sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
就是所谓的缓冲区,一开始先把main这个函数扔进缓冲区睡觉,
直到my这个进程里面与你交互的信号量不为0,你才给我出来,当然,你也不要经常向我问信号量是否为0,你问一次就给我睡4s,也就是你4s才好给我问一次,不然我烦死,也就是,我不想我程序出现如计算机网络里面的“广播风暴”。
直到main离开这个缓冲区,它才能执行下面的统计代码,并输出。
(2)MyThread进程
//改写Runnable接口,实际上说明自己是一个被多线程操作的进程,群P^_^呵呵
class MyThread implements Runnable {
//假设有20张票,其实以下的4个线程每一个动作就是使这个ticket值自减到0
private int ticket = 20;
//这个是自减互斥信号量
private int semaphore = 0;
//这些东西是要被主函数访问的所以为public,上面的两个值只能在这个进程用,所以是private
//countX是进程X的得票统计,semaphoretomain与主函数的信号量
public int count1 = 0;
public int count2 = 0;
public int count3 = 0;
public int count4 = 0;
public int semaphoretomain = 0;
//进程的执行,在下面的run()方法写
public void run() {
//当票不为0就开始愉快地玩耍咯
while (this.ticket > 0) {
//Thread.currentThread().getName()这个方法可以取现在进行这个进程的线程名
switch (Thread.currentThread().getName()) {
//如果是线程1,进入了这个线程先给我睡1s,以此类推,实际上在控制每个线程的速度
//如果你不让线程先睡一会儿,他们就会以CPU的频率,每3.19GHZ的倒数的一个动作的速率在跑
//部分神机估计更快,这个实验出不到结果
case "线程1":
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
case "线程2":
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
case "线程3":
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
case "线程4":
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
}
//这也是缓冲区,一旦有进程操作票,它必须把这个信号量改成1,让其他进程进入缓冲区
//当它操作完,又把信号量改回0,所有在缓冲区的进程竞争操作票的资格
//也就不会对一张票进行同时操作,把一张票抢烂
while (semaphore > 0) {
System.out.println(Thread.currentThread().getName() + "于缓冲区");
try {
//你们这群线程X,也被经常给我检查这个信号量是否为0,我不想烦死
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (this.ticket > 0) {
semaphore++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.ticket--;
System.out.println("票" + (20 - this.ticket) + "被"
+ Thread.currentThread().getName() + "买走,当前票数剩余:"
+ this.ticket);
switch (Thread.currentThread().getName()) {
case "线程1":
this.count1++;
break;
case "线程2":
this.count2++;
break;
case "线程3":
this.count3++;
break;
case "线程4":
this.count4++;
break;
}
semaphore--;
} else {
//所有票被买完了,那么就通知主函数main,你开始离开缓冲区,取统计了
semaphoretomain++;
//eclipse中这个stop()方法会被划横线警告,毕竟stop()方法可能会引起死锁,java中已经封装了互斥锁与同步锁了
//eclipse建议这个程序用互斥锁与同步锁实现
Thread.currentThread().stop();
}
}
}
}
上面的注释已经比较清晰了,
还请大家注意到对票操作的这段代码:
if (this.ticket > 0) {
semaphore++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.ticket--;
System.out.println("票" + (20 - this.ticket) + "被"
+ Thread.currentThread().getName() + "买走,当前票数剩余:"
+ this.ticket);
switch (Thread.currentThread().getName()) {
case "线程1":
this.count1++;
break;
case "线程2":
this.count2++;
break;
case "线程3":
this.count3++;
break;
case "线程4":
this.count4++;
break;
}
semaphore--;
}
如果现在是考研或者本科的操作系统期末考,改卷老师绝对要看你操作前后是否有对信号量的处理。semaphore++;semaphore--;这个对子,在线程的操作中,必须线程对应,正如汇编语言与接口技术,里面如果出现对函数、过程的操作,记得进栈出栈一个对子来保存现场!