前言
本篇博文是本人回顾多线程写的随笔。本篇博客的结构简单适用于初学者,各种细节根据本人理解所述,内容十分到位,希望对在学习多线程的亲们有所帮助!
主要介绍有以下几点:
1.进程与线程
2.线程的作用
3.JVM的主线程
4.多线程的第一例
5.线程休眠
6.多线程内部类格式
7.线程基本属性
8.异常处理机制与多线程
9.继承Thread类
10.多线程原理
11.同步机制
12.同步方法
多线程
多线程的概述
1.进程与线程
进程就是正在运行的程序
进程是可以独立运行的
进程有自己独立的内存空间,不与其他进程共享数据
一个进程里面可以有n个线程
进程的理解:比如说IDEA编码软件,开启这个软件,IDEA这个进程就会在CPU上跑了(必须开启才叫进程)
线程就是进程中的控制单元
线程是进程中一个子任务执行者
线程不能独立运行,必须依赖进程而运行
同一个进程中的多个线程之间是可以共享数据的
2.线程的作用
多线程提高了程序的执行效率,因为它可以让程序中的多个任务并发执行
阅读一个小例子理解线程:可以把线程比喻成任务执行者(人)
如果我们的进程中只有一个人但是有两个任务,那么这个人需要先执行完第一个任务再去执行第二个任务,这个种就属于串行化的完成任务。
如果我们的进程中有两个人,那么可以给第一个人分配一个任务,给第二人分配一个任务,两个人同时执行任务,这种属于并发
3.JVM的主线程
JVM中有一个主线程,它的任务就是执行main()方法
当程序运行时JVM会为我们分配主线程
主线程的工作时执行mian()方法的内容
当main()结束后,主线程结束
public class Test {
//程序运行时JVM会为我们分配主线程
//主线程的任务就是执行main方法中的内容
//main方法运行结束,主线程结束
public static void main(String[] args) {
//方法体
}
}
4.多线程的第一例
编写多线程程序的步骤分析
多线程程序实现
1.创建任务对象(创建一个类实现Runnable接口【我们可以把它看成是任务接口】)
2.创建线程对象(Thread)
3.把任务交给线程(Runnable —> Thread)
4.启动线程(start())
1.Runnable接口(任务接口)
Runnable接口表示线程的任务,他只有一个方法 void run() 。
注意:
Runnable不是线程,它只是线程的任务,如同main()方法是主线程的任务一样
2.Thread类(线程类)
Thread类表示线程类,可以把Thread理解成执行任务的人(执行者),在创建Thread对象时我们需要把任务交给Thread对象
每个Thread对象必须与一个Runnable对象绑定,即执行者对象与任务对象绑定在一起
//实现Runnable接口的类 我把它简称为任务类
MyThread task = new MyThread();
//创建Thread类 执行者(人)
//把任务交给人去完成(将任务对象传入人对象的构造方法中)
Thread people = new Thread(task);
3.启动线程
创建类线程对象并不代表线程就开始执行了,想让线程开始执行任务还需要调用Thread类的start()方法启动线程,这时线程才是正真开始工作
//实现Runnable接口的类 我把它简称为任务类
MyThread task = new MyThread();
//创建Thread类 执行者(人)
//把任务交给人去完成(将任务对象传入人对象的构造方法中)
Thread people = new Thread(task);
//开始执行任务 (开启线程)
people.start();
多线程案例分析
1.多线程案例(该案例演示并发)
任务类:实现Runnable接口的类
public class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("我是新的线程,我正在执行循环打印:测试测试");
}
}
}
测试类:主线程(mian)和新创建的线程(new Thread(Runnable target))
public class Test {
public static void main(String[] args) {
MyThread task = new MyThread();
Thread people = new Thread(task);
people.start();
for (int i = 0; i < 10; i++) {
System.out.println("我是主线程,我正在执行打印:测试测试");
}
}
}
执行结果如图所示:
结果分析:
根据代码的运行规则可以知道,编写的程序是由上往下执行的。但是可以很明显的看到这段代码并没有。
在一个线程中的代码执行规则是从上往下执行,如过有了多个线程同时执行,那么它们之间就会发生抢夺CPU执行自己任务(也就是代码),你抢一下,我抢一下,这种就属于并发
5.线程休眠
Thread类的静态方法:static void sleep(long)方法可以让当前线程休眠指定的毫秒,可以把它放到线程任务中,无论那个线程执行到这行代码多会休眠一会
注意:
该方法声明了InterrupedException异常,所以需要处理
案例说明
测试类:
public class Test01 {
public static void main(String[] args) {
/**
* 创建任务类对象,将任务交给执行者
* 执行者开始执行任务 (开启线程)
*/
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
for (int i = 0; i < 30; i++) {
System.err.println("主线程执行");
try {
System.err.println("我是主线程,我需要休眠1秒.....");
Thread.sleep(1000); //创建休眠定时器,设置休眠时间
System.err.println("我是主线程,我休眠了1秒,现在我继续开始执行任务了...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
任务类
public class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("新的线程正在执行任务");
try {
System.out.println("我是新的线程,我需要休眠1秒....");
Thread.sleep(1000); //创建休眠定时器,设置休眠时间
System.out.println("我是新的线程,我休眠了1秒,现在我继续开始执行任务了...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
执行结果
6.多线程内部类格式
也是创建线程的一种方式,与前面的那一种一样,使用时看个人喜好
代码演示:
public class Test02 {
public static void main(String[] args) {
//使用内部类的方式创建一个线程,并开启这个线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//定义一个循环,模拟这个新的线程执行的任务
for (int i = 0; i < 30; i++) {
System.out.println("我是使用内部类的方式创建的线程,我正在执行任务");
}
}
});
thread.start(); //开启线程
//定义一个循环,模拟主线程执行的任务
for (int i = 0; i < 30; i++) {
System.err.println("我的主线程,我正在执行任务");
}
}
}
测试结果:
7.线程基本属性
1.线程名称
Java提供了设置名称的构造方法,一旦确定名称就不能修改,所以Thread并没有提供setName()方法
代码演示
public class Test02 {
public static void main(String[] args) {
//创建任务类
MyThread myThread = new MyThread();
//将任务类交给执行者,并给这个执行者起一个名字(这个线程)
Thread thread = new Thread(myThread,"FirstNewThread");
//通过Thread提供的非静态方法getName()方法获取线程名字
String threadName = thread.getName();
System.out.println("这个线程的名字为:"+threadName); //打印结果为:【这个线程的名字为:FirstNewThread】
}
}
2.当前线程
一个任务可能会被多个线程执行,有时候与当前正在执行任务的线程交互,这就需要在任务中获取当前线程。Thread类的静态方法currentThread() 可以获取当前线程
代码演示:当前线程正在执行当前任务
任务类:MyThread
public class MyThread implements Runnable{
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println("新的线程:" + threadName + "正在执行任务");
}
}
测试类
public class Test02 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread,"FirstNewThread");
thread.start();
String thisThreadName = Thread.currentThread().getName();
System.out.println("主线程:" + thisThreadName + "正在执行任务");
}
}
测试结果
8.异常处理机制与多线程
当某一个线程抛出异常时,JVM只回终止抛出异常的线程中,而不是终止所有线程。
大白话描述:那个线程抛出了异常,JVM就终止那个线程
Runnable接口中的run()方法并没有声明异常,这说明run()方法只能抛出RuntimeException(这个大家应该回很熟悉:运行时异常),当run(方法抛出异常后JVM回终止该线程)
代码演示
测试类:
public class Test02 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread, "FirstNewThread");
thread.start();
for (int i = 0; i < 30; i++) {
System.err.println("主线程:" + Thread.currentThread().getName() + "正在执行任务" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
任务类:MyThread
public class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("MyThread:" + "线程" + Thread.currentThread().getName() + "执行" + i);
try {
//设置线程休眠时间,处理异常
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//判断for循环 如果i的值等于8就抛出运行是异常,此时JVM就会终止这个线程
if (i > 8) {
throw new RuntimeException();
}
}
}
测试结果:
结果分析:
根据编写的代码可以知道,在新的线程的run()方法中定义了一个for循环,循环打印语句,运行到了休眠代码时,休眠。try catch掉编译时报的错误,if判断for循环的数据,设定抛出异常,测试类中也是定义了一个for循环休眠打印数据,最后开启进程(运行JVM开启进程),线程开始执行(main和新创建的线程),当新的线程执行到if条件满足条件时抛出运行时异常,JVM终止了这个线程,而主线程并没有受到影响
9.继承Thread类
上面已经介绍到Thread是 执行类,Runnable是任务,创建Thread对象时把Runnable接口类对象指定给Thread,这样执行者就与任务绑定在一起了,
其实在创建Thread类对象时可以不为其指定Runnable接口实现类对象,而且Thread类有自己的默认任务,只不过默认任务是空的而已,但我们可以继承Thread类,然后重写Thread类的run()方法的方式来为线程指定任务
代码演示
测试类
public class Test03 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.setName("FirstNewThread");
myThread.start();
for (int i = 0; i < 10; i++) {
System.err.println("main:" + Thread.currentThread().getName() + "线程正在执行");
}
}
}
MyThread类继承Thread类
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("MyThread:"+Thread.currentThread().getName()+"线程正在执行");
}
}
}
测试结果
10.多线程原理
1.多任务操作系统
CPU就像是计算机的大脑,所有的进程都是由CPU运行的,支持同时运行多个进程的操作系统就是多任务任务系统,现在所有知名的操作系统都是多任务系统
2.抢占式
阅读一下列子理解抢占式:
假如你现在在你的电脑上开启了多个进程,比如IDEA,网易云音乐,QQ,微信,DBeaver,你在使用的时候好像感觉这几个进程是在同时运行,其实不是,正真工作的只有一个CPU,它在这几个进程之间调整切换(或者说快速切换),通常来说CPU运算的速度是非常快的,如果开启的进程过多时,CPU切换的周期过慢,那么你就会感觉到电脑卡。
还有一种就是如果你的电脑的CPU是多核的,就好比有多个CPU,对于单核CPU开启10个进程,相当于一个人完成10个任务,如果是双核的CPU,就相当于两个人完成10个任务
3.多线程与多任务
多线程和多任务是同一个道理,系统会随机分配CPU给某个线程,这时这个线程得到了唯一的CPU(对于单核CPU而言),运行一段时间后需要主动放弃运行权,让CPU空闲,这时系统再把运行权随机分配给其他线程,如果线程不主动放弃运行权,系统会过一段时间后强行剥夺运行权给其他线程。通常线程使用sleep()等方法主动放弃运行权
11.同步机制
进程之间是不能共享数据的,但是线程可以
线程安全问题
多线程之间共享数据是存在风险的,因为线程间可能会相互干扰,导致共享数据出现问题。
以下介绍几点为线程安全,其他的都为不安全
1.共享对象没有存取能力的,多个线程共享它是安全的
2.对象没有成员变量
3.对象有成员变量,但是成员变量的类型都为长量
4.多线程只对共享对象做取操作,没有存操作,共享也是安全的
同步代码快
同步代码块可以解决线程之间共享数据的问题。
原理是:当某些代码不希望多个线程同步执行时,就让线程排队执行即可,这需要指定一个对象为锁,以及一个区域作为互斥区域
Java中使用同步代码块的语法如下:
synchronized (obj){ //obj表示任何引用类型对象
......
}
互斥区域
同步代码块的大括号指定互斥区域,只有这个区域才需要排队执行
对象锁
obj就是对象锁,它控制着互斥区域,在Java中任何对象都有同步锁定和未锁定状态,而这一切都不需要开发人员做什么,都由JVM自动控制
obj未锁定状态:当obj为未锁定状态时,说明没有线程在执行互斥区域的代码,这时线程对象可以获得对象锁,所谓的对象锁就是把obj的状态从未锁定变成锁定
obj锁定状态:当obj为锁定状态时,其他线程无法执行互斥区域代码,这时线程就进入到等待对象锁状态,我们称之为等待状态,直到前一个线程离开互斥区域,obj从锁定到未锁定,其它的线程才能获得对象锁
多线程经典的卖票案例演示同步代码块的作用
需求:
分四个窗口,去卖30张票(卖多少都可以,我这里是为了能截到所有数据给了一个较小的数据)
演示出现线程安全问题
测试类
public class Test04 {
public static void main(String[] args) {
Thread thread1 = new Thread(new Station(),"一号");
Thread thread2 = new Thread(new Station(),"二号");
Thread thread3 = new Thread(new Station(),"三号");
Thread thread4 = new Thread(new Station(),"四号");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
任务类
public class Station implements Runnable{
//定义全局变量记录票数
public static int ticket = 30;
@Override
public void run() {
while (true) {
//判断票数
if (ticket > 0) {
System.out.println("Station:" + Thread.currentThread().getName() + "售票口,售出了第" + (31-ticket) + "张票");
//卖出去了一张票 总票数减一
--ticket;
} else {
System.out.println("票已售完");
break;
}
}
}
}
测试结果
看图分析
出现线程安全问题:可能会出现同票,0票,负票,出现线程安全性问题,不是我想要的数据,原因在于有多个线程在操作同一共享数据就是票数
解决方案:
用同步来解决:同步就可以让CPU在某段时间内只让一个线程进来做事情,其他线程不能进来,等你做完了,才能一个个进行做事情,排队单线程的
使用同步代码块解决线程安全问题 (为了方便观察数据我把票数该到30)
测试类
public class Test04 {
public static void main(String[] args) {
Thread thread1 = new Thread(new Station(),"一号");
Thread thread2 = new Thread(new Station(),"二号");
Thread thread3 = new Thread(new Station(),"三号");
Thread thread4 = new Thread(new Station(),"四号");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
任务类
public class Station implements Runnable{
//定义全局变量记录票数
public static int ticket = 30;
@Override
public void run() {
while (true) {
synchronized (Station.class) {
//判断票数
if (ticket > 0) {
System.out.println("Station:" + Thread.currentThread().getName() + "售票口,售出了第" + (31-ticket) + "张票");
//卖出去了一张票 总票数减一
--ticket;
} else {
System.out.println("票已售完");
break;
}
}
}
}
}
测试结果
看图分析
同步代码块完美的解决了线程的安全问题,线程在抢夺CPU执行任务,当一个线程抢夺到CPU并且执行到了互斥区域时,此时的CPU归那一个线程执行,只有当该线程执行完,CPU再次被释放,线程再次抢夺CPU。
12.同步方法
除了同步代码块,Java还提供了同步方法,同步方法的语法如下
public synchronized void fun() {
}
对象锁:当某个线程调用了某个对象的某个同步方法,那么这个对象就被锁定,其他线程再使用该对象调用任何同步方法都会进入等待状态
排斥区域:某个对象的所有同步方法,而不是你个同步方法,也不是非同步方法
代码演示
测试类
public class Test05 {
public static void main(String[] args) {
final SyncClass syncClass = new SyncClass();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开始....");
syncClass.fun01();
System.out.println(Thread.currentThread().getName()+"结束....");
}
},"线程一").start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开始....");
syncClass.fun02();
System.out.println(Thread.currentThread().getName()+"结束....");
}
},"线程二").start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开始....");
syncClass.fun03();
System.out.println(Thread.currentThread().getName()+"结束....");
}
},"线程三").start();
}
}
SyncClass类:该类中编写了多个同步方法
public class SyncClass {
public synchronized void fun01(){
System.out.println(Thread.currentThread().getName()+"fun01.......");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void fun02(){
System.out.println(Thread.currentThread().getName()+"fun02.......");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void fun03(){
System.out.println(Thread.currentThread().getName()+"fun03.......");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试结果
看图分析结果
程序运行线程开始抢夺CPU,由图不难看出来,首先是线程一抢到了CPU并执行了它run方法中对象调用的同步方法,此时同步方法运行到了方法内部的线程休眠代码,线程一休眠了5秒这时候线程二和线程三开始强夺CUP,但是线程一没有完成它要完成的任务同步方法锁定对象,其他线程进入等待状态,OK,过了一会线程一休眠醒来,继续执行,执行完,释放CPU,接下来线程二和线程三开始抢夺CPU