Java面试题-day07线程

线程

1) 创建线程方式

实现多线程可以通过继承Thread类和实现Runnable接口。

(1)继承Thread
定义一个类继承Thread类
复写Thread类中的public void run()方法,将线程的任务代码封装到run 方法中
直接创建Thread的子类对象,创建线程
调用start()方法,开启线程(调用线程的任务run方法)
另外可以通过Thread的getName()获取线程的名称。

(2)实现Runnable接口;
定义一个类,实现Runnable接口;
覆盖接口的public void run()的方法,将线程的任务代码封装到run方法中;
创建Runnable接口的子类对象
将Runnabl接口的子类对象作为参数传递给Thread类的构造函数,创建 Thread类对象
(原因:线程的任务都封装在Runnable接口子类对象的run方法中。
所以要在线程对象创建时就必须明确要运行的任务)。
调用start()方法,启动线程。

	两种方法区别:
	(1)实现Runnable接口避免了单继承的局限性
	(2)继承Thread类线程代码存放在Thread子类的run方法中
	实现Runnable接口线程代码存放在接口的子类的run方法中;
	
	在定义线程时,建议使用实现Runnable接口,因为几乎所有多线程都可以使用这种方式实现
	推荐使用线程池

2) 启动一个线程是用run()还是start()?

启动一个线程是调用start()方法,使线程就绪状态,以后可以被调度为运行状态,一个线程必须关联一些具体的执行代码,run()方法是该线程所关联的执行代码。

3) 进程和线程的区别是什么?

进程是执行着的应用程序,而线程是进程内部的一个执行序列。一个进程可以有多个线程。
线程又叫做轻量级进程。

4) 并发编程的3个概念:原子性、可见性、有序性

	并发程序正确地执行,必须要保证原子性、可见性以及有序性。
	只要有一个没有被保证,就有可能会导致程序运行不正确。

原子性:一个操作或多个操作要么全部执行完成且执行过程不被中断,要么就不执行。
可见性:当多个线程同时访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性:程序执行的顺序按照代码的先后顺序执行。
对于单线程,在执行代码时jvm会进行指令重排序,处理器为了提高效率,可以对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证保存最终执行结果和代码顺序执行的结果是一致的。
Java语言对原子性、可见性、有序性的保证

1、原子性
在java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断,要么执行,要么不执行。
X=10; //原子性(简单的读取、将数字赋值给变量)
Y = x; //变量之间的相互赋值,不是原子操作
X++; //对变量进行计算操作
X = x+1;
语句2实际包括两个操作,它先要去读取x的值,再将y值写入,两个操作分开是原子性的。合在一起就不是原子性的。
语句3、4:x++ x=x+1包括3个操作:读取x的值,x+1,将x写入
注:可以通过 synchronized和Lock实现原子性。因为synchronized和Lock能够保证任一时刻只有一个线程访问该代码块。

2、可见性
Java提供了volatile关键字保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中读取新值。
Synchronized和Lock也可以保证可见性,因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中.

3、有序性
在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

5) 线程生命周期

在这里插入图片描述
线程的生命周期:一个线程从它创建到启动,然后运行,直到最后执行完的整个过程。

  • 新建状态:即创建一个新的线程对象,注意新创建的线程对象如果没有调 用start()方法将永远得不到运行。
  • 就绪状态:当新的线程对象调用start()方法时,就进入了就绪状态,进入就 绪状态的线程不一定立即就开始运行。
  • 运行状态:进入运行状态的线程,会由CPU处理其线程体中的代码。
  • 阻塞状态:运行状态的线程有可能出现意外情况而中断运行,比如进行IO 操作,内存的读写,等待键盘输入数据(注意不是出错,出错将提前终止 线程)而进入阻塞状态。当阻塞条件解除后,线程会恢复运行。但其不是 立即进入运行状态,而是进入就绪状态。
  • 终止状态:当线程中run()方法语句执行完后进入终止状态。

6) sleep()和wait()区别

sleep()方法是Thread类中方法,而wait()方法是Object类中的方法。

sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是 他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态,在 调用sleep()方法的过程中,线程不会释放对象锁。而当调用wait()方法的 时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对 象调用notify()方法后本线程才进入对象锁定池准备。

7) stop()和suspend()方法为何不推荐使用?

反对使用stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。结果很难检查出真正的问题所在。suspend()方法容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被"挂起"的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend(),而应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。

8) 互斥锁

  • 在Java中,引入对象互斥锁的概念,来保证共享数据操作的完整性。
  • 每个对象都对应于一个可称为”互斥锁”的标记,这个标记用来保证在任 一时刻,只能有一个线程访问该对象。
  • 关键字synchronized来与对象的互斥锁联系。当某个对象用synchronized 修饰时,表明该对象在任一时刻只能由一个线程访问。
  • 尽量一要去对一个方法进行synchronized,对代码块来进行synchronized

9) 乐观锁和悲观锁

悲观锁(Pessimistic Lock),
顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

乐观锁(Optimistic Lock),
顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

10) 线程死锁

产生死锁的原因:

一.因为系统资源不足。
二.进程运行推进的顺序不合适。
三.资源分配不当。

并发运行的多个线程间彼此等待、都无法运行的状态称为线程死锁。(并发运行的线程分别占有对方需要的资源,但又都不肯首先释放自己占用的资源,这样导致并发的线程无法继续运行下去,而进入阻塞状态)

为避免死锁,在线程进入阻塞状态时应尽量释放其锁定的资源,以为其他的线程提供运行的机会。

  • public final void wait()
  • public final void notify() //唤醒一个指定的线程
  • public final void notifyAll()//唤醒所有等待的线程
  •   死锁的预防
      1:按照特定顺序加锁
      2:代码双重判断检测死锁
      3:给锁对象赋值唯一对象值判断
    

11) 如何确保N个线程可以访问 N个资源同时又不导致死锁?

使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序, 并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序 加锁和释放锁,就不会出现死锁了。

12) 如何停止一个线程

停止一个线程意味着在任务处理完任务之前停掉正在做的操作,也就是放弃当 前的操作。停止一个线程可以用Thread.stop()方法,但最好不要用它。虽然它 确实可以停止一个正在运行的线程,但是这个方法是不安全的,而且是已被废 弃的方法。

	在java中有以下3种方法可以终止正在运行的线程:
	使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
	使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume		一样都是过期作废的方法。
	使用interrupt方法中断线程

interrupt()方法的使用效果并不像for+break语句那样,马上就停止循环。调用interrupt方法是在当前线程中打了一个停止标志,并不是真的停止线程。

13) 多线程间通讯

多线程间通讯就是多个线程在操作同一资源,但是操作的动作不同.
     (1)为什么要通信
	多线程并发执行的时候, 如果需要指定线程等待或者唤醒指定线程, 那么就需要通信.比如生产者消费者的问题,生产一个消费一个,生产的时候需要负责消费的进程等待,生产一个后完成后需要唤醒负责消费的线程,同时让自己处于等待,消费的时候负责消费的线程被唤醒,消费完生产的产品后又将等待的生产线程唤醒,然后使自己线程处于等待。这样来回通信,以达到生产一个消费一个的目的。		
 	 (2)怎么通信
	在同步代码块中, 使用锁对象的wait()方法可以让当前线程等待, 直到有其他线程唤醒为止.使用锁对象的notify()方法可以唤醒一个等待的线程,或者notifyAll唤醒所有等待的线程.
	多线程间通信用sleep很难实现,睡眠时间很难把握。

/*
线程通讯:一个线程完成了自己的任务时,要通知另外一个线程去完成另外一个任务.
生产者与消费者
wait():  等待如果线程执行了wait方法,那么该线程会进入等待的状态,等待状态下的线程必须要被其他线程调用notify方法才能唤醒。
notify():唤醒唤醒线程池等待线程其中的一个。
notifyAll() : 唤醒线程池所有等待线程。

wait与notify方法要注意的事项:
	1. wait方法与notify方法是属于Object对象的。
	2. wait方法与notify方法必须要在同步代码块或者是同步函数中才能使用。
	3. wait方法与notify方法必需要由锁对象调用。
问题一:出现了线程安全问题。价格错乱了...苹果价格为2.0 
生产者获得cpu,i=0生产苹果,价格6.5,当生产者继续获得cpu,i=1生产香蕉,价格为2.0, 此时P对象的名字是香蕉,价格是2.0,
当生产者继续获得cpu,i=3生产苹果,当刚执行完p.name="苹果", 还没有来的及执行p.price=6.5时,消费者获得cpu运行,导致此时p的name="苹果",但价格还是2.0
 */
class Product{
    
    
	String name;  //名字
	doubleprice;  //价格
	booleanflag = false; //产品是否生产完毕的标识,默认情况是没有生产完成。
}
//因为生产者和消费者共享一个产品,所以他们是线程类。
//生产者类
class Producer extends Thread{
    
    
	Product  p ;  	//产品
	public Producer(Product p) {
    
    
		this.p  = p ;
	}
	@Override
	publicvoid run() {
    
    
		inti = 0 ; //为产生不同的产品
		while(true){
    
    
		synchronized (p) {
    
     //让生产者与消费者是同一个对象
			if(p.flag==false){
    
    
				if(i%2==0){
    
    
					p.name = "苹果";
					p.price = 6.5;
				 }else{
    
    
					p.name="香蕉";
					p.price = 2.0;
				 }
		System.out.println("生产者生产出了:"+ p.name+" 价格是:"+ p.price);
				p.flag = true;
				i++;
				p.notifyAll(); //唤醒消费者去消费
			}else{
    
    
				//已经生产完毕,等待消费者先去消费
				try {
    
    
					p.wait();   //生产者等待,并释放锁对象
				} catch (InterruptedException e) {
    
    
					e.printStackTrace();
				}
			}
			
		}	
	  }	
	}
}

//消费者
class Customer extends Thread{
    
    
	Product p; 
	public  Customer(Product p) {
    
    
		this.p = p;
	}
	@Override
	publicvoid run() {
    
    
		while(true){
    
    
			synchronized (p) {
    
    	//利用p对象做为锁对象,p是相同的
				if(p.flag==true){
    
      //产品已经生产完毕
			System.out.println("消费者消费了"+p.name+" 价格:"+ p.price);
					p.flag = false; 
					p.notifyAll(); // 唤醒生产者去生产
				}else{
    
    
					//产品还没有生产,应该等待生产者先生产。
					try {
    
    
						p.wait(); //消费者也等待了...
					} catch (InterruptedException e) {
    
    
						e.printStackTrace();
					}
				}
			}
		}	
	}
}

publicclass Demo9 {
    
    
	publicstaticvoid main(String[] args) {
    
    
		Product p = new Product();  //产品
		//创建生产对象
		Producer producer = new Producer(p);
		//创建消费者
		Customer customer = new Customer(p);
		//调用start方法开启线程
		producer.start();
		customer.start();
	}
}










14) 多线程安全问题

原因:
当程序的多条语句在操作线程共享数据时(如买票例子中的票就是共享资源),由于线程的随机性导致一个线程对多条语句,执行了一部分还没执行完,另一个线程抢夺到cpu执行权参与进来执行,此时就导致共享数据发生错误。比如买票例子中打印重票和错票的情况。

解决方法:
同步是用来解决多线程的安全问题的,在多线程中,同步能控制对共享数据的访问。如果没有同步,当一个线程在修改一个共享数据时,而另外一个线程正在使用或者更新同一个共享数据,这样容易导致程序出现错误的结果。

15) 同步和异步有何异同,在什么情况下分别使用他们?举例说明。

同步是指所有操作串行化执行,顺序不能改变,前一操作未完成,后个操作不执行。
异步是指所有操作可以并行执行,顺序无关。
例如寄信

同步:如果没有寄完,不能吃饭,邮递员10天后送到,发送人被饿死
异步:寄出后可以立即吃饭,邮递员送完后,通知发送人送信结果。
如果强调执行顺序的话,用同步。如果顺序无关,则可以用异步。
异步执行效率比同步高。

16) 多线程有几种实现方法?同步有几种实现方法?

多线程有两种实现方法,分别是继承Thread类与实现Runnable接口

同步的实现方面有五种,分别是synchronized、wait与notify、sleep、suspend、join
synchronized: 一直持有锁,直至执行结束

wait():使一个线程处于等待状态,并且释放所持有的对象的lock,需捕获异常。

sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,需捕获异常,不释放锁。

notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。

notityAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。

17) synchronized如何使用

synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:

1). 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2). 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3). 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4). 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

18) 简述synchronized和java.util.concurrent.locks.Lock的异同?

主要相同点:Lock能完成synchronized所实现的所有功能
主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。Lock的锁定是通过代码实现的,而synchronized是在JVM层面上实现的,synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。Lock还有更强大的功能,例如,它的tryLock方法可以非阻塞方式去拿锁。Lock锁的范围有局限性,块范围,而synchronized可以锁住块、对象、类。

19) 什么是线程池(thread pool)

在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
Java线程池的构造方法,里面参数的含义? ThreadPoolExecutor是线程池的真正实现,参数有:corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(闲置超时时间)等
Java 5+中的Executor接口定义一个执行线程的工具。它的子类型即线程池接口是ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类Executors面提供了一些静态工厂方法,生成一些常用的线程池,如下所示:

  • newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
  • newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
  • newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
  • newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
  • newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。

20) 什么是ThreadLocal类,怎么使用它?

ThreadLocal类提供了线程局部 (thread-local) 变量。是一个线程级别的局部变量,并非“本地线程”。

线程局部变量(ThreadLocal)其实功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,
是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,
就好像每一个线程都完全拥有一个该变量。 

应用场景:用ThreadLocal 来管理 Hibernate Session
我们知道Session是由SessionFactory负责创建的,而SessionFactory的实现是线程安全的,多个并发的线程可以同时访问一个SessionFactory并从中获取Session实例,那么Session是否是线程安全的呢?答案是否定的。Session中包含了数据库操作相关的状态信息,那么说如果多个线程同时使用一个Session实例进行CRUD,就很有可能导致数据存取的混乱
使用ThreadLocal集合保存当前业务线程中的SESSION
private static ThreadLocal session = new ThreadLocal();
ThreadLocal底层是一个Map集合,key是当前线程对象,value是session的副本。
线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java 提供 ThreadLocal 类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。

21) 假如新建T1、T2、T3三个线程,如何保证它们按顺序执行?

T3先执行,在T3的run中,调用t2.join,让t2执行完成后再执行t3
在T2的run中,调用t1.join,让t1执行完成后再让T2执行


package com.xxxx.controller;

public class Test {
    
    
    //现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,	T3在T2执行完后执行
    public static void main(String[] args) {
    
    
        final Thread t1 = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("t1");
            }
        });
        final Thread t2 = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                try {
    
    
//引用t1线程,等待t1线程执行完
                    t1.join();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println("t2");
            }
        });
        Thread t3 = new Thread(new Runnable() {
    
    

            @Override
            public void run() {
    
    
                try {
    
    
//引用t2线程,等待t2线程执行完
                    t2.join();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println("t3");
            }
        });
        t3.start();
        t2.start();
        t1.start();//这三个线程启动没有先后顺序的
    }
}






猜你喜欢

转载自blog.csdn.net/m0_56368068/article/details/120754656