Java中的线程状态有6种,如下:
public enum State {
// 新创建了一个线程,但还没有调用start方法
NEW,
// 运行状态,线程创建后调用了该线程的start后的状态,在Java中将就绪状态和运行状态统称为运行状态。
RUNNABLE,
// 阻塞状态,表示线程因抢锁失败而处于挂起状态
BLOCKED,
// 等待状态,调用了Object对象的wait方法或者调用了Condition的await方法后进入此状态
WAITING,
// 线程处于等待状态,但是这个等待是有时限的,调用Thread#sleep(),Object#wait(long),Thread#join(long)
TIMED_WAITING,
// 终止状态,表示线程以执行完
TERMINATED;
}
线程状态示意图如下:
当线程处于等待状态(WAITING)或者有超时等待状态(TIMED_WAITING)时,可以通过调用线程的 interrupt() 方法来中断线程的等待,此时会抛出InterruptedException。例如,public static native void sleep(long millis) throws InterruptedException;
我们可以捕捉异常并实现自己的处理逻辑,也可以不处理继续向上层抛出异常。对于一些没有抛出InterruptedException的方法的中断逻辑只能由自己实现,例如,在一个循环中,可以自己判断当前线程的状态,然后选择是否中断操作。
当线程处于阻塞状态(BLOCKED)或者运行状态(RUNNING)时,调用线程的interrupt方法只能将线程的状态标志改为true。停止线程的逻辑需要自己实现。
- interrupt()的作用是用来中断线程,但只是形式上的中断,并不真正意义上的打断一个正在运行的线程。而是修改这个线程中的中断状态(interrupt)。同时,对于sleep()、wait()和join()方法阻塞下的线程会抛出一个中断异常。
- isInterrupt()方法会返回线程的中断标记位。默认返回false,在调用了interrupt()方法后就会返回true。
- interrupted()是Thread类的一个静态方法,它的作用与isInterrupt()作用类似,都会返回线程的标记位。只不过interrupted()方法会将标记位重置为false。
在线程的run方法中可以根据interrupt标记位来决定是否停止线程,即可以通过isInterrupt()方法或者interrupted()方法来判断,如果为true,则不执行业务逻辑。所以说Java的线程是协作式的,而非抢占式的。
-
start() 它的作用是启动一个新线程。 通过start()方法来启动的新线程,处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行相应线程的run()方法,这里方法run()称为线程体,它包含了要执行的这个线程的内容,run方法运行结束,此线程随即终止。start()不能被重复调用。用start方法来启动线程,真正实现了多线程运行,即无需等待某个线程的run方法体代码执行完毕就直接继续执行下面的代码。这里无需等待run方法执行完毕,即可继续执行下面的代码,即进行了线程切换。
-
run()就和普通的成员方法一样,可以被重复调用。如果直接调用run方法,并不会启动新线程!程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到多线程的目的。
总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。
- sleep()方法是Thread中的一个方法,调用sleep()方法后线程会释放CPU,进入阻塞状态,但线程不会释放资源锁。
- yield()方法是Thread中的一个方法,调用yield()方法后线程会让出CPU执行权,进入就绪状态。调用yield()方法后线程不会释放资源锁。
- Join()方法是Thread中的一个方法,调用join可以使一个线程在另一个线程结束后再执行。暂停的线程会进入阻塞状态并释放CPU。例如,在线程A中执行线程B的join方法,会使A让出CPU进入阻塞状态,让B先执行,等到B执行完后再执行A。使用join方法可以保证两个线程顺序执行。
- wait()方法是Object中的方法,调用wait()方法会使当前线程暂停执行并释放CPU,进入阻塞状态,且会释放资源锁。通过notify()方法或者notifyAll()使线程进入就绪状态。
可以通过interrupt()方法来安全的停止线程。interrupt()方法会修改线程内部的interrupt标记位,在线程的run方法中根据标记位决定是否执行线程逻辑:
public static void main(String[] args) {
Thread thread = new Thread(()->{
int counter =0;
while(!Thread.currentThread().isInterrupted()){
try {
Thread.sleep(1000);
System.out.println("cuonter:"+counter++);
} catch (Exception e) {
e.printStackTrace();
//注意这里,需要再次调用intercept
Thread.currentThread().interrupt();
}
}
});
thread.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
在线程调用interrupt()方法后,如果线程正在sleep则会重置标记位为false,因此需要在catch中再次调用intercept()方法。
java中线程分为两种类型:用户线程和守护线程。通过Thread.setDaemon(false)设置为用户线程;通过Thread.setDaemon(true)设置为守护线程。如果不设置次属性,默认为用户线程。
用户线程和守护线程的区别:
-
主线程结束后用户线程还会继续运行,JVM存活;主线程结束后守护线程和JVM的状态有下面第2条确定。
-
如果没有用户线程,都是守护线程,那么JVM结束(随之而来的是所有的一切烟消云散,包括所有的守护线程)
stop方法已不推荐使用,它在某些方面与中断机制有着相似之处,如当前线程在等待内置锁或IO时,stop跟interrupt一样,不会终止这些操作。当catch注stop导致的异常时,程序也可以继续执行,虽然stop本意是要停止线程,这么做会让线程变得更加混乱。stop与interrupt的区别在于,中断需要程序自己检查然后做响应的处理,而stop会直接在代码执行过程中抛出ThreadDeath错误,这是一个Error的子类。举个例子:
public class TestStop {
private static final int[] array = new int[80000];
private static final Thread t = new Thread() {
public void run() {
try {
System.out.println(sort(array));
} catch (Error err) {
err.printStackTrace();
}
System.out.println("in thread t");
}
};
static {
Random random = new Random();
for(int i = 0; i < array.length; i++) {
array[i] = random.nextInt(i + 1);
}
}
private static int sort(int[] array) {
for (int i = 0; i < array.length-1; i++){
for(int j = 0 ;j < array.length - i - 1; j++){
if(array[j] < array[j + 1]){
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
return array[0];
}
public static void main(String[] args) throws Exception {
t.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("go to stop thread t");
t.stop();
System.out.println("finish main");
}
}
这个例子很简单,线程 t 里面做了一个非常耗时的排序操作,排序方法中,只有简单的加、减、赋值、比较等操作,一个可能的执行结果如下:
go to stop thread t
java.lang.ThreadDeath
at java.lang.Thread.stop(Thread.java:758)
at com.ticmy.interrupt.TestStop.main(TestStop.java:44)
finish main
in thread t
这里 sort 方法是个非常耗时的操作,也就是说主线程休眠一秒钟后调用 stop 的时候,线程 t 还在执行 sort 方法。就是这样一个简单的方法,也会抛出错误!换一句话说,调用 stop 后,大部分 Java 字节码都有可能抛出错误,哪怕是简单的加法!
如果线程当前正持有锁,stop 之后则会释放该锁。由于此错误可能出现在很多地方,那么这就让编程人员防不胜防,极易造成对象状态的不一致。例如,对象 obj 中存放着一个范围值:最小值 low,最大值 high,且 low 不得大于 high,这种关系由锁 lock 保护,以避免并发时产生竞态条件而导致该关系失效。假设当前 low 值是 5,high 值是 10,当线程 t 获取 lock 后,将 low 值更新为了 15,此时被 stop 了,真是糟糕,如果没有捕获住 stop 导致的 Error,low 的值就为 15,high 还是 10,这导致它们之间的小于关系得不到保证,也就是对象状态被破坏了!如果在给 low 赋值的时候 catch 住 stop 导致的 Error 则可能使后面 high 变量的赋值继续,但是谁也不知道 Error 会在哪条语句抛出,如果对象状态之间的关系更复杂呢?这种方式几乎是无法维护的,太复杂了!如果是中断操作,它决计不会在执行 low 赋值的时候抛出错误,这样程序对于对象状态一致性就是可控的。
正是因为可能导致对象状态不一致,stop 才被禁用。
suspend()方法也被标记为废弃,suspend会挂起线程但是并不会释放锁,使得其他线程无法访问公共同步对象,可能产生死锁的问题。
(1)可以使用join方法来实现,比如在T2线程中调用了T1的join方法,那么T2就会等待T1执行结束之后再执行
public class ThreadLoopOne {
public static void main(String[] args) {
Thread t1 = new Thread(new Work(null));
Thread t2 = new Thread(new Work(t1));
Thread t3 = new Thread(new Work(t2));
t1.start();
t2.start();
t3.start();
}
static class Work implements Runnable {
private Thread beforeThread;
public Work(Thread beforeThread){
this.beforeThread = beforeThread;
}
@Override
public void run() {
// 如果有线程,就 join 进来,没有的话就直接输出
if (beforeThread != null ){
try {
beforeThread.join();
System.out.println("thread start : " + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
System.out.println("thread start : " + Thread.currentThread().getName());
}
}
}
}
(2)使用单个线程池实现
public class ThreadLoopThree {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread start : " + Thread.currentThread().getName() + " run one");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread start : " + Thread.currentThread().getName() + " run two");
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread start : " + Thread.currentThread().getName() + " run three");
}
});
ExecutorService executor = Executors.newSingleThreadExecutor();
// 将线程依次加入到线程池中
executor.submit(t1);
executor.submit(t2);
executor.submit(t3);
// 及时将线程池关闭
executor.shutdown();
}
}
(3)可以使用CountDownLatch
public class ThreadLoopTwo {
public static void main(String[] args) {
// 设置线程 1 的信号量为 0
CountDownLatch cOne = new CountDownLatch(0);
// 设置线程 2 的信号量为 1
CountDownLatch cTwo = new CountDownLatch(1);
// 设置线程 3 的信号量为 1
CountDownLatch cThree = new CountDownLatch(1);
// 因为 cOne 为 0 ,故 t1 可以直接执行
Thread t1 = new Thread(new Work(cOne,cTwo));
// 线程 t1 执行完毕之后,此时的 cTwo 为 0 , t2 开始执行
Thread t2 = new Thread(new Work(cTwo,cThree));
// 线程 t2 执行完毕,此时 cThree 为 0 , t3 开始执行
Thread t3 = new Thread(new Work(cThree,cThree));
t1.start();
t2.start();
t3.start();
}
static class Work implements Runnable{
CountDownLatch cOne;
CountDownLatch cTwo;
public Work(CountDownLatch cOne, CountDownLatch cTwo){
super();
this.cOne = cOne;
this.cTwo = cTwo;
}
@Override
public void run() {
try {
// 当前一个线程信号量为 0 时,才执行
cOne.await();
System.out.println("thread start : " + Thread.currentThread().getName());
// 后一个线程信号量减 1
cTwo.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
首先来看下进程与线程的定义:
进程:是执行中一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。进程是表示资源分配的的基本概念,又是调度运行的基本单位,是系统中的并发执行的单位。
线程:线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。
要注意面试官问这个问题的目的不是让面试者背出进程与线程的定义,而是想要看面试者对于操作系统、内存、进程、与线程之间关系的理解。这些内容涉及到操作系统为进程分配内存、虚拟机分配堆内存与栈内存。可以说大多数面试者回答这道问题都没get到点。
操作系统的任务是管理与配置内存、调度CPU、决定系统资源供需的优先级、控制输入输出设备等。其中管理与配置内存是操作系统的重要功能之一。操作系统会为每一个程序分配特定的内存空间,然后将程序的代码载入这块内存区域,并执行代码的逻辑,这段被加载到内存的程序代码与内存就构成了一个操作系统的进程,为了保证进程的安全,这个进程只能运行在自己的内存空间中,各个进程之间不能随意访问其他进程的内存空间,这在操作系统上叫做内存隔离,内存隔离保证了程序的执行是安全的。
线程则是对于进程而言的,它是进程的真正执行者,操作系统通过调度CPU为每个进程分配时间片来执行进程中的逻辑,多个进程轮换获取CPU,达到类似并行执行的效果。对于进程而言CPU分配的时间片的真正获得者就是进程中的线程,所以说线程是进程中的实际运作单位。线程在创建过程中虚拟机会给线程分配一定的内存空间,这个内存空间就是所谓的栈内存。线程执行的过程是以方法为单位,每个方法被封装成一个栈帧,线程执行代码时就对应栈帧入栈和出栈的过程。栈帧又分为局部变量表、操作数、动态连接、方法返回等。常说的栈中存储的基本数据类型与对象的引用就是在栈帧的局部变量表中存储着。对于进程而言它也希望能够向操作系统一样并发的执行多个进程。因此,就有了多线程的概念。进程中允许多个线程同时执行,每个线程都有自己的私有栈空间,线程与线程之间不能共享彼此的私有空间,但是可以共享进程的公共空间,即堆内存。由此也引申出了线程的同步与安全问题。
以下内容看看就好。
-
一个线程只能属于一个进程,但是一个进程可以拥有多个线程。多线程处理就是允许一个进程中在同一时刻执行多个任务。
-
线程是一种轻量级的进程,与进程相比,线程给操作系统带来侧创建、维护、和管理的负担要轻,意味着线程的代价或开销比较小。
-
线程没有地址空间,线程包含在进程的地址空间中。线程上下文只包含一个堆栈、一个寄存器、一个优先权,线程文本包含在他的进程 的文本片段中,进程拥有的所有资源都属于线程。所有的线程共享进程的内存和资源。 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段, 寄存器的内容,栈段又叫运行时段,用来存放所有局部变量和临时变量。
-
父和子进程使用进程间通信机制,同一进程的线程通过读取和写入数据到进程变量来通信。
-
进程内的任何线程都被看做是同位体,且处于相同的级别。不管是哪个线程创建了哪一个线程,进程内的任何线程都可以销毁、挂起、恢复和更改其它线程的优先权。线程也要对进程施加控制,进程中任何线程都可以通过销毁主线程来销毁进程,销毁主线程将导致该进程的销毁,对主线程的修改可能影响所有的线程。
-
子进程不对任何其他子进程施加控制,进程的线程可以对同一进程的其它线程施加控制。子进程不能对父进程施加控制,进程中所有线程都可以对主线程施加控制。
相同点:
进程和线程都有ID/寄存器组、状态和优先权、信息块,创建后都可更改自己的属性,都可与父进程共享资源、都不鞥直接访问其他无关进程或线程的资源。
所以sleep()和wait()方法的最大区别是:
sleep()睡眠时,保持对象锁,仍然占有该锁;
而wait()睡眠时,释放对象锁。
但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。
在语言层面有两种方式。java.lang.Thread 类的实例就是一个线程但是它需要调用java.lang.Runnable接口来执行,由于线程类本身就是调用的Runnable接口所以你可以继承java.lang.Thread 类或者直接调用Runnable接口来重写run()方法实现线程。
可以通过继承Thread类或者调用Runnable接口来实现线程,问题是,那个方法更好呢?什么情况下使用它?这个问题很容易回,Java不支持类的多重继承,但允许实现多个接口。所以如果你要继承其他类,当然是调用Runnable接口好了。
这个问题经常被问到,但还是能从此区分出面试者对Java线程模型的理解程度。start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。
这又是一个刁钻的问题,因为多线程可以等待单监控锁,Java API 的设计人员提供了一些方法当等待条件改变的时候通知它们,但是这些方法没有完全实现。notify()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。而notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。
这是个设计相关的问题,它考察的是面试者对现有系统和一些普遍存在但看起来不合理的事物的看法。回答这些问题的时候,你要说明为什么把这些方法放在Object类里是有意义的,还有不把它放在Thread类里的原因。一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。
ThreadLocal是一个线程内部的数据存储类,通过它可以在制定的线程中存储数据,数据存储以后,只有在指定线程中才可以获取到存储的数据,对于其它线程来说无法获取到数据。
Java多线程中的死锁 死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,死锁的发生必须满足以下四个条件:
互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。这篇教程有代码示例和避免死锁的讨论细节。
不能被中断,Lock 可以被中断
1)synchronized和lock的用法区别
synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
lock:需要显示指定起始位置和终止位置。一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类做为对象才能保证锁的生效。且在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
用法区别比较简单,这里不赘述了,如果不懂的可以看看Java基本语法。
2)synchronized和lock性能区别
synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。
说到这里,还是想提一下这2中机制的具体区别。据我所知,synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。
现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
3)synchronized和lock用途区别
先说第一种情况,ReentrantLock的lock机制有2种,忽略中断锁和响应中断锁,这给我们带来了很大的灵活性。比如:如果A、B2个线程去竞争锁,A线程得到了锁,B线程等待,但是A线程这个时候实在有太多事情要处理,就是一直不返回,B线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候ReentrantLock就提供了2种机制,第一,B线程中断自己(或者别的线程中断它),但是ReentrantLock不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此);第二,B线程中断自己(或者别的线程中断它),ReentrantLock处理了这个中断,并且不再等待这个锁的到来,完全放弃。
wait():使一个线程处于等待状态,并且释放所持有的对象的lock。
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
allnotity():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。
同步和异步有何异同,在什么情况下分别使用他们?举例说明。 如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。 当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。
有两种实现方法,分别是继承Thread类与实现Runnable接口用synchronized关键字修饰同步方法。
反对使用stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状 态下检查和修改它们。结果很难检查出真正的问题所在。suspend()方法容易发生死锁。
调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被"挂起"的线程恢复运行。对任何线程来说, 如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend(),而应在自己的Thread 类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用 wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。