java面试突击-java基础 多线程
多线程多态的实现机制:
(1) 掌握 Executors 可以创建的三种线程池的特点及适用范围。
1.继承 Thread 类,重写父类 run()方法 2.实现 runnable 接口 3.使用 ExecutorService、Callable、Future 实现有返回结果的多线程(JDK5.0 以后)
(2) 多线程同步机制。
在需要同步的方法的方法签名中加入 synchronized 关键字。 使用 synchronized 块对需要进行同步的代码段进行同步。 使用 JDK 5 中提供的 java.util.concurrent.lock 包中的 Lock 对象。 一段 synchronized 的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在 java 里边就是拿到某 个同步对象的锁(一个对象只有一把锁); 如果这个时候同步对象的锁被其他线程拿走了,他(这个线程) 就只能等了(线程阻塞在锁池 等待队列中)。 取到锁后,他就开始执行同步代码(被 synchronized 修饰的代 码);线程执行完同步代码后马上就把锁还给同步对象,其他在锁池中 等待的某个线程就可以拿到锁执行同 步代码了。这样就保证了同步代码在统一时刻只有一个线程在执行。
(3)线程的几种可用状态。
线程在执行过程中,可以处于下面几种状态: 就绪(Runnable):线程准备运行,不一定立马就能开始执行。 运行中(Running):进程正在执行线程的代码。 等待中(Waiting):线程处于阻塞的状态,等待外部的处理结束。 睡眠中(Sleeping):线程被强制睡眠。 I/O 阻塞(Blocked on I/O):等待 I/O 操作完成。 同步阻塞(Blocked on Synchronization):等待获取锁。 死亡(Dead):线程完成了执行。
(4)什么是死锁(deadlock)?
两个进程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是两个进程都陷入了无限的等 待中。
(5)如何确保 N 个线程可以访问 N 个资源同时又不导致死锁?
使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获 取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。
(6)解释下多线程:
首先,什么是线程,线程是程序的执行路径,或者可以说是程序的控制单元。 一个进程可能包含一个或多个进程,当一个进程存在多条执行路径时,就可以将该执行方式称为多线程。 线程的执行方式大致可分为就绪(wait),执行(run),阻塞(block)三个状态,而三个状态的转换实质上是在抢 夺 cpu 资源过程中造成的,正常情况下 cpu 资源不会被线程独自占用,因此多个线程在运行中相互抢夺资源, 造成线程在上述的三个状态之间不断的相互转换。而这也是多线程的执行方式。
(7)线程锁对象详解:
多线程可以同时运行多个任务 但是当多个线程同时访问共享数据时,可能导致数据不同步,甚至错误! so,不使用线程锁, 可能导致错误 应用场景: I/O 密集型操作 需要资源保持同步 优缺点: 优点:保证资源同步 缺点:有等待肯定会慢 扩展(死锁与递归锁) 如果多个线程要调用多个对象,而 A 线程调用 A 锁占用了 A 对象,B 线程调用了 B 锁占用了 B 对象,A 线程不能 调用 B 对象,B 线程不能调用 A 对象,于是一直等待。这就造成了线程“死锁”。 Threading 模块中,也有一个类,RLock,称之为可重入锁。该锁对象内部维护着一个 Lock 和一个 counter 对 象。counter 对象记录了 acquire 的次数,使得资源可以被多次 require。最后,当所有 RLock 被 release 后, 其他线程才能获取资源。在同一个线程中,RLock.acquire 可以被多次调用,利用该特性,可以解决部分死锁 问题。 就是把 lock=threading.Lock()改成 lock = threading.RLock();
(8)同步方法的实现方式:
1.同步方法
即有 synchronized 关键字修饰的方法。 由于 java 的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。 代码如: public synchronized void save(){} 注: synchronized 关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类
2.同步代码块
即有 synchronized 关键字修饰的语句块。 被该关键字修饰的语句块会自动被加上内置锁,从而实现同步 代码如: synchronized(object){ } 注:同步是一种高开销的操作,因此应该尽量减少同步的内容。 通常没有必要同步整个方法,使用 synchronized 代码块同步关键代码即可。
3.使用特殊域变量(volatile)实现线程同步
a.volatile 关键字为域变量的访问提供了一种免锁机制, b.使用 volatile 修饰域相当于告诉虚拟机该域可能会被其他线程更新, c.因此每次使用该域就要重新计算,而不是使用寄存器中的值 d.volatile 不会提供任何原子操作,它也不能用来修饰 final 类型的变量 注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修 改操作该域的方法。 用 final 域,有锁保护的域和 volatile 域可以避免非同步的问题。
4.使用重入锁实现线程同步
在 JavaSE5.0 中新增了一个 java.util.concurrent 包来支持同步。 ReentrantLock 类是可重入、互斥、实现了 Lock 接口的锁, 它与使用 synchronized 方法和快具有相同的基本行为和语义,并且扩展了其能力 ReenreantLock 类的常用方法有: ReentrantLock() : 创建一个 ReentrantLock 实例 lock() : 获得锁 unlock() : 释放锁 注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推 荐使用 注:关于 Lock 对象和 synchronized 关键字的选择: a.最好两个都不用,使用一种 java.util.concurrent 包提供的机制, 能够帮助用户处理所有与锁相关的代码。 b.如果 synchronized 关键字能满足用户的需求,就用 synchronized,因为它能简化代码 c.如果需要更高级的功能,就用 ReentrantLock 类,此时要注意及时释放锁,否则会出现死 锁,通常在 finally 代码释放锁
5.使用局部变量实现线程同步
如果使用 ThreadLocal 管理变量,则每一个使用该变量的线程都获得该变量的副本, 副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。 ThreadLocal 类的常用方法 ThreadLocal() : 创建一个线程本地变量 get() : 返回此线程局部变量的当前线程副本中的值 initialValue() : 返回此线程局部变量的当前线程的"初始值" set(T value) : 将此线程局部变量的当前线程副本中的值设置为 value 注:ThreadLocal 与同步机制 a.ThreadLocal 与同步机制都是为了解决多线程中相同变量的访问冲突问题。 b.前者采用以"空间换时间"的方法,后者采用以"时间换空间"的方式 6.使用阻塞队列实现线程同步 前面 5 种同步方式都是在底层实现的线程同步,但是我们在实际开发当中,应当尽量远离底层结构。 使用 javaSE5.0 版本中新增的 java.util.concurrent 包将有助于简化开发。 本小节主要是使用 LinkedBlockingQueue<E>来实现线程的同步 LinkedBlockingQueue<E>是一个基于已连接节点的,范围任意的 blocking queue。 队列是先进先出的顺序(FIFO) LinkedBlockingQueue 类常用方法 LinkedBlockingQueue() : 创建一个容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue put(E e) : 在队尾添加一个元素,如果队列满则阻塞 size() : 返回队列中的元素个数 take() : 移除并返回队头元素,如果队列空则阻塞
7.使用原子变量实现线程同步
需要使用线程同步的根本原因在于对普通变量的操作不是原子的。 那么什么是原子操作呢? 原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作 即-这几种行为要么同时完成,要么都不完成。 在 java 的 util.concurrent.atomic 包中提供了创建了原子类型变量的工具类, 使用该类可以简化线程同步。 其中 AtomicInteger 表可以用原子方式更新 int 的值,可用在应用程序中(如以原子方式增加的计数器), 但不能用于替换 Integer;可扩展 Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。 AtomicInteger 类常用方法: AtomicInteger(int initialValue) : 创建具有给定初始值的新的 AtomicInteger addAddGet(int dalta) : 以原子方式将给定值与当前值相加 get() : 获取当前值
(9)sleep 方法和 wait 方法的区别
来自不同的类: wait 方法是 Object 类的方法,sleep 方法是 Thread 类的方法。 对于锁的占用情况不同:最主要是 sleep 方法没有释放锁,而 wait 方法释放了锁,使得其他线程可以使用同 步控制块或者方法。 使用范围: wait,notify 和 notifyAll 只能在同步控制方法或者同步控制块里面使用,而 sleep 可以在任 何地方使用) 唤醒方式:调用 sleep()方法的线程通常是睡眠一定时间后,自动醒来。对象调用 wait()方法,必须采用 notify()或者 notifyAll()方法唤醒。
sleep 方法
sleep 方法属于 Thread 类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可 运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间,一个线程对象调用了 sleep 方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。但在 sleep 的过程中 有可能被其他对象调用它的 interrupt(),产生 InterruptedException 异常,如果你的程序不捕获这个异常, 线程就会异常终止,进入 TERMINATED 状态,如果你的程序捕获了这个异常,那么程序就会继续执行 catch 语 句块(可能还有 finally 语句块)以及以后的代码。 注意 sleep()方法是一个静态方法,也就是说他只对当前对象有效,通过 t.sleep()让 t 对象进入 sleep,这 样的做法是错误的,它只会是使当前线程被 sleep 而不是 t 线程。
wait 方法
wait 属于 Object 的成员方法,一旦一个对象调用了 wait 方法,必须要采用 notify()和 notifyAll()方法唤 醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了 wait()后,这个线程就会释放它持有的所 有同步资源,而不限于这个被调用了 wait()方法的对象。wait()方法也同样会在 wait 的过程中有可能被其他 对象调用 interrupt()方法而产生 InterruptedException,效果以及处理方式同 sleep()方法。
(10)什么是守护线程?
守护线程(即 daemon thread),是个服务线程,准确地来说就是服务其他的线程,这是它的作用——而其他 的线程只有一种,那就是用户线程。所以 java 里线程分 2 种, 1、守护线程,比如垃圾回收线程,就是最典型的守护线程。 2、用户线程,就是应用程序里的自定义线程。 1、守护线程,专门用于服务其他的线程,如果其他的线程(即用户自定义线程)都执行完毕,连 main 线程也 执行完毕,那么 jvm 就会退出(即停止运行)——此时,连 jvm 都停止运行了,守护线程当然也就停止执行了。 2、再换一种说法,如果有用户自定义线程存在的话,jvm 就不会退出——此时,守护线程也不能退出,也就 是它还要运行,干嘛呢,就是为了执行垃圾回收的任务啊。 用户自定义线程 1、应用程序里的线程,一般都是用户自定义线程。 2、用户也可以在应用程序代码自定义守护线程,只需要调用 Thread 类的设置方法设置一下即可。
(11)同步和异步的区别:
同步(Sync)
所谓同步,就是发出一个功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作。 根据这个定义,Java 中所有方法都是同步调用,应为必须要等到结果后才会继续执行。我们在说同步、异步 的时候,一般而言是特指那些需要其他端协作或者需要一定时间完成的任务。 简单来说,同步就是必须一件一件事做,等前一件做完了才能做下一件事。
异步(Async)
异步与同步相对,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这 个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制。 举个例子简单说明下两者的区别: 同步:火车站多个窗口卖火车票,假设 A 窗口当卖第 288 张时,在这个短暂的过程中,其他窗口都不能卖这张 票,也不能继续往下卖,必须这张票处理完其他窗口才能继续卖票。直白点说就是当你看见程序里出现 synchronized 这个关键字,将任务锁起来,当某个线程进来时,不能让其他线程继续进来,那就代表是同步 了。 异步:当我们用手机下载某个视频时,我们大多数人都不会一直等着这个视频下载完,而是在下载的过程看看 手机里的其他东西,比如用 qq 或者是微信聊聊天,这种的就是异步,你执行你的,我执行我的,互不干扰。 比如上面卖火车票,如果多个窗口之间互不影响,我行我素,A 窗口卖到第 288 张了,B 窗口不管 A 窗口,自 己也卖第 288 张票,那显然会出错了
--内容 来自传智播客 面试宝典