系统化学习多线程(一)
大纲
-------------------------学前必读----------------------------------
学习不能快速成功,但一定可以快速入门
整体课程思路:
1.实践为主,理论化偏少
2.课程笔记有完整的案例和代码,(为了学习效率)再开始之前我会简单粗暴的介绍知识点案例思路,
有基础的同学听了之后可以直接结合笔记写代码,
如果没听懂再向下看视频,我会手把手编写代码和演示测试结果;
3.重要提示,学编程和学游泳一样,多实践学习效率才高,理解才透彻;
4.编码功底差的建议每个案例代码写三遍,至于为什么...<<卖油翁>>...老祖宗的智慧
-------------------------------------------------------------------------
1.线程
1.1.什么是线程
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。(来自百度百科)
一个进程可以有很多线程,每条线程并行执行不同的任务。
1.2.多线程hello word
需求:模拟在计算上一边听歌一边打游戏
三种实现方案如下:
1 package com.wfd360.thread; 2 3 import com.wfd360.thread.demo01.GameRunnable; 4 import com.wfd360.thread.demo01.MusicRunnable; 5 import com.wfd360.thread.demo02.GameThread; 6 import com.wfd360.thread.demo02.MusicThread; 7 import org.junit.Test; 8 9 /** 10 * @author 姿势帝-博客园 11 * @address https://www.cnblogs.com/newAndHui/ 12 * @WeChat 851298348 13 * @create 05/03 5:27 14 * @description 需求分析: 15 * 1.模拟一边打游戏一边听音乐,在控制台打印输出模拟 16 * 2.把两个业务封装成独立的线程,实现接口Runnable或继承Thread,通过看源码你会发现Thread类实现了接口Runnable,使用本质上这两种方法时一样的。 17 * 3.Thread类提供两个方法,线程主题方法run,启动线程方法start 18 */ 19 public class TestDemo { 20 /** 21 * 方式1:实现接口Runnable 22 */ 23 @Test 24 public void testRunnable() throws InterruptedException { 25 System.out.println("-------test start-------"); 26 // 实例对象 27 MusicRunnable music = new MusicRunnable(); 28 GameRunnable game = new GameRunnable(); 29 // 创建线程 30 Thread musicThread = new Thread(music); 31 Thread gameThread = new Thread(game); 32 // 启动线程 33 musicThread.start(); 34 gameThread.start(); 35 System.out.println("--------等待其他线程执行--------------"); 36 Thread.sleep(5 * 1000); 37 System.out.println("-------test end-------"); 38 } 39 40 /** 41 * 方式2:继承Thread 42 */ 43 @Test 44 public void testThread() throws InterruptedException { 45 System.out.println("-------test start-------"); 46 // 创建线程 47 MusicThread musicThread = new MusicThread(); 48 GameThread gameThread = new GameThread(); 49 // 启动线程 50 musicThread.start(); 51 gameThread.start(); 52 System.out.println("--------等待其他线程执行--------------"); 53 Thread.sleep(5 * 1000); 54 System.out.println("-------test end-------"); 55 } 56 57 /** 58 * 方式3:简写,这种写法一般我们在做模拟测试的使用,在正式代码中建议不使用,可读性较差 59 */ 60 @Test 61 public void testThreadSimple() throws InterruptedException { 62 System.out.println("-------test start-------"); 63 // 创建线程 64 Thread musicThread = new Thread(() -> { 65 for (int i = 0; i < 100; i++) { 66 System.out.println("=======听音乐中============" + i); 67 } 68 }); 69 Thread gameThread = new Thread(() -> { 70 for (int i = 0; i < 100; i++) { 71 System.out.println("=======打游戏中============" + i); 72 } 73 }); 74 // 启动线程 75 musicThread.start(); 76 gameThread.start(); 77 System.out.println("--------等待其他线程执行--------------"); 78 Thread.sleep(5 * 1000); 79 System.out.println("-------test end-------"); 80 } 81 }
实现接口Runnable
1 package com.wfd360.thread.demo01; 2 3 /** 4 * @author 姿势帝-博客园 5 * @address https://www.cnblogs.com/newAndHui/ 6 * @WeChat 851298348 7 * @create 05/03 5:31 8 * @description 9 */ 10 public class GameRunnable implements Runnable { 11 @Override 12 public void run() { 13 for (int i = 0; i < 100; i++) { 14 System.out.println("=======打游戏中============" + i); 15 } 16 } 17 }
1 package com.wfd360.thread.demo01; 2 3 /** 4 * @author 姿势帝-博客园 5 * @address https://www.cnblogs.com/newAndHui/ 6 * @WeChat 851298348 7 * @create 05/03 5:29 8 * @description 9 */ 10 public class MusicRunnable implements Runnable { 11 @Override 12 public void run() { 13 for (int i = 0; i < 100; i++) { 14 System.out.println("=======听音乐中============"+i); 15 } 16 } 17 }
继承Thread
1 package com.wfd360.thread.demo02; 2 3 /** 4 * @author 姿势帝-博客园 5 * @address https://www.cnblogs.com/newAndHui/ 6 * @WeChat 851298348 7 * @create 05/03 6:00 8 * @description 9 */ 10 public class GameThread extends Thread { 11 @Override 12 public void run() { 13 for (int i = 0; i < 100; i++) { 14 System.out.println("-------游戏中----------"+i); 15 } 16 } 17 }
1 package com.wfd360.thread.demo02; 2 3 /** 4 * @author 姿势帝-博客园 5 * @address https://www.cnblogs.com/newAndHui/ 6 * @WeChat 851298348 7 * @create 05/03 6:00 8 * @description 9 */ 10 public class MusicThread extends Thread { 11 @Override 12 public void run() { 13 for (int i = 0; i < 100; i++) { 14 System.out.println("-------音乐中----------" + i); 15 } 16 } 17 }
总结
启动线程两种方式:
1.通过继承Thread类
2.实现Runnable接口
使用哪种方式更好?
区别:
一个类如果继承了其他类,就无法在继承Thread类,在Java中,一个类只能继承一个类,而一个类如果实现了一个接口,还可以实现其他接口,接口是可以多实现的,所以说
Runable的扩展性更强,但是继承的方式更简单,个人建议一般情况下使用Thread;
实现接口Runnable或继承Thread,通过看源码你会发现Thread类实现了接口Runnable,使用本质上这两种方法是一样的
启动线程流程:
创建启动线程的方式一:继承Thread类
1.将业务方法封装成线程对象,自定义类t extends Thread类;
2.覆写run方法: 覆写第一步中的run方法;
3.创建自定义对象t
4.启动线程 t.start();
创建启动线程方式二:实现Runnable接口
1.将业务方法封装成线程对象,自定义类t implements Runnable接口;
2.实现第一步中的run方法
3.创建自定义对象t
4.启动线程 new Thread(t).start();
1.3.对主线程与创建线程执行顺序的理解
问题:
直接写一个简单的HelloWorld 程序,有没有线程?
==>有一个主线程,在垃圾回收的时候,有gc 线程。
1 package com.wfd360.thread; 2 3 import org.junit.Test; 4 5 /** 6 * @author 姿势帝-博客园 7 * @address https://www.cnblogs.com/newAndHui/ 8 * @WeChat 851298348 9 * @create 05/04 11:09 10 * @description <p> 11 * 问题: 12 * 直接写一个简单的HelloWorld 程序,有没有线程? 13 * ==>有一个主线程,在垃圾回收的时候,有gc 线程。 14 * 结论:一旦线程启动起来之后就是独立的,和创建环境没有关系; 15 * 启动线程不能直接调用run方法,必须调用start方法; 16 * </p> 17 */ 18 public class TestDemo02 { 19 /** 20 * 如果把创建线程放在循环语句的 下 面,会交替出现吗 21 * ==>否,因为主线程执行完成后才会启动hello线程 22 * 23 * @throws Exception 24 */ 25 @Test 26 public void test1() throws Exception { 27 System.out.println("---test start-------"); 28 // 执行主线程 29 for (int i = 0; i < 100; i++) { 30 System.out.println("-----test1--------" + i); 31 } 32 // 启动hello线程 33 new HelloThread().start(); 34 System.out.println("=======等待执行完成==========="); 35 Thread.sleep(5 * 1000); 36 System.out.println("---test end-------"); 37 } 38 39 /** 40 * 如果把创建线程放在循环语句的 上 面,会交替出现吗 41 * ==>可能会,可能不会,可能出现for循环完之后,线程还没有启动完; 42 * 43 * @throws Exception 44 */ 45 @Test 46 public void test2() throws Exception { 47 System.out.println("---test start-------"); 48 // 启动hello线程 49 new HelloThread().start(); 50 // 执行主线程 51 for (int i = 0; i < 100; i++) { 52 System.out.println("-----test1--------" + i); 53 } 54 System.out.println("=======等待执行完成==========="); 55 Thread.sleep(5 * 1000); 56 System.out.println("---test end-------"); 57 } 58 59 /** 60 * 采用内部类的方式定义一个hello线程对象 61 */ 62 class HelloThread extends Thread { 63 @Override 64 public void run() { 65 for (int i = 0; i < 100; i++) { 66 System.out.println("-----HelloThread--------" + i); 67 } 68 } 69 } 70 }
结论:一旦线程启动起来之后就是独立的,和创建环境没有关系;
启动线程不能直接调用run方法,必须调用start方法;
1.4.对sleep方法的理解
package com.wfd360.thread; /** * @author 姿势帝-博客园 * @address https://www.cnblogs.com/newAndHui/ * @WeChat 851298348 * @create 05/04 11:34 * @description <p> * Thread类的方法: * static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行); * </p> */ public class TestSleep { /** * 做一个简易倒计时,10秒钟,控制台每一秒输出一个数字,如10,9,8,7.....0 */ public static void main(String[] args) throws Exception { System.out.println("---test start-------"); for (int i = 10; i >= 0; i--) { Thread.sleep(1 * 1000); System.out.println(i); } System.out.println("---test end-------"); } }
1.5.线程名称的设置与获取
继承方式
简单需求:使用多线程模拟多窗口售票
1 package com.wfd360.thread.demo03Ticket; 2 3 /** 4 * @author 姿势帝-博客园 5 * @address https://www.cnblogs.com/newAndHui/ 6 * @WeChat 851298348 7 * @create 05/04 11:55 8 * @description <p> 9 * 模拟多线程售票 10 * </p> 11 */ 12 public class TicketThread extends Thread { 13 // 假定票总是100张 14 private static Integer num = 100; 15 16 @Override 17 public void run() { 18 // 只要有票就一直售票 19 while (num > 0) { 20 System.out.println("正在出售第" + num + "张票"); 21 --num; 22 } 23 System.out.println("===售票结束==="); 24 } 25 }
test
/** * 测试模拟三个窗口售票 * @throws InterruptedException */ @Test public void testTicketThread() throws InterruptedException { System.out.println("---test start-------"); // 模拟多3个窗口售票 TicketThread ticketThread1 = new TicketThread(); TicketThread ticketThread2 = new TicketThread(); TicketThread ticketThread3 = new TicketThread(); // 启动线程售票 ticketThread1.start(); ticketThread2.start(); ticketThread3.start(); System.out.println("======等待售票============"); Thread.sleep(5 * 1000); System.out.println("---test end-------"); }
结果:
1.在售票过程中不能区分售出的票是那个窗口售出的,解决通过线程名称判断
2.有重复售出的票(后面的线程同步解决)
解决第一个问题,设置获取线程名称,通过Thread对象里面自带的getName,setName方法
具体代码
设置线程名称
获取线程名称
上面讲了继承的方式获取线程名称,那么实现接口Runnable的方式怎么获取设置勒
继承Thread的方式,可以通过getName的方式获取当前线程的名称?
那使用Runnable的方式,能通过getName获取嘛?
getName方法是Thread类的,但是TicketThread现在并没有继承Thread类,而是实现了Runnable接口.
问题:如果实现Runnable接口,怎么获取线程名称?
思考:TicketThread类里面的代码要执行,它肯定存在于某个线程中, 就比如写个helloword打印语句,是不是也处于一个主线程中,那这里怎么获取线程名称?
通过动态获取,当程序正在执行的时候,获取当前正在执行的线程名称。怎么获取?
在Thread类里面有个静态的方法currentThread() 方法,返回当前正在执行的线程引用;
Thread.currentThread().getName
那怎么设置线程名称?
Thread类里面有个name字段,相当于Thread类把它包装了一下:
通过源码可以发现,构造方法里面还有可以传一个名字:
具体实现代码如下
总结:
继承方式设置\获取线程名称通过 Thread对象里面的 setName,getName方法;
实现接口方式设置名称通过 new Thread('线程实例对象', "线程名称"),获取线程名称通过:Thread.currentThread().getName
1.6.Thread的join方法
void join() 方法 :等待该线程终止
void join(long millis) 方法 :等待该线程终止的时间最长为millis毫秒
需求: 当主线程运行到20的时候(i =20)的时候,让JoinThread线程加进来直到执行完成,在执行主线程.
1 package com.wfd360.thread; 2 3 import org.junit.Test; 4 5 /** 6 * @author 姿势帝-博客园 7 * @address https://www.cnblogs.com/newAndHui/ 8 * @WeChat 851298348 9 * @create 05/04 6:31 10 * @description 11 */ 12 public class Test05Join { 13 /** 14 * 需求: 15 * 当主线程for循环到i=20时,等JoinThread线程执行完成后,在执行for循环的线程 16 * @throws InterruptedException 17 */ 18 @Test 19 public void testJoinThread() throws InterruptedException { 20 System.out.println("---test start-------"); 21 // 开启线程 22 JoinThread thread = new JoinThread(); 23 thread.start(); 24 // 循环打印线程 25 for (int i = 0; i < 100; i++) { 26 System.out.println("======testJoinThread=========="+i); 27 Thread.sleep(1); 28 if (i==20){ 29 // 等线程JoinThread执行完成 30 thread.join(); 31 } 32 } 33 System.out.println("=============等待线程执行完成==================="); 34 Thread.sleep(10*1000); 35 System.out.println("---test end-------"); 36 } 37 38 class JoinThread extends Thread { 39 @Override 40 public void run() { 41 for (int i = 0; i < 100; i++) { 42 System.out.println("=====JoinThread=======" + i); 43 // 模拟处理很多业务耗时1毫秒 44 try { 45 Thread.sleep(1); 46 } catch (InterruptedException e) { 47 e.printStackTrace(); 48 } 49 } 50 } 51 } 52 }
1.7.线程优先级
直接上代码
1 package com.wfd360.thread; 2 3 /** 4 * @author 姿势帝-博客园 5 * @address https://www.cnblogs.com/newAndHui/ 6 * @WeChat 851298348 7 * @create 05/05 8:17 8 * @description <p> 9 * 1.==>线程优先级的理解: 10 * 线程的优先级和生活中类似,高优先级线程的执行优先于低优先级线程; 11 * 并不是绝对的,可能优先级高的线程优先 比 优先级低的线程先执行,只能说,高优先级的线程优先执行的几率更多; 12 * (比如两个线程,一个优先级高,一个优先级低,如果一共运行一个小时,优先级高的线程执行远远大于优先级低的但是并不是说优先级高的先执行完, 13 * 在执行优先级低的) 14 * 2.==>重新设置线程优先级 15 * int getPriority() 返回线程的优先级。 16 * void setPriority(int newPriority) 更改线程的优先级。Java线程的优先级从1到10级别,值越大优先级越高. 17 * 3.==>线程的默认优先级受创建线程的环境影响,默认值5,自定义线程的默认优先级和创建它的环境的线程优先级一致 18 * </p> 19 */ 20 public class Test06Priority { 21 /** 22 * 测试获取线程优先级,设置线程优先级,验证线程优先级受创建环境影响 23 * @param args 24 */ 25 public static void main(String[] args) { 26 Thread threadMain = Thread.currentThread(); 27 // 获取默认优先级数字 28 System.out.println("main线程默认优先级:" +threadMain.getPriority());// 5 29 // 重新设置默认优先级数字 30 threadMain.setPriority(8); 31 // 再次重新获取优先级数字 32 System.out.println("main线程修改后的优先级:" +threadMain.getPriority());// 8 33 // 创建一个线程查看优先级 34 Thread thread = new Thread(); 35 System.out.println("thread线程的优先级:" +thread.getPriority());// 8 受创建环境影响 36 } 37 }
1.8.后台线程,即守护线程
直接看代码
1 package com.wfd360.thread; 2 3 import com.wfd360.thread.demo04Daemon.DaemonThreaad; 4 5 /** 6 * @author 姿势帝-博客园 7 * @address https://www.cnblogs.com/newAndHui/ 8 * @WeChat 851298348 9 * @create 05/05 9:12 10 * @description <p> 11 * 后台线程,即守护线程 12 * 后台线程:指为其他线程提供服务的线程,也称为守护线程。JVM的垃圾回收线程就是一个后台线程。 13 * 需求:尝试把线程标记为后台线程或者标记为(前台)线程; 14 * Thread类提供的方法: 15 * 方法1: void setDaemon(boolean on) 将该线程标记为守护线程或用户线程,true为后台线程,false为用户线程(前台线下) 16 * 怎样测试该线程是否是守护线程? 17 * 方法2:isDaemon() 测试该线程是否为守护线程. true为后台线程,false为用户线程(前台线下) 18 * <p> 19 * 结论1:活动的线程(已经在执行的线程t.start())不能设置后台线程,即主线程不能设置为后台线程。 20 * 结论2: 自定义线程的默认状态和环境有关,后台线程中创建的线程默认是后台线程,前台线程中创建的线程为前台线程. 21 * 结论3: 前台线程执行完后,会直接关闭后台线程,即自定义的后台线程不一定能执行完成 22 * </p> 23 */ 24 public class Test07Daemon { 25 /** 26 * 测试1 27 * 查看主线程的状态,尝试更改 28 * 结论:活动的线程不能设置为后台线程 29 * 30 * @param args 31 */ 32 public static void main1(String[] args) { 33 Thread threadMain = Thread.currentThread(); 34 System.out.println("是后台线程么:" + threadMain.isDaemon());// false 35 threadMain.setDaemon(true); // 报错,活动的线程不能设置为后台线程 36 System.out.println("修改后是后台线程么:" + threadMain.isDaemon()); 37 } 38 39 /** 40 * 测试2 41 * 查看主线程中 创建线程的状态,尝试更改; 42 * 43 * @param args 44 */ 45 public static void main2(String[] args) { 46 Thread thread = new Thread(); 47 // false 48 System.out.println("是后台线程么:" + thread.isDaemon()); 49 // 修改为后台线程 50 thread.setDaemon(true); 51 System.out.println("修改后是后台线程么:" + thread.isDaemon()); 52 } 53 54 /** 55 * 测试3 56 * 查看主线程中 创建线程的状态,尝试更改,让线程处于活动状态在修改->报错; 57 * 58 * @param args 59 */ 60 public static void main3(String[] args) { 61 DaemonThread thread = new DaemonThread(); 62 // 让线程处于活跃状态 63 thread.start(); 64 // false 65 System.out.println("是后台线程么:" + thread.isDaemon()); 66 // 修改为后台线程,报错,当前已经是活跃状态(thread.start())不能修改为后台线程 67 thread.setDaemon(true); 68 System.out.println("修改后是后台线程么:" + thread.isDaemon()); 69 } 70 71 /** 72 * 测试4 73 * 前台线程执行完后,会直接关闭后台线程,即如果后台线程不一定能执行完成 74 * 可以通过修改等待执行时间来观察DaemonThread线程的数组输出变化 75 * 76 * @param args 77 */ 78 public static void main(String[] args) throws InterruptedException { 79 DaemonThread thread = new DaemonThread(); 80 // 修改为后台线程 81 thread.setDaemon(true); 82 // 让线程处于活跃状态 83 thread.start(); 84 System.out.println("========等待后台线程执行============"); 85 Thread.sleep(5 * 1000); 86 } 87 }
1 package com.wfd360.thread.demo04Daemon; 2 3 /** 4 * @author 姿势帝-博客园 5 * @address https://www.cnblogs.com/newAndHui/ 6 * @WeChat 851298348 7 * @create 05/05 9:27 8 * @description 9 */ 10 public class DaemonThread extends Thread { 11 @Override 12 public void run() { 13 for (int i = 0; i < 10; i++) { 14 System.out.println("===="+i); 15 try { 16 Thread.sleep(1000); 17 } catch (InterruptedException e) { 18 e.printStackTrace(); 19 } 20 } 21 } 22 }
线程基础相关的方法定义就先到这里,下一篇我们将进入线程同步.
https://www.cnblogs.com/newAndHui/p/12831089.html
系统化的在线学习:点击进入学习