黑马程序员 —— Java多线程1 (第十一天)
来源:互联网 发布:森林优化 编辑:程序博客网 时间:2024/05/20 18:50
------- android培训、java培训、期待与您交流! ----------
一 概述
1.引子
要了解“线程”,就要先了解“进程”的概念,我们可以在Windows系统任务管理器中看到一个个“进程”。
这么多的进程,看上去是同时执行的,但其实只是CPU在做快速的切换。
而一个进程,包含至少一个或者包含多个线程。
2.进程与线程的定义
(1) 进程:一个执行中的程序,是系统进行资源分配和调度的独立单元。
(每一个进程执行都有一个执行顺序,该顺序就是执行路径)
(2) 线程:进程中的一个独立的(执行顺序)控制单元。
(3) 关于Java.exe :Java JVM 启动的时候会有一个进程 Java.exe
该进程中至少有一个(当然就是多个)线程负责 Java 程序的执行。
而且这个线程运行的代码存在于main方法中。
该线程称为主线程。
例如:“迅雷”这个进程,它除了把下载分为多个下载的任务,还把任务分为多个部分,
同时进行发出请求,(将下载起点分成了N个地方),这就是多线程的下载了。
补充:
线程是进程中的内容,每个应用程序里面都有线程,它是程序的控制单元或者执行路径。
进程在内存中开辟了空间,而线程才是真正执行程序的单元。
其实更细节说明虚拟机JVM,JVM启动不止主线程一个线程,还有负责垃圾回收机制的线程。
javac 命令执行的之后可以在Windows的任务管理器看到编译进程javac.exe,java命令同理可以看到虚拟机执行的进程java.exe.
JVM启动的时候,会有一个进程java.exe,该进程中至少有一个线程在负责java程序的执行,
而且这个线程运行的代码存在于main方法中,该线程称之为主线程。
多线程存在的意义:可以让程序产生“同时执行”的效果,多段代码同时执行,提高效率。
二 了解如何创建线程
创建新执行线程有两种方法:
- 继承Thread类,该子类应重写Thread 类的 run 方法。
- 创建线程的另一种方法是声明实现 Runnable 接口的类。
1.继承Thread类
步骤:定义一个继承Thread类的类,重写Thread类中的run方法,调用线程的start方法。
start方法有两个作用:启动线程,调用run方法。
(注意!!!不调用start方法,这个线程是不会执行的,run里面的方法不会有效)
public class Test { public static void main(String[] args) { Demo demo = new Demo(); demo.start(); for (int x = 0; x <= 60; x++) { System.out.println("demo --" + x); } }}class Demo extends Thread { public void run() { for (int x = 0; x <= 60; x++) { System.out.println("run --" + x); } }}上面的程序:输出是
run --59run --60demo --10demo --11……两者交替输出,各自到达60。
观察上面的程序,可发现运行结果每一次都不同,
是因为多个线程都在获取CPU的执行权。CPU执行到谁,谁就运行。
明确一点,在某一个时刻,只能有一个程序在运行。(多核除外)
CPU在做着快速的切换,以达到看上去同时运行的效果。
我们可以形象地把多线程运行行为理解为在互相抢夺CPU的执行权。
这就是多线程的一个特性:随机性。谁抢到谁执行,至于执行多长时间,CPU说了算。
题:解释为什么要重写run方法?
答:首先,Thread类用于描述线程。该类就定义了一个方法,用于存储线程要运行的代码,也就是run方法。
Thread类中的run方法,是用于存储线程要运行的代码。也就是说,要让你的程序多线程执行,就需要重写run方法。
所以就要重写run方法,将自定义代码存储在run方法中,让你想要执行的功能执行。
题:请说一下run()方法和start()方法的区别?
答:如果仅仅调用run()方法,则结果就是程序还是一个单线程程序;只有调用start()方法,程序才能启动多线程。
run()方法就像个容器,仅仅是封装多线程要运行的代码,并不能启动多线程;
Java程序不会创建线程,Java程序通过调用start()方法,
然后start()方法调用OS的底层代码,去启动多线程。
Demo d = new Dmo(); //创建好一个线程 //demo.start(); //开启线程并执行该线程的run方法 demo.run(); //这样会按顺序,执行完run -- 输出,再执行demo --输出。 //仅仅是对象调用方法,而线程创建了,并没有运行。run仅仅是封装了线程所要执行的代码而已。
2.实现Runnable接口
步骤:
(1)定义类实现Runnable接口
(2)覆盖Runnable接口中的run方法。将线程要运行的代码存放在该run方法中。
(3)通过Thread类建立线程对象。
(4)将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法。
(5)调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。
示例代码:
public class Test { public static void main(String[] args) { RunnableTest runnableTest = new RunnableTest(); Thread thread = new Thread(runnableTest); thread.start(); /* 简化到一步到为,new Thread(new Runnable()).start(); */ }}class RunnableTest implements Runnable { public void run() { System.out.println("线程运行代码"); }}
不常用的简化模式:
public class Test { public static void main(String[] args) { new Thread(new Runnable() { public void run() { System.out.println("线程运行代码"); } }).start(); }}
题:为什么要将Runnable接口的子类对象传递给Thread的构造方法?
答:因为自定义的run方法所属的对象是Runnable接口的子类对象。
由此看见,我们必须要让线程去执行指定对象的run方法,就必须明确该run方法所属的对象。
3.对比两种创建线程的区别
继承Thread类的方式:将线程运行代码放在Thread子类的run方法;
实现Runnable接口的方式:将线程代码存放在接口的子类的run方法中;
对比可知,实现接口的方式可避免单继承的局限性,更能体现面向对象的思想,
在创建线程时,也建议优先使用实现Runnable接口的方式。
三 线程运行的状态
线程的状态有:新建、运行、冻结、消亡、临时阻塞。
其实应该说是线程的“生命周期”,不同教程有不同的线程状态转换图,
在此以毕老师的视频教程图例为标准。它们之间的状态转换如下:
各种状态的简单解释:
新建:Thread t = new Thread();就创建了一个线程
运行:t.start();
冻结:t.sleep(500); t.wait(); (sleep有时间限制,而wait没有)
消亡:t.stop();
临时状态(阻塞):线程被start后,有不运行,因为CPU还要先运行完其它进程,该线程正在等CPU的执行权。
四 获取线程对象及名称
每个线程都有自己默认的名称,一般是"Thread-编号",编号从0开始。
而主线程的名称,则是“main”。
Thread类 封装了获取线程名称的方法:
- static Thread currentThread():获取当前线程对象。
- getName():获取线程名称。
- 设置线程名称:通过setName()方法或者构造方法设置。
- 注意:主线程不能设置名称,它的线程名是默认的(main)。
class ThreadTest extends Thread { public void run() { while (true) { System.out.println(Thread.currentThread().getName() + "...run"); } }}public class Test { public static void main(String[] args) { ThreadTest t1 = new ThreadTest(); ThreadTest t2 = new ThreadTest(); ThreadTest t3 = new ThreadTest(); t3.setName("ThreadTest"); t1.start(); //Thread-0...run t2.start(); //Thread-1...run t3.start(); //ThreadTest...run while (true) { System.out.println(Thread.currentThread().getName() + "...run"); //main...run } }}
上面的程序,就是 注释里面的语句一直交替输出。
五 售票的例子
需求:简单的卖票程序,多个窗口同时卖票。
// 多个窗口同时卖100张票public class Demo { public static void main(String[] args) { Ticket t = new Ticket(); new Thread(t, "窗口1").start(); new Thread(t, "窗口2").start(); new Thread(t, "窗口3").start(); new Thread(t, "窗口4").start(); }}class Ticket implements Runnable { public static int tickets = 100; public void run() { while (tickets > 0) { System.out.println(Thread.currentThread().getName() + " 卖出第 " + tickets-- + " 张票."); } }}
六 多线程存在的安全隐患问题
1.模拟案例
像上面那样多线程卖票,会不会出现问题呢?
下面通过Thread的sleep()方法刻意去模仿这种情况的出现
class Ticket extends Thread { private static int tick = 100; public void run() { while (tick > 0) { if(tick==20){ //注意这里 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + " ...sale:" + tick--); } }}public class TicketDemo { public static void main(String[] args) { new Ticket().start(); new Ticket().start(); new Ticket().start(); new Ticket().start(); }}最后的输出是:
……Thread-1 ...sale:1Thread-0 ...sale:0Thread-2 ...sale:-1想象一下这个极端情况,如果四个线程只卖1张票,
线程0在判断票数>0后,暂停了一下,
线程1进入判断后卖票了,票数减去1了,变成0张票,
当线程0暂停完毕,再卖票,得出的结果就会是-1。
所以,多线程就出现了安全问题。
不相关的知识点:接口的方法不能抛异常,只能try……catch。
2.问题原因(重点)
当多条语句在操作同一个线程共享数据时,
一个线程对多条语句只执行了一部分(,CPU就切换到其他线程去了),还没执行完,
另一个线程就参与进来执行,导致共享数据的错误。
3.解决方法
对多条操作共享数据的语句,只能让每一个线程都执行完。在执行过程中,其他线程不能参与。
Java对多线程的安全问题提供了专业的解决方式,
就是同步代码块:
synchronized(对象){需要被同步的代码}
操作共享数据的代码,就需要同步。
一个值得思考的问题:
用继承Thread类的方式,去写卖票程序,Ticket票数必须加上static。
而用实现接口Runnable的方式,Ticket票数类,则不需要加static。
否则,程序将不满足要求,为什么呢?
首先,用继承Thread类的方式,Ticket已经是Therad的子类了,
每次new Ticket()都会产生新的100张票,这显然不符合4个售票窗口卖100张票的要求。
再次,用实现Runnable接口的方式,它先要有一个实现Runnable接口的类Tick,
这个类定义好了线程所要执行的run()方法,再传入Thread类的构造方法中,
此时不会影响Tick类,它还是只有100张票,新建的4个线程就会共同操作Tick类的100张票,
因此,满足题目的要求,不需要加上static。
七 线程同步1 - 引入概念
1.简单示例代码
以上面卖票程序为例,加入同步代码块。
class Ticket implements Runnable { private int tick = 100; Object obj = new Object(); public void run() { while (true) { synchronized (obj) { //注意这里 if (tick > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " sale: " + tick--); } } } }}class TicketDemo { public static void main(String[] args) { Ticket t = new Ticket(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); Thread t3 = new Thread(t); t1.start(); t2.start(); t3.start(); }}
可以看到,上面这个示例代码,用obj作为同步代码块的“锁”,使线程在同步中执行,保证了运行结果的正确。
2.“同步”的原理 - 加锁!
synchronized的原理,相当于加锁,
好像运行代码块里面的代码前,会先有道门(类似于标志位的机制,0是关,1是开),
当线程0运行 synchronized代码块里面的代码时,
先判断代码块里的代码有没有被上锁,如果没有那它自己就给标志位加锁,
然后就运行代码,这样就算它sleep了并且其他线程拿到了CPU执行权,
也只能等线程0运行完,释放锁后其他线程才能继续运行。
这样就避免了不同步导致的安全问题。
生活中的例子:火车里面每节车厢那唯一 一个厕所,要上厕所的人就是线程。
3.“同步”的前提
- 必须要有两个或者两个以上的线程。
- 必须是多个线程使用同一个锁。
必须保证同步中中只能有一个线程在运行。
这里注意第(2)点,加的“锁”可以是随便一个new Object(),
可以是 "随便".getClass() ,可以是 你这个文件名.class 都行,
只要你保证这么多个线程,它们用的锁,是同一个对象,就行了!
4.“同步”的利弊
好处:解决了多线程的安全问题。
弊端:多个线程都需要判断锁,较为耗费资源。
上锁后,明显感觉程序运行的时间慢了。越安全越耗资源,你加越多锁,越慢。
八 线程同步2 - 同步方法
1.银行金库问题
需求:银行有一个金库,有两个储户分别存300元,每次存100,存3次。
目的:该程序是否有安全问题,如果有,如何解决?
class Bank {private int sum;// private Object obj = new Object();public synchronized void add(int num)// 同步方法{// synchronized(obj)// {sum = sum + num;// -->try {Thread.sleep(10);} catch (InterruptedException e) {}System.out.println("sum=" + sum);// }}}class Cus implements Runnable {private Bank b = new Bank();public void run() {for (int x = 0; x < 3; x++) {b.add(100);}}}class BankDemo {public static void main(String[] args) {Cus c = new Cus();Thread t1 = new Thread(c);Thread t2 = new Thread(c);t1.start();t2.start();}}
2.解题思路
如何找到问题?
- 明确哪些代码是多线程运行代码。 → run方法和add方法
- 明确共享数据。 → b和sum是共享数据
- 明确多线程运行代码哪些语句是操作共享数据的。 → sum=sum+n
所以,经过分析,sum=sum+n;那三句应该被同步。
3.同步方法
题:同步代码块是用来封装代码的,方法也是用来封装代码的,那它两有什么不一样呢?
答:同步代码块封装代码具备了同步性。
题:那如果让方法具备同步性呢?
答:可以把synchronized作为关键字修饰方法,让方法具备同步性。
因此引入了同步方法的概念。
public synchronized void add(int n) { sum = sum + n; System.out.println("sum= " + sum);}
九 线程同步3 - 同步方法的锁
1.同步方法的锁
同步方法用的锁是this!!!
验证:使用两个线程来卖票。一个在同步代码块,一个在同步方法,
都在执行买票动作。如果没同步,则会出现错误的票。
线程t1一开启就运行同步代码块,线程t2一开启就运行同步函数。
class Ticket implements Runnable {private static int Ticket = 500;public static boolean flag = true;public void run() {if (flag) {while (true) {synchronized (this) {if (Ticket > 0) {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+ " code--- " + Ticket--);}}}} else {while (true)show();}}public synchronized void show() {if (Ticket > 0) {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " show--- "+ Ticket--);}}}public class Prove {public static void main(String[] args) throws InterruptedException {Ticket Ticket = new Ticket();Thread t1 = new Thread(Ticket, "买家1");Thread t2 = new Thread(Ticket, "买家2");t1.start();Thread.sleep(20);Ticket.flag = false;t2.start();}}方法需要被对象调用,那么函数都有一个所属对象引用,就是this。
所以,同步方法使用的锁是 this!
2.静态同步方法的锁
如果同步函数被静态static修饰后,使用的锁是什么呢?
通过验证,将上面程序的同步函数加静态后(注意票数也要加static修饰了),
执行,发现不再是this。
因为静态方法也不可以定义this。
内存中有一个对象,字节码文件对象,Ticket.class 。
静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象。
类名.class,该对象类型是Class类型。
静态的同步方法,使用的锁是该方法所在类的字节码文件对象。
类名.class,该对象在内存中是唯一的。
十 死锁
1.死锁的概念
死锁:你有一个锁,我有一个锁,你要锁定我的锁才能修改,我也要锁定你的锁才能修改。
通常死锁出现的情况,就是 同步中嵌套同步。
我们应该尽量避免死锁。
百度所谓死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力协助下,永远分配不到必需的资源而无法继续运行,这就产生了一种特殊现象:死锁。
2.死锁的示例代码
class MyTest extends Thread { private boolean flag; public MyTest(boolean flag) { super(); this.flag = flag; } public void run() { if (flag) { synchronized (Lock.LockA) { System.out.println("if locka"); synchronized (Lock.LockB) { System.out.println("if lockb"); } } } else { synchronized (Lock.LockB) { System.out.println("else lockb"); synchronized (Lock.LockA) { System.out.println("else locka"); } } } }}class Lock { public static Object LockA = new Object(); public static Object LockB = new Object();}public class DeadLockDemo { public static void main(String[] args) { new MyTest(true).start(); new MyTest(false).start(); }}关键是要理解,“锁”作为线程在同步情况下所先要取得的资源,
只能在一个时刻被某一个线程锁定,一锁上就要等代码块执行完,
释放锁后,才能继续执行。
十一 单例设计模式
单例设计模式有两种方式,分别是饿汉式和懒汉式。
饿汉式:一上来就初始化了。
懒汉式:用到的时候才初始化。
1.饿汉式
//饿汉式class Single { private final static Single s = new Single(); private Single() { } public static Single getInstance() { return s; }}
2.懒汉式
// 懒汉式class AnotherSingle { private static AnotherSingle as = null; private AnotherSingle() { } public static AnotherSingle getInstance() { if (as == null) { synchronized (AnotherSingle.class) { if (as == null) as = new AnotherSingle(); } } return as; }}public class Single { public static void main(String[] args) { AnotherSingle.getInstance(); }}懒汉式多线程访问下的安全隐患:
A线程判断完类实例为null后挂起了,这时候B线程获取到CPU资源判断进来,也挂起了,
然后A线程醒了new实例后B线程又醒了new实例,就不符合单例设计模式的要求了。
怎么改?加同步,并且用双重判断提高效率。
3.两种方式的对比
饿汉式和懒汉式的区别就在于 延时加载。
懒汉式有什么弊端?懒汉式在多线程运行下,可能会出现线程安全隐患。
该如何解决?加同步,并双重否定提高效率。
------- android培训、java培训、期待与您交流! ----------
- 黑马程序员 —— Java多线程1 (第十一天)
- 黑马程序员——第十一天(Java面向对象-多线程一)
- 【黑马程序员】多线程(一) 第十一天
- 黑马程序员--第十一天:多线程
- 黑马程序员--Java基础学习(多线程)第十一天
- 黑马程序员——>第十一天<多线程(创建-安全-同步-单例设计模式)>
- 黑马程序员------毕老师视频笔记第十一天------多线程(1)
- java基础第十一天——多线程
- 黑马程序员——多线程第一天
- 黑马程序员第十一天
- 黑马程序员——java基础学习笔记——第十一天
- 黑马程序员-第十一天(String类)
- 黑马程序员------毕老师视频笔记第十一天------多线程(2)
- 黑马程序员——Java多线程1
- 黑马程序员——Java(多线程)
- 黑马程序员_第十一天
- 黑马程序员java学习—多线程1
- 黑马程序员—java多线程
- Swift之网络编程-请求缓存
- HDU 1102 最小生成树
- N-Queens -- leetcode
- ACM #1014 : Trie树
- django 模板中使用数组形式
- 黑马程序员 —— Java多线程1 (第十一天)
- 关于继承,函数的继承
- Android应用开发基础之广播与服务
- andriod之String小知识
- 对覆盖和隐藏较好的解释
- bzoj 2464 题解
- 快速排序quicksort
- 2014年度IT图书盘点之:数据库类精选篇
- Sql Server服务远程过程调用失败