今天内容: (1)线程 (2)线程的同步机制
1.线程
1.1 基本概念
程序 - 数据结构 + 算法,主要指存放在硬盘上的可执行文件。
进程 - 主要指运行在内存中的程序。
目前主流的操作系统都支持多进程,为了使得操作系统能够同时执行多个任务(核心),但进程是重量级的,新建进程对系统的资源消耗比较大,因此进程的数量比较局限。
while(true){fork();}—》fork炸弹,不过2019年之后就不太好用了
线程是进程内部的程序流,也就是操作系统中支持多进程,而每个进程的内部又可以支持多线程,线程是轻量级的,新建线程会共享所在进程的系统资源,因此以后的开发中都采用多线程技术。
多线程技术采用时间片轮转法实现并发执行,所谓并发就是指宏观并行微观串行。
1.2 线程的创建(重中之重)
(1)创建和启动的方式
java.**lang.**Thread类用于描述线程,Java 虚拟机允许应用程序并发地运行多个执行线程,而线程的创建和启动方式两方法如下:
(1)自定义类继承Thread类并重写run方法(不重写它什么也不干,重写之后它在才能干我们自己想让它干的事情),创建该类的实例调用start方法。
SubThreadRunTest类:
package cn.learn.day17;
public class SubThreadRunTest extends Thread {
@Override
public void run() {
System.out.println("这就是重写以后的run方法,"
+ "该线程需要执行的任务可以写在这里哦!");
}
public static void main(String[] args) {
// 声明Thread类型的引用指向子类的对象,形成多态
Thread t1 = new SubThreadRunTest();
// 调用run方法,编译阶段调用父类的,运行阶段调用子类的
t1.run();
}
}
重写:
package ThreadTest1;
public class SubThreadRunTest extends Thread {
//重写run方法
@Override
public void run(){
System.out.println("这就是重写以后的run方法," +
"该线程需要执行的任务可以写在这里哟");
}
public static void main(String[] args) {
// 声明Thread类型的引用指向子类的对象,形成多态
Thread t1=new SubThreadRunTest();
// 调用run方法,编译阶段调用父类的,运行阶段调用子类的
t1.run();
}
}
多态:引用指向子类的对象,形成多态,编译阶段调用父类的,运行阶段调用子类的
(2)自定义类实现Runnable接口并重写run方法,创建该类的实例作为实参来构造 Thread类型的对象,然后使用Thread类型的对象调用start方法。
package ThreadTest1;
public class SubRunnableTest implements Runnable{
@Override
public void run(){
for(int i = 0; i < 20; i++) {
System.out.println("run方法中:i = " + i);
}
}
public static void main(String[] args) {
// 声明自定义类的引用指向该类的对象
SubRunnableTest srt=new SubRunnableTest();
// 使用上述引用作为实参构造Thread类型的对象
// 由源码可知:srt作为实参一路传递,最终赋值给了Thread类中成员变量target
Thread t1=new Thread(srt);
// 使用Thread类型的对象调用start方法
//srt.start(); error start方法来自于Thread类而不是实现类
// 启动该线程,由Java虚拟机自动调用该线程的run方法,调用Thread类的run方法
// 由run方法源码可知:target != null的条件成立,执行target.run()的代码
// 而此时target已经变成了srt,因此最终调用上面重写的run方法。
t1.start();
for(int i = 0; i < 20; i++) {
System.out.println("------主方法中:i = " + i);
}
}
}
(2)相关方法的解析
不传引用:
Thread() - 使用无参方式构造对象。
run方法什么也不干:
package ThreadTest1;
public class ThreadrunTest {
public static void main(String[] args) {
// 1:声明Thread类型的引用指向改类型的对象
// 由构造方法的源码可知:Thread类中成员变量target的数值为null
Thread t1=new Thread();
// 2:调用run方法进行测试
// 由run方法发源码可知:成员变量target的数值为空,导致条件不成立
// 由此{}括起来的代码不会执行,因此run方法确实啥也不干
t1.run();
// 3:打印一句话
System.out.println("山无棱,天地合,才敢与君绝");
}
}
Thread(String name) - 根据参数指定的名称来构造对象。
传引用:
Thread(Runnable target) - 根据参数指定的接口引用来构造对象。
Thread(Runnable target, String name) - 根据参数指定的引用和名称构造对象。
void run() - 若使用Runnable类型的引用构造出来的对象调用该方法,则最终调用
引用所指向对象的run方法,否则调用该方法啥也不做。
void start() - 用于启动线程,Java虚拟机会自动调用该线程的run方法。
package ThreadTest1;
public class SubThreadstartTest extends Thread {
@Override
public void run(){
for (int i=0;i<10;i++){
System.out.println("run方法中:i="+i);
}
}
public static void main(String[] args) {
// 声明Thread类型的引用指向子类的对象,形成多态
Thread t1=new SubThreadstartTest();
// 调用run方法就是成员方法的调用,根据方法名跳转过去执行完再跳转回来
// 调用run方法没有交错的效果
// t1.run();
// 调用start方法后两个方法的输出语句会发生交错打印,每次交错的效果可能不一样
t1.start();
for (int i=0;i<10;i++){
System.out.println("----主方法中---:i="+i);
}
}
}
(3)原理分析
a.执行main方法的线程叫做主线程,执行run方法的线程叫做子线程。
b.main方法是程序的入口,最开始只有主线程来依次执行main方法中的代码,当
start方法调用成功后,线程的个数瞬间由1个变成了2个,其中子线程去执行run
方法,主线程继续执行main方法的代码,两个线程各自独立运行互不影响。
c.当run方法执行完毕后子线程结束,当main方法执行完毕后主线程结束,但两个
线程谁先执行没有明确的规定,取决于操作系统的调度算法。
注意:思考:线程创建和启动两种方式的优缺点是什么???
线程创建和启动的方式一相对来说代码简单,但Java语言中只支持单继承,若该类继承Thread类后则无法继承其它类;而方式二相对来说代码复杂,但不影响该类继承其它类以及实现其它接口,因此以后开发中推荐方式二。
1.3 线程的编号(PNO)和名称(会用即可)
线程的编号相当于:人的身份证号
线程的名称相当于:人的名字
名字可以改,编号不能改
成员方法中有一个隐藏的this.的作用
1.4常用方法:
测试:
package ThreadTest1;
public class SubThreadIdTest extends Thread{
public SubThreadIdTest(String string) {
super(string);
}
@Override
public void run(){
System.out.println("当前线程的编号是:"+getId()+",名称是:"+getName());
setName("zhangfei");
System.out.println("修改后线程的编号是:" + getId()
+ ",名称是:" + getName());
}
public static void main(String[] args) {
Thread t1=new SubThreadIdTest("shimmer");
t1.start();
System.out.println("---------------------");
// 获取当前正在执行线程的引用,而当前正在执行的线程是主线程
// 获取主线程的引用
Thread t2= Thread.currentThread();
// 获取主线程的编号和名称
System.out.println("主线程的编号是:"+t2.getId()+",名称是:"+t2.getName());
}
}
package ThreadTest1;
public class NonameTest {
public static void main(String[] args) {
//匿名内部类的语法格式:父类/接口类型 变量名 = new 父类/接口类型(){重写};
// 1.使用继承和匿名内部类的方式创建和启动线程
/*Thread t1=new Thread(){
@Override
public void run(){
System.out.println("张三:在吗?");
}
};
t1.start();*/
new Thread(){
@Override
public void run(){
System.out.println("shimmer:在吗?");
}
}.start();
/*Runnable rr=new Runnable() {
@Override
public void run() {
System.out.println("李四:在呀,咋啦?");
}
};
Thread t2=new Thread(rr);
t2.start();*/
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("祝祝:在的呢");
}
}).start();
}
}
package ThreadTest1;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SubsleepThreadTest extends Thread{
private boolean flag =true;
// 子类重写的方法不能抛出更大的异常
@Override
public void run(){
while (flag){
// 获取当前系统时间
Date d1=new Date();
// 调整一下时间的格式
SimpleDateFormat sdf=new
SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("当前系统时间:"+sdf.format(d1));
// 睡眠一秒后再次打印系统时间 1秒 = 1000毫秒
try{
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// 让主线程睡眠5秒钟,5秒后将子线程停止
Thread t1=new SubsleepThreadTest();
t1.start();
System.out.println("主线程开始睡眠....");
try{
Thread.sleep(5000);
}catch (Exception e){
e.printStackTrace();
}
((SubsleepThreadTest)t1).flag=false;
System.out.println("子线程终止了!");
}
}
方法重写不能抛出更大的异常
DEMO1:
Daemon
package ThreadTest1;
public class ThreadDaemonTest extends Thread{
@Override
public void run() {
// 对于新建的线程来说,默认不是守护线程
// 当子线程不是守护线程时,主线程先结束,但子线程依然会打印完毕所有数据
// 当子线程是守护线程时,守护线程通常为其它线程服务,此时会发现若主线程
// 结束,则子线程会随之结束
System.out.println(isDaemon() ? "是守护进程" : "不是守护进程");
for (int i = 0; i < 10; i++) {
System.out.println("子线程中:i = " + i);
}
}
public static void main(String[] args) {
Thread t1=new ThreadDaemonTest();
// 设置线程为守护线程
t1.setDaemon(true);
t1.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程中:i = " + i);
}
}
}
Join:
package ThreadTest1;
public class Threadjoin extends Thread{
@Override
public void run(){
System.out.println("倒计时开始...");
for (int i=0;i<10;i++){
System.out.println(i);
try{
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
}
System.out.println("happy every day,learning for everyday");
}
public static void main(String[] args) {
Thread t1=new Threadjoin();
t1.start();
System.out.println("主线程开始的等待");
try {
// 表示当前正在执行的线程等待调用对象所表示的线程终止
// 也就是主线程等待子线程终止
// t1.join(); 海枯石烂
t1.join(3000);//等待3秒
}catch (Exception e){
e.printStackTrace();
}
System.out.println("Say Goodbye");
}
}
Priority:
package ThreadTest1;
public class ThreadPriorityTest extends Thread{
@Override
public void run(){
// 5 一般的优先级 ctrl+/ 采用单行注释
// 优先级越高的线程不一定先执行,但该线程获取到时间片的机会越多
// 也就是越有可能它先执行完毕
// System.out.println("当前线程的优先级是:" + getPriority());
for (int i=0;i<30;i++){
System.out.println("子线程中:i="+i);
}
}
public static void main(String[] args) {
Thread t1=new ThreadPriorityTest();
// 设置子线程的优先级为最高优先级
t1.setPriority(MAX_PRIORITY);
t1.start();
for (int i=0;i<30;i++){
System.out.println("-----主线程中:i="+i);
}
}
}
作业:
1.重点掌握线程创建的两种方式和sleep以及join方法的使用。
2.编程创建两个线程,线程一负责打印1 ~ 100之间的所有奇数;
其中线程二负责打印1 ~ 100之间的所有偶数;
在main方法启动上述两个线程同时执行,主线程等待两个线程终止;
3.编程实现Account类的封装,特征有:账户余额;
编程实现AccountTest类,在main方法中创建对象并传入1000元,最后打印余额
作业代码,重点实例:https://shimo.im/docs/QxxXKhPj6hVg3GVX