Java多线程(二)
目录
一. Thread类及常见方法
1.1 Thread类的常见构造方法
1.2 Thread 的几个常见属性
1.3 启动一个线程
1.4 中断一个线程
1.4.1 通过共享的标记来进行沟通
1.4.2 使用Thread.interrupted 或 Thread.currentThread().isInterrupted() 代替自定义标志位
1.5 等待一个线程
1.6 线程状态
一. Thread类及常见方法
每一个线程都有一个唯一的Thread对象与之关联。
1.1 Thread类的常见构造方法
方法 | 说明 |
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target,String name) | 使用Runnable对象创建线程对象,并命名 |
下面是例子:
Thread t=new Thread();Thread b=new Thread(new MyRunnable());Thread n=new Thread("t1");Thread l=new Thread(new MyRunnable(),"t2");
1.2 Thread 的几个常见属性
注意:
- ID是线程的身份标识,每一个线程都有唯一的的ID
- 名称是在调试工具中才会用到的
- 后台线程:不会阻止整个就进程结束(后台线程的结束不会影响到其他线程的结束)
- 前台线程:会阻止整个就进程结束(一个进程中前台线程可以不止有一个,只有当所有的前台线程都结束后,整个进程才会结束)
- main线程与代码中手动创建的线程被默认为前台线程
- 设置后台(守护)线程必须要在start之前将该线程设置为后端线程(使用 setDaemon()
)
下面是setDaemon()方法的定义:
在Java中, setDaemon 是 Thread 类中的一个实例方法,用于将指定的线程设置为守护线程。以下是关于它的详细定义及相关信息:
方法签名
java
public final void setDaemon(boolean on)
参数说明
on :是一个布尔类型的参数。当 on 的值为 true 时 ,表示将调用该方法的线程设置为后台线程(守护线程);当 on 的值为 false 时,表示将其设置为前台线程(非守护线程)。
1.3 启动一个线程
使用 start()
我们只要记住:调用start方法,才真的在操作系统的底层创建出一个线程.
class MyRunnable implements Runnable{@Overridepublic void run() {System.out.println("hi");}
}
public class text {public static void main(String[] args) {Thread b=new Thread(new MyRunnable());b.start();}
}
注意:一个线程对象只能调用一次start()(线程的生存周期具有不可逆性)
1.4 中断一个线程
1.4.1 通过共享的标记来进行沟通
import java.util.Scanner;public class text {public volatile static boolean mm=true;public static Thread t=new Thread(()->{while(mm){System.out.println("hi");try {Thread.sleep(10000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("线程已中断");});public static void main(String[] args) throws InterruptedException {t.start();Thread.sleep(10000);System.out.println("是否中断,如要中断请输入0");Scanner sc=new Scanner(System.in);int n= sc.nextInt();if(n==0){mm=false;}}
}
上述代码的mm变量就是一个共享变量,通过该标记就可以实现类似与对线程的中断
注意:在使用该方法进行线程的中断时,共享标记要加上volatile(volatile可以解决内存的可见性问题)
内存可见性问题的定义:当一个线程修改一个共享变量的值时,这个值会被保存在该线程的本地内存中,而不是直接写入主内存中。如果其他线程需要读取该共享变量的值,它们可能会从自己的本地内存中读取旧值,而不是从主内存中读取最新的值,从而导致数据不一致的问题。这时volatile就可以帮助我们解决这个问题
1.4.2 使用Thread.interrupted 或 Thread.currentThread().isInterrupted() 代替自定义标志位
下面是中断线程的一种方式,但是在我们在日常开发中一般不会使用这种方法,在后面会提出中断线程更科学的方法
import java.util.Scanner;public class text {public static void main(String[] args) {Thread t=new Thread(()->{Thread m=Thread.currentThread();while(!m.isInterrupted()){System.out.println("hi");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();System.out.println("是否中断线程,中断请输入0");Scanner sc=new Scanner(System.in);int n=sc.nextInt();if(n==0){t.interrupt();}}
}
下面是中断线程要用到的方法
注意:
- Thread.currentThread()是一个静态方法,它能返回一个引用,这个引用指向当前正在执行的线程
- Thread m=Thread.currentThread();
while(!m.isInterrupted())这两行代码可以改为while(Thread.currentThread().isInterrupted()),效果是一样的 - 中断标志位:在Thread类中一个boolean状态的属性,初始是false(false:表示线程未被请求中断 true:表示线程已被请求中断 )
- interrupt():发出请求线程中断的指令(实际在代码逻辑中并不是interrupt直接中断的线程,而是间接的),将中断标志位设置为true。如果sleep正在执行时(只要是sleep/join/wait等让线程阻塞的方法都会),interrupt()会“唤醒”sleep(提前结束sleep导致的阻塞状态),并且触发并抛出异常,从而中断线程,并且这时sleep会自动把当前线程中断的标志位重新设置为false
- interrupted():只是检查当前线程的中断标志位,并清除当前中断标志位(即 将 true 变回false),不会主动唤醒处于sleep状态的线程
- isInterrupted():只是判断当前线程的中断标志位(只做判断,不会修改当前线程的中断标志位)。如果中断标志位是true则返回true,反之则返回false
上述代码中还有一个隐藏的问题:lambada表达式中我们在引用当前线程时,为什么不直接用 t 呢,而是用Thread.currentThread()获取当前线程的引用呢?
答:在使用lambada表达式构建Thread对象时,是先执行lambada表达式中的内容,再将其作为Thread类构造方法的参数,所以在lambada表达式中t这个概念并不存在。而Thread.currentThread()获取的引用就是lambada表达式所在线程的引用(与t所指向的线程是一致的)。
下面我们来看一个类似的的代码
import java.util.Scanner;public class text {public static void main(String[] args) {Thread t=new Thread(()->{Thread m=Thread.currentThread();while(!m.isInterrupted()){System.out.println("hi");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t.start();System.out.println("是否中断线程,中断请输入0");Scanner sc=new Scanner(System.in);int n=sc.nextInt();if(n==0){t.interrupt();}}
}
我把 throw new RuntimeException(e)删掉,加上 e.printStackTrace();这样有什么不一样的效果呢?
在调用t.interrupt()时,中断标志位被设置成true,并且唤醒sleep,并引发异常,而e.printStackTrace()只是打印异常而不是抛出异常,所以线程不会被中断。然后sleep将中断标志位重新设置为false,while的判断条件就会变为true,导致while循环一直被循环执行,不会中断线程。执行结果就是:在输入0后,打印一遍异常,在这后一直循环打印hi(注意:一次interrupt()调用,对于正在sleep的线程只会唤醒一次sleep,当循环下一次执行到sleep时,sleep只会引起线程阻塞,而不会引发异常)
如果想要立即退出,我们可以才用以下改变(使用break退处即可)
import java.util.Scanner;public class text {public static void main(String[] args) {Thread t=new Thread(()->{Thread m=Thread.currentThread();while(!m.isInterrupted()){System.out.println("hi");try {Thread.sleep(1000);} catch (InterruptedException e) {break;}}});t.start();System.out.println("是否中断线程,中断请输入0");Scanner sc=new Scanner(System.in);int n=sc.nextInt();if(n==0){t.interrupt();}}
}
1.5 等待一个线程
等待线程:干预两个线程的结束顺序,后结束的线程等待先执行的线程执行完才开始执行
用到的方法:join()
下面代码执行的功能:先让线程t执行(count++自增)完毕,再让主线程打印出自增结束最后得到的count的值
public class Test {public static int count=0;public static void main(String[] args) throws InterruptedException {Thread t=new Thread(()->{for(int i=0;i<5000;i++){count++;}});t.start();t.join();System.out.println("count自增后的值为: "+count);}
}
注意:
- 由于join()会让线程陷入阻塞状态,因此要处理异常(与sleep类似)
- 在主线程中0调用t.join()是让主线程(main线程)等待t线程执行完后再执行
- 一开始执行的是主线程,然后执行t.start()方法后就变成了多线程
在上述代码中如果让join()与print语句调换顺序会得到不一样的效果,这是为什么呢
代码里 join() 和 print 的“书写顺序”,会决定主线程“有机会执行时”的优先选择。比如:
- 若代码是“ t.start(); print; join; ”:主线程竞争到 CPU 时,会先执行 print ,再执行 join ;- 若代码是“ t.start(); join; print; ”:主线程竞争到 CPU 时,会先执行 join (然后阻塞等子线程),再执行 print 。
简单说:CPU 调度决定“主线程何时能执行”,而代码顺序决定“主线程能执行时,先做哪件事”。
上述的代码是主线程等待t线程完成功能的,其实也可以让t线程等待主线程完成功能,下面是具体代码:
public class text {public static int count=0;public static void main(String[] args) throws InterruptedException {Thread mainThread=Thread.currentThread();//首先获得主线程的引用Thread t=new Thread(()-> {//然后创建并启用t线程try {mainThread.join();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(count);});t.start();for (int i = 0; i < 5000; i++) {count++;}}
}
注意:
- join的调用位置:如果是t线程要等待m线程,则在t线程中调用m线程的join实例方法
- Thread.currentThread()是获取当前所在线程的引用,要注意其使用的位置
补充:
join() | 等待线程结束(死等) |
join(log millis) | 等待线程结束,最多等 millis 毫秒 |
join(log millis ,int nanos) | 同理,但可以更高精度 |
sleep(0)--特殊情况:主动放弃当前线程的剩余时间片(固定时长的CPU使用时间),让CPU优先调度其他就绪状态的线程--->降低CPU的使用率
1.6 线程状态
- NEW : 线程对象已建立(new Thread()),但尚未调用start()方法
- TERMINATED :操作系统中线程已经销毁,但是Thread对象还在(线程入口方法执行完毕)
- RUNNABLE : 线程即将执行(线程在就绪队列中)或者正在执行
- BLOCKED :特指由于锁引起的阻塞
- WAITING : 死等进入的阻塞状态(如 join())
- TIMED_WAITING :带有超时时间的阻塞等待