——- 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的执行权。
}
}
}
}
总结
多线程的实现有三个方法,我们常用的是第二种,所以第二种是要必须掌握的,其他两种了解即可。对于线程的状态转换和执行图,也是必须要理解的,这样有利于学习多线程。等待唤醒机制,是很符合现实的生活一个机制,需要熟练掌握。