黑马程序员--Java多线程
来源:互联网 发布:u深度制作ubuntu启动盘 编辑:程序博客网 时间:2024/05/18 01:12
——- android培训、java培训、期待与您交流! ———-
进程与线程
要想了解多线程,必须先了解线程,而要想了解线程,必须先了解进程,因为线程是依赖于进程而存在。
什么是进程
通过任务管理器我们就看到了进程的存在。
而通过观察,我们发现只有运行的程序才会出现进程。
进程:就是正在运行的程序。
进程是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。
多进程的意义
单进程的计算机只能做一件事情,而我们现在的计算机都可以做多件事情。
举例:一边玩游戏(游戏进程),一边听音乐(音乐进程)。
也就是说现在的计算机都是支持多进程的,可以在一个时间段内执行多个任务。
并且呢,可以提高CPU的使用率。
一边打游戏,一边听音乐,不是同时进行的,单CPU在某一时间点上只能做一件事。
CPU在做程序间的高效切换让我们感觉打游戏和听音乐是同时进行的。
什么是线程
在同一个进程内又可以执行多个任务,而这每一个任务我就可以看出是一个线程。
- 线程:是程序的执行单元,执行路径。是程序使用CPU的最基本单位。
- 单线程:如果程序只有一条执行路径。
- 多线程:如果程序有多条执行路径。
多线程的意义
多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率。
程序的执行其实都是在抢CPU的资源,CPU的执行权。
多个进程是在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权。
我们是不敢保证哪一个线程能够在哪个时刻抢到,所以线程的执行有随机性。
并行和并发
- 前者是逻辑上同时发生,指在某一个时间内同时运行多个程序。
- 后者是物理上同时发生,指在某一个时间点同时运行多个程序。
Java程序的运行原理
由java命令启动JVM,JVM启动就相当于启动了一个进程。
接着由该进程创建了一个主线程去调用main方法。
JAVA虚拟机的启动是多线程的,除了主线程,至少还需要垃圾回收线程,否则内存很快就会溢出。
如何实现多线程
由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。
而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。
Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。
但是Java可以去调用C/C++写好的程序来实现多线程程序。
由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,
然后提供一些类供我们使用。我们就可以实现多线程程序了。
方式一:继承Thread类
首先自定义一个继承Thread的方法
public class MyThread extends Thread { @Override public void run() { for (int x = 0; x < 200; x++) { System.out.println(x); } }}
重写run()方法的作用
不是类中的所有代码都需要被线程执行的。
为了区分哪些代码能够被线程执行,java提供了Thread类中的run()用来包含那些被线程执行的代码。
创建线程对象测试
public class MyThreadDemo { public static void main(String[] args) { // 创建两个线程对象 MyThread my1 = new MyThread(); MyThread my2 = new MyThread(); //my1.run();//这是单线程 //启动线程 my1.start(); my2.start(); }}
调用run()方法为什么是单线程?
因为run()方法直接调用其实就相当于普通的方法调用,所以你看到的是单线程的效果
run()和start()的区别?
- run():仅仅是封装被线程执行的代码,直接调用是普通方法
- start():首先启动了线程,然后再由jvm去调用该线程的run()方法。
获取和设置线程对象的名称
public final String getName():获取线程的名称
public final void setName(String name):设置线程的名称
public class MyThreadDemo { public static void main(String[] args) { // 创建线程对象 //无参构造+setXxx() // MyThread my1 = new MyThread(); // MyThread my2 = new MyThread(); // //调用方法设置名称 // my1.setName("李延旭"); // my2.setName("康小广"); // my1.start(); // my2.start(); //带参构造方法给线程起名字 // MyThread my1 = new MyThread("赵磊"); // MyThread my2 = new MyThread("王澳"); // my1.start(); // my2.start(); //我要获取main方法所在的线程对象的名称,该怎么办呢? //遇到这种情况,Thread类提供了一个方法: //public static Thread currentThread():返回当前正在执行的线程对象 System.out.println(Thread.currentThread().getName()); }}
线程调度
我们知道,一般我们的计算机只有一个CPU,而CPU在某一个时刻只能运行一条指令,线程只有得到CPU的使用权,才能执行线程,那么Java是如何对线程进行调度的呢?
线程有两种调度模型:
- 1.分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片。
- 2.抢占式调度模型,优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取CPU时间片的几率稍大。
Java使用的是抢占式调度模型
设置和获取线程的优先级
public final int getPriority():返回线程对象的优先级
public final void setPriority(int newPriority):更改线程的优先级。
注意:
- 程默认优先级是5。
- 优先级的范围是:1-10。
- 先级高仅仅表示线程获取的 CPU时间片的几率高,但是要在次数比较多,或者多次运行的时候才能看到比较好的效果。
public class ThreadPriorityDemo { public static void main(String[] args) { //创建线程对象 ThreadPriority tp1 = new ThreadPriority(); ThreadPriority tp2 = new ThreadPriority(); ThreadPriority tp3 = new ThreadPriority(); //设置对象名称 tp1.setName("徐凤年"); tp2.setName("李淳罡"); tp3.setName("黄阵图"); // 获取默认优先级 // System.out.println(tp1.getPriority()); // System.out.println(tp2.getPriority()); // System.out.println(tp3.getPriority()); // 设置线程优先级 // tp1.setPriority(100000);//报错 //设置正确的线程优先级 tp1.setPriority(10); tp2.setPriority(1); tp1.start(); tp2.start(); tp3.start(); }}
线程休眠
重写run()方法,加入休眠时间
import java.util.Date;/* * 线程休眠 * public static void sleep(long millis) */public class ThreadSleep extends Thread { @Override public void run() { for (int x = 0; x < 100; x++) { System.out.println(getName() + ":" + x + ",日期:" + new Date()); // 设置休眠时间 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }}
加入线程
/* * public final void join():等待该线程终止。 * 别的线程需要等待这个加入的线程执行结束才能执行。 */public class ThreadJoinDemo { public static void main(String[] args) { //创建线程对象 ThreadJoin tj1 = new ThreadJoin(); ThreadJoin tj2 = new ThreadJoin(); ThreadJoin tj3 = new ThreadJoin(); //设置线程名称 tj1.setName("黄三甲"); tj2.setName("邓太阿"); tj3.setName("曹长卿"); //加入线程 tj1.start(); try { tj1.join(); } catch (InterruptedException e) { e.printStackTrace(); } //启动线程 tj2.start(); tj3.start(); }}
礼让线程
/* * public static void yield():暂停当前正在执行的线程对象,并执行其他线程。 * 让多个线程的执行更和谐,但是不能靠它保证一人一次。 */public class ThreadYieldDemo { public static void main(String[] args) { ThreadYield ty1 = new ThreadYield(); ThreadYield ty2 = new ThreadYield(); ty1.setName("李延旭"); ty2.setName("黑马"); ty1.start(); ty2.start(); }}
守护线程
/* * public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。 * 当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。 * */public class ThreadDaemonDemo { public static void main(String[] args) { ThreadDaemon td1 = new ThreadDaemon(); ThreadDaemon td2 = new ThreadDaemon(); td1.setName("关羽"); td2.setName("张飞"); // 设置守护线程 td1.setDaemon(true); td2.setDaemon(true); td1.start(); td2.start(); Thread.currentThread().setName("刘备"); for (int x = 0; x < 5; x++) { System.out.println(Thread.currentThread().getName() + ":" + x); } }}
中断线程
/* * 中断线程: * public void interrupt() */public class TreadDemo { public static void main(String[] args) { // 创建线程 MyThread m1 = new MyThread(); MyThread m2 = new MyThread(); // 设置线程名称 m1.setName("黑马"); m2.setName("白马"); // 中断线程,后面的线程还可以继续运行 m1.interrupt(); // 启动线程 m2.start(); }}
线程的生命周期
方式二:实现Runnable接口
步骤:
* A:自定义类MyRunnable实现Runnable接口
* B:重写run()方法
* C:创建MyRunnable类的对象
* D:创建Thread类的对象,并把C步骤的对象作为构造参数传递
public class MyRunnable implements Runnable { @Override public void run() { for (int x = 0; x < 100; x++) { // 由于实现接口的方式就不能直接使用Thread类的方法了,但是可以间接的使用 System.out.println(Thread.currentThread().getName() + ":" + x); } }}
测试类
public class MyRunnableDemo { public static void main(String[] args) { // 创建MyRunnable类的对象 MyRunnable my = new MyRunnable(); // 构造实现:Thread(Runnable target, String name) Thread t1 = new Thread(my, "林青霞"); Thread t2 = new Thread(my, "刘意"); t1.start(); t2.start(); }}
已有方式一,为何会有方式二出现?
1.可以避免由于java单继承带来的局限性
2.适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码,数据有效分离,较好的体现了面向对象的设计思想。
方式三:实现Callable接口
需要和线程池结合使用
实现类
import java.util.concurrent.Callable;/* * Callable<V>:带泛型的接口 * 接口中只有一个方法:V call() * 接口中的泛型是call()方法的返回值类型 * */public class MyCallable implements Callable { @Override public Object call() throws Exception { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } return null; }}
测试类
import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class MyCallableDemo { public static void main(String[] args) { // 创建线程池对象 ExecutorService pool = Executors.newFixedThreadPool(2); // 添加Callable实现类 pool.submit(new MyCallable()); pool.submit(new MyCallable()); // 结束线程池 pool.shutdown(); }}
线程安全
解决线程安全的基本思想(判断线程是否有问题的标准)
- 是否是多线程环境
- 是否有共享数据
- 是否有多条语句操作共享数据
电影票案例
public class SellTicket implements Runnable { // 定义100张票 private int tickets = 100; @Override public void run() { while (true) { if (tickets > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第"+ (tickets--) + "张票"); } } }}
测试类
public class SellTicketDemo { public static void main(String[] args) { // 创建资源对象 SellTicket st = new SellTicket(); // 创建三个线程对象 Thread t1 = new Thread(st, "窗口1"); Thread t2 = new Thread(st, "窗口2"); Thread t3 = new Thread(st, "窗口3"); // 启动线程 t1.start(); t2.start(); t3.start(); }}
这样会出现以下问题
1.相同的票卖了多次
CPU的一次操作必须是原子性的
2.出现负数票
线程的随机性和延迟导致的
线程安全的解决方式
解决方式一:同步代码块
/* * synchronized(对象){ * 代码; * } * * 注意:同步代码块可以解决安全问题的根本原因在对象上,该对象如果锁一样的功能,别的线程不能进入。 * 这个对象可以是任意对象,最好是用本身this作为这个对象。 * */public class SellTicekt implements Runnable { private int ticket = 100; @Override public void run() { while (true) { synchronized (this) { if (ticket > 0) { try { Thread.sleep(100);} catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+ "正在出售第" + (ticket--) + "张票"); } } } }}
解决方式二:同步方法
/* * synchronized关键字修饰方法 * 锁对象是this */public class SellTicekt implements Runnable { private int ticket = 100; @Override public synchronized void run() { while (true) { if (ticket > 0) { try { Thread.sleep(100); } catch(InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第"+ (ticket--) + "张票"); } } }}
同步的特点:
前提:多个线程
解决问题的时候要注意:多个线程使用的是同一个锁对象
同步的好处
同步的出现解决了多线程的安全问题。
同步的弊端
当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
Lock锁
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。
import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class SellTicket implements Runnable { // 定义票 private int tickets = 100; // 定义锁对象 private Lock lock = new ReentrantLock(); @Override public void run() { while (true) { try { // 加锁 lock.lock(); if (tickets > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+ "正在出售第" + (tickets--) + "张票"); } } finally { // 释放锁 lock.unlock(); } } }}
死锁
两个或两个以上的线程在争夺资源的过程中,发生的一种相互等待的现象。
举例:
中国人,美国人吃饭案例。
正常情况:
中国人:筷子两支
美国人:刀和叉
现在:
中国人:筷子1支,刀一把
美国人:筷子1支,叉一把
public class MyLock { // 创建两把锁对象 public static final Object objA = new Object(); public static final Object objB = new Object();}
public class DieLock extends Thread { private boolean flag; public DieLock(boolean flag) { this.flag = flag; } @Override public void run() { if (flag) { synchronized (MyLock.objA) { System.out.println("if objA"); synchronized (MyLock.objB) { System.out.println("if objB"); } } } else { synchronized (MyLock.objB) { System.out.println("else objB"); synchronized (MyLock.objA) { System.out.println("else objA"); } } } }}
测试类
public class DieLockDemo { public static void main(String[] args) { DieLock dl1 = new DieLock(true); DieLock dl2 = new DieLock(false); dl1.start(); dl2.start(); }}
线程的状态转换图及常见执行情况
线程间通信
指不同种类的线程针对同一资源的操作
资源类
/* * 定义学生类 */public class Student { String name; int age; boolean flag;// 用来判断是否存在资源,默认是flash,没有资源 public synchronized void set(String name, int age) { // 生产者,如果有数据就等待 if (!this.flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 设置数据 this.name = name; this.age = age; // 修改标记 this.flag = false; // 唤醒线程 this.notify(); } public synchronized void get() { if (this.flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(this.name + ":" + this.age); // 修改标记 this.flag = true; // 唤醒线程 this.notify(); }}
设置类
/* * 设置学生信息的线程 */public class SetThread implements Runnable { private Student s; private int i; public SetThread(Student s) { this.s = s; } @Override public void run() { while (true) { if (i % 2 == 0) { s.set("小明", 5); } else { s.set("汪汪", 2); } i++; } }}
获取类
/* * 设置获取学生信息的线程 */public class GetThread implements Runnable { private Student s; public GetThread(Student s) { this.s = s; } @Override public void run() { while (true) { s.get(); } }}
测试类
public class StudentDemo { public static void main(String[] args) { // 创建资源 Student s = new Student(); // 创建SetThread和GetThread对象 SetThread st = new SetThread(s); GetThread gt = new GetThread(s); // 创建线程 Thread t1 = new Thread(st); Thread t2 = new Thread(gt); // 开启线程 t1.start(); t2.start(); }}
这样线程安全是解决了,但是还存在着以下问题。
1.如果消费者先抢到CPU执行权,消费数据,这时数据如果是空,就没有意义。
应该等着数据生产出来,再去消费,这样才具有意义。
2.如果生产者先抢到CPU执行权,生产数据,但是生产完一定数量的数据以后,还继续持有执行权,
它还会继续生产数据,这还现实情况不符,需要等着消费者把数据消费以后,再生产。
正常思路:
1.生产者
先看是否有数据,有就等待,没有就生产,生产完通知消费者消费
2.消费者
先看是否有数据,有就消费,没有就等待,消费完通知生产者生产
java提供了一个等待唤醒机制来解决这个问题。
Object类中提供了三个方法:
wait():等待
notify():唤醒单个线程
为什么等待唤醒方法定义在Object类中:
这些方法都是通过锁对象进行调用的,锁对象可以是任意的
所以,这些方法必须定义在Object类中。
测试类
public class StudentDemo { public static void main(String[] args) { //创建资源 Student s = new Student(); //设置和获取的类 SetThread st = new SetThread(s); GetThread gt = new GetThread(s); //线程类 Thread t1 = new Thread(st); Thread t2 = new Thread(gt); //启动线程 t1.start(); t2.start(); }}
资源类
public class Student { String name; int age; boolean flag; }
生产者类
public class GetThread implements Runnable { private Student s; public GetThread(Student s) { this.s = s; } @Override public void run() { while (true) { synchronized (s) { if(!s.flag){ try { s.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(s.name + "---" + s.age); //修改标记 s.flag = false; //唤醒线程 s.notify(); } } }}
消费者类
public class SetThread implements Runnable { private Student s; private int x = 0; public SetThread(Student s) { this.s = s; } @Override public void run() { while (true) { synchronized (s) { //判断有没有 if(s.flag){ try { s.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } if (x % 2 == 0) { s.name = "李延旭"; s.age = 21; } else { s.name = "黑马"; s.age = 22; } x++; //x=1 //修改标记 s.flag = true; //唤醒线程 s.notify(); //唤醒t2,唤醒并不表示你立马可以执行,必须还得抢CPU的执行权。 } } }}
总结
多线程的实现有三个方法,我们常用的是第二种,所以第二种是要必须掌握的,其他两种了解即可。对于线程的状态转换和执行图,也是必须要理解的,这样有利于学习多线程。等待唤醒机制,是很符合现实的生活一个机制,需要熟练掌握。
——- android培训、java培训、期待与您交流! ———-
- 黑马程序员-java多线程
- 黑马程序员--java 多线程
- 黑马程序员-java多线程
- 黑马程序员--Java多线程
- 黑马程序员--java多线程
- 黑马程序员:Java多线程
- 黑马程序员 Java多线程
- 黑马程序员---Java多线程
- 黑马程序员---多线程【java】
- 黑马程序员----JAVA----多线程---
- 黑马程序员-java多线程
- 黑马程序员-java 多线程
- 黑马程序员-------Java多线程
- 黑马程序员-java多线程
- 黑马程序员-Java多线程
- 黑马程序员 java 多线程
- 黑马程序员--java多线程
- 黑马程序员-java多线程
- 《高质量的C/C++编程指南》读书笔记
- 2015年8月30日17:20:36
- 脚本文件基础记录case
- MySQL数据类型
- 顺序队列和链式队列
- 黑马程序员--Java多线程
- 图片缓存
- Mysql存储引擎及选择方法
- xcode的快捷键
- 矩阵行列式mod M
- 再谈typedef
- C++标准库类型string
- 如何在sulime text 3上安装emmet插件
- JDK环境变量配置问题