文章目录
- 1.并发入门
- 2.我们有什么办法使他运行正确吗?
- 3.如何解决这个售票问题?
- 4.现在我们知道了锁,锁在并发意义重大
- 4.1然而锁在操作系统是怎么实现的呢?
- 题外话理论
- 1.什么是并发?与并行的差别?
- 2.并发比串行快吗?
- 2.1我们使用vmstat来证明我们确实存在上下文切换
- 3.并发编程时要考虑的资源限制
- 3.1网盘的资源限制引发的效率问题
- 4.线程为什么不能start两次?
1.并发入门
使用多线程出售售票系统的代码:
package com.tl.skyLine.thread;
/**
* Created by tl on 17/3/6.
*/
public class SellTicket {
public static void main(String[] args) {
TicketWindow tw = new TicketWindow();
Thread t1 = new Thread(tw, "一号窗口");
Thread t2 = new Thread(tw, "二号窗口");
Thread t3 = new Thread(tw, "三号窗口");
t1.start();
t2.start();
t3.start();
}
}
class TicketWindow implements Runnable {
private int tickets = 10;
@Override
public void run() {
while (true) {
if (tickets > 0) {
System.out.println("还剩余票:" + tickets + "张");
tickets--;
System.out.println(Thread.currentThread().getName() + "卖出一张火车票,还剩" + tickets + "张");
} else {
System.out.println("余票不足,暂停出售!");
// wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
try {
Thread.sleep(1000 * 60 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
我们这里经常会出现不按顺序输出和经常会卖出很多票的情况
为什么会这样呢?
我们画一个图来展示这个异常中的最终一段流程
我们知道main也是一个线程,在main方法中我们创建了t1,t2,t3的线程,并且使用了start运行他的t1,t2,t3方法。start会运行线程中的run()方法,问题是 线程是抢占CPU运行的,CPU只有一个(单核,先不考虑双核电脑),哪个线程抢占了CPU,哪个线程就具有执行权,这样子好像看起来他其中一个线程抢占了CPU后,其他线程理应等待。
毕竟CPU大部分电脑只有一个(单核,先不考虑双核电脑),这样子也合情合理,毕竟CPU也是程序,他也只能执行一行行的命令。可现实如果是这样的话,我打开了N个程序,他就必须等第一个程序打开完成后,才能打开第二个程序,又才能打开第三个程序那岂不是慢死,我查询了书籍后发现CPU是采用一种时间轮转的(操作系统书籍里介绍的轮转调度),大概是20ms一次轮转,他每一次轮转都会给程序一段运行时间,然后创建上下文并保留上下文后(运行环境),去到另一个进程继续给他运行一个时间,所以看起来像是多线程运行。
所以t1,t2,t3线程都可能会运行run() 方法,所以会产生了可能两个线程运行tickets–;导致了不一致问题
2.我们有什么办法使他运行正确吗?
很多同学会说加synconized关键字,但synconized关键字有什么问题?
public synchronized void run() { //加了syncoized
虽然的确是保证了他一定是售票成功的,因为我们知道了synconized是给加上锁,t1,t2,t3都有可能抢到这个方法的执行,执行过程会加上锁,等待抢到方法执行的 线程运行完,才会把锁释放。
所以你注意到了吗,如果是二号窗口抢到执行权的话,
因为我们代码中的,就说明了只能让二号窗口全部卖完,这一点和我们原来的需求三个窗口同时卖想违背。
while(true)
有些同学其实也可能想到了如下程序,把ticketwindows放在了程序的main方法里
package chapter01;
/**
* Created by tl on 17/3/6.
*/
public class SellTicket {
public static void main(String[] args) {
TicketWindow tw = new TicketWindow();
Thread t1 = new Thread(tw, "一号窗口");
Thread t2 = new Thread(tw, "二号窗口");
Thread t3 = new Thread(tw, "三号窗口");
while (TicketWindow.tickets>=0) {
t1.start();
t2.start();
t3.start();
}
}
}
class TicketWindow implements Runnable {
static int tickets = 1000;
@Override
public synchronized void run() {
if (tickets > 0) {
System.out.println("还剩余票:" + tickets + "张");
tickets--;
System.out.println(Thread.currentThread().getName() + "卖出一张火车票,还剩" + tickets + "张");
} else {
System.out.println("余票不足,暂停出售!");
// wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
try {
Thread.sleep(1000 * 60 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行了之后你也知道了,因为程不能start
执行多次。搜百度和谷歌会发现这个问题被很多人提及,后面再说这个问题
3.如何解决这个售票问题?
其实我们不应该锁方法,应该锁对象synoized(this)
就能解决了
package chapter01;
/**
* Created by tl on 17/3/6.
*/
public class SellTicket {
public static void main(String[] args) {
TicketWindow tw = new TicketWindow();
Thread t1 = new Thread(tw, "一号窗口");
Thread t2 = new Thread(tw, "二号窗口");
Thread t3 = new Thread(tw, "三号窗口");
t1.start();
t2.start();
t3.start();
}
}
class TicketWindow implements Runnable {
private int tickets = 1000;
@Override
public void run() {
while (true) {
synchronized (this) {
if (tickets > 0) {
System.out.println("还剩余票:" + tickets + "张");
tickets--;
System.out.println(Thread.currentThread().getName() + "卖出一张火车票,还剩" + tickets + "张");
} else {
System.out.println("余票不足,暂停出售!");
// wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
try {
Thread.sleep(1000 * 60 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
4.现在我们知道了锁,锁在并发意义重大
我们知道了如果想要让多线程并发串行起来,好像都得加锁,变得串行
其实我们首先应该要想好锁究竟能锁什么,按道理来说能锁变量,能锁方法,能锁类,这样子可以说是简单的锁的应用。
synoized 对于同步方法,锁的是当前实例对象
synoized 对于静态同步方法,锁的是当前累的Class对象
对于同步方法块,锁的是synonized 括号里配置的对象
4.1然而锁在操作系统是怎么实现的呢?
待更新。。。
题外话理论
1.什么是并发?与并行的差别?
并发当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。.这种方式我们称之为并发(Concurrent)。
并行:当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
区别:并发和并行是即相似又有区别的两个概念,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。在多道程序环境下,并发性是指在一段时间内宏观上有多个程序在同时运行,但在单处理机系统中,每一时刻却仅能有一道程序执行,故微观上这些程序只能是分时地交替执行。倘若在计算机系统中有多个处理机,则这些可以并发执行的程序便可被分配到多个处理机上,实现并行执行,即利用每个处理机来处理一个可并发执行的程序,这样,多个程序便可以同时执行。
按照个人理解的推论是,CPU没有时间轮转时是一行一行的执行汇编指令,这些汇编指令只能来自于一个进程(运行中的程序)。我们把多个运行中的程序变成了一大堆的汇编指令汇总给CPU,然后CPU负责按序执行。显然这种方法效率是很低的,然后CPU有了时间轮转后,他应该也把进程也分类了,然后分时间轮转片去给这些进程一些运行时间,当然每次切换一个进程都得保留他们的进程上下文环境方便下一次再来执行这个进程时能不用从头执行到尾,只执行未完成的部分即可。
线程是寄放在进程里的小段程序,一个进程可以包含多个线程,但是也依照这个原理来实行。
多核CPU环境下,就有多个CPU来分配执行不同进程的汇编指令,所以能得到更快的执行效率。也就多任务同时处理的并行。
2.并发比串行快吗?
理解来自《并发编程的艺术》方腾飞注
我们刚刚看到上文也知道,CPU可能使用时间轮转来分配给每个进程一定的执行时间,这个时间会创建上下文和上下文的切换。由于上下文切换,在处理量比较少时会损耗一定的性能。我们可以使用如下程序来测试一下
package chapter01;
public class ConcurrencyTest {
private static final long count = 10000l;
public static void main(String[] args) throws InterruptedException {
concurrency();
serial();
}
private static void concurrency() throws InterruptedException {
long start = System.currentTimeMillis();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
System.out.println(a);
}
});
thread.start();
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
thread.join();
long time = System.currentTimeMillis() - start;
System.out.println("concurrency :" + time + "ms,b=" + b);
}
private static void serial() {
long start = System.currentTimeMillis();
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
long time = System.currentTimeMillis() - start;
System.out.println("serial:" + time + "ms,b=" + b + ",a=" + a);
}
}
2.1我们使用vmstat来证明我们确实存在上下文切换
摘自:http://blog.51cto.com/huwho/1977265 的解析
vmstat (virtual memory statistics)是一款类unix服务器性能的监控工具,他主要用来监控进程、内存、swap虚拟内存、块设备的输入输出,cpu的使用率等。vmstat应用于类unix系统。
如果使用后发现他一直在运行可用ctrl +c 把他截断
你可以很清晰地看到vmstat总共监控6大块,分别为:进程(proc)、
内存(memory)、虚拟内存(swap)、磁盘IO、系统相关(system)、cpu使用率。
r 即running,表示运行队列(就是说多少个进程真的分配到CPU)。
b 即block,表示阻塞队列。其实国外解释是:The number of processes in uninterruptible sleep.
连续性睡眠的进程,你可以理解为阻塞队列。
swapd,表示虚拟内存的总共使用量。指定一个间隔时间,例如每隔3秒监测swapd的变化,如果swapd一直在增长,
则表示虚拟内存正在被使用,这个信息告诉我们当前系统的物理内存不足,这个时候,我们就要分析内存不足的原因,
我通常是用top命令,再接着按M键,这样就会按内存使用率的大小将进程排序,由此分析占用内存较高的进程。
free,空闲物理内存的大小。
buff,内存中使用缓冲的总量。
cache,内存中使用缓存的总量。
si,即swamp in disk,每秒从磁盘读入虚拟内存的大小总量。Amount of memory swapped in from disk
(per second).
这个值大于0表示虚拟内存被使用,要注意内存泄露的问题。
so,swamp output to disk,即每秒从虚拟内存写入磁盘的大小总量。Amount of memory swapped to disk
(per second).
这个值大于0表示虚拟内存被使用,要注意内存泄露的问题。
bi,Blocks received from a block device (blocks per second),即每秒从块设备接收到的块设备量,即读块设备的量。
bo,Blocks sent to a block device (blocks/s),即每秒从块设备写入的块设备量,即写块设备的量。
in,即interrupts,cpu每秒中断的次数,包括时间中断。
cs,即content switch,上下文切换。上下文切换频繁会造成CPU不必要的浪费。
us,即user time,用户CPU时间。
sy,即system time,即系统CPU时间。
id,即idle time,即CPU闲置时间。
wa,即wait time,等待IO时间,这个值过高,表示磁盘IO繁忙。
st,即stolen。偷取时间,与虚拟机相关。
3.并发编程时要考虑的资源限制
自身条件:
硬件的限制有:硬盘读写和CPU处理速度
软件的限制有:数据库的连接数,socket的连接数
外部条件:
网盘的资源提供限制
3.1网盘的资源限制引发的效率问题
我们理解的一点是,程序里必须串行的地方,不应该用并发来处理,因为并发的上下文切换可能会导致效率下降,当并发执行效率不高时,可以考虑换个业务方案进行串行处理。
推荐使用java封装好的类,因为已经通过了大师的测试
4.线程为什么不能start两次?