欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java基础之线程(Threads)与并发

程序员文章站 2022-07-05 21:23:31
课上内容线程的生命周期调用start前不参与调度只有在run结束后,该线程才会结束(不会被强制关闭)yield()有让的意思,指正在运行的程序主动让出CPU。线程操作其中sleep和yield是静态成员函数,这意味着sleep和yield只能对当前正在运行的线程起作用函数的synchronized当函数所有的操作都需要被同步,并且它的key是this时,可以为函数加synchronized关键字。解决死锁的方案是放弃synchronized??......

零、课上内容

线程的生命周期

Java基础之线程(Threads)与并发

  • 调用start前不参与调度
  • 只有在run结束后,该线程才会结束(不会被强制关闭)
  • yield()有让的意思,指正在运行的程序主动让出CPU。

线程操作

其中sleep和yield是静态成员函数,这意味着sleep和yield只能对当前正在运行的线程起作用

函数的synchronized

当函数所有的操作都需要被同步,并且它的key是this时,可以为函数加synchronized关键字。

解决死锁的方案是放弃synchronized??

一、线程基础

1. 基础信息

线程是任务执行和CPU使用的最小单元,是各自运行的任务体,线程之间可以共享变量。

线程类型(Thread)实现了Runnable接口(含有无参的run方法)。
线程可以接受Runnable实现实例化后的runnable对象,进而获取它的runnable方法。

Java基础之线程(Threads)与并发
对于线程对象:Thread t = new Thread(r);,有上图所示操作:

  • 它可以调用t.start()启动线程,这意味着该线程将被放入ready queue中。当它被分配到时间片中时,就会执行其run方法。(从此看出,应当避免直接调用run方法,而应当在Thread中使用start方法,从而获得多线程的效果)
  • Thread.sleep()可以让当前进程挂起,等待一定时间(传入和millis参数)后再放入ready queue中,它挂起的同时其他线程也可以执行。
  • stop、suspend和resume已经不再使用。
  • run方法执行完成后,线程就会结束。stop弃用之后,线程不能被强制结束。
  • interrupt()方法是成员方法,尝试调用的线程中断。线程自己对自己的中断是始终被允许的,其他线程令某线程中断时就需要检查(可能会报错),如果一个线程处于阻塞状态,无法中断并返回InterruptedException。

2. 线程的优先级

Thread.setPriority(int n):可以看到这是一个静态方法,用与设置当前线程的优先级,1~10,默认5。

3. 线程的状态与生命周期

Java线程的状态有以下几种:

  • New:新创建的线程,尚未执行,这时线程不参与调度
  • Ready:执行了start方法后的线程
  • Runnable:运行中的线程,正在执行run()方法的Java代码;
  • Blocked:运行中的线程,因为某些操作被阻塞而挂起;
  • Waiting:运行中的线程,因为某些操作在等待中;
  • Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
  • Finished:线程已终止,因为run()方法执行完毕。
    Java基础之线程(Threads)与并发
    线程终止的原因:
  • run执行结束或遇到return语句
  • run方法抛出异常(这里指的是未处理的,应当避免的Exception,如下标越界等)
  • 不再使用stop

join()的用法

一个线程可以等待另一个线程直到其运行结束。A线程可以调用b.join(),进而等待t线程结束后再继续运行。

对结束后的线程调用join会立刻返回

也可以使用join(long millis),从而让调用的线程等待目标线程等待指定上限时间,之后则不再等待。

4. 线程的中断

如果线程需要执行一个长时间任务,它可能需要被中断,如中断速度过慢的下载。这个中断信号应当由其他线程给予,并且该线程能够自行判断情况并处理,Java使用interrupt()和相关调用解决问题。

其他线程可以调用目标线程的interrupt()方法,从而在j检查通过的情况下使得目标函数的中断状态置位。而可能会被中断的线程应当反复检测自身的中断状态(调用isInterrupted()方法)。

目标线程有2种方法察觉到自己被调用中断方法:

  • 在目标线程正在运行的时候,通过检测检测到了isInterrupted()为真。此时它应当做出决策,尽快结束自己(但其实并不是强制的)。
  • 当目标线程正在被**阻塞(Blocked)**时(三个使线程进入阻塞状态的操作:sleep、wait和join都声明抛出InterruptedException,所以理论上它们都应当在try-catch块中),如果阻塞动作抛出InterruptedException,这说明他们被调用中断方法,此时它们要做的也是尽快结束自己。

进程对自身调用中断函数是始终被允许的

volatile关键字

线程间的共享变量应当使用volatile关键字声明。

在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。

volatile关键字的目的是告诉虚拟机:

  • 每次访问变量时,总是获取主内存的最新值;
  • 每次修改变量后,立刻回写到主内存。

5. 守护线程(Deamon Thread)

Java程序入口是由JVM启动main线程,main线程可启动其他线程,所有线程结束后程序结束。

但确实需要某种无限循环(比如一直定时触发任务)的线程,而在其它线程结束时,程序也应当结束。此时就应当使用守护线程:

Thread t = new MyThread();
t.setDaemon(true);
t.start();

将线程t设置为守护线程后,JVM退出时就不会管理守护线程是否结束。

显然守护线程不应该持有任何需要关闭的资源,否则就会在JVM退出时可能产生错误。

二、简单的线程同步

1. synchronized关键字

多线程对共享变量的操作可能会导致数据的不一致性,这需要保证操作代码的原子性。java最简单的原子性操作通过synchronized关键字实现原子操作。synchronized关键字的实现见此链接

多个线程在进入synchronized语句块时,必须先获得锁(即传入的参数),否则就会阻塞。执行结束后,在synchronized语句块结束后,对应的进程就会释放锁。可以看到它确保了安全性,但性能下降。显然它是一种排他锁。

一些注意事项:

  • 无论是否有异常,都会在synchronized结束处正确释放锁
  • 当某个成员方法的函数体内所有语句都需要同步,并且这个语句的锁是当前对象(即this),可以对方法使用synchronized关键字
  • 对类的静态方法使用synchronized关键字时,没有对象可供锁,因此锁住的是类对应的Class对象
  • Java的synchronized获取的锁是可重入的(已获取锁的进程再次请求获取锁是合法的),也即:
synchronized(lock){
	...
	synchronized(lock){
		...
	}
}

是被允许的

不需要synchronized的操作

  • 单行基本类型(long和double除外)赋值,例如:int n = m;(long和double是64位数据,JVM没有明确规定64位赋值操作是不是一个原子操作,不过在x64平台的JVM是把long和double的赋值作为原子操作实现的。)
  • 多行引用类型赋值,例如:List<String> list = anotherList

类的线程安全问题

没有特殊说明时,类一般默认是非线程安全的

以下是一些例外:

  • String,Integer,LocalDate等所有成员变量都是final,多线程访问时不可写的不变类是线程安全的
  • StringBuffer是线程安全的
  • 类似Math等只提供静态方法,没有成员变量的类是线程安全的

2. 死锁

简而言之就是循环等待

3. wait和notify

本文地址:https://blog.csdn.net/weixin_44662670/article/details/109628876

相关标签: Java基础