【面试】JAVA 多线程并发(上)

 

1.JAVA 线程实现/创建方式

   1.1继承 Thread 类

Thread类实质上是实现了Runnable接口的一个实例,代表一个线程的实例,启动的方法通过Thread类的start()方法,start()方法是一个native()方法,它将启动一个新的线程,并执行run()方法。

public class MyThread extends Thread { 
 public void run() { 
 System.out.println("MyThread.run()"); 
 } 
} 
MyThread myThread1 = new MyThread(); 
myThread1.start();

1.2实现 Runnable 接口

如果一个类已经extends另外一个类,就无法直接 extends Thread类了。此时可以实现一个线程接口,Runnable接口

public class MyThread extends OtherClass implements Runnable { 
 public void run() { 
 System.out.println("MyThread.run()"); 
 } 
}
//启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:
MyThread myThread = new MyThread(); 
Thread thread = new Thread(myThread); 
thread.start(); 
//事实上,当传入一个 Runnable target 参数给 Thread 后,Thread 的 run()方法就会调用
target.run()
public void run() { 
 if (target != null) { 
 target.run(); 
 } 
}

1.3ExecutorService、Callable<class>、Future 有返回值线程

有返回值的任务必须实现Callable接口,无返回值的任务用Runnable接口。执行Callable任务后,可以获取一个Future对象,在该对象上调用get()方法就可以获取callable任务返回的Object对象,再结合线程池的ExecutorService就可以实现有返回结果的多线程。

//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);

//创建多个有返回值的任务
List<Future> list = new ArraryList<Future>();
for (int i = 0; i < taskSize; i++) { 
Callable c = new MyCallable(i + " "); 
// 执行任务并获取 Future 对象
Future f = pool.submit(c); 
list.add(f); 
} 
// 关闭线程池
pool.shutdown(); 
// 获取所有并发任务的运行结果
for (Future f : list) { 
// 从 Future 对象上获取任务的返回值,并输出到控制台
System.out.println("res:" + f.get().toString()); 
}

1.4基于线程池的方式

线程和数据库连接资源非常宝贵,每次需要调用的时候创建,不需要的时候销毁,是非常浪费资源的。可以使用缓存策略,也就是线程池。

		//创建线程池
		ExecutorService threadPool = Executors.newFixedThreadPool(10);
	    while(true) {
	    	threadPool.execute(new Runnable() {     //提交多个线程任务,并执行
				
				@Override
				public void run() {
					System.out.println(Thread.currentThread()
                                       .getName()+" is running ");
					try {
						Thread.sleep(3000);
					} catch (InterruptedException e) {
						 
						e.printStackTrace();
					}
				}
			});
	    }

2.四种线程池

Java里线程池的顶级接口是Executor ,但严格意义上讲Executor并不是一个线程池,而是一个执行线程的工具。真正的线程池接口是ExecutorServcie 。

newCachedThreadPool

    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

  • 按需创建新的线程,如果没有可用线程则创建新的线程,之前用过的线程可能会再次被使用;
  • 因为空闲线程会被移除线程池,因此,如果线程池长时间不被使用也不会消耗系统资源、
  • 一个线程如果在 60秒还没有被使用的话会被移除线程池。因此,长时间保持空闲的线程池不会使用任何资源
  • 阻塞队列使用SynchronousQueue

newFixedThreadPool

  ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);//nThread

  • 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。
  • 阻塞队列使用LinkedBlockingQueue,一个共享的无界队列
  • 在任何情况下最多只有nThread个线程工作,多余的Task将会被存放到队列中等待;
  • 如果线程在执行任务中被终止,终止之前会创建其他的线程代替原来的;
  • 线程将会一直存在在线程池中,直到调用shutDown()方法

newScheduledThreadPool

ScheduledExecutorService scheduledThreadPool =Executors.newScheduledThreadPool(5);//corePoolSize

  • 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行
  • 核心线程数将会一直存在线程池中,除非设置了allowCoreThreadTimeOut

newSingleThreadExecutor

 ExecutorService singleExecutorService = Executors.newSingleThreadExecutor();

  • 线程池中最多同时只有一个线程活跃
  • 同一时刻只有一个任务执行
  • 多余的任务放在LinkedBlockingQueue中

3.线程的5种状态以及转换

1、新建状态(New)  线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
2、就绪状态(Runnable)

线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。随时可能被CPU调度执行。

例如,thread.start()。        

3、运行状态(Running)  线程获取CPU权限进行执行。注意:线程只能从就绪状态进入到运行状态。
4、阻塞状态(Blocked) 

 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。

 阻塞的情况分三种:

(01) 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。 

(02) 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。

(03) 其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

5、死亡状态(Dead) 

线程结束生命周期:

正常结束:run()方法或call()方法执行完成。

异常结束:线程抛出未捕获的异常exception ,error。

调用stop()方法:容易导致死锁。

4.终止线程的4种方式

  1. 正常运行结束:等待run()或者是call()方法执行完毕 ,线程自动结束

  2. 设置共享变量,如boolean flag。flag作为线程是否继续执行的标志

            一般 run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的运行,只有在外部某些             条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:最直接的方法就是设一个 boolean 类型的标               志,并通过设置这个标志为 true 或 false 来控制 while循环是否退出,

public class ThreadSafe extends Thread {
 public volatile boolean exit = false; 
 public void run() { 
 while (!exit){
 //do something
 }
 } 
}

       定义了一个退出标志 exit,当 exit 为 true 时while  循环退出,exit 的默认值为 false.在定义 exit时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。


    3.Interrupt 方法结束线程

线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后break 跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。

线程未处于阻塞状态:使用 isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理

 public class ThreadSafe extends Thread {
 public void run() { 
 while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
 try{
 Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
 }catch(InterruptedException e){
 e.printStackTrace();
 break;//捕获到异常之后,执行 break 跳出循环
 }
 }
 } 
}

   4.stop 方法终止线程(线程不安全)

直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用 stop 方法来终止线程

5.sleep 与 wait 区别

sleep()方法属于 Thread 类 wait()方法属于Object 类中
sleep方法没有释放锁 wait方法释放了锁,使得其他线程可以使用同步控制块或者方法
sleep可以在任何地方使用 wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用
sleep必须捕获异常 wait,notify和notifyAll不需要捕获异常
   

推荐 JAVA线程sleep和wait方法区别  https://www.cnblogs.com/diegodu/p/7866073.html

6.start 与 run 区别

  1. start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码。
  2. 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。
  3. 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。

补充:

线程让步 yield() 

该方法会让当前线程交出cpu权限,但是不能确定具体时间,和sleep方法一样不会释放当前线程所持有的锁,该方法会让线程直接进入就绪状态。目的是为了让同等优先级的线程获得cpu执行的机会。

等待其他线程终止 join()

如果调用该方法的对象线程已启动,那么当前线程释放该对象锁,并进入条件等待队列里面,等待改线程执行完(线程如果在同步代码块中结束会自动调用notifyAll()方法来唤醒等待队列里面的线程)。在线程A上下文中执行了线程B.join()语句,其含义是线程B执行结束后,join()方法才会返回,线程A才可继续执行。

为什么要用 join()方法?——>  很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要在子线程结束后再结束,这时候就要用到 join() 方法。

System.out.println(Thread.currentThread().getName() + "线程运行开始!");
 Thread6 thread1 = new Thread6();
 thread1.setName("线程 B");
 thread1.join();
System.out.println("这时 thread1 执行完毕之后才能执行主线程");

notify()    方法会随机唤醒条件等待队列的任意一个线程,并将其放到锁池里面。

notifyAll()    方法则是唤醒条件等待队列的所有线程,并将其都放到锁池里面。

interrupt() 线程中断  

中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)

  1. 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
  2. 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出InterruptedException,从而使线程提前结束 TIMED-WATING 状态。
  3. 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。
  4. 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程 thread 的时候,可以调用 thread.interrupt()方法,在线程的 run 方法内部可以根据 thread.isInterrupted()的值来优雅的终止线程。

7.守护线程和用户线程

  • 用户(User)线程:运行在前台,执行具体任务,如程序的主线程,连接网络的子线程等自定义的。
  • 守护(Daemon)线程 :运行在后台,为其他前台线程服务,也可以说守护线程是JVM中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作。

main 函数所在的线程就是一个用户线程啊,main函数启动的同时在JVM内部同时还启动了好多守护线程,比如垃圾回收线程。

守护线程和用户线程有区别

用户线程结束,JVM退出,不管这个时候有没有守护线程运行。而守护线程不会影响 JVM 的退出。

守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。

当 JVM 中所有的线程都是守护线程的时候,JVM 就可以退出了;如果还有一个或以上的非守护线程则 JVM 不会退出

8.

猜你喜欢

转载自blog.csdn.net/fcvtb/article/details/89227217
今日推荐