在上一篇总结中,我们提到了synchronized关键字,在这里还要提及一个关键字,volatile。这两个关键字是在我们处理线程安全问题中非常重要的问题。
- synchronized 被该关键字所修饰的代码块,同一时间只能由获得锁的线程执行,其他线程进入阻塞状态
- volatile 关键字则是用于修饰共享变量,使该变量能够在缓存中被改变后能被其他线程及时更新
这里不进行过多的赘述,这两个关键字使用方法都在这里Volatile关键字。
下面来看下Thread中的其他方法
- yield() 当前线程转让处执行权,让其他就绪线程获取CPU执行权
- sleep() 使线程阻塞一段时间,时间到达后进入就绪状态
- join() 通过该操作能够让线程串行执行
public class Mode implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "线程打印数字为" + i);
}
}
public static void main(String[] args) {
Mode t = new Mode();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try {
t1.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
t2.start();
}
}
我们来看这段代码,通过对t1线程执行join方法后,t1和t2线程会按照顺序执行,首先会将t1线程所有操作执行完成,然后才会执行t2线程。
- interrupt
Java线程之中,一个线程的生命周期分为:初始、就绪、运行、阻塞以及结束。当然,其中也可以有四种状态,初始、就绪、运行以及结束。
一般而言,可能有三种原因引起阻塞:等待阻塞、同步阻塞以及其他阻塞(睡眠、jion或者IO阻塞);对于Java而言,等待阻塞是调用wait方法产生的,同步阻塞则是由同步块(synchronized)产生的,睡眠阻塞是由sleep产生的,jion阻塞是由jion方法产生的。
言归正传,要中断一个Java线程,可调用线程类(Thread)对象的实例方法:interrupte();然而interrupte()方法并不会立即执行中断操作;具体而言,这个方法只会给线程设置一个为true的中断标志(中断标志只是一个布尔类型的变量),而设置之后,则根据线程当前的状态进行不同的后续操作。如果,线程的当前状态处于非阻塞状态,那么仅仅是线程的中断标志被修改为true而已;如果线程的当前状态处于阻塞状态,那么在将中断标志设置为true后,还会有如下三种情况之一的操作:
-
如果是wait、sleep以及jion三个方法引起的阻塞,那么会将线程的中断标志重新设置为false,并抛出一个InterruptedException;
-
如果是java.nio.channels.InterruptibleChannel进行的io操作引起的阻塞,则会对线程抛出一个ClosedByInterruptedException;(待验证)
-
如果是轮询(java.nio.channels.Selectors)引起的线程阻塞,则立即返回,不会抛出异常。(待验证)
如果在中断时,线程正处于非阻塞状态,则将中断标志修改为true,而在此基础上,一旦进入阻塞状态,则按照阻塞状态的情况来进行处理;例如,一个线程在运行状态中,其中断标志被设置为true,则此后,一旦线程调用了wait、jion、sleep方法中的一种,立马抛出一个InterruptedException,且中断标志被清除,重新设置为false。
通过上面的分析,我们可以总结,调用线程类的interrupted方法,其本质只是设置该线程的中断标志,将中断标志设置为true,并根据线程状态决定是否抛出异常。因此,通过interrupted方法真正实现线程的中断原理是:开发人员根据中断标志的具体值,来决定如何退出线程。
public class Mode implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "线程打印数字为" + i);
try { //此处我们加入sleep,会抛出打断异常
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Mode t = new Mode();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
t1.interrupt();
t2.start();
}
}
-
stop() 该方法也能够停止线程,但是该方法并不会推荐使用
synchronized (this) {
x = x+1;
y = y+1;
}
就在这句代码块中,如果我们调用interrupt方法,因为线程是非阻塞状态,只会将其状态标志改为true,等待阻塞后再将线程打断。而stop方法则立即停止线程,很有可能代码处理一般线程就停止,这样会产生残缺的数据,数据不完整可能会对全局造成巨大影响。这也是为什么stop方法并不被推荐使用。
其他一些方法。
线程优先级与守护线程
线程优先级很容易从字面理解,广义上来讲就是在众多线程中,优先级最高的所获得执行权的可能性就越大。优先级的设置有利于我们更好规划任务。比如现在要下载一部1G的电影和一部100M的电影,我们可以将100M设置为优先,下载完成后可以在看100M电影时下载1G的。
在Java中线程优先级被分为1-10这十个等级,如果在我们设置时出现了超过范围的情况,则会抛出IllegalArgumentException异常。
下面是优先级的源码
public final void setPriority(int newPriority) {
ThreadGroup g;
checkAccess();
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
throw new IllegalArgumentException();
}
if((g = getThreadGroup()) != null) {
if (newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
setPriority0(priority = newPriority);
}
}
而在创建线程时,其内部预设了三个优先级
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
对于优先级的理解,我们需要关注几个特性
线程优先级特性:
-
继承性 比如A线程启动B线程,则B线程的优先级与A是一样的。
-
规则性 高优先级的线程总是大部分先执行完,但不代表高优先级线程全部先执行完。
-
随机性 优先级较高的线程不一定每一次都先执行完。
守护线程
在Java线程中有两种线程,一种是User Thread(用户线程),另一种是Daemon Thread(守护线程)。
Daemon的作用是为其他线程的运行提供服务,比如说GC线程。其实User Thread线程和Daemon Thread守护线程本质上来说去没啥区别的,唯一的区别之处就在虚拟机的离开:如果User Thread全部撤离,那么Daemon Thread也就没啥线程好服务的了,所以虚拟机也就退出了。
守护线程并非虚拟机内部可以提供,用户也可以自行的设定守护线程,方法:public final void setDaemon(boolean on) ;但是有几点需要注意:
-
thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。 (备注:这点与守护进程有着明显的区别,守护进程是创建后,让进程摆脱原会话的控制+让进程摆脱原进程组的控制+让进程摆脱原控制终端的控制;所以说寄托于虚拟机的语言机制跟系统级语言有着本质上面的区别)
-
在Daemon线程中产生的新线程也是Daemon的。 (这一点又是有着本质的区别了:守护进程fork()出来的子进程不再是守护进程,尽管它把父进程的进程相关信息复制过去了,但是子进程的进程的父进程不是init进程,所谓的守护进程本质上说就是“父进程挂掉,init收养,然后文件0,1,2都是/dev/null,当前目录到/”)
-
不是所有的应用都可以分配给Daemon线程来进行服务,比如读写操作或者计算逻辑。因为在Daemon Thread还没来的及进行操作时,虚拟机可能已经退出了。
线程死锁问题
synchronized关键字的使用能保证多线程安全问题,但是处理不当也可能导致死锁问题。用synchronized所修饰的代码块在同一时间只能允许一条线程进入。就像2个人过独木桥,线程1占据一定地方,线程2占据一定地方,谁都不想让,这样就造成程序卡在这里无法继续执行。
产生死锁的四个必要条件?
(1)互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
(2)请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
(3)不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
(4)环路等待条件:是指进程发生死锁后,必然存在一个进程--资源之间的环形链
代码实现死锁实例
public class Mode extends Thread {
public static void main(String[] args) {
Thread t1 = new DeadLock(true, "线程一");
Thread t2 = new DeadLock(false, "线程二");
t1.start();
t2.start();
}
}
class DeadLock extends Thread {
public DeadLock(boolean sign, String str) {
super(str);
this.sign = sign;
}
private boolean sign = false;
private static Object objA = new Object();
private static Object objB = new Object();
@Override
public void run() {
while (true) {
if (sign) {
//此时线程A想要获得资源必须等待线程B释放
synchronized (objA) {
System.out.println(Thread.currentThread().getName() + "获得资源A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (objB) {
System.out.println(Thread.currentThread().getName() + "获得资源B");
}
}
} else {
//线程B获得资源必须等待A释放锁
synchronized (objB) {
System.out.println(Thread.currentThread().getName() + "获得资源B");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (objA) {
System.out.println(Thread.currentThread().getName() + "获得资源A");
}
}
}
}
}
}