1、文章介绍
- 线程是什么
- 实现创建启动线程的方式1
- 实现创建启动线程的方式2
- Thread类常用方法
- 线程同步3种方式
2、线程是什么
2.1、线程常识引入
- 我们知道CPU是计算机中央处理器,用来处理任务的,那么他是如何处理任务的,我们计算机存在一个任务管理器,查阅线程,
- 一个进程就是一个软件,对应多个线程,多线程软件运行速度比较快,比如迅雷,快播等..下载速度比较快
2.2、什么是线程
- 任务管理器可以有多个进程,每个进程运行的都是可执行程序,一个可执行程序就是一个软件,可执行程序的本质就是在计算机当中运行的一块代码
进程:可以看成是在计算机当中运行的一块代码
线程:可以看成是在计算机当中运行的一小块代码
2.3、线程与进程的关系
- 一个进程中可以有多个线程,至少得有一个线程;
- 上面说一个进程可以狭隘的看成是一大段代码,那其实线程也是一段代码
- 线程是进程中的最小单位;
- 也可以把线程看成是一个轻量级的进程
注意常识 : 计算机安装一个软件,软件是由代码构成,当启动一个软件之后,代码被放入内存中,为什么会放入到内存中,因为内存的读写速度比较快,这时候CPU就开始处理内存当中的任务,也就是当前软件程序[ 代码 ]运行起来了。
2.4、CPU如何处理任务?
- 在单位时间时间片上只能执行一个线程
- CPU看到内存中有很多的线程,CPU在单位时间片(时间片:很微小的时间单位)上高速切换线程执行
2.5、多线程下载软件为什么快
- 问题:很多使用多线程技术开发的软件,下载速度比较快,例如,迅雷.....某些软件,QQ影音,快播..迅雷影音等等...为什么下载速度会比较快
- 假设上面软件都是运行在同一台电脑上面,两款软件运行,肯定是由一个CPU在处理该任务
- CPU处理任务最小单位是线程,CPU是通过资源分配的方式,在多个线程之间,以时间片(时间片:很微小的时间单位)为单位,高速切换内存中要执行的线程任务。
- 在同一个时间片上,只能处理一个线程
- 在CPU的眼中,只看到内存中有很多线程,大家都是平等的,获取到CPU处理的机会是均等的,CPU会平均分配资源给每一个线程
- 假设每个线程执行一分钟,快播软件占用CPU时间为三分钟,迅雷占用CPU处理任务的时间为1分钟,自然快播处理的任务会更多,下载的内容更多
2.6、总结线程的作用
- 线程的作用:看下面两种理解方式:
- 可以将代码中(软件)的某些独立的功能包装起来,单独作为任务交给CPU处理!
- 将需做的某个功能封装成一个线程体,该线程可以独立的获得CPU分配的资源
从而实现多功能同时运行。
3、实现创建启动线程的方式一
3.1、自定义第一个线程场景描述
- 开发一个游戏(LOL),实现的功能一边玩游戏,一边播放背景音乐
3.2、实现流程分析
- 游戏的本质也是软件:该软件包含两项功能
- 玩游戏,暂时使用一个打印语句来代替该功能的演示
- 播放背景音乐,暂时也使用一个打印语句来代替该功能
3.3、代码实现
思考:根据上面的分析如何下手写?
- 将功能代码写在哪里
- 将功能主体代码写到Thread类中run方法里面?
- 如果Thread类当中,写了游戏功能,那么播放音乐怎么办???
- 所以不能写在Thread类当中
- 我们需要自己定义类继承Thread类,不但具有里面的东西,还具有Thread类的特性,自定义类也是一个线程类,然后覆写run方法,然后把我们的代码写在我们覆写的run方法里面,然后启动
- 根据上面的场景我们需要创建哪些类?
- 玩游戏的线程类
- 放音乐的线程类
- 测试类:创建① ②的对象,然后调用start方法启动
- 代码清单:
public class GameThread extends Thread { @Override public void run() { // 包装独立的功能 for (int i = 0; i < 100; i++) { System.out.println(i+" 吃鸡......"); } } }
public class MusicThread extends Thread{ @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(i+" 凉凉....."); } } }
测试代码:
public class Test { public static void main(String[] args) { GameThread gt = new GameThread(); MusicThread mt = new MusicThread(); gt.start(); mt.start(); } }
3.4、小结:创建启动线程的方式一(继承Thread类)
- 先明确我们需要把什么事情封装成一个线程对象
- 自定义一个类 extends Thread
- 覆写run方法,在这里写1步中的代码
- 创建一个自定义类的对象 t
- 启动线程 t.start();
- 注意执行过程:本质是代码执行到一个位置之后,如果切换到另一个线程,在切换回来,那么会从刚才切换走的代码位置继续执行:产生线程安全问题的原因,就在此...
4、线程注意事项
4.1、直接调用run方法和start的区别?
- 可以直接调用run方法,但是没有启动一个独立的线程;
- 只有调用start 才会启动一个独立的线程;
4.2、自己启动的线程和主线程有关系吗?
- 直接写一个最简单的hello word 程序,就有一个主线程
- 一个线程一旦启动就是独立的了,和创建启动它的环境没有直接的包含关系
public class Test2 {
/**
测试主线程执行完毕我们自定义的线程还是会继续执行(前提就是主线程完了,自定义的线程还没有执行 完)
*/
public static void main(String[] args) {
new ThreadTest().start();
for (int i = 0; i < 100; i++) {
System.out.println("main"+i);
}
}
}
class ThreadTest extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("ThreadTest"+i);
}
}
}
5、经典案例:多线程售票示例
5.1、需求分析
思考:上面示例应该这么下手?
1.票,用什么来存放票:票池
可以使用一个变量int num = 50;来表示票池,当卖出一个张票,该变量就自减一次num--
2.本质就是要创建启动线程(流程)
明确需要把什么事情封装成独立的线程对象
卖(一张)票的操作
怎么操作
判断是否有票,如果有票就卖一张
票总数减一
3.怎么实现:
自定义一个类 extends Thread类
实现功能自定义类覆写Thread类当中的run方法,实现伪代码功能
创建线程对象调用start方法启动线程
问题:----------------------->
- 共享问题,用来存放票量的,变量应该如何被多个窗口共享
- 应该自定义几个类?1
之前游戏有不同功能,写了不同类,卖票都是一个功能,所以定义一个类就行了
- 应该创建几个对象?3
三个窗口,可以创建三个对象来表示三个窗口
5.2、代码实现及分析
5.2.1、代码实现版本1:
问题:为什么只卖了三张票 且 票号都是50
- 三张:线程对象run方法当中为线程主体:该程序只会执行一次,所以线程主体程序结束了,线程任务也就完成了。
- 都是票号50:每个线程主体都是独立的,各自使用自己的实例变量初始值都是50
5.2.2、代码版本2:解决售票3张的问题票号固定50:使用while(num>0)直到票池销售完毕
public class Ticket extends Thread {
int num = 50;
@Override
public void run() {
while(num>0){
System.out.println("您的票号 : " + num);
num--;
}
}
}
问题:为什么总售票数量150
- 每个线程体都是独立的,num是实例变量,每个线程对象都有自己的一个num变量,各自销售50张
5.2.3、代码版本3:解决售票数量150问题:给num变量添加static 是的num多个线程对象共同享有一个变量
public class Ticket extends Thread {
static int num = 50;
@Override
public void run() {
while(num>0){
System.out.println("您的票号 : " + num);
num--;
}
}
}
问题:为什么销售的是51张
- 因为线程安全
5.2.4、最终代码:处理线程安全及业务需求,销售一张票应该添加if判断
public class Ticket extends Thread {
static int num = 50;
@Override
public void run() {
while(num>0){
if(num>0){
System.out.println("您的票号 : " + num);
num--;
}
}
}
}
6、实现创建启动线程方式二
6.1、分析线程实现方式2
6.1.1、线程创建方式我们已经体验过一种了,不过线程我们还有其他方式可以实现,Thread impldements Runable这个接口
6.1.2、分析Runable接口与Thread类之间的关系:
6.1.3、Thread线程类本质是实现Runnable接口
- 通过查看API得知,Thread当中的run方法不是来自于自身,而是通过实现Runable接口里面的run方法,从而实现某个类的实例,可以通过线程的方式实现功能,类必须定义一个名为run的无参数方法
- 本质Thread也是通过实现接口来实现线程功能的
- 如果自定义一个类,完全可以通过实现该接口从而,通过线程实现功能
3.1.4、自定义类通过实现Runable的方式来实现线程,如何启动
- 通过实现Runable实现线程的,自定义类,的对象A。放在一个空壳的Thread线程对象当中
- 然后通过该对象来调用start方法启动线程A
代码清单:
public class TicketThread implements Runnable{
private int num = 50;
public void run() {
// 最终的代码
while(num>0){
System.out.println("您的票号是:"+num);
num--;
}
}
}
测试代码:
TicketThread tt = new TicketThread();
Thread t1 = new Thread(tt);
Thread t2 = new Thread(tt);
Thread t3 = new Thread(tt);
t1.start();
t2.start();
t3.start();
问题:
为什么上面没有static,也只销售50张票左右,而没有销售150张,本质其实只创建了一个对象,在被三个线程对象共享
7、继承Thread 和实现Runnable的区别
- 继承有局限,Java中类只能够单继承
- 实现的方式,我们的类在业务上可以继承它本应该有的类,同时可以实现接口变成一个线程类
- 关于数据共享的问题:就看所谓被共享的数据所在的类的对象被创建了几个
8、Thread类
8.1、线程休眠sleep
8.1.1、什么是线程休眠
线程类Thread当中有一个static void sleep(long millis)方法,在指定的毫秒数内让当前正在执行的线程休眠
System.out.println(1);
Thread.sleep(5000); // 休眠5秒钟之后才执行下一个语句
System.out.println(2);
注意 : 当前正在执行的线程就是主线程
8.1.2、线程休眠应用
- 可以做倒计时:代码清单:
// ①创建了一个顶层窗体的对象 JFrame frame = new JFrame(); Button button = new Button(); button.setFont(new Font("宋体", 66, 80)); button.setForeground(Color.red); frame.add(button); // ②设置顶层窗体的大小 frame.setSize(1000, 500); // ③设置顶层窗体的位置居中 frame.setLocationRelativeTo(null); // 居中 // ④设置窗体可见 frame.setVisible(true); for (int i = 600; i >=0; i--) { button.setLabel("你生命剩下的时间:"+i+""); Thread.sleep(1000); }
- 可以用来模拟网络延迟
9、线程同步[解决线程安全问题]
9.1、为什么需要线程同步
- 解决问题: 线程安全问题(例如1单例模式的懒汉模式;例如2多线程模拟多窗口售票-)
9.2、线程同步方式一:同步代码
9.2.1、基本语法结构
synchronized (同步监听对象) {
可能引发线程安全问题的代码
}
上面的结构相当于把{ }中的代码捆绑成一个整体,线程只能够一个一个的进来,执行完一个,下一 个才能进来
9.2.2、语法特点:
- 上面的同步监听对象可以是任意的对象;
- 保证所有的线程共享一个同步监听对象的;也就是保证被同步监听对象是被所有线程共享的。
- 很多时候可以写this,但是请先参照②
- 常用的方式:使用类的字节码对象 XXX.class
9.2.3、示例演示:
- 同步代码块方式①同步整块代码
synchronized (Ticket.class) {
while(num > 0){ // 循环判断,是否有就卖
System.out.println(getName()+" 您的票号是:"+num);
num--;
}
}
结果:一个线程卖完
原因:把整个循环同步了,一旦有一个线程进来就会执行完里面的代码,其它线程才能进来
重新思考:我们需要真正的同步什么代码? 保证每销售一张票的操作是同步的就可以了
2. 同步代码块方式② 同步关键业务代码
synchronized (TicketThread.class) {
if(num>0){
System.out.println(this.getName()+"您的票号是:"+num);
num--;
}
}
结果:有0 -1 感觉判断失效了
原因:假设仅剩下一张票,所同步内容没有判断是否还有票,当第二线程易进入while结构体,但其他线程正在执行销售最后一张票,之后num=0;但第二线程已经进入while结构体,会执行同步代码,销售num=0的这张票
3. 同步代码块方式③同步关键业务代码
public class TicketThread extends Thread{
private static int num = 50;
public void run() {
while(num>0){ // 只应该同步销售的一张票的操作代码
synchronized (TicketThread.class) {
// 下面的代码是销售一张票,每卖一张票的前提判断是否有票
if(num>0){
System.out.println(this.getName()+"您的票号是:"+num);
num--;
}
}
}
}
}
或者:将关键业务代码提出来包装成一个方法
public class TicketThread extends Thread{
private static int num = 50;
public void run() {
while(num>0){
saleOne();
}
}
private void saleOne(){ // 写一个方法:销售一张票
// 只应该同步销售的一张票的操作代码
synchronized (TicketThread.class) {
// 下面的代码是销售一张票,每卖一张票的前提判断是否有票
if(num>0){
System.out.println(this.getName()+" 您的票号是:"+num);
num--;
}
}
}
}
9.3、线程同步方式二:同步方法
- 就是在需要被同步的方法上面加关键字 synchronized
- 加的位置 :在返回值类型的前面
- 不需要也不能够显示的写同步监听对象
- 如果是一个非static的方法,那么同步监听对象就是this;
- 如果是static修饰的方法,那么同步监听对象就是当前方法所在的类的字节码对象
售票示例同步方法代码清单:
public class TicketThread implements Runnable{
private int num = 50;
public void run() {
while(num>0){
saleOne();
}
}
synchronized private void saleOne(){
if(num>0){
System.out.println(" 您的票号是:"+num);
num--;
}
}
}
9.4、线程同步方式三:锁机制
1. 学习方式(查找API文档方式)锁-- Lock(API)接口 ---XXX实现类
public class Ticket extends Thread {
public Ticket(String name) {
super(name);
}
static Lock lock = new ReentrantLock(); // 必须保证多个线程访问的是同一把锁
static int num = 50;
@Override
public void run() {
while(num > 0){ // 循环判断,是否有就卖
lock.lock();
try {
if(num>0){ // 判断卖一张票的操作
System.out.println(getName()+" 您的票号是:"+num);
num--;
}
} finally {
lock.unlock();
}
}
}
}
2. 结果: 没有同步到
3. 原因:lock是一个实例变量,因此创建了3个TicketThread对象就有3个lock对象,没有同步到
4. 解决办法: static lock ; 或者使用实现的方式