Java基础多线程上篇

阅读: 评论:0

Java基础多线程上篇

Java基础多线程上篇

个人推荐:

📢📢📢 前些天发现了一个蛮有意思的人工智能学习网站,8个字形容一下 "通俗易懂,风趣幽默",感觉非常有意思,忍不住分享一下给大家。点击跳转到教程。

本篇博文目录:

      • 一.线程相关概念
        • 1.什么是程序(program)
        • 2.什么是进程
        • 3.什么是线程
        • 4.单线程和多线程
        • 5.并发和并行
      • 二.线程的三种实现方式
        • 1.通过重写Runnable 接口的run方法
        • 2.通过继承Thread 类,重写run方法
        • 3.通过重写Callable接口的call()方法
      • 三.线程的终止
      • 四.线程常用方法
        • 1.第一组常用方法
        • 2.第二组常用方法
      • 五.用户线程和守护线程
      • 六.线程生命周期
      • 七.线程同步

前言:

📢📢📢本篇博文主要对B站韩顺平老师的<<一天学会线程 Thread Synchronized 互斥锁 进程 并行 并发 死锁等:>>和廖雪峰博客多线程进行学习。

【韩顺平讲Java】一天学会线程 Thread Synchronized 互斥锁 进程 并行 并发 死锁等:=333.999.0.0

廖雪峰多线程连接:

一.线程相关概念

1.什么是程序(program)

是为完成特定任务、用某种语言编写的一组指令的集合。简单的说就是我们写的代码(数据结构+算法)。

备注:软件不等于程序,软件可以简单理解为由相关开发文档和程序组成

2.什么是进程

① 进程是指运行中的程序,比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存空间。当我们使用迅雷,又启动了一个进程,操作系统将为迅雷分配新的内存空间。
② 进程是程序的一次执行过程,或是正在运行的一个程序。是动态过程:有它自身的产生、存在和消亡的过程。
进程是系统进⾏资源分配和调度的独⽴单位,每⼀个进程都有它⾃⼰的内存空间和系统资源。

3.什么是线程

为了提⾼系统的执⾏效率,减少处理机的空转时间和调度切换的时间,以及便于系统管理,所以有了线程,线程取代了进程调度的基本功能(线程由进程创建,是进程的一个实体)。

4.单线程和多线程

单线程:

单线程:同一个时刻,只允许执行一个线程

多线程:

多线程:同一个时刻,可以执行多个线程,比如:一个qq进程,可以同时打开多个聊天窗口,一个迅雷进程,可以同时下载多个文件

5.并发和并行

并发:

并发:同一个时刻,多个任务交替执行,造成一种“貌似同时”的错觉,简单的说单核cpu实现的多任务就是并发。

并行:

并行:同一个时刻,多个任务同时执行。多核cpu可以实现并行。

二.线程的三种实现方式

创建线程的方式:

  • 通过重写Runnable接口的run方法;
  • 通过继承 Thread 类,重写run方法;
  • 通过重写Callable接口的call方法。
1.通过重写Runnable 接口的run方法
public class RunnableTest implements Runnable
{@Overridepublic void run() {for (int i = 1; i <= 10; i++) {System.out.println("执行方式一:执行第" + i + "次");}}
}class Test{public static void main(String[] args) {// 方式一Thread thread = new Thread(new RunnableTest());thread.start();// 方式二Thread thread1 = new Thread(()->{for (int i = 1; i <= 10; i++) {System.out.println("执行方式二:执行第" + i + "次");}});thread1.start();}
}

备注: ()->{} 这种写法是Java8的新特性Lambda表达式
运行效果:

2.通过继承Thread 类,重写run方法
class Thread_NEW extends Thread {@Overridepublic void run() {// 重写run方法for (int i = 1; i <= 10; i++) {System.out.println("执行方式三:执行第" + i + "次");}}
}class Test2{public static void main(String[] args) {Thread_NEW thread_new = new Thread_NEW();thread_new.start();}
}

运行效果:


分析方式一和方式二:

线程通过start()方法启动,实际上该方法调用的是start0(),该方法被native关键字修饰,表示该方法是一个外部方法。

start()方法调用start0()方法后,该线程并不一定会立马执行,只是将线程变成了可运行状态。具体什么时候执行,取决于CPU,由CPU统一调度。

线程的执行方法体就是Runnable接口的run方法(下面的代码在Thread类中)


我们需要执行的内容,只需要重写Runnable接口的run方法,在run方法中编写我们要执行的内容即可

方式一和方式二区别:

① 从java的设计来看,通过继承Thread或者实现Runnable接口来创建线程本质上没有区别,从jdk帮助文档我们可以看到Thread类本身就实现了Runnable接口。
② 实现Runnable接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制,建议使用Runnable。

3.通过重写Callable接口的call()方法
class Test3{public static void main(String[] args) throws ExecutionException, InterruptedException {// 方式1Callable callable1 = new CallableTask();// 创建Callable接口FutureTask futureTask1 = new FutureTask(callable1);// 创建FutureTaskThread thread1 = new Thread(futureTask1);// 创建线程thread1.start();// 启动线程System.out.println("方式1:执行情况:"&#());// 获取返回结果// 方式2FutureTask futureTask2 = new FutureTask(()->{try {System.out.println("执行任务");System.out.println("计算:4/2="+4/2);} catch (Exception e) {return "fail:"&#Message();}return "Success";});// 创建FutureTaskThread thread2 = new Thread(futureTask2);// 创建线程thread2.start();// 启动线程System.out.println("方式2:执行情况:"&#());// 获取返回结果}
}// 实现Callable接口,并重写 call()方法
class CallableTask implements Callable<String>{@Overridepublic String call() throws Exception {try {System.out.println("执行任务");System.out.println("计算:4/0="+4/0);} catch (Exception e) {return "fail:"&#Message();}return "Success";}
}

运行效果:

分析方式三:

  • FutureTask<V> 类是实现于 RunnableFuture<V> 接口:

  • RunnableFuture<V> 接口继承于 RunnableFuture<V> 接口

  • 因为继承于Runnable接口,所以可以通过new Thread(futureTask1) 来创建线程

  • 并且线程通过start()方法启动,等待执行时会执行FutureTask的run(),进入到FutureTask源码中发现,最终run()方法执行的是Callable接口的call()方法。

  • FutureTask类有二种初始化方式,Callable接口和Runnable接口。

  • 从上面FutureTask源码中我们可以看出FutureTask其实执行的是Callable接口的call()方法,并没有使用Runnable接口的run(),这是因为使用Callable接口进行初始化的时候,通过 Executors.callable(runnable, result); 获得了一个Callable 对象,并通过该Callable对象进行初始化。

  • 因为继承之Future接口,所以在RunnableFuture类中有isCancelled(),isDone(),cancel()和get()方法的实现,Future接口代表一个未来能获取结果的对象,比如通过Future对象的get()方法就可以获取到执行任务完成后返回的值。

三.线程的终止

① 当线程完成任务后,会自动退出。
② 还可以通过使用变量来控制run方法退出的方式来停止线程,即通知方式

需求:主线程休眠1s之后停止子线程运行,实现代码如下:

class RunnableController implements Runnable
{// 定义一个线程状态标识private boolean start;private int i;public RunnableController() {this.start = true;}@Overridepublic void run() {while (start) {++i;System.out.println("执行方式一:执行第" + i + "次");}}// 通过set进行属性设置public void setStart(boolean start) {this.start = start;}
}class Test4{public static void main(String[] args) throws InterruptedException {RunnableController runnableController = new RunnableController();Thread thread = new Thread(runnableController);thread.start();// 1s钟后停止线程Thread.sleep(1000);runnableController.setStart(false);}
}

运行效果:

四.线程常用方法

1.第一组常用方法

  • setName(),getName(),getPriority(),setPriority()
public class ThreadMethod {public static void main(String[] args) {Thread thread = new Thread(()-> System.out.println("当前线程名称:"+Thread.currentThread().getName()+"该线程的优先级:"+Thread.currentThread().getPriority()));thread.setName("线程1");thread.setPriority(1);// 1~10Thread thread2 = new Thread(()-> System.out.println("当前线程名称:"+Thread.currentThread().getName()+"该线程的优先级:"+Thread.currentThread().getPriority()));thread2.setName("线程2");thread2.setPriority(10);// 1~10Thread thread3 = new Thread(()-> System.out.println("当前线程名称:"+Thread.currentThread().getName()+"该线程的优先级:"+Thread.currentThread().getPriority()));thread2.setName("线程3");thread.start();thread2.start();thread3.start();}
}

运行效果:

优先级必须设置在1~10闭区间,不然会引发 IllegalArgumentException 异常,数值越大优先级越高,

默认优先级为5,最小优先级为1,最大优先级为10

优先级越高并不是一定先执行,只是获得更多的执行机会

 @Testpublic void test() {Thread.currentThread().setPriority(6);for (int j = 0; j < 50; j++) {if (j == 10) {Thread thread1 = new Thread(() -> {for (int i = 0; i < 50; i++) {System.out.println("优先级为---------" + Thread.currentThread().getPriority());}});thread1.setPriority(1);thread1.start();} else if (j == 20) {Thread thread2 = new Thread(() -> {for (int i = 0; i < 50; i++) {System.out.println("优先级为---------" + Thread.currentThread().getPriority());}});thread2.setPriority(10);thread2.start();}}}

运行效果:

  • run()

注意:执行run()方法并不是开启线程,只是通过对象调用run()方法!

 @Testpublic void test2() {Thread thread = new Thread(() -> System.out.println("-------调用run方法-----" + Thread.currentThread().getName()));thread.run();}

运行效果:

  • sleep

调用sleep会让当前线程从Running进入Timed Waiting状态(阻塞),其他线程可以通过interrupt方法打断正在休眠的线程,这时sleep方法会抛出InterruptException异常,睡眠结束后的线程未必会立刻执行,最后推荐使用TimeUnit的sleep代替Thread的sleep来获得更好的可读性。

TimeUnit可以指定休眠时间单位(sleep只有毫秒为单位)

实例代码:

运行效果:

  • interrupt

中断线程,但并没有真正的结束线程。所以一般用于中断正在休眠线程(sleep)和等待(wait)的线程

实例代码:

运行效果:

我们可以通过isInterrupted()判断当前线程是否被中断,返回true表示是,返回false表示否,此方法不会影响中断标志的状态,当调用interrupted()方法可以获取中断情况但是会改变中断的标志状态。:

实例代码:

运行效果:

实例代码:

运行效果:

2.第二组常用方法
  • yield线程礼让

调用yield会让当前线程从Running进入Runnable就绪状态,然后调度执行其他线程,具体的实现依赖于操作系统的任务调度器,所以礼让的时间不确定,也不一定定礼让成功。

实例代码:

运行效果( 由于礼让不一定成功无法控制,所以不怎么使用,也看不出什么效果 ):

  • join线程的插队

线程插队插队成功后,先执行完插入的形成的所有任务,再执行后面的任务,调用join可以表示等待某一个线程结束。

实例代码:

运行效果:

可以通过给join传入一个时间来设置最大等待时间:


运行效果:

五.用户线程和守护线程

用户线程:

用户线程:也叫工作线程,当线程的任务执行完或通知方式结束。

守护线程:

守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束,常见的守护线程,垃圾回收机制。

通过 setDaemon(true); 设置该线程为守护线程,实例代码如下:

    @Testpublic void test5() throws InterruptedException {Thread thread = new Thread(()->{try {while (true){System.out.println("小偷正在偷钱..");Thread.sleep(50);}} catch (InterruptedException e) {e.printStackTrace();}});// 将thread设置为守护线程thread.setDaemon(true);thread.start();for (int i = 0; i < 50; i++) {System.out.println("警察正在来的路上.....");Thread.sleep(50);}System.out.println("警察赶到,小偷被抓,小偷停止偷钱");}

运行效果:

六.线程生命周期

JDK 中用 Thread.State 枚举表示了线程的几种状态

具体代码如下:

线程状态转换图

下面的内容来源于:/md/java/thread/java-thread-x-thread-basic.html

  • 新建(New)
    创建后尚未启动。
  • 可运行(Runnable)
    可能正在运行,也可能正在等待 CPU 时间片。
    包含了操作系统线程状态中的 Running 和 Ready。
  • 阻塞(Blocking)
    等待获取一个排它锁,如果其线程释放了锁就会结束此状态。
  • 无限期等待(Waiting)
    等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。
进入方法退出方法
没有设置 Timeout 参数的 Object.wait() 方法
没有设置 Timeout 参数的 Thread.join() 方法被调用的线程执行完毕
LockSupport.park() 方法-
  • 限期等待(Timed Waiting)
    无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。 调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。 调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。 睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。 阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。
进入方法退出方法
Thread.sleep() 方法时间结束
设置了 Timeout 参数的 Object.wait() 方法时间结束 / ify() / ifyAll()
设置了 Timeout 参数的 Thread.join() 方法时间结束 / 被调用的线程执行完毕
LockSupport.parkNanos() 方法-
LockSupport.parkUntil() 方法-
  • 死亡(Terminated)
    可以是线程结束任务之后自己结束,或者产生了异常而结束。

七.线程同步

什么是线程同步:

① 在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性。
② 也可以这里理解:线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作.。
③ 被Synchronized关键字修饰的同步方法的效率会变低。

同步使用Synchronized关键字进行修饰,共二种写法同步代码块和同步方法,如下:

同步原理:
类似于生活中,在寝室上厕所,一次进入一个人(线程),等线程出来之后,再放其他线程进入(这里包括从厕所出来的线程),进入就关门(上锁,互斥锁),出来开门。

需求:网上售票,一段时间同时有多人进行购票,要保证同一张票不能被多人购买,造成票数出现负数情况。
出现票数负数的代码:

synchronization;public class SynChronZedTest {public static void main(String[] args) {RunnableNew runnableNew = new RunnableNew();Thread thread = new Thread(runnableNew);thread.setName("售票窗口1");thread.start();Thread thread2 = new Thread(runnableNew);thread2.setName("售票窗口2");thread2.start();Thread thread3 = new Thread(runnableNew);thread3.setName("售票窗口3");thread3.start();Thread thread4 = new Thread(runnableNew);thread4.setName("售票窗口4");thread4.start();}
}class RunnableNew implements Runnable {private  Integer tick = 50;@Overridepublic  void run() {while (true) {if (tick <= 0) {System.out.println("售票结束");break;}try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}// 进行出售System.out.println(Thread.currentThread().getName() + "售出,还剩票数" + (--tick));}}
}

改进使用同步代码块:

synchronization;public class SynChronZedTest {public static void main(String[] args) {RunnableNew runnableNew = new RunnableNew();Thread thread = new Thread(runnableNew);thread.setName("售票窗口1");thread.start();Thread thread2 = new Thread(runnableNew);thread2.setName("售票窗口2");thread2.start();Thread thread3 = new Thread(runnableNew);thread3.setName("售票窗口3");thread3.start();Thread thread4 = new Thread(runnableNew);thread4.setName("售票窗口4");thread4.start();}
}class RunnableNew implements Runnable {private  Integer tick = 50;@Overridepublic void run() {while (true) {synchronized (RunnableNew.class) {if (tick <= 0) {System.out.println("售票结束");break;}try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}// 进行出售System.out.println(Thread.currentThread().getName() + "售出,还剩票数" + (--tick));}}}
}

运行效果:

改进使用同步方法:

synchronization;public class SynChronZedTest {public static void main(String[] args) {RunnableNew runnableNew = new RunnableNew();Thread thread = new Thread(runnableNew);thread.setName("售票窗口1");thread.start();Thread thread2 = new Thread(runnableNew);thread2.setName("售票窗口2");thread2.start();Thread thread3 = new Thread(runnableNew);thread3.setName("售票窗口3");thread3.start();Thread thread4 = new Thread(runnableNew);thread4.setName("售票窗口4");thread4.start();}
}class RunnableNew implements Runnable {private boolean tage = true;private  Integer tick = 50;@Overridepublic void run() {while (tage) {sell();}}private synchronized void sell() {if (tick <= 0) {System.out.println("售票结束");this.tage = false;return;}try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}// 进行出售System.out.println(Thread.currentThread().getName() + "售出,还剩票数" + (--tick));}
}

运行效果:

其实上面二种方式本质是一样的:

  • 同步方法如果没有使用static修饰,默认锁对象为this
  • 同步方法如果使用static修饰,默认锁对象为.class

使用this进行上锁和未用static修饰的同步方法进行上锁本质上是一致的都是对当前对象进行上锁(上面锁的就是runnableNew对象,四个线程都是使用该对象进行上锁可以保证线程同步)


为了验证上面的说法,这里我们给四个线程分别传递不同的RunnableNew对象,该对象中tick通过static进行修饰保证数据共享

详细代码如下:

synchronization;public class SynChronZedTest {public static void main(String[] args) {Thread thread = new Thread(new RunnableNew());thread.setName("售票窗口1");thread.start();Thread thread2 = new Thread(new RunnableNew());thread2.setName("售票窗口2");thread2.start();Thread thread3 = new Thread(new RunnableNew());thread3.setName("售票窗口3");thread3.start();Thread thread4 = new Thread(new RunnableNew());thread4.setName("售票窗口4");thread4.start();}
}class RunnableNew implements Runnable {private static Integer tick = 50;@Overridepublic void run() {while (true) {synchronized (this) {if (tick <= 0) {System.out.println("售票结束");break;}try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}// 进行出售System.out.println(Thread.currentThread().getName() + "售出,还剩票数" + (--tick));}}}
}

运行效果( 再次运行会发现票数出现负数 ):


上面的操作类似于下图:

使用this表示对当前对象进行上锁,但是传入的几个对象都不相同所以当tick=3时,四个线程同时进入,且tick>0满足条件,d都执行tick-1,所以最后的结果为tick=-1。

改进方法,同步代码块中将this换成RunnableNew.class或同步方法加上static关键字(提高锁对象的作用域)


运行效果( 问题解决 ):


上面的操作类似与下图:

通过.class进行上锁是对类进行上锁,四个对象都属于同一个类,所以当有一个对象执行时就会上锁,另外三个就无法进入,需要等待进去的线程出来后,再次找机会进入。

实际上像上面这种操作,效率是非常低的,运行多次发现大多数情况下只有一个线程在跑:

本文发布于:2024-02-03 00:36:06,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170689176647502.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:多线程   上篇   基础   Java
留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23