目录
多线程概述
进程: 正在运行的程序,是系统进行资源分配和调用的独立单位。 每一个进程都有它自己的内存空间和系统资源。
线程: 是进程中的单个顺序控制流,是一条执行路径。一个进程如果只有一条执行路径,则称为单线程程序。一个进程如果有多条执行路径,则称为多线程程序。
线程包含在进程中
三个关键词:
1、串行
指的是一个程序中所有的任务都是按照先后顺序执行的,在前一个任务还没有处理完的情况下,是不会进行处理下一个任务的
2、并行
指的是将任务分给不同的处理器去处理,每一个处理器中再进行串行处理。
3、并发
实质上是一种现象,并发需要处理器的支持,比如在出库一个任务的时候操作系统可以进行调用再处理其他的任务,不论串行还是并行 都需要操作系统的支持并发。假设喝水是一个任务,每个火车站售票员,再售票的同时也能喝水,这就表示支持并发。
JVM启动的时候是单线程还是多线程?
多线程:main(主线程),垃圾回收线程。
所以在JVM启动的时候,最低要求要有两个线程存在,所以JVM启动的时候是多线程的。
创建线程
创建线程的第一种方式:继承Thread类
1、创建一个自定义类继承Thread类;
2、该类要重写Thread类中的run方法:当线程启动后,执行的代码逻辑仅是run()方法中的代码逻辑;
3、根据这个类创建线程对象
4、启动线程
每次创建一个对象,等同于创建一个新的线程对象
启动线程调用的方法:start()
代码举例:
实体类继承Thread类:
public class Tthread1 extends Thread{
@Override
public void run() {
for(int i=1;i<10;i++){
System.out.print(i);
}
}
}
测试类:
public class Tdemo1 {
public static void main(String[] args) {
Tthread1 t1 = new Tthread1();
Tthread1 t2 = new Tthread1();
// 启动线程
t1.start();
t2.start();
}
}
输出结果:
这里使用run方法启动线程表示的是一个对象调用普通的方法,这样启动仍然是单线程程序,要想实现多线程效果,必须要使用start方法启动。
调用start()与调用run()的区别:
run()方法仅仅是封装了被线程执行的代码,但直接调用run()方法与调用普通的方法方式没有区别
start()方法的调用,首先单独启动了一个线程,然后由JVM调用该线程类中的run()方法。
当一个线程对象启动多次的时候,报错:IllegalThreadStateException 非法的线程状态异常
注意事项:
1、启动线程调用的是start()方法;2、线程的先后启动顺序,对结果没有影响
Thread的基本获取和设置方法
通过构造方法给当前线程起名字
Thread(String name) :分配一个新的Thread对象
通过调用Thread的成员方法给当前现场永恒起名字:
public final void setName(String name):将此线程的名称更改为等于参数name 。
获取线程的名字
成员方法:public final String getName() :返回此线程的名称
如何获取main方法所在的线程名字?
获取当前线程:
public static Thread currentThread():返回对当前正在执行的线程对象的引用。
使用:System.out.println(Thread.currentThread().getName())即可获取当前线程的名称
代码举例:
实体类:
public class Tthread2 extends Thread{
public Tthread2() {
}
public Tthread2(String name) {
super(name);
}
@Override
public void run() {
for(int i=1;i<=5;i++){
System.out.println(getName()+":"+i);
}
}
}
测试类:使用无参构造,调用setName()方法给线程命名:
public class Tdemo2 {
public static void main(String[] args) {
Tthread2 t1 = new Tthread2();
Tthread2 t2 = new Tthread2();
Tthread2 t3 = new Tthread2();
t1.setName("藏獒");
t2.setName("边牧");
t3.setName("阿拉斯加");
t1.start();
t2.start();
t3.start();
}
}
使用带参构造命名线程:
public class Tdemo2 {
public static void main(String[] args) {
Tthread2 t1 = new Tthread2("藏獒");
Tthread2 t2 = new Tthread2("边牧");
Tthread2 t3 = new Tthread2("阿拉斯加");
t1.start();
t2.start();
t3.start();
}
}
输出结果:
线程调度
假如我们的计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到 CPU时间片,也就是使用权,才可以执行指令。那么Java是如何对线程进行调用的呢?
线程有两种调度模型:
分时调度模型 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
抢占式调度模型 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。
Java使用的是抢占式调度模型。
设置和获取线程优先级
两个方法:
public final int getPriority():返回此线程的优先级
public final void setPriority(int newPriority):更改此线程的优先级
上述代码都没有设置优先级,所以会有一个默认的优先级,线程默认优先级为5
根据查看API发现:线程优先级含有一个范围:
public final static int MAX_PRIORITY = 10; 线程可以拥有的最大优先级。
public final static int MIN_PRIORITY = 1; 线程可以拥有的最小优先级。
总结:
1、线程的默认优先级为5
2、线程优先级的范围是1-10
3、线程优先级高仅表示获取时间片的几率会高一些,但不代表绝对会获取到。
代码举例:
实体类:
public class Tthread3 extends Thread{
@Override
public void run() {
for(int i=1;i<=5;i++){
System.out.println(getName()+":"+i);
}
}
}
测试类:
public class Tdemo3 {
public static void main(String[] args) {
Tthread3 t1 = new Tthread3();
Tthread3 t2 = new Tthread3();
Tthread3 t3 = new Tthread3();
t1.setName("藏獒");
t2.setName("边牧");
t3.setName("阿拉斯加");
// 获取优先级
System.out.println(t1.getPriority());
System.out.println(t2.getPriority());
System.out.println(t3.getPriority());
// 设置优先级
t1.setPriority(10);
t2.setPriority(2);
t3.setPriority(1);
t1.start();
t2.start();
t3.start();
}
}
输出结果:
线程控制
线程休眠
public static void sleep(long millis)
代码举例:
实体类:
public class SleepThread extends Thread{
@Override
public void run() {
for(int i=1;i<=5;i++){
System.out.println(getName()+":"+i);
// 输出一次休眠一次
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 休眠一秒钟 1秒=1000毫秒
}
}
}
测试类:
public class SleepDemo {
public static void main(String[] args) {
SleepThread st1 = new SleepThread();
SleepThread st2 = new SleepThread();
SleepThread st3 = new SleepThread();
st1.setName("藏獒");
st2.setName("边牧");
st3.setName("阿拉斯加");
st1.start();
st2.start();
st3.start();
}
}
输出结果:
线程加入
public final void join():
线程对象调用join的目的是让当前线程先执行完毕,完毕后其他的线程再进行执行
代码举例:
实体类:
public class Jthread extends Thread{
@Override
public void run() {
for(int i=1;i<=5;i++){
System.out.println(getName()+":"+i);
}
}
}
测试类:
public class Jdemo {
public static void main(String[] args) {
Jthread j1 = new Jthread();
Jthread j2 = new Jthread();
Jthread j3 = new Jthread();
// 设置名曾
j1.setName("藏獒");
j2.setName("边牧");
j3.setName("阿拉斯加");
// 启动线程
j1.start();
// 将j1设为join。会先执行j1,执行完后j2与j3再进行抢占资源调度
try {
j1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
j2.start();
j3.start();
}
}
输出结果:
注意:join方法的调用必须要在线程执行完之前和线程启动之后,即join的调用必须在被调用的线程start之后。
线程礼让
public static void yield()
暂停当前正在执行的线程,并执行其他线程,他的作用是为了让多个线程之间的运行看起来更加的和谐,但并不保证多个线程一人执行一次
代码举例:
实体类:
public class Ythread extends Thread{
@Override
public void run() {
for(int i=1;i<=5;i++){
System.out.println(getName()+":"+i);
Thread.yield();
}
}
}
测试类:
public class Ydemo {
public static void main(String[] args) {
Ythread y1 = new Ythread();
Ythread y2 = new Ythread();
Ythread y3 = new Ythread();
// 设置名称
y1.setName("藏獒");
y2.setName("边牧");
y3.setName("阿拉斯加");
// 启动线程
y1.start();
y2.start();
y3.start();
}
}
输出结果:
后台线程
又称守护线程。
将此线程标记为daemon线程或用户线程。 当运行的唯一线程都是守护进程线程时,Java虚拟机将退出
Java中有两类线程:用户线程,守护线程
用户线程:在学习多线程之前,运行起来的线程都是用户线程
守护线程:指的是程序在运行的时候,在后天提供了一个通用的服务线程,比如说垃圾回收线程,他就是一个守护线程。并且这种线程并不是一定存在的,所以反过来说,只要程序存在守护线程,程序就不会终止。
设置守护线程:
public final void setDaemon(boolean on)
注意:1、守护线程必须在线程启动之前设置;
2、当运行的程序只有一个线程的时候并且这个线程是守护线程的时候,Java虚拟机退出(程序停止)。
代码举例:
实体类:
public class Dthread extends Thread{
@Override
public void run() {
for(int i=1;i<=50;i++){
System.out.println(getName()+":"+i);
}
}
}
测试类:
public class Ddemo {
public static void main(String[] args) {
Dthread d1 = new Dthread();
Dthread d2 = new Dthread();
Dthread d3 = new Dthread();
d1.setName("藏獒");
d2.setName("边牧");
d2.setDaemon(true);
d3.setName("阿拉斯加");
d3.setDaemon(true);
d1.start();
d2.start();
d3.start();
}
}
输出结果:这里将边牧线程和阿拉斯加线程设置为守护线程
通过结果发现,当其他线程运行完毕后,只剩下守护线程未执行完毕,此时Java虚拟机将退出,但由于是抢占资源调度,所以在最后一段小的时间片内边牧和阿拉斯加抢占资源调用,所以在Java虚拟机停止之前两个守护线程分别抢占资源运行了一小段时间,此时Java虚拟机停止,不在运行。
若其他线程执行未结束,则守护线程继续和其他线程进行抢占资源调度运行,直到其他线程运行完毕或者守护线程在其他线程未执行完毕之前执行完毕,此时将继续执行其他线程直到所有线程完毕。
中断线程
中断线程就是在线程运行或者睡眠时打断该动作,进行其他操作
public final void stop():强制打断当前操作,程序停止。
public void interrupt():打断当前操作,直接运行下一步操作。并抛出异常
代码举例:
实体类:
import java.util.Date;
public class Stopthread extends Thread{
@Override
public void run() {
for(int i=1;i<=5;i++){
System.out.println("开始执行时间:"+ new Date());
// 进项休眠操作,休眠十秒
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("结束时间:"+new Date());
}
}
}
测试类:调用stop方法
public class Stopdemo {
public static void main(String[] args) {
Stopthread s1 = new Stopthread();
// 设置名称
s1.setName("藏獒");
s1.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
s1.stop();
}
}
输出结果:
这里使用stop在休眠三秒后打断当前线程,程序停止,因此后面没有结束时间的输出
调用interrupt方法打断线程:
public class Stopdemo {
public static void main(String[] args) {
Stopthread s1 = new Stopthread();
// 设置名称
s1.setName("藏獒");
s1.start();
try {
Thread.sleep(3000);
s1.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果:
使用interrupt在休眠三秒后打断线程,当前线程打断休眠操作,继续执行输出后面的结束时间。并抛出异常。
线程的生命周期
创建线程的第二种方式:实现Runnable接口
实现步骤:
1、自定义一个类实现Runnable接口
2、实现run()方法
3、创建自定义对象
4、创建Thread线程对象,将自定义的对象作为参数传递到构造方法中
代码举例:
实体类:
public class RunnableImpl implements Runnable {
@Override
public void run() {
for (int i=1;i<=5;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
// 这里由于不是继承Thread类,所以不能直接使用getName获取名称
// 需要使用currentThread获取当前线程对象再调用getName获取名称
}
}
}
测试类:
public class Rdemo {
public static void main(String[] args) {
RunnableImpl runnable = new RunnableImpl();
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.setName("藏獒");
t2.setName("边牧");
t1.start();
t2.start();
}
}
输出结果:
例题:
某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票
代码实现:
这里使用Runable接口实现:
为了更好的模拟卖票,加入sleep延迟。
实体类:
public class TicketRunable implements Runnable{
private int ticket=100;
@Override
public void run() {
while(ticket>0){
System.out.println(Thread.currentThread().getName()+"正在出售第"+(ticket--)+"张票");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试类:
public class TicketDemo {
public static void main(String[] args) {
TicketRunable ticketRunable = new TicketRunable();
Thread t1 = new Thread(ticketRunable);
Thread t2 = new Thread(ticketRunable);
Thread t3 = new Thread(ticketRunable);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
输出结果:
按照正常逻辑,加入sleep延迟后,出现了出售同一张票的情况,还有第0张票的情况,甚至还有出现第-1张票的情况
解释:
出现重复的原因是,CPU的操作是原子性,由于tickets--是两步操作,先赋值输出再-- ,当窗口1赋值输出后,还没有来得及--,这时候窗口2也执行到了这一步,此时tickets的值还没有发生变化,所以出现了相同的票卖了多次。即CPU很小的一段时间片足够运行多次线程
出现第0张票的现象解释:两个或3个窗口同时在tickets的值为1的时候,都进入到if之中,都会进行一次睡眠,当第一个窗口睡眠结束,也打印结束,此时的tickets的值从1变成0,所以当后面的线程睡眠结束打印结果是0
负数的来源是当tickets的值为1的时候,三个线程都进入if语句可能会造成的现象。
第0张票和负数票的原因:线程的执行具有随机性和延迟性导致的,加入sleep后,线程变成阻塞状态,让其他线程执行。
加入延迟操作后出现的问题,其实就称之为:线程安全问题
是否会出现线程安全问题的三个必要条件:
1、是否存在多线程环境
2、是否存在共享数据/共享变量
3、是否有多条语句操作者共享数据/共享变量
若三个条件同时满足,若是出现问题则是正常现象,称之为同步安全问题,也就是线程不安全
解决办法:需要一个办法可以将多条语句操作共享数据的代码包装成一个整体,再某个线程执行过程中,别的线程进不来直到某个线程调用完run()方法后,其他线程才可以进入执行。
Java提供了一个机制用于解决线程安全的问题:同步安全机制
解决方案1:同步代码块
语句格式:synchronized(对象){需要同步的代码;}
这里的对象可以是Object,需要同步的代码就是多条语句操作共享数据的代码
最终加入同步代码块后的代码:
public class TicketRunable implements Runnable{
private int ticket=100;
private Object object=new Object();
@Override
public void run() {
while(true){
synchronized (object){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"正在出售第"+(ticket--)+"张票");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
输出结果:
同步的好处:
解决了线程的安全问题
同步的弊端:
加入一个同步块之后,就相当于加了一把锁,每次进入同步代码块的时候都会去判断一下,无形之中降低了执行效率。
同步代码块上的锁对象是任意对象,但多个线程之间的锁对象要保持一致
同步方法:
将synchronized关键字放在方法的定义上
同步方法的锁对象是this。
静态同步方法的锁对象是class文件对象,是线程类的字节码文件对象,其他的字节码文件不行
代码举例:
实体类:使用同步方法,锁对象是this
public class RunableDemo1 implements Runnable{
private static int ticket=100;
private int i=0;
@Override
public void run() {
while(true){
if(i%2==0){
// 此时这里的锁对象需要和同步方法相同,都是this
synchronized (this){
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在出售第"+(ticket--)+"张票");
}
}
}else{
sellticket();
}
i++;
}
}
// 同步方法:
public synchronized void sellticket(){
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在出售第"+(ticket--)+"张票");
}
}
}
实体类:使用静态同步方法,实体类是线程类的字节码文件
public class RunableDemo1 implements Runnable{
private static int ticket=100;
private int i=0;
@Override
public void run() {
while(true){
if(i%2==0){
// 若这里使用的是静态同步方法,则锁对象为线程类RunableDemo1
synchronized (RunableDemo1.class){
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在出售第"+(ticket--)+"张票");
}
}
}else{
sellticket();
}
i++;
}
}
public synchronized static void sellticket(){
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在出售第"+(ticket--)+"张票");
}
}
}
测试类:
public class TicketDemo1 {
public static void main(String[] args) {
RunableDemo1 runableDemo1 = new RunableDemo1();
Thread t1 = new Thread(runableDemo1);
Thread t2 = new Thread(runableDemo1);
Thread t3 = new Thread(runableDemo1);
t1.setName("大黑");
t2.setName("二黑");
t3.setName("三黑");
t1.start();
t2.start();
t3.start();
}
}
输出结果: