多线程
进程概述及多进程的意义
线程和进程
线程之前要先说进程,因为线程是依赖于进程存在的
进程概述
通过任务管理器我们可以看到进程的存在
概念:进程就是正在运行的程序,是系统进行资源分配和调用的独立单位,每一个进程都有它自己的内存空间和系统资源
多进程的意义
单进程计算机只能做一件事情,现在的计算机可以一边玩游戏(游戏进程),一边听音乐(音乐进程),所以我们常见的操作系统都是多进程操作系统,。例如:Windows,Mac和Linux等,能在同一时间段内执行多个任务。
对于单核计算机来讲,游戏进程和音乐进程也不是同时进行的,因为CPU在某个时间点上只能做一件事,计算机是在游戏进程和音乐进程间做着频繁切换,且切换速度很快,所以我们感觉游戏和音乐在同时进行,但并不是同时执行的。多进程的作用不是提高执行速度,而是提高CPU的使用率
线程概述及多线程的意义及并行和并发的区别
什么是线程
在一个进程内部又可以执行多个任务,而这每一个任务我们就可以看成是一个线程,是程序使用CPU的基本单位,所以进程是拥有资源的基本单位,线程是CPU调度的基本单位
多线程有什么意义
多线程的作用不是提高执行速度,而是为了提高应用程序的使用率
怎么理解这个问题呢?
我们程序在运行的时候,都是在抢CPU的时间片(执行权),如果是多线程的程序,那么在抢到CPU的执行权的概率应该比较单线程程序抢到的概率要大,也就是说,CPU在多线程程序中执行的时间要比单线程多,所以就提高了程序的使用率,但是即使是多线程程序,他们中哪个线程能抢占到CPU的资源,这个是不确定的,所以多线程具有随机性
并行和并发的区别
并行是逻辑上同时发生,指在某一个时间内同时运行多个程序
并发是物理上同时发生,指在某一个时间点同时运行多个程序
什么是并发
并发:指应用能够交替执行不同的任务,并发有点类似于多线程的原理,多线程并非是多个线程同时执行多个任务,而是以飞快的速度不断去切换这几个任务
什么是并行
并行:指应用能够同时执行不同的任务
Java程序运行原理和JVM的启动是多线程的吗
java程序运行原理
java命令会启动java虚拟机,启动JVM,等于启动了一个应用程序,也就是启动了一个进程
该进程会自动启动一个“主线程”,然后主线程去调用某个类的main方法
所以main方法运行在主线程中
JVM的启动是多线程吗
JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的
多线程程序实现的方式1
如何实现多线程
由于线程是以来进程而存在的,所以我们应该先创建一个进程(JVM)出来的
而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程
因为java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序
但是java可以去调用C/C++写好的程序来实现多线程程序
由C/C++去调用系统功能创建进程,然后由java去调用,然后提供一些类供我们使用,我们就可以实现多线程程序了
参考 Thread类
多线程程序实现的方式1
-
继承Thread类
-
步骤及代码演示
-
几个小问题:
启动线程使用的是那个方法
线程不能多次启动
run()和start()方法的区别
我们启动线程使用不是run方法,而是start方法,使该线程开始执行
java虚拟机调用该线程的run方法
为什么要重写run方法
为了保证写的其他方法中封装的代码都被线程执行,使用重写的run方法中要封装必须被线程执行的代码
run方法中的代码的书写原则:一般是比较耗时的代码
public class Student extends Thread {
private String name;
@Override
public void run() {
for (int i = 1; i <= 10000; i++) {
System.out.println("第" + i + "名学生入学");
}
}
}
public class Teacher extends Thread {
private String name;
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println("老师" + i + "来了");
}
}
}
public class MyTest {
public static void main(String[] args) {
Student student = new Student();
Teacher teacher = new Teacher();
student.start();
teacher.start();
}
}
获取和设置线程对象名称
Thread类的基本获取和设置方法
- public final String getName():获取线程名称
- public final void setName(String name):设置线程名称
思考:
如何获取main方法所在的线程名称呢?
public static Thread currentThread():获取当前执行的线程
public class MyTest {
public static void main(String[] args) {
Thread thread = new Thread();
//获取线程名称
System.out.println(thread.getName());//Thread-0
//设置线程名称
thread.setName("秀儿");
System.out.println(thread.getName());//秀儿
//获取主线程也就是main方法所在的线程名称
System.out.println(thread.currentThread().getName());//main
}
}
线程调度及获取和设置线程优先级
线程的执行
一个CPU在某一时刻只能执行一条指令,线程只有得到CPU时间片也就是使用权才可以执行命令
线程的两种调度模型
- 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
- 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些,java使用的是抢占式调度模型
如何设置和获取线程优先级
- public final int getPriority():获取线程的优先级
- public final void setPriority(int newPriority):设置线程的优先级
public class MyTest {
public static void main(String[] args) {
Thread thread = new Thread();
//获取线程优先级
//线程的默认优先级是5
System.out.println(thread.getPriority());//5
//设置线程优先级(1-10)
thread.setPriority(8);
System.out.println(thread.getPriority());//8
}
}
注意事项:有的时候我们给线程设置了指定的优先级,但是该线程并不是按照优先级高的线程执行,那是为什么呢?
因为线程的优先级的大小仅仅表示这个线程被CPU执行的概率增大了,但是我们都知道多线程具有随机性,所以有的时候一两次的运行说明不了问题
线程控制之休眠线程
休眠线程
- public static void sleep(long millis):线程休眠
public class MyTest {
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
try {
//休眠2000毫秒
sleep(2000);
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
线程控制之加入线程
加入线程
意思就是:等待该线程执行完毕了以后,其他线程才能再次执行
注意事项:在线程启动之后,再调用方法
public class MyTest {
public static void main(String[] args) {
Thread th2 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("B" + i);
}
}
};
Thread th = new Thread(){
@Override
public void run() {
for (int i = 0; i < 50; i++) {
//A抢到了,输出了第一个后B就加入进来并执行完毕了
System.out.println("A"+i);
try {
th2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
th.start();
th2.start();
}
}
线程控制之礼让线程
礼让线程
- public static void yield():暂停当前正在执行的线程对象(暂停的时间很短),并执行其他线程(包括自己在内的所有线程接着抢)
public class MyTest {
public static void main(String[] args) {
Thread th = new Thread(){
@Override
public void run() {
for (int i = 0; i < 50; i++) {
//A每抢到一次,就礼让B一次
System.out.println("A"+i);
Thread.yield();
}
}
};
Thread th2 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("B" + i);
}
}
};
th.start();
th2.start();
}
}
线程控制之守护线程
守护线程
- public final void setDaemon((boolean on):将该线程标记为守护线程。当正在运行的线程都是守护线程时,Java 虚拟机退出
注意事项:必须在启动线程前调用
public class MyTest {
public static void main(String[] args) {
Thread th = new Thread(){
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("A"+i);
}
}
};
Thread th2 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("B" + i);
}
}
};
//A为守护线程,所有线程都关闭后A就直接关闭了
th.setDaemon(true);
th.start();
th2.start();
}
}
Java用户线程和守护线程
1.用户线程和守护线程的区别
用户线程和守护线程都是线程,区别是Java虚拟机在所有用户线程dead后,程序就会结束。而不管是否还有守护线程还在运行,若守护线程还在运行,则会马上结束。很好理解,守护线程是用来辅助用户线程的,如公司的保安和员工,各司其职,当员工都离开后,保安自然下班了。
2.用户线程和守护线程的适用场景
由两者的区别及dead时间点可知,守护线程不适合用于输入输出或计算等操作,因为用户线程执行完毕,程序就dead了,适用于辅助用户线程的场景,如JVM的垃圾回收,内存管理都是守护线程,还有就是在做数据库应用的时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监听连接个数、超时时间、状态等。
3.创建守护线程
调用线程对象的方法setDaemon(true),设置线程为守护线程。
1)thread.setDaemon(true)必须在thread.start()之前设置。
2)在Daemon线程中产生的新线程也是Daemon的。
3)不是所有的应用都可以分配给Daemon线程来进行服务,比如读写操作或者计算逻辑。
因为Daemon Thread还没来得及进行操作,虚拟机可能已经退出了。
4.Java守护线程和Linux守护进程
两者不是一个概念。Linux守护进程是后台服务进程,没有控制台。
在Windows中,你可以运行javaw来达到释放控制台的目的,在Unix下你加&在命令的最后就行了。所以守护进程并非一定需要的。
线程控制之中断线程
中断线程
- public final void stop():停止线程的运行
- public void interrupt():中断线程(这个翻译不太好),查看API可得当线程调用wait(),sleep(long time)方法的时候处于阻塞状态,可以通过这个方法清除阻塞
public class MyTest {
public static void main(String[] args) {
Thread th = new Thread(){
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(i);
if (i==20){
//停止线程的运行
stop();
}
}
}
};
th.start();
}
}
public class MyTest {
public static void main(String[] args) {
Thread th = new Thread(){
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(i);
interrupt();
try {
sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
th.start();
}
}
多线程程序实现的方式2
实现Runnable接口
这种方式扩展性强,实现一个接口,还可以再去继承其他类
可以避免由于java单继承带来的局限性
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"==="+i);
}
}
}
public class MyTest {
public static void main(String[] args) {
/* //创建线程的第二种方式
创建线程的另一种方法是
1.声明实现 Runnable 接口的类。
2.该类然后实现 run 方法。
3.然后可以分配该类的实例,
4.在创建 Thread 时作为一个参数来传递并启动。*/
// Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。
/* void run ()
使用实现接口 Runnable 的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用对象的 run 方法。*/
//创建任务
MyRunnable myRunnable = new MyRunnable();
//把任务传递进来
Thread th = new Thread(myRunnable,"AAA");
Thread th2 = new Thread(myRunnable,"BBB");
th.start();
th2.start();
}
}
多线程程序实现的方式3
- 实现 Callable 接口。相较于实现Runnable接口的方式,方法可以有返回值,并且可以抛出异常。
- 执行 Callable 方式,需要FutureTask实现类的支持,用于接收运算结果。FutureTask 是 Future 接口的实现类
- 实现步骤
- 创建一个类实现Callable接口
- 创建一个FutureTask类将Callable接口的子类对象作为参数传进去
- 创建Thread类,将FutureTask对象作为参数传进去
- 开启线程
import java.util.concurrent.Callable;
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
//call()将来是有线程来线程
System.out.println("线程过来执行了");
return null;
}
}
import java.util.concurrent.FutureTask;
public class MyTest {
public static void main(String[] args) {
//创建线程的方式3
/* 实现步骤
1. 创建一个类实现Callable 接口
2. 创建一个FutureTask类将Callable接口的子类对象作为参数传进去
3. 创建Thread类, 将FutureTask对象作为参数传进去
4. 开启线程*/
MyCallable myCallable = new MyCallable();
FutureTask<Integer> task = new FutureTask<>(myCallable);
Thread thread = new Thread(task);
thread.start();
}
}
继承Thread类的方式卖电影票案例
public class CellThread extends Thread{
//共享数据,被多个线程所共享。
static int piao = 100;
@Override
public void run() {
while (true){
if(piao>0){
System.out.println(this.getName()+"正在出售:"+(piao--)+" 张票");
}
}
}
}
public class MyTest {
public static void main(String[] args) {
/* 需求:某电影院目前正在上映贺岁大片,
共有100张票,而它有3个售票窗口售票,
请设计一个程序模拟该电影院售票。
通过继承Thread类实现*/
CellThread th1 = new CellThread();
CellThread th2 = new CellThread();
CellThread th3 = new CellThread();
th1.setName("窗口1");
th2.setName("窗口2");
th3.setName("窗口3");
th1.start();
th2.start();
th3.start();
}
}
实现Runnable接口的方式卖电影票
public class CellRunnable implements Runnable{
//共享数据,被多个线程所共享。
int piao = 100;
@Override
public void run() {
while (true) {
if (piao > 0) {
System.out.println(Thread.currentThread().getName() + " 正在出售:" + (piao--) + " 张票");
}
}
}
}
public class MyTest {
public static void main(String[] args) {
CellRunnable cellRunnable = new CellRunnable();
Thread th1 = new Thread(cellRunnable);
Thread th2 = new Thread(cellRunnable);
Thread th3 = new Thread(cellRunnable);
th1.setName("窗口1");
th2.setName("窗口2");
th3.setName("窗口3");
th1.start();
th2.start();
th3.start();
}
}
买电影票出现了同票和负数票的原因分析
延迟
在现实生活中,售票时网络是不能实时传输的,会出现延迟的情况,所以在售出一张票以后,需要有一点时间的延迟,每次卖票延迟100毫秒
此时就会出现线程安全问题
线程安全问题的产生原因分析
为什么会出现问题(判断是否有问题的标准)
是否是多线程环境
是否有共享数据
是否有多条语句操作共享数据
如何解决多线程安全问题
基本思想:让程序没有安全问题的环境
如何实现:把多个语句操作共享数据的代码锁起来,让任意时刻只能有一个线程执行
将操作共享数据的多条语句看作一个整体,当一个线程执行这个整体的时候,其他的线程处于等待状态,可以使用同步代码块完成这个操作
同步代码块
格式:
synchronized(对象){//不能在括号了直接new 对象 new 了 就没效果
要被同步的代码;
}
这个同步代码块保证数据的安全性的一个主要因素就是这个对象(这个对象要定义为静态成员变量,才能被所有线程共享)
这个对象被所有线程对象所共享,这个对象其实就是一把锁,这个对象习惯叫做监视器
同步代码块的方式解决线程安全问题及解释以及同步的特点及好处和弊端
同步代码块的格式
synchronized(对象){ //同步代码代码块上的锁,是一个互斥锁。
死循环
需要同步的代码;
}
同步的好处
同步的出现解决了多线程的安全问题
同步的弊端
当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
同步代码块的锁问题以及同步方法的应用和锁问题
同步代码块的锁对象:任意一个对象
同步方法的锁对象:是this
静态同步方法的锁对象:就是当前类对应的字节码文件对象
JDK5之后的Lock锁的概述和使用
Lock锁的概述
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock和ReentrantLock
void lock()加锁
void unlock()释放锁
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
死锁问题概述和使用
死锁问题概述
如果出现了同步嵌套,就容易产生死锁问题
是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象
死锁:两个或者两个以上的线程,在抢占CPU的执行权的时候,都处于等待状态
完