c站,聊大 网站设计,网站设计 优帮云,泉州企业网站建设公司#x1f495;i need your breath#x1f495; 作者#xff1a;Mylvzi 文章主要内容#xff1a;线程学习(2) 前情回顾#xff1a; 在上一篇博客中介绍到了进程与线程的区别#xff0c;以及初步了解如何在Java实现多线程编程#xff0c;通过内置的Thread类来… i need your breath 作者Mylvzi 文章主要内容线程学习(2) 前情回顾 在上一篇博客中介绍到了进程与线程的区别以及初步了解如何在Java实现多线程编程通过内置的Thread类来实现多线程充分利用多核cpu资源要充分认识到每一个线程都是一个独立的执行流本篇文章继续讲解和Thread有关的一些操作
一.Thread类的创建方式
1.继承Thread 重写run
//创建一个类 继承于Thread类
class MyThread extends Thread {Overridepublic void run() {// 线程的入口 告诉线程要执行哪些逻辑System.out.println(hello thread);try {// 休眠1sThread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}
}
public class Test {public static void main(String[] args) throws InterruptedException {// 首先要实例化出一个Thread类Thread thread new MyThread();
// start和run都是Thread类的成员
// run只是告诉线程要去执行那些逻辑
// start是真正的调用系统的api创建出一个线程再让线程去执行runthread.start();
// thread.run();while (true) {System.out.println(hello main);// 休眠1sThread.sleep(1000);}}
}
2.实现Runnable 重写run 创建自定义类时让其实现Runnable接口这样写的原因本质在于Thread类也实现了Runnable接口 class MyThread implements Runnable {Overridepublic void run() {while(true) {System.out.println();try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
public class Demo3 {public static void main(String[] args) throws InterruptedException {// 使用向上转型 是Java生态中的常见方式// 先实现一个Runnable接口Runnable runnable new MyThread();Thread thread new Thread(runnable);thread.start();while (true) {System.out.println();Thread.sleep(1000);}}
}
说明
Runnable表示的是一个可以运行的任务这个任务是交给线程执行还是交给其他是体执行Runnable本身并不关心~
Runnable接口用来表示一个可以在线程中单独执行的任务一个类只要实现了Runnable接口并且实现他的run方法那么这个类的实例就能够单独在线程中执行Runnable接口就像是一个点石成金的魔法师只要被他修饰过就具有了可被执行的属性这个任务不仅仅可以通过线程来执行也可以通过线程池和执行器来执行 使用Runnable接口有哪些好处呢?直接继承Thread类不是更简单么?使用Runnable接口最大的好处就是可以解耦合降低代码之间的联系性代码之间的联系性越高耦合度就越高反之亦然耦合度过高不利于我们之后对代码进行修改~就像你和你最好的哥们一起创业分钱肯定是不好分的~ 上述两种创建Thread类的方式有所不同第一种是直接通过MyThread类来实例化一个Thread类第二种是先通过MyThread类先实例化一个Runnable接口再通过这个接口去实例化一个Thread类。为什么第二种方式耦合度更低呢原因在于第二种方式自定义类和Thread类之间的联系性降低了他们之间是通过Runnable接口来联系起来的以后使用更多线程的时候就都可以通过Runnable这个接口来实现请看第二种方式创建线程的图解 第一种方式的图解 很明显第二种方式代码之间的耦合性更低
3.继承Thread重写run使用匿名内部类
public class Demo4 {public static void main(String[] args) throws InterruptedException {// 继承Thread 使用匿名内部类Thread t new Thread() {Overridepublic void run() {while (true) {System.out.println(hello thread);try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}};t.start();while (true) {System.out.println(hello main);Thread.sleep(1000);}}
}
4.实现Runnable 重写run使用匿名内部类
public class Demo11 {public static void main(String[] args) {// 实现Runnable 重写run 使用匿名内部类Runnable runnable new Runnable() {Overridepublic void run() {while(true) {System.out.println(hello thread);try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}};Thread t new Thread(runnable);t.start();}
}
5.使用lambda表达式Runnable接口推荐方式
Runnable接口是一个函数式接口只有一个抽象方法run所以可以使用lambda表达式来实现
public class Demo12 {public static void main(String[] args) throws InterruptedException {// 使用lambda表达式Runnable runnable () - {while (true) {System.out.println(Mythread);try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}};Thread thread new Thread(runnable);thread.start();while (true) {System.out.println(main);Thread.sleep(1000);}}
}
使用这种方式创建线程代码既简洁又优雅耦合性也低推荐大家使用这种方式创建线程
Thread类的其他构造方法 Thread(String name) 创建线程对象并命名 这个构造方法主要用于给线程命名方便后续进行调试 // 可以为线程起一个名字作为标识 对线程的执行没有影响 就是单纯的一个标识 方便之后调试进行检查区分Thread t new Thread(() - {while (true) {System.out.println(hello thread);try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}},这是一个线程名字); Thread(Runnable target, String name) 使用 Runnable 对象创建线程对象并命名 【了解】Thread(ThreadGroup group, Runnable target) 线程可以被用来分组管理分好的组即为线程组 二.Thread类的一些属性
1.ID
线程的唯一标识是Java为每个线程分配的身份标识
获取方法 getId() Thread t new Thread();long tid t.getId();// 返回值是一个长整型System.out.println(线程ID tid);// 输出线程ID20
2.名称name
就是线程的名字便于后序进行调试
获取方法 getName() Thread t new Thread(我是线程);String tName t.getName();System.out.println(tName);// 输出我是线程
注意此方法在源码中是被final修饰的意味着子类无法重写方法
3.状态 state
进程最常见的两种状态是就绪状态和阻塞状态线程也有自己的一些属性 // 获取线程的所有状态for (Thread.State state : Thread.State.values()) {System.out.print(state );} NEW Thread 对象已经存在 但是还没有通过start方法调用RUNNABLE 就绪状态 线程已经在cpu上执行/等在在cpu上执行TERMINATED Thread对象还在 但系统内核中的线程不存在TIMED_WAITING 阻塞 由于sleep这种固定时间的方式产生的阻塞WAITING 阻塞 由于wait这种不固定时间的方式产生的阻塞BLOCKED 阻塞 由于锁竞争导致的阻塞 Thread t new Thread(() - {try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}});System.out.println(t.getState());// Thread类存在但是还没有调用start方法状态为NEWt.start();System.out.println(t.getState());// RUNNABLEThread.sleep(3000);System.out.println(t.getState());// TERMINATED
4.优先级priority
获取线程的优先级
获取方法 getPriority Thread t new Thread(我是线程);int tPriority t.getPriority();System.out.println(tPriority);
说明其实此方法很鸡肋因为线程的优先级是由cpu的调度器决定的在我们写代码的过程中很少去关注优先级一是我们根本就观察不到二是根本也没这个必要
5.是否是后台线程
线程可以分为两类前台线程和后台线程默认情况下是前台线程。后台线程又叫做守护线程就像一场表演的后台工作人员一样对于后台线程来说后台线程不结束不影响整个进程的结束(表演完了可后台人员还需要处理后事他们的工作还没结束)而对于前台线程来说一个Java程序中如果还有前台进程没有结束则整个进程是一定不会结束的
获取方法 isDaemon // 源码规定 默认是前台线程/* Whether or not the thread is a daemon thread. */private boolean daemon false;Thread t new Thread(我是线程);boolean isDaemon t.isDaemon();System.out.println(isDaemon);// 输出false
代码验证 public static void main(String[] args) {Thread t new Thread(() - {while (true) {System.out.println(hello thread);try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();// 默认是前台线程 持续打印hello thread}
对于这个代码来说主线程中没有要执行的语句也就是说他的主线程是在一瞬间就执行完了但是由于t是前台线程前台线程不结束整个进程就不会结束如果将t设置为后台线程呢 public static void main(String[] args) {Thread t new Thread(() - {while (true) {System.out.println(hello thread);try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});// 在线程开启前将其设置为后台线程 t.setDaemon(true);t.start();} 可以看到什么也没有打印。因为主线程是前台线程飞快执行完毕之后没有其他的前台线程整个进程终止也就是说t线程没来得及执行整个进程就结束了。也就是说只要一个进程中的所有前台线程结束就代表整个进程的结束
验证先让主线程休眠3s3s之后主线程会立即结束尽管t线程内部还有语句没有执行由于前台线程的结束导致整个进程结束 public static void main(String[] args) throws InterruptedException {Thread t new Thread(() - {while (true) {try {Thread.sleep(5000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(hello thread);}});// 在线程开启前将其设置为后台线程t.setDaemon(true);t.start();System.out.println(主线程开启);Thread.sleep(3000);} 6.是否存活 判断内核线程是否还存活
在Java中我们通过Thread类来创建出一个线程但实际上Thread类的生命周期要比内核中的线程要长一些也就是说线程已经不存在了但是你创建的Thread类仍然存在使用isAlive判定内核线程是否已经结束 isAlive() public static void main(String[] args) throws InterruptedException {// 创建一个线程Thread t new Thread(() - {System.out.println(线程开始);try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(线程结束);});// 开启线程t.start();System.out.println(t.isAlive());// 输出trueThread.sleep(3000);System.out.println(t.isAlive());// 输出false} 线程t内部的方法我们称之为回调方法当回调方法执行完毕之后就代表t这个线程的终止但是Thread类对象的生命周期并未结束 System.out.println(t.isAlive());// 输出false// 开启线程t.start();
如果在线程开启之前打印输出false因为此时t线程还没有被创建 三.线程的中断
终止/打断 interrupt
在Java中要想销毁/中断一个线程的方法是比较唯一的 就是想办法让run方法尽快执行完毕 那么如何实现呢这里提供两种方法
1.手动设置标志位来作为run方法结束的条件
很多线程之所以会持续很久是因为run方法内部存在循环结束run方法就是终止循环 // 将标志位设置为类变量private static boolean isQuit false;public static void main(String[] args) throws InterruptedException {Thread t new Thread(() - {while (!isQuit) {System.out.println(Thread is working);}});t.start();// 五秒后改变标志位Thread.sleep(2000);isQuit true;}
通过设置标志位并在主线程中修改标志位这样就实现了在5秒之后中断此线程的效果
注意
1.isQuit不能设置为main方法中的局部变量因为在lambda表达式中使用的变量必须是被final修饰的常量如果设置为局部变量就无法再次更改isQuit导致无法结束循环。
2.将isQuit设置为类变量lambda表达式此时访问这个成员就不再是变量捕获了而是内部类访问外部类这个语法了。此时就没有final的限制 上述方法虽然能够结束run方法但是过于繁琐且不优雅需要人为的手动设置标志位同时如果在主线程中我们改变了标志位的值但是此时线程却在sleep那就只能等到线程再次苏醒才能终止该线程所以说通过设置标志位的方法来终止线程还有反应不及时的问题
2.使用Thread内部自带的标志位 其实在Thread类中有自带的标志位isInterrupted默认是false public static void main(String[] args) throws InterruptedException {Thread t new Thread(() - {// 先获取当前Thread的实例 在判断其自带的标志位isInterruptedwhile (!Thread.currentThread().isInterrupted()) {while (true) {System.out.println(Thread is working);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}});t.start();Thread.sleep(3000);// 此方法就是将自带的标志位isInterrupted设置为truet.interrupt();}
通过t.interrupt()方法将标志位设置为true来终止线程这种方法的一个优点是即使线程内部处于阻塞状态(sleep)也能够强制将其唤醒终止run方法反应更加及时
总结两种中断线程的方法逻辑都是一样的即设置合适的标志位并修改该标志位来终止run方法从而终止整个线程但是更加推荐第二种方法
但是上述代码的运行结果是什么呢请看 异常被抛出且被捕获但是t线程仍在工作并没有发生中断这是为什么呢通过interrupt方法唤醒线程之后此时sleep方法会抛出异常同时自动清除刚才设置的标志位相当于白白设置标志位了为什么要这么做呢是为了让我们有更多的操作空间在捕获到异常之后我们可以自由采用以下三种处理方式 try {Thread.sleep(1000);} catch (InterruptedException e) {// 1.方式1 不管不顾 让t线程继续运行e.printStackTrace();// 2.方式2 使用break直接中断进程// 3.方式3 捕获到线程之后处理其他工作的代码// 此处就存放需要解决的其他工作的代码}
四.线程的等待 一个线程等待另一个线程执行结束再继续执行。线程等待的本质是控制线程结束的顺序
在Java中使用join来实现线程等待效果
主线程中使用join就是主线程等待另一个线程结束再继续执行主线程的其余代码 public static void main(String[] args) throws InterruptedException {Thread t new Thread(() - {for (int i 0; i 5; i) {System.out.println(线程工作中);try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();System.out.println(线程开启);t.join();// 让主线程等待t线程System.out.println(线程结束);} t.join()的工作过程
如果t线程还没有结束就让主线程等待t线程执行结束再去执行主线程中剩余的代码此时主线程就是一个阻塞状态如果t线程已经结束了直接返回不存在阻塞状态
在哪个线程中调用join方法就是让哪个线程等待另一个线程 说明join方法默认是死等即如果被等待的线程没结束就不会执行其余代码但这种方式存在一个问题如果被等待的线程是死循环那其余代码就永远无法执行在实际的开发中我么更推荐有时间的等待 此处表示主线程只等待1s1s之后就会去执行主线程中剩余代码 补充关于调度开销
当我们使用Thread.sleep方法时我们通过设置一定的时间让线程处于阻塞状态结束之后再恢复为就绪状态由阻塞到就绪其时间一定等于sleep的时间么 long beg System.currentTimeMillis();Thread.sleep(1000);// 休眠1slong end System.currentTimeMillis();System.out.println(时间 (end - beg) ms);// 输出1003 可见由阻塞到就绪这部分的时间并不等于sleep的时间原因在于休眠结束之后线程并不是立马就变为就绪状态而是需要通过调度器进行调度而这种调度是需要时间的这部分由于调度器调度所产生的时间就叫做调度开销
五.线程安全问题
有些代码如果只是一个线程单独去执行执行结果是完全正确的
但是同样的代码如果使用多个线程同时去执行执行结果就可能产生问题这种就是线程安全问题/线程不安全
比如我们要对一个数使其自增1w如果只使用一个线程来解决其结果一定正确 public static void main(String[] args) {// 在主线程中单独执行int cnt 0;for (int i 0; i 10000; i) {cnt;}System.out.println(cnt);// 输出10000}
如果使用两个线程实现这个目标则应该是一个线程自增5000次加起来一共自增1w次 private static int cnt 0;public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() - {for (int i 0; i 5000; i) {cnt;}});Thread t2 new Thread(() - {for (int i 0; i 5000; i) {cnt;}});// 线程开启t1.start();t2.start();// 让主线程等待两个线程结束t1.join();t2.join();// 输出打印System.out.println(cnt);// 输出7351
}最后的打印结果是一个莫名其妙的数不是我们想的1w如果继续重复尝试发现每次打印的结果还都不相同 程序出现bug了这种问题就是在并发编程中常遇到的线程安全问题 为什么会出现这种问题呢此时就要深入底层去看下cnt这个操作是如何实现的
cnt的实现在底层中分为三步
load 把数据从内存中 读取到cpu寄存器中add 把寄存器中的数据1save 把寄存器中的数据保存到内存之中 站在cpu的角度cnt这个操作分别对应着三条cpu指令是由这三条指令实现的~
如果使用多线程来执行上述代码由于线程之间的调度顺序是随机的就会导致在一些调度顺序下发生错误下面来看都有哪些可能的调度顺序 可以看出调度顺序的种类其实是无数种一是调度操作的逻辑顺序二是每个线程执行多少次我们并不知道在图中只有前两种的调度顺序才能达到我们想要的结果下面以一个反例来验证其他顺序的错误 由于线程调度的随机性也就说上述调度顺序也是随机的所以最终产生的结果也是随机的(但是最终的结果一定比1w小因为只有前两种调度顺序才能实现数字的正确增加)
那一定比5000大么这也是不一定的如果在t1自增一次的过程中t2自增了两次一共消耗了三次自增但实际上只自增了一次如果这种逻辑顺序占多数就有可能出现5000的情况 产生线程安全问题的原因 操作系统中线程的调度顺序是随机的(抢占式执行) 罪魁祸首多个线程针对同一个变量进行修改(上述例子就是)修改操作不是原子的cnt这个操作是分三步执行的不是原子的。什么是原子的呢》比如存在一个cpu指令能同时完成cnt的三个操作内存可见性问题指令重排序问题
说明
对于第二种原因改变一些描述就不是线程安全问题了
一个线程针对同一个变量进行修改 ok多个线程针对不同的变量进行修改 ok多个线程针对不同的变量进行读取 ok
通过加锁就能解决上述问题
六.锁 synchronized 如何给Java的代码进行加锁呢其中最常用的方法是通过synchronized关键字(最好还是掌握下他的发音和含义) synchronnized在使用的时候需要搭配{}来使用进了{}就相当于加锁出了{}就是解锁,在已经加锁的状态下如果另一个线程也尝试同样加这个锁就会发生锁竞争/锁冲突后一个线程就会阻塞等待
加锁我们要明确是给谁加锁也就是要对具体的对象进行加锁只有当两个线程针对同一个对象进行加锁才会发生冲突针对不同的对象加锁就不会发生冲突(可以把加锁理解为确立男女朋友关系一旦确立(加锁)就不允许其他人再进入了除非原先的关系破裂(解锁)不能脚踏两只船~~~)
代码实现: // 锁竞争的对象Object locker new Object();Thread t1 new Thread(() - {for (int i 0; i 5000; i) {// 使用synchronized关键字进行加锁synchronized(locker) {cnt;}}});Thread t2 new Thread(() - {for (int i 0; i 5000; i) {// 使用synchronized关键字进行加锁synchronized(locker) {cnt;}}});
在这个代码中我们先是创建了一个用于加锁的对象locker,接着进行加锁如何加锁呢根据上述引发线程安全的2多个线程针对同一个变量进行修改我们要限制的是两个线程不能同时对同一个变量进行修改所以应该加锁的操作是cnt,使用synchronized(locker){}对其进行加锁 这种情况是我们上述所说的会引发线程安全问题的一种调度顺序下面看看加锁是如何解决这个问题的 今天线程的学习就到这里敬请期待后续章节