javaday13续
目录
1 第十三天:高级:多线程+售票. 1
1.1 进程. 1
1.1.1 什么是进程. 1
1.1.2 进程特点. 1
1.1.3 并发存在吗?. 1
1.1.4 CPU分时调度. 1
1.2 线程. 1
1.2.1 概念. 1
1.2.2 守护线程. 1
1.2.3 进程和线程的关系. 1
1.2.4 查看线程. 1
1.3 多线程并发编程. 1
1.3.1 并发编程三要素. 1
1.3.2 死锁. 1
1.3.3 状态. 1
1.3.4 优先级. 1
1.3.5 线程B怎么知道线程A修改了变量. 1
1.3.6 线程同步有哪些方法. 1
1.3.7 评论. 1
1.4 单线程. 1
1.4.1 单线程. 1
1.4.2 执行过程. 1
1.5 创建多线程的两种方式. 1
1.5.1 继承Thread1
1.5.2 实现Runnable1
1.5.3 比较. 1
1.5.4 评论. 1
1.6 线程状态. 1
1.7 线程的方法. 1
1.8 线程共享访问数据冲突. 1
1.9 线程同步. 1
1 回顾
l 什么是面向对象
n 封装,继承,多态
l 封装
n 类, 模板
n 对象, 实例
n 引用, 保存实例的内存地址, 遥控器
n 构造方法
u 新建实例时执行
u 不定义,有默认
u 构造方法可以重载
n this
u this.xxxx
l this是特殊引用,引用当前实例的地址
u this(...)
l 构造方法之间调用
l 必须是首行代码
n 方法重载 Overload
u 同名不同参
n private
u 隐藏,方便维护修改,不影响其他代码
l 继承
n 作用: 代码重用,复用
n 单继承
n 子类的实例
u 先创建父类实例
u 再创建子类实例
u 两个实例绑定,整体作为一个实例
u 调用成员时,先找子类再找父类
n 方法重写 Override
u 继承的方法,在子类中重新定义重新编写
n super
u super.xxxx()
l 重写时,调用父类中同一个方法的代码
u super(...)
l 调用父类的构造方法
l 默认 super()
l 手动 super(参数)
l 必须是首行代码
l 多态
n 作用: 一致的类型
所有子类型实例,都可以被当做一致的父类型来处理
n 向上转型,向下转型
n instanceof
运行期类型识别
对真实类型及其父类型判断,都得到true
2 第十三天:高级:多线程+售票
2.1 进程
2.1.1 什么是进程
几乎所有操作系统都支持同时运行多个任务,听歌,看文档,聊QQ,一个都不耽误。一个任务通常就是一个程序,每个运行中的程序就是一个进程(Process)。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程。
2.1.2 进程特点
l 独立性:进程是系统中独立存在的实体,它可以拥有自己的独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
l 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念,进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
l 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。
2.1.3 并发存在吗?
我们一边听歌,一边看文档,除此之外,电脑还运行着其他很多程序,杀毒软件、QQ、微信、eclipse等等,这些进程看上去是同时工作。
但事实真相是,对于一个CPU而言,它在某个时间点只能执行一个程序,也就是说,只能运行一个进程,CPU不断地在这些进程之间轮换执行。那为什么我们感觉不到呢?这是因为CPU的执行速度惊人(如果启动的程序过多,我们还是能感受到速度变慢),所以虽然CPU在多个进程中切换执行,但我们感觉是多个程序同时在执行。
2.1.4 CPU分时调度
时间片即CPU分配给各个程序的时间,每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程,将当前进程挂起。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换,而不会造成CPU资源浪费。当又切换到之前执行的进程,把现场恢复,继续执行。
注意:进程在切换时有挂起和现场恢复,这些操作都是额外的,单进程就没有这个问题。进程如此线程亦如此,所以有时我们会发现开启多个线程,感觉会很快,可实际发现多线程有时不一定快,反而还慢,就是这个原因造成的。
在宏观上:我们可以同时打开多个应用程序,每个程序并行,同时运行。但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。多核提高了并发能力,但本质不变。
2.2 线程
进程内部并行的任务
2.2.1 概念
线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程可以开启多个线程。多线程扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。
简而言之,一个程序运行后至少一个进程,一个进程里包含多个线程。
2.2.2 守护线程
守护线程是特殊的线程,一般用于在后台为其他线程提供服务。
例如:我们启动一个杀毒软件360,它会同时启动一个守护进程,监控360杀毒程序。如果这个程序被关闭,则自动重新启动这个程序。我们为何关不掉它,现在知道原因了吧。
2.2.3 进程和线程的关系
一个进程可以分成多个线程,线程是最小执行单元。
进程有自己的私有内存,这块内存会在主存中有一段映射,而所有的线程共享JVM中的内存。在现代的操作系统中,线程的调度通常都是集成在操作系统中的,操作系统能通过分析更多的信息来决定如何更高效地进行线程的调度,这也是为什么Java中会一直强调,线程的执行顺序是不会得到保证的,因为JVM自己管不了这个,所以只能认为它是完全无序的。
另外,类java.lang.Thread中的很多属性也会直接映射为操作系统中线程的一些属性。Java的Thread中提供的一些方法如sleep和yield其实依赖于操作系统中线程的调度算法。
从上图中可以看出一个操作系统中可以有多个进程,一个进程中可以有多个线程,每个进程有自己独立的内存,每个线程共享一个进程中的内存,每个线程又有自己独立的内存。(记清这个关系,非常重要!)
2.2.4 查看线程
Windows10中查询查看线程:选中线程,默认不展示。
可以看到chrome.exe的多个进程中,每个进程又启动了多个线程。
2.3 多线程并发编程
2.3.1 并发编程三要素
l 原子性:指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。原子性是数据一致性的保障。
l 可见性:指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。
l 有序性:程序的执行顺序按照代码的先后顺序来执行。单线程简单的事,多线程并发就不容易保障了。
2.3.2 死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
2.3.3 状态
线程生命周期,总共有五种状态:
1) 新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
2) 就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
3) 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
4) 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态;
5) 根据阻塞产生的原因不同,阻塞状态又可以分为三种:
a) 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
b) 同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
c) 其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
6) 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
l 启动线程不要自己调用run()方法,那样就成普通方法了,只能等待OS操作系统自己去调用
l 如果start()一个线程后想其立即执行,技巧:Thread.sleep(1)
l 一个多处理器的机器上,将会有多个线程并行(Parallel)执行,当线程数大于处理器数时,才会出现多个线程并发(Concurrent),在一个CPU上轮换执行
l 并行和并发的差异,一个是同时执行,一个是交替执行
l 线程在运行中,一般会被中断,目的是给其他线程执行机会,雨露均沾
l 线程调用sleep()方法主动放弃所占用的CPU资源
l 调用yield()方法可以让运行状态的线程转入就绪状态
l join()方法是一个线程等待另外一个线程
2.3.4 优先级
值越小优先级越低,值越大优先级越高
package javapro.thread;
public class TestPriority {
public static void main(String[] args) {
Thread t = new Thread();
System.out.println(t.MAX_PRIORITY); //10
System.out.println(t.NORM_PRIORITY); //默认5
System.out.println(t.MIN_PRIORITY); //1
}
}
2.3.5 线程B怎么知道线程A修改了变量
l volatile修饰变量,提供多线程共享变量可见性和禁止指令重排序优化
l synchronized修饰,它是悲观锁,属于抢占式,会引起其他线程阻塞
l wait 释放对象锁,notify 通知
l while轮询
2.3.6 线程同步有哪些方法
l Synchronized关键字
l Lock锁实现
l 分布式锁:Redis、ZooKeeper
2.3.7 评论
【了解】进程线程是很难理解的,即使多年工作经验的人员也难以说清。
2.4 单线程
2.4.1 单线程
单线程,顺序执行,不会变异
package javapro.thread;
public class TestSingleThread {
public static void main(String[] args) {
exec();
}
public static void exec() {
System.out.println( “exec start” );
fn1();
fn2();
System.out.println( “exec end” );
}
public static void fn1() {
System.out.println( “fn1” );
}
public static void fn2() {
System.out.println( “fn2” );
}
}
执行结果:
exec start
fn1
fn2
exec end
2.4.2 执行过程
日常大多程序都是按单线程过程执行的。
2.5 创建多线程的两种方式
l 继承Thread
l 实现Runnable
2.5.1 继承Thread
Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。Start()方法是一个native方法,它将通知底层操作系统,最终由操作系统启动一个新线程,操作系统将执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。
模拟开启多个线程,每个线程调用run()方法
package day13;
public class Test1 {
public static void main(String[] args) {
T1 t1 = new T1();
T1 t2 = new T1();
t1.start();
t2.start();
System.out.println("------");
}
static class T1 extends Thread {
@Override
public void run() {
// 获得线程名
String n = getName();
// 打印1到100
for (int i = 1; i <= 100; i++) {
System.out.println(n + " - " + i);
}
}
}
}
2.5.2 实现Runnable
如果自己的类已经extends另一个类,就无法直接extends Thread,此时,可以实现一个Runnable接口。
package day13;
public class Test2 {
public static void main(String[] args) {
//封装代码的对象
R1 r1 = new R1();
//代码交给线程
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r1);
//两个线程启动后,
//都执行 r1.run()
t1.start();
t2.start();
//获得main线程
Thread t = Thread.currentThread();
String n = t.getName();
System.out.println(n);
}
/*
* 用来封装代码
* run()方法代码要放入线程执行
*/
static class R1 implements Runnable {
@Override
public void run() {
//获得正在执行的线程对象
Thread t = Thread.currentThread();
String n = t.getName();
//打印1到100
for(int i=1;i<=100;i++) {
System.out.println(n+" - "+i);
}
}
}
}
2.5.3 比较
方式 |
优点 |
缺点 |
Thread |
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。 |
线程类已经继承了Thread类,所以不能再继承其他父类 |
Runnable |
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。 在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。 |
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。 |
2.5.4 评论
【了解】多线程是一个非常棘手的概念,即使多年工作经验的开发者,提到多线程也是一脑袋浆糊。这里我们只要记住多线程会引发怎样的问题,已经多线程问题基础的解决方案即可。
2.6 线程状态
2.7 线程的方法
l Thread.currentThread()
获取当前正在执行的线程实例
l Thread.sleep(毫秒值时长)
当前线程暂停指定的毫秒值时长
l Thread.yield()
让步,主动放弃 cpu 时间片,让给其他线程执行
l getName(), setName()
l start()
启动线程,线程启动后,并行执行run()方法中的代码
l interrupt()
打断另一个线程的暂停状态,被打断的线程,会出现InterruptedException
l join()
当前线程暂停,等待被调用的线程接收后,再继续执行
a线程 -------------------------
b线程 ---------| |------------
a.join()
l setDaemon(true)
后台线程、守护线程
JVM虚拟机退出条件,是所有前台线程结束,当所有前台线程结束,虚拟机会自动退出
不会等待后台线程结束
例如:垃圾回收器是一个后台线程
l getPriority(), setPriority()
优先级,1到10,默认是5
package day13;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Test3 {
public static void main(String[] args) {
T1 t1 = new T1();
t1.start();
}
static class T1 extends Thread {
@Override
public void run() {
SimpleDateFormat f =
new SimpleDateFormat("HH:mm:ss");
while(true) {
String s = f.format(new Date());
System.out.println(s);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
}
}
package day13;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Scanner;
public class Test3_v2 {
public static void main(String[] args) {
T1 t1 = new T1();
t1.start();
Thread t2 = new Thread() {
@Override
public void run() {
System.out.println("按回车捅醒 t1");
new Scanner(System.in).nextLine();
t1.interrupt();
}
};
//虚拟机不会等待后代线程结束
//所有前台线程结束时,虚拟机会自动退出
t2.setDaemon(true);
t2.start();
}
static class T1 extends Thread {
@Override
public void run() {
SimpleDateFormat f =
new SimpleDateFormat("HH:mm:ss");
for(int i=0; i<10; i++) {
String s = f.format(new Date());
System.out.println(s);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("被打断");
break;
}
}
}
}
}
package day13;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Scanner;
public class Test3_v3 {
public static void main(String[] args) {
T1 t1 = new T1();
t1.start();
Thread t2 = new Thread() {
@Override
public void run() {
System.out.println("按回车捅醒 t1");
new Scanner(System.in).nextLine();
t1.interrupt();
}
};
//虚拟机不会等待后代线程结束
//所有前台线程结束时,虚拟机会自动退出
t2.setDaemon(true);
t2.start();
}
static class T1 extends Thread {
@Override
public void run() {
SimpleDateFormat f =
new SimpleDateFormat("HH:mm:ss");
for(int i=0; i<10; i++) {
String s = f.format(new Date());
System.out.println(s);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("被打断");
break;
}
}
}
}
}
package day13;
public class Test5 {
public static void main(String[] args) throws InterruptedException {
//1000万内,有多少个质数
//2,3,5,7,11,13,17,19,23....
System.out.println("\n--单线程-----------------");
f1();
System.out.println("\n--5个线程-----------------");
f2();
}
private static void f1() throws InterruptedException {
long t = System.currentTimeMillis();
T1 t1 = new T1(0, 10000000);
t1.start();
//main线程暂停等待 t1 的执行结果
t1.join();
int c = t1.count;
t = System.currentTimeMillis()-t;
System.out.println("数量:"+c);
System.out.println("时间:"+t);
}
private static void f2() throws InterruptedException {
long t = System.currentTimeMillis();
int n = 5;
int m = 10000000/n;
T1[] a = new T1[n];
for (int i = 0; i < a.length; i++) {
a[i] = new T1(m*i, m*(i+1));
a[i].start();
}
int sum = 0;
for (T1 t1 : a) {
t1.join();
sum += t1.count;
}
t = System.currentTimeMillis()-t;
System.out.println("数量:"+sum);
System.out.println("时间:"+t);
}
static class T1 extends Thread {
int from;
int to;
int count;//计数变量
public T1(int from, int to) {
if(from<3) {
from = 3;
count = 1;
}
this.from = from;
this.to = to;
}
@Override
public void run() {
for (int i = from; i < to; i++) {
if (isPrime(i)) {//判断i是否是质数
count++;
}
}
}
private boolean isPrime(int i) {
/*
* 从2到 (i开方+1)
* 找有能把i整除的,i不是质数
* 都不能吧i整除,i是质数
*/
double d = Math.sqrt(i) + 1;
for (int j = 2; j < d; j++) {
if (i%j == 0) {
return false;
}
}
return true;
}
}
}
2.8 线程共享访问数据冲突
package day13;
import java.util.Arrays;
public class Test6 {
static char[] a = {'-','-','-','-','-'};
static char c = '*';
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run() {
while(true) {
for (int i = 0; i < a.length; i++) {
a[i] = c;
}
c = (c=='*'?'-':'*');
}
}
};
Thread t2 = new Thread() {
@Override
public void run() {
while(true) {
System.out.println(Arrays.toString(a));
}
}
};
t1.start();
t2.start();
}
}
2.9 线程同步
线程同步 synchronized
让多个线程共享访问数据时,步调一致的执行。
一个线程修改时,其他线程等待修改完成后才能执行;
一个线程访问时,其他线程等待访问结束
任何实例,都有一个“同步锁”,synchronized 关键字,要求一个线程必须抢到同步锁才能执行
synchronized(对象) {
共享的数据访问代码
}
必须抢到指定对象的锁,才能执行
synchronized void f() {
}
抢当前实例(this)的锁
static synchronized void f() {
}
抢类的锁
使用synchronized时,考虑一下两点:
l 抢哪个对象
l 哪段代码是在修改/访问数据
synchronized效率低
l 为了保证线程安全,必须牺牲性能,降低效率
l 尽量锁定越少的代码越好
package day13;
import java.util.Arrays;
public class Test7 {
static char[] a = {'-','-','-','-','-'};
static char c = '*';
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run() {
while(true) {
synchronized (a) {
for (int i = 0; i < a.length; i++) {
a[i] = c;
}
}
c = (c=='*'?'-':'*');
}
}
};
Thread t2 = new Thread() {
@Override
public void run() {
while(true) {
synchronized (a) {
System.out.println(Arrays.toString(a));
}
}
}
};
t1.start();
t2.start();
}
}
package day13;
public class Test8 {
public static void main(String[] args) {
R1 r1 = new R1();
Thread t1 = new Thread(r1);
// r1.run(), r1.add(),r1.add(),r1.add(),r1.add(),r1.add(),
t1.start();
//main
// r1.get(), r1.get(),r1.get(),r1.get(),r1.get(),r1.get(),
while(true) {
int i = r1.get();
if(i % 2 == 1) {
System.out.println(i);
System.exit(0);//关闭虚拟机
}
}
}
static class R1 implements Runnable {
static int i=0;
public synchronized void add() {
i++;
i++;
}
public synchronized int get() {
return i;
}
@Override
public void run() {
while(true) {
add();
}
}
}
}
3 小笔记