Android工程师面试准备(一)第三部分

24 篇文章 0 订阅

(四) 线程、多线程和线程池

1、开启线程的三种方式?

  • 继承Thread

public class java_thread extends Thread{

public static void main(String args[])

{

(new java_thread()).run();

System.out.println("main thread run ");

}

public synchronized void run()

{

System.out.println("sub thread run ");

}

}

  • 实现Runnable接口

public class java_thread implements Runnable{

public static void main(String args[])

{

(new Thread(new java_thread())).start();

System.out.println("main thread run ");

}

public void run()

{

System.out.println("sub thread run ");

}

}

  • 创建FutureTask对象,创建Callable子类对象,复写call(相当于run)方法,将其传递给FutureTask对象(相当于一个Runnable)。 创建Thread类对象,将FutureTask对象传递给Thread对象。调用start方法开启线程。这种方式可以获得线程执行完之后的返回值

比较:

实现Runnable接口优势:

1)适合多个相同的程序代码的线程去处理同一个资源

2)可以避免java中的单继承的限制

3)增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。

继承Thread类优势:

1)可以将线程类抽象出来,当需要使用抽象工厂模式设计时。

2)多线程同步

在函数体使用优势

1)无需继承thread或者实现Runnable,缩小作用域。

2、线程和进程的区别?

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

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位;线程是进程的一个实体,是CPU调度和分派的基本单位,是比进程更小的能独立运行的基本单位。线程的划分尺度小于进程,这使得多线程程序的并发性高;进程在执行时通常拥有独立的内存单元,而线程之间可以共享内存。使用多线程的编程通常能够带来更好的性能和用户体验,但是多线程的程序对于其他程序是不友好的,因为它可能占用了更多的CPU资源。当然,也不是线程越多,程序的性能就越好,因为线程之间的调度和切换也会浪费CPU时间。时下很时髦的Node.js就采用了单线程异步I/O的工作模式。

3、为什么要有线程,而不是仅仅用进程?

进程是什么?

程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。

在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。这是这样的设计,大大提高了CPU的利用率。进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。

 

有了进程为什么还要线程?

进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。很多人就不理解了,既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的,主要体现在两点上:

  • 进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
  • 进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。

如果这两个缺点理解比较困难的话,举个现实的例子也许你就清楚了:如果把我们上课的过程看成一个进程的话,那么我们要做的是耳朵听老师讲课,手上还要记笔记,脑子还要思考问题,这样才能高效的完成听课的任务。而如果只提供进程这个机制的话,上面这三件事将不能同时执行,同一时间只能做一件事,听的时候就不能记笔记,也不能用脑子思考,这是其一;如果老师在黑板上写演算过程,我们开始记笔记,而老师突然有一步推不下去了,阻塞住了,他在那边思考着,而我们呢,也不能干其他事,即使你想趁此时思考一下刚才没听懂的一个问题都不行,这是其二。

现在你应该明白了进程的缺陷了,而解决的办法很简单,我们完全可以让听、写、思三个独立的过程,并行起来,这样很明显可以提高听课的效率。而实际的操作系统中,也同样引入了这种类似的机制——线程。

 

线程的优点

因为要并发,我们发明了进程,又进一步发明了线程。只不过进程和线程的并发层次不同:进程属于在处理器这一层上提供的抽象;线程则属于在进程这个层次上再提供了一层并发的抽象。如果我们进入计算机体系结构里,就会发现,流水线提供的也是一种并发,不过是指令级的并发。这样,流水线、线程、进程就从低到高在三个层次上提供我们所迫切需要的并发!

除了提高进程的并发度,线程还有个好处,就是可以有效地利用多处理器和多核计算机。现在的处理器有个趋势就是朝着多核方向发展,在没有线程之前,多核并不能让一个进程的执行速度提高,原因还是上面所有的两点限制。但如果讲一个进程分解为若干个线程,则可以让不同的线程运行在不同的核上,从而提高了进程的执行速度。

例如:我们经常使用微软的Word进行文字排版,实际上就打开了多个线程。这些线程一个负责显示,一个接受键盘的输入,一个进行存盘等等。这些线程一起运行,让我们感觉到我们输入和屏幕显示同时发生,而不是输入一些字符,过一段时间才能看到显示出来。在我们不经意间,还进行了自动存盘操作。这就是线程给我们带来的方便之处。

 

进程与线程的区别

  • 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
  • 线程是进程的一个实体, 是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
  • 一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序 健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

 3、run()和start()方法区别

创建一个线程  Thread t1 = new Thread()

t1.run();  只是调用了一个普通方法,并没有启动另一个线程,程序还是会按照顺序执行相应的代码。

t1.start();  则表示,重新开启一个线程,不必等待其他线程运行完,只要得到cup就可以运行该线程

4、如何控制某个方法允许并发访问线程的个数?

public class SemaphoreTestMain { static Semaphore sSemaphore = new Semaphore(6); public static void main(String[] args) { final SemaphoreTestMain semaphoreTestMain = new SemaphoreTestMain(); for (int i = 0 ; i < 1000;i++) { Thread myThread = new Thread() { @Override public void run() { super.run(); try { semaphoreTestMain.test(); } catch (InterruptedException e) { e.printStackTrace(); } } }; myThread.setName("threat index:" + i); myThread.start(); } } public void test() throws InterruptedException { sSemaphore.acquire(); System.out.println(Thread.currentThread().getName() + "--in"); Thread.sleep(1000); System.out.println(Thread.currentThread().getName() + "--out" ); sSemaphore.release(); }}

static Semaphore sSemaphore = new Semaphore(6) 表示该方法最多允许几个线程访问

sSemaphore.acquire():调用一次,允许进入方法的线程数量减一

sSemaphore.release():调用一次,允许进入方法的线程数量加1

当允许进入方法的线程数量减为0的时候其他线程就等待,直到允许的线程数量大于0

sSemaphore.availablePermits():可以获取当前允许进入方法的线程数量

5、在Java中wait和seelp方法的不同

对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。

sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

在调用sleep()方法的过程中,线程不会释放对象锁。而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

6、谈谈wait/notify关键字的理解

等待对象的同步锁,需要获得该对象的同步锁才可以调用这个方法,否则编译可以通过,但运行时会收到一个异常:IllegalMonitorStateException。

调用任意对象的 wait() 方法导致该线程阻塞,该线程不可继续执行,并且该对象上的锁被释放。

唤醒在等待该对象同步锁的线程(只唤醒一个,如果有多个在等待),注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。

调用任意对象的notify()方法则导致因调用该对象的 wait()方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。

7、什么导致线程阻塞?

阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。

一种是调用它里面的stop()方法

另一种就是你自己设置一个停止线程的标记 (推荐这种)

8、如何实现线程同步? 

1、synchronized关键字修改的方法。2、synchronized关键字修饰的语句块3、使用特殊域变量(volatile)实

9、数据一致性如何保证?

1)、声明式事务。@Transcation

   ----  问题: 大量的操作在一个函数里,会导致锁的时间长,特别是中间夹杂第三方操作的时候,进而导致响应超时,或者数据库线程池被占光。

2)、编程使事务 TranscationTemplate

    并且用带版本号的乐观锁。

            表中加version字段,表示版本。

    先确定要修改记录的version值位 100

       在TranscationTemplate的执行方法中,执行SQL语句:UPDATE TABLE SET COL1='',VERSION=VERSION+1 WHERE COL2='' AND VERSION = 100;

      并且获取返回值,也就是修改掉的条数。

        如果条数为1,表示修改成功,也就是可以往下走了。

        如果条数不为1,表示修改失败,也就是不可以往下走。

         ----- 问题,没法回滚了。

10、如何保证线程安全?

1)synchronized;

2)Object方法中的wait,notify;

3)ThreadLocal机制  来实现的。

11、线程间操作List 

List list = Collections.synchronizedList(new ArrayList());

12、Synchronized用法

1)作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;

2)作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;

3) 作用于代码块,这需要指定加锁的对象,对所给的指定对象加锁,进入同步代码前要获得指定对象的锁。

13、谈谈对Synchronized关键字,类锁,方法锁,重入锁的理解 

java的对象锁和类锁:java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的

 14、谈谈volatile关键字的用法

volatile 关键字作用是,使系统中所有线程对该关键字修饰的变量共享可见,可以禁止线程的工作内存对volatile修饰的变量进行缓存。

1.可见性:Java提供了volatile关键字来保证可见性。

  当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

  而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

  另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

先看一段代码,假如线程1先执行,线程2后执行:

?

1

2

3

4

5

6

7

//线程1

boolean stop = false;

while(!stop){

doSomething();

}

//线程2

stop = true;

  这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。

  下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。

  那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

  但是用volatile修饰之后就变得不一样了:

  第一:使用volatile关键字会强制将修改的值立即写入主存;

  第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

  第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

  那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。

  那么线程1读取到的就是最新的正确的值。

2.保证有序性

?

1

2

3

4

5

6

7

8

9

volatile boolean inited = false;

//线程1:

context = loadContext();

inited = true;

//线程2:

while(!inited ){

sleep()

}

doSomethingwithconfig(context);

确保context已经初始化完成。

3.double check

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

class Singleton{

private volatile static Singleton instance = null;

private Singleton() {

}

public static Singleton getInstance() {

if(instance==null) {

synchronized (Singleton.class) {

if(instance==null)

instance = new Singleton();

}

}

return instance;

}

}

15、谈谈NIO的理解

在说NIO之前,首先要清楚几个概念:

1. 同步/异步:通俗来说,同步或异步的主要区别点是,IO操作的主体是谁。

同步,我们的应用程序(代码)是直接参与IO操作,直到数据准备就绪;或者采用轮询的方式实时检查数据的状态,如果数据准备就绪就获取数据。

异步,是由操作系统进行所有的IO操作,我们程序不需要关心IO操作是怎么进行的,当操作系统完成IO操作之后,给我们的程序发送通知,我们直接拿走数据即可。

2. 阻塞/非阻塞:主要区别是在数据没有准备好时,此线程会不会一直处于等待状态,直到数据准备就绪。

阻塞,当某一线程进行IO操作,此线程会一直等待操作完成,而不能去做其他的事情。

非阻塞,线程不会一直等待着某一个IO操作完成,而是IO操作完成之后,直接去取数据。

NIO(同步非阻塞IO):

NIO三大组件: 1.channel(进行读写操作)

                         2.selector(相当于一个管家,轮询检查数据的就绪状态)

                         3.buffer(读取数据的缓冲)

NIO,采用多路复用IO模型,改善了传统IO对线程资源的消耗,将channel注册在selector上,通过selecotor轮询检查数据的就绪状态,一旦有数据准备好,会通知channel去取数据。当多个客户端连接时,并不是为每个客户端创建一个线程为其服务,而是通过注册的方式,当有客户端准备好数据时,再去取数据。

需要注意的是,同步非阻塞IO是读写方法非阻塞,select收到IO准备就绪的状态,还是需要用户自己进行读写操作。NIO围绕ByteBuffer进行读写操作,通过不断检查ByteBuffer的状态来判断IO操作是否完成。

16、synchronized 和volatile 关键字的区别

  • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
  • volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

17、synchronized与Lock的区别

  • 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
  • synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
  • synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
  • 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
  • synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
  • Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

18、死锁的四个必要条件?

死锁产生的原因

1. 系统资源的竞争

系统资源的竞争导致系统资源不足,以及资源分配不当,导致死锁。

2. 进程运行推进顺序不合适

互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。

循环等待条件: 若干进程间形成首尾相接循环等待资源的关系

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

19、怎么避免死锁?

死锁的避免与预防:

死锁避免的基本思想:

系统对进程发出每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则予以分配。这是一种保证系统不进入死锁状态的动态策略。

理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。所以,在系统设计、进程调度等方面注意如何让这四个必要条件不成立,如何确定资源的合理分配算法,避免进程永久占据系统资源。此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。

死锁避免和死锁预防的区别:

死锁预防是设法至少破坏产生死锁的四个必要条件之一,严格的防止死锁的出现,而死锁避免则不那么严格的限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁。死锁避免是在系统运行过程中注意避免死锁的最终发生。

20、对象锁和类锁是否会互相影响?

  •   对象锁:Java的所有对象都含有1个互斥锁,这个锁由JVM自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁,那么当前线程会等待;synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的1个好处,方法抛异常的时候,锁仍然可以由JVM来自动释放。
  •   类锁:对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步。其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。我们都知道,java类可能会有很多个对象,但是只有1个Class对象,也就是说类的不同实例之间共享该类的Class对象。Class对象其实也仅仅是1个java对象,只不过有点特殊而已。由于每个java对象都有1个互斥锁,而类的静态方法是需要Class对象。所以所谓的类锁,不过是Class对象的锁而已。获取类的Class对象有好几种,最简单的就是MyClass.class的方式。
  •  类锁和对象锁不是同1个东西,一个是类的Class对象的锁,一个是类的实例的锁。也就是说:1个线程访问静态synchronized的时候,允许另一个线程访问对象的实例synchronized方法。反过来也是成立的,因为他们需要的锁是不同的

21、什么是线程池,如何使用?

创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。从JDK1.5开始,Java API提供了Executor框架让你可以创建不同的线程池。比如单线程池,每次处理一个任务;数目固定的线程池或者是缓存线程池(一个适合很多生存期短的任务的程序的可扩展线程池)。

22、谈谈对多线程的理解

1.多线程的概念

       解决多任务同时执行的需求,合理使用CPU资源。多线程的运行是根据CPU切换完成,如何切换由CPU决定,因此多线程运行具有不确定性。

2.多线程的优势

(1)发挥多核CPU的优势

       随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至16核的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%。单核CPU上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的。

(2)防止阻塞

       从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。

(3)程序响应更快 

3.多线程的劣势

(1)设计更复杂

       一般多线程程序比单线程程序的设计要复杂。在多线程访问共享数据的时候,这部分代码需要特别的注意。线程之间的交互往往非常复杂。不正确的线程同步产生的错误非常难以被发现,并且重现以修复。

(2)上下文切换的开销

       线程太多会导致效率的降低,因为线程的执行依靠的是CPU的来回切换。当CPU从执行一个线程切换到执行另外一个线程的时候,它需要先存储当前线程的本地的数据,程序指针等,然后载入另一个线程的本地数据,程序指针等,最后才开始执行。这种切换称为“上下文切换”(“context switch”)。CPU会在一个上下文中执行一个线程,然后切换到另外一个上下文中执行另外一个线程。

(3)增加资源消耗

       Java线程的线程栈所占用的内存是在Java堆外的,所以是不受java程序控制的,只受系统资源限制,默认一个线程的线程栈大小是1M(当让这个可以通过设置-Xss属性设置,但是要注意栈溢出问题),但是,如果每个用户请求都新建线程的话,1024个用户光线程就占用了1个G的内存,如果系统比较大的话,一下子系统资源就不够用了,最后程序就崩溃了 。

4.多线程的使用场景

1、后台任务,例如:定时向大量(100w以上)的用户发送邮件;

2、异步处理,例如:发微博、记录日志等;

3、分布式计算 

23、谈谈你对并发编程的理解并举例说明

  并发编程,为什么要并发编程?

    其实是在提高资源的利用率而且让大家都有机会去共享资源,要不然不用时,资源放在那里也是白白浪费。

    比如说人可以一边跑步,一边听歌,一边唱歌;而不是先去跑步,再去听歌,再去唱歌。

    有人会问,并发编程这么爽,那没有缺点吗?

    当然有,当并发编程共享资源时,会产生一些现象

    1、多条线程在共享相同内存地址时,可能会修改并访问其他线程正在使用的变量。就像一边跑步一边唱歌,都会用到气息,当唱歌与呼气同时进行一个特殊点时,可能就会气息紊乱。

    2、当有不同线程竞争同一资源是,双方僵持导致程序无法进行

就像只有一座独木桥,甲乙两个人在桥的两侧,双方仇深似海,只能在两则进行僵持了,谁也不能过河

    有了这样的缺点难道我们不用并发编程了吗?

    当然不是,我会有一些方法解决这种问题

    1、安全线程类并不会发生这种情况,但是什么事安全线程类

    安全线程类就是在单线程环境或者并发环境下都不会被破坏的类

    无状态的类一定是安全线程类。

    2、synchronized加锁,确保线程有序运行避免死锁产生。(具体方法,我会以代码形式解释,在以后专门写一篇文章)

    3、封装性越强,线程类越安全。

24、谈谈你对多线程同步机制的理解?

线程同步是为了确保线程安全,所谓线程安全指的是多个线程对同一资源进行访问时,有可能产生数据不一致问题,导致线程访问的资源并不是安全的。如果多线程程序运行结果和单线程运行的结果是一样的,且相关变量的值与预期值一样,则是线程安全的。

Java中与线程同步有关的关键字/类包括:

volatile、synchronized、Lock、AtomicInteger等concurrent包下的原子类。。。等

接下来讨论这几种同步方法。

volatile

volatile一般用在多个线程访问同一个变量时,对该变量进行唯一性约束,volatile保证了变量的可见性,不能保证原子性。

用法(例):private volatile booleanflag = false

保证变量的可见性:volatile本质是告诉JVM当前变量在线程寄存器(工作内存)中的值是不确定的,需要从主存中读取,每个线程对该变量的修改是可见的,当有线程修改该变量时,会立即同步到主存中,其他线程读取的是修改后的最新值。

不能保证原子性:原子性指的是不会被线程调度机制打断的操作,在java中,对基本数据类型的变量的读取和赋值操作是原子性操作。自增/自减操作不是原子性操作。例如:i++,其实是分成三步来操作的:1)从主存中读取i的值;2)执行+1操作;3)回写i的值。volatile关键字并不能保证原子性操作。非原子操作都会产生线程安全的问题,那么如何实现自增/自减的原子性呢?后续将有讲解。

synchronized

synchronized提供了一种独占的加锁方式,是比较常用的线程同步的关键字,一般在“线程安全的单例”中普遍使用。该关键字能够保证代码块的同步性和方法层面的同步。

用法(例):1)代码块同步

//使用synchronized关键字实现线程安全的单例模式

private static Singleton instance;

public static Singleton getInstance(){

if(instance == null){

synchronized (Singleton.class)

{

if(instance == null){

instance = new Singleton();

}

}

}

return instance;

}

privateSingleton(){ }

2)方法同步

public static synchronized Singleton getInstance2(){

if(instance == null){

instance = new Singleton();

}

return instance;

}

private Singleton(){ }

很多人说synchronized在性能上存在较大问题,但并没有真实环境产生的数据比较说明,因此在这里不好讨论性能问题。

volatile和synchronized的区别

1)      volatile通过变量的可见性,指定线程必须从主存中读取变量的最新值;synchronized通过阻塞线程的方式,只有当前线程能访问该变量,锁定了当前变量。

2)      volatile使用在变量级别;synchronized可以使用在变量、方法、类级别

3)      volatile不会造成线程阻塞;synchronized可能会造成线程阻塞

4)      volatile不能保证原子性;synchronized能保证原子性

5)      volatile标记的变量不会被编译器优化;synchronized标记的变量有可能会被编译器优化(指令重排)。

如何保证自增/自减的原子性

1)      使用java.util.concurrent包下提供的原子类,如AtomicInteger、AtomicLong、AtomicReference等。

用法:   

AtomicInteger atomicInteger = new AtomicInteger();

atomicInteger.getAndIncrement();//实现原子自增

atomicInteger.getAndDecrement();//实现原子自减

2)使用synchronized同步代码块

Synchronized(this){

value++;

}

3)使用Lock显示锁同步代码块

private Lock lock = new ReentrantLock();

private final int incrementAndGet(){

lock.lock();

try

{

return value++;

}

finally

{

// TODO: handle finally clause

lock.unlock();

}

}

25、Java中堆和栈有什么不同?

为什么把这个问题归类在多线程和并发面试题里?因为栈是一块和线程紧密相关的内存区域。每个线程都有自己的栈内存,用于存储本地变量,方法参数和栈调用,一个线程中存储的变量对其它线程是不可见的。而堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时volatile 变量就可以发挥作用了,它要求线程从主存中读取变量的值。

26、有三个线程T1,T2,T3,怎么确保它们按顺序执行?

在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值