1、线程的加入
线程在启动后,并不一定能立即争抢到CPU,但使用join()方法后,线程会优先抢到CPU,示例代码如下:
class Test implements Runnable{
public void run(){
for(int i = 1 ;i<=10;i++){
System.out.println(Thread.currentThread().getName()+"十循第"+i+"次");
}
}
}
public class Demo1 {
public static void main(String[] args) {
Test t = new Test();
Thread t0 = new Thread(t);
Thread t1 = new Thread(t);
t0.start();
t1.start();
for(int i = 1 ;i<=10;i++){
System.out.println(Thread.currentThread().getName()+"十循第"+i+"次");
}
}
}
此时结果如下:
主线程与T0、T1线程在争抢CPU,此时对代码稍加修改:
class Test implements Runnable{
public void run(){
for(int i = 1 ;i<=10;i++){
System.out.println(Thread.currentThread().getName()+"十循第"+i+"次");
}
}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
Test t = new Test();
Thread t0 = new Thread(t);
Thread t1 = new Thread(t);
t0.start();
/*
* 此时只有主线程与T0线程
* T0线程虽然启动但不一定抢得到CPU
* 但使用join()方法后
* 主线程会将CPU让给T0线程
*/
t0.join();
t1.start();
for(int i = 1 ;i<=10;i++){
System.out.println(Thread.currentThread().getName()+"十循第"+i+"次");
}
}
}
此时结果如下:
因为主线程将CPU让给了T0线程,此时只有T0线程在执行任务,因此结果是T0线程先循环10次,之后主线程抢到CPU并且启动了T1线程,然后主线程再与T1线程争抢CPU。
再次修改示例代码,将t0.join()放在T1线程启动之后:
class Test implements Runnable{
public void run(){
for(int i = 1 ;i<=10;i++){
System.out.println(Thread.currentThread().getName()+"十循第"+i+"次");
}
}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
Test t = new Test();
Thread t0 = new Thread(t);
Thread t1 = new Thread(t);
t0.start();
t1.start();
// 将t0.join()放在T1线程启动之后
t0.join();
for(int i = 1 ;i<=10;i++){
System.out.println(Thread.currentThread().getName()+"十循第"+i+"次");
}
}
}
此时结果如下:
出现这种结果是因为join()方法只会让主线程等待,其他线程不受影响,此时t0.join()放在T1线程启动之后,当主线程进入等待,T0线程与T1线程仍然会争抢CPU,只要T0线程执行完任务代码,主线程就可以开始执行,并且与T1线程开始争抢CPU。
2、线程的正确停止
如何让线程停止?虽然在API文档中可以查阅到stop()方法,但已经过时,目前来说,只能等待线程将任务代码运行完成,之后自然停止。但线程内大多使用循环,为了能够使循环结束,应该使用设置退出标志的办法来实现线程停止,示例代码如下:
class Test implements Runnable{
//创建退出标志flag
boolean flag = true;
public void run(){
//使用退出标志
while(flag){
System.out.println(Thread.currentThread().getName()+"测试线程启动");
}
}
}
public class Demo2 {
public static void main(String[] args) {
Test t = new Test();
Thread t0 = new Thread(t);
Thread t1 = new Thread(t);
t0.start();
t1.start();
/*
* 设置能改变退出标志的任意方法
* 例如定义一个变量i
* 当i循环到500时
* 改变退出标志为false
* 实现任务代码的循环结束
* 从而结束线程
*/
int i = 1 ;
while(true){
if(i++==500){
t.flag=false;
//结束该死循环
break;
}
}
}
}
此时结果如下:
<br />
<br />
<br />
<br />
<br />
并不是忘记上传图片,而是因为示例代码本身运行就是没有结果的。出现这种现象的原因是当主线程启动T0、T1线程之后,CPU依然被主线程占有,于是主线程继续向下执行,将flag改为了false,因此当T0、T1线程抢到CPU之后,经过判断退出标志为false,就直接跳过了循环,什么也没有执行,因此当然没有结果。针对这种现象,对代码做如下修改:
class Test implements Runnable{
boolean flag = true;
public void run(){
while(flag){
System.out.println(Thread.currentThread().getName()+"测试线程启动");
/*
* 此时可以不使用sleep()方法
* 线程仍然会正常停止
* 使用只是为了减少循环的次数
* 方便查看结果
*/
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
Test t = new Test();
Thread t0 = new Thread(t);
Thread t1 = new Thread(t);
t0.start();
t1.start();
//此处的sleep()方法必不可少
Thread.sleep(20);
int i = 1 ;
while(true){
if(i++==500){
t.flag=false;
break;
}
}
}
}
此时结果如下:
修改的方式即为在T0、T1线程启动之后,增加sleep()方法,使目前正在运行的线程放弃CPU执行休眠,也就是使主线程休眠20毫秒,这时T0、T1线程就会去各自执行任务代码,等主线程重新抢占到CPU并且向下执行改变退出标记的值后,T0与T1线程经过判断flag为false,就结束循环,线程也因此结束。
3、线程的“错误”停止
interrupt()方法虽然是中断线程,但实际上该方法并不能正确的停止线程。
API文档中关于该方法有提到中断状态这一概念,需要结合下面两个方法来理解:
public static boolean interrupted()
interrupted()方法是一个静态方法,用于测试当前线程是否已经中断。线程的中断状态由该方法清除。如果当前线程已经中断,则返回 true,否则返回 false。
public boolean isInterrupted()
isInterrupted()方法是一个实例方法,用于测试线程是否已经中断。线程的中断状态不受该方法的影响。如果该线程已经中断,则返回 true,否则返回 false。
简单来讲,这两个方法都会返回一个boolean类型的值来表示我们当前的线程是否中断,当线程在某些条件(详情查阅 Java官方API)都不成立的情况下,调用interrupt()方法会设置该线程的中断状态。之后当我们调用interrupted()方法或isInterrupted()方法时会得到一个true值, 表明线程已中断。在某些条件之中,重点关注这种情况:
在调用Object类的wait()、wait(long)或 wait(long, int)方法,或者该类的join()、join(long)、join(long, int)、sleep(long)或 sleep(long, int)方法过程中受阻,则其中断状态将被清除,它还将收到一个InterruptedException。
当线程在调用上述方法时,一旦其他线程调用interrupted()方法,就会收到一个InterruptedException,这也就是在使用上述方法时需要用try-catch块捕获异常的原因。不过不仅如此,注意重点,中断状态会被清除,这就意味着当我们再去调用interrupted()方法或isInterrupted()方法时,将无法得到正确的返回值,示例代码如下:
public class InterruptThread extends Thread{
public static void main(String[] args) {
InterruptThread thread = new InterruptThread();
System.out.println("启动线程");
thread.start();
try {
sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("中断线程");
thread.interrupt();
try {
sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("程序停止");
}
public void run(){
while(true){
System.out.println("线程正在运行……");
/*
* 减少运行结果的输出
* 使每秒只输出一行结果信息
* 便于查看
* 相当于sleep(1000)
* 那为何不直接使用sleep(1000)呢?
*/
long time = System.currentTimeMillis();
while((System.currentTimeMillis()-time<1000)){
}
}
}
}
run()方法中是一个无限循环的任务代码,始终执行输出"线程正在运行……",当启动线程后,输出"启动线程",之后休眠3秒,会提示"中断线程",中断线程后再次休眠3秒会提示"程序停止",那么程序会停止吗?
此时结果如下:
可以很明显的看到interrupt()方法并没有使线程停止,当然还可以使用正确的线程停止方式来修改上述代码:
public class InterruptThread extends Thread{
public static void main(String[] args) {
InterruptThread thread = new InterruptThread();
System.out.println("启动线程");
thread.start();
try {
sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("中断线程");
thread.interrupt();
try {
sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("程序停止");
}
public void run(){
//使用修改退出标志的方式使循环结束
while(!this.isInterrupted()){
System.out.println("线程正在运行……");
long time = System.currentTimeMillis();
while((System.currentTimeMillis()-time<1000)){
}
}
}
}
此时结果如下:
此时线程正确的停止了,是因为通过修改退出标志使得while循环结束,从而结束了线程。这里再解释一下之前注释中提到的问题,为什么使用while((System.currentTimeMillis()-time<1000)){}而不直接使用Thread.sleep(1000),因为当Thread调用sleep(1000)方法时,就会导致中断状态被清除,同时抛出异常,因此while循环的退出标志!this.isInterrupted()也会失效,不能返回正确的中断状态,线程因此也无法停止。
4、守护线程
守护线程(Daemon Thread),是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个经典的守护线程,并且这种线程并不属于程序中不可或缺的部分,与守护线程对应的是用户线程(User Thread)。因此,当所有的非守护线程结束时,不论守护线程的任务代码是否执行完毕,程序都将会停止。
在使用守护线程时需要注意一下几点:
- 线程使用setDaemon()必须在线程启动,也就是start()方法之前,否则会抛出IllegalThreadStateException异常,即不能把正在运行的常规线程设置为守护线程。
- 在守护线程中产生的新线程也是守护线程。
- 守护线程切记不要去访问固有资源,如文件、数据库,因为它可能会在任何时候发生中断。
将线程转换为守护线程可以通过调用Thread对象的setDaemon()方法来实现,示例代码如下:
//创建守护线程
class DaemonThread implements Runnable{
public void run(){
for(long i = 0;i<9999999L;i++){
System.out.println("守护线程T1执行第"+i+"次");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//创建用户线程
class UserThread extends Thread{
public void run(){
for(int i = 0;i<5;i++){
System.out.println("---用户线程执T0行第"+i+"次---");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo3 {
public static void main(String[] args) {
DaemonThread dt = new DaemonThread();
UserThread ut = new UserThread();
Thread t0 = new Thread(ut);
Thread t1 = new Thread(dt);
//将T1线程设置为守护线程
t1.setDaemon(true);
t0.start();
t1.start();
}
}
此时结果如下:
通过结果可以看出虽然守护线程设置了足够多的循环次数,但是当用户线程执行完任务代码之后,守护线程尚未执行完毕也跟随停止了。
5、volatile关键字
与之前讲解的synchronized类似,Volatile相当于轻量级的synchronized,在某些情况下比synchronized的开销更小,它在多线程开发中保证了共享变量的“可见性”,即当一个线程修改一个共享变量时,另外一个线程能读到这个修改后的值。
最常见的用法是修饰退出标志,当退出标志被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取最新的退出标志。但普通的共享变量被修改之后,什么时候被写入主存是不确定的,因此当其他线程去读取时,此时内存中可能还是原来的旧值,来看代码示例:
//假设线程1先执行,线程2后执行:
//线程1
boolean stop = false;
while(!stop){
//任务代码
do();
}
//线程2
stop = true;
代码示例很常见,之前列举的设置退出标志也是这样操作的。但是事实上,这段代码并不一定会完全运行正确,将线程中断。在极大多数的时候,示例代码是能够把线程中断的,但是也有非常小的概率导致无法中断线程,一旦发生这种情况就会造成死循环了。
出现这种情况的原因是每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将退出标志的值拷贝一份放在自己的工作内存当中。当线程2更改了退出标志的值之后,但是还没来得及写入主存当中,便去执行其他代码了,那么线程1由于不知道线程2对退出标志做出了更改,因此还会一直循环下去。
但是用volatile关键字修饰后,当线程2进行修改时,会导致线程1的工作内存中缓存的退出标志无效,因此线程1再次读取退出标志时会去主存读取,那么线程1读取到的就是最新的正确的值。
版权声明:欢迎转载,欢迎扩散,但转载时请标明作者以及原文出处,谢谢合作! ↓↓↓