多线程之普通
1. Java多线程面试问题:
2.多线程实现方式及并发与同步
3.java多线程面试题整理及答案(2018年)
-----
12.线程同步问题 (java多线程面试题整理及答案(2018年))
1)Java中synchronized 和 ReentrantLock 有什么不同?
synchronized来实现互斥,它有一些缺点。比如你不能扩展锁之外的方法或者块边界, 尝试获取锁时不能中途取消等。
Java 5通过Lock接口提供更复杂控制来解决这些问题。ReentrantLock类实现了Lock,它拥有与synchronized 相同的并发性和内存语义,且它还有可扩展性。
2)ReadWriteLock是什么?
读写锁是用来提升并发程序性能的锁分离技术的成果。
Java中的ReadWriteLock是Java 5 中新增的一个接口,一个ReadWriteLock维护一对关联的锁,一个用于只读操作一个用于写。
在没有写线程的情况下一个读锁可能会同时被多个读线程持有。写锁是独占的,你可以使用JDK中的ReentrantReadWriteLock来实现这个规则,它最多支持65535个写锁和65535个读锁。
3)Semaphore是什么?
Semaphore是一个计数信号,信号量常常用于多线程代码中,如数据库连接池。
信号量维护了一个许可集合。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release()添加一个许可,从而可能释放一个正在阻塞的获取者。
但是,不使用实际的许可对象,Semaphore只对可用许可的号码进行计数,并采取相应的行动。
4) 什么是ThreadLocal变量?
ThreadLocal是Java里一种特殊的变量。每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。
它是为创建代价高昂的对象获取线程安全的好方法,
比如你可以用ThreadLocal 让SimpleDateFormat变成线程安全的,因为那个类创建代价高昂 且每次调用都需要创建不同的实例,所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。
首先,通过复用减少了代价高昂的对象的创建个数。 其次,你在没有使用高代价的同步 或者 不变性的情况下获得了线程安全。
线程局部变量的另一个不错的例子是 ThreadLocalRandom类,它在多线程环境中,减少了创建代价高昂的Random对象的个数。
什么是ThreadLocal?
ThreadLocal用于创建线程的本地变量,我们知道一个对象的所有线程会共享它的全局变量,所以这些变量不是线程安全的,我们可以使用同步技术。
但是当我们不想使用同步的时候,我们可选择ThreadLocal变量。
每个线程都会拥有他们自己的Thread变量,它们可以使用get()\set()方法去获取他们的默认值或 在线程内部改变他们的值。
13 什么是线程安全? Vector是一个线程安全类吗?
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量 的值也和预期的是一样的,就是线程安全的。一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失误。很显然你可以将集合类分 成两组,线程安全和非线程安全的。Vector 是用同步方法来实现线程安全的, 而和它相似的ArrayList不是线程安全的。
1) 如果同步块内的线程 抛出异常会发生什么?
这个问题坑了很多Java程序员,若你能想到锁是否释放这条线索来回答还有点希望答对。无论你的同步块是正常还是异常退出的,里面的线程都会释放锁,
所以对比锁接口,我更喜欢同步块,因为它不用我花费精力去释放锁,该功能可以在finally block里释放锁实现。
2) 如何在两个线程间共享数据?
你可以通过共享对象来实现这个目的,或者 是使用 像阻塞队列 这样并发的数据结构。
这篇教程《Java线程间通信》(涉及到在两个线程间共享对象)用wait和notify方法实现了生产者消费者模型。
3) 如何写代码来解决生产者消费者问题?
在现实中你解决的许多线程问题都属于生产者消费者模型,就是一个线程生产任务供其它线程进行消费,你必须知道怎么进行线程间通信来解决这个问题。
比较低级的办法是用wait和notify来解决这个问题,
比较赞的办法是 用Semaphore 或者 BlockingQueue来实现生产者消费者模型,这篇教程有实现它。
14 Java中同步集合与并发集合有什么区别?
同步集合与并发集合都为多线程和并发提供了 合适的线程安全的集合,不过并发集合的可扩展性更高。
在Java1.5之前程序员们只有同步集合来用 且在 多线程并发的时候会导致争用,阻碍了系统的扩展性。
Java5介绍了并发集合像ConcurrentHashMap,不仅提供线程安全,还用 锁分离和 内部分区 等现代技术提高了可扩展性。
Java中ConcurrentHashMap的并发度是什么?
ConcurrentHashMap把实际map划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap类构造函数的一个可选参数,默认值为16,这样在多线程情况下就能避免争用。
多线程实现方式及并发与同步:
1、wait()。 使一个线程处于等待状态,并且释放所有持有对象的lock锁,直到notify()/notifyAll()被唤醒后放到锁定池(lock blocked pool ),释放同步锁使线程回到可运行状态(Runnable)。
2、notify()。使一个等待状态的线程唤醒, 注意并不能确切 唤醒等待状态线程,是由JVM决定且不按优先级。
3、notifyAllAll()。使所有等待状态的线程唤醒,注意并不是给所有线程上锁,而是让它们竞争。
4、join()。 使一个线程中断,IO完成会回到Runnable状态,等待JVM的调度。
5、sleep()。 使一个线程处于睡眠状态,是一个静态方法,调用此方法要捕捉Interrupted异常,醒来后进入runnable状态,等待JVM调度。
6、synchronized()。 使Running状态的线程加同步锁使其进入(lock blocked pool ),同步锁被释放 进入可运行状态(Runnable)。
注意:当线程在runnable状态时是处于被调度的线程,此时的调度顺序是不一定的。
Thread类中的yield方法可以让一个running状态的线程转入runnable。
12.什么是Java
Cuncurrency API
中Lock 接口?相对同步(synchronization)
有什么优势?
首先我们看下Lock接口的定义:
Lock是一个控制多线程访问共享资源的工具类,比使用synchronized方法或者语句有了更加扩展性的操作,结构灵活,可以有完全不同的属性,
也可以支持多个相关类的条件对象。使用方法:
{
Lock l = ...;
l.lock();
try {
// 访问锁保护的共享资源
} finally {
l.unlock();
}}
- 相对于synchronization 有如下优点:
- 可以使锁更公平
- 可以使线程在等待锁的时候响应中断;
- 可以让线程尝试获取锁,并在无法获取锁的时候立即返回 或者等待一段时间
- 可以在不同的作用域,以不同的顺序获取和释放锁
13. 什么是原子操作? Java Concurrency API中有哪些原子操作类?
原子操作是执行单个任务单元的操作,这个操作不需要干扰其他操作,可以理解为当前情况下不可再分的操作,远在操作是多线程环境下避免数据不一致而存在的必需品。
int++就不是原子操作,如果一个线程读取它的值并行+1操作,而另外一个线程读取了旧的值,则会导致错误的结果。
为了解决这个问题,我们需要确保递增操作是原子的,可以使用同步原语(synchronization),也可以使用Java5 包装的AtomicInteger直接完成原子操作。
在包java.util.concurrent.atomic下面的一Atomic开头的类都是原子类。例如AtomicInteger,AtomicReference等
21.什么是阻塞队列(BlockingQueue)? 如何使用阻塞队列实现生产者—消费者问题?
BlockingQueue是一个阻塞队列,顾名思义,当进行检索和删除的时候,如果队列为空,则阻塞等待直至队列非空,当添加元素到队列的时候,如果没有空间会阻塞等待到队列有空间为止。接口定义如下图所示:
BlockingQueue不接受Null值,如果存储Null会抛出空指针异常。BlocingQueue的实现都是线程安全的,使用内部锁或者其他的并发控制方法。
BlockingQueue定义的常用方法如下:
- add(anObject):把anObject加到BlockingQueue里,如果BlockingQueue可以容纳,则返回true,否则抛出异常。
- offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false。
- put(anObject):把anObject加到BlockingQueue里,如果BlockingQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里有空间再继续。
- poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null。
- take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的对象被加入为止。
BlockingQueue有四个具体的实现类,根据不同需求,选择不同的实现类:
- ArrayBlockingQueue:规定大小的BlockingQueue,其构造函数必须带一个int参数来指明其大小。其所含的对象是以FIFO(先入先出)顺序排序的。
- LinkedBlockingQueue:大小不定的BlockingQueue,若其构造函数带一个规定大小的参数,生成的BlockingQueue有大小限制,若不带大小参数,所生成的BlockingQueue的大小由Integer.MAX_VALUE来决定。其所含的对象是以FIFO顺序排序的。
- PriorityBlockingQueue:类似于LinkedBlockingQueue,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数所带的Comparator决定的顺序。
- SynchronousQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成的。
LinkedBlockingQueue
和ArrayBlockingQueue
比较起来,它们背后所用的数据结构不一样,导致LinkedBlockingQueue的数据吞吐量要大于ArrayBlockingQueue,但在线程数量很大时其性能的可预见性低于ArrayBlockingQueue。实现生产者—消费者代码如下:Message.java
public class Message {
private String msg ;
public String getMsg() {
return msg;
}
public void setMsg(final String msg) {
this.msg = msg;
}
public Message(final String msg) {
this.msg = msg;
}
}
Producer.java
public class Producer implements Runnable {
private BlockingQueue<Message> messageBlockingDeque;
public Producer(final BlockingQueue<Message> messageBlockingDeque) {
this.messageBlockingDeque = messageBlockingDeque;
}
@Override
public void run() {
for (int i = 0; i <100 ; i++) {
Message msg = new Message(" "+i);
try {
Thread.sleep(i);
messageBlockingDeque.put(msg);
System.out.println("Produced:"+msg.getMsg());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Message msg = new Message("exit");
try {
messageBlockingDeque.put(msg);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Consumer.java
public class Consumer implements Runnable {
private BlockingQueue<Message> blockingDeque;
public Consumer(final BlockingQueue<Message> blockingDeque) {
this.blockingDeque = blockingDeque;
}
@Override
public void run() {
Message message;
try {
while((message= blockingDeque.take()).getMsg()!="exit"){
Thread.sleep(10);
System.out.println("Consumed:"+message.getMsg());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
ProductConsumerService.java
public class ProductConsumerService {
public static void main(String[] args) {
BlockingQueue<Message> queue = new ArrayBlockingQueue<Message>(10);
Producer producer = new Producer(queue);
Consumer consumer = new Consumer((queue));
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(producer);
executorService.submit(consumer);
try {
Thread.sleep(10000);
}
executorService.shutdown();
}
}
22.什么是Executors类?
Executors为Executor,ExecutorService,ScheduledExecutorService,ThreadFactory和Callable类提供了一些工具方法。
Executors可以用于方便的创建线程池。
什么是Executors框架?
在Java5中, Executor框架随同java.util.concurrent.Executor
接口一同被引入,Executor框架标准化了线程的调用,调度,执行以及异步任务的控制。
无节制的线程创建会引起内存溢出,创建有限线程数的线程池(ThreadPool)是一个好的选择,线程可以预先创建,回收再利用。
Executor框架促进了Java中线程池的创建,代码如下:
public class SimpleThreadPool {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
Runnable worker = new WorkerThread("" + i);
executor.execute(worker);
}
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
23.什么是并发集合类?
Java集合类都是快速失效的,这就意味着当集合被改变且一个线程在使用迭代器遍历集合的时候,迭代器的next()方法
将抛出ConcurrentModificationException异常。
并发容器支持并发的遍历和并发的更新。主要的类有ConcurrentHashMap, CopyOnWriteArrayList 和CopyOnWriteArraySet
Java 8中,Concurrency API有哪些改进?
一些改进如下:
ConcurrentHashMap 改进了compute(), forEach(), forEachEntry(), forEachKey(), forEachValue(), merge(), reduce() 和 search() 方法.
CompletableFuture that may be explicitly completed (setting its value and status).
Executors 的newWorkStealingPool() 方法创建一个 work-stealing 线程池,使用目前机器上可用的处理器作为它的并行级别。
什么是FutureTask 类?
FutureTask是一个可取消的异步计算类,它实现了Future接口,因为可以在Executors中使用,执行异步处理操作,大多数情况下,我们不需要FutureTask类。
仅仅当我们打算重写Future接口的一些方法并保持原来基础的实现是,它就变得非常有用。我们可以仅仅继承于它并重写我们需要的方法。
什么是Callable和Future?
Java 5在concurrency包中引入了java.util.concurrent.Callable 接口,它和Runnable接口很相似,但它可以返回任何一个对象或者抛出一个异常。
Callable接口使用泛型去定义它的返回类型。Executors类提供了一些有用的方法去在线程池中执行Callable内的任务。由于Callable任务是并行的,我们必须等待它返回的结果,java.util.concurrent.Future应运而生。 在线程池提交Callable任务后返回了一个Future对象,使用它我们可以知道Callable任务的状态和得到Callable返回的执行结果。Future提供了get()方法让我们可以等待Callable结束并获取它的执行结果,示例代码如下:
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(1000);
return Thread.currentThread().getName();
}
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);
List<Future<String>> list = new ArrayList<Future<String>>();
Callable<String> callable = new MyCallable();
for (int i = 0; i < 10; i++) {
Future<String> future = service.submit(callable);
list.add(future);
}
for (Future<String> fut : list) {
try {
System.out.println(new Date() + "::" + fut.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
service.shutdown();
}
}
运行结果:
Mon Oct 06 17:24:52 CST 2014::pool-1-thread-1
Mon Oct 06 17:24:53 CST 2014::pool-1-thread-2
Mon Oct 06 17:24:53 CST 2014::pool-1-thread-3
Mon Oct 06 17:24:53 CST 2014::pool-1-thread-4
Mon Oct 06 17:24:53 CST 2014::pool-1-thread-5
Mon Oct 06 17:24:53 CST 2014::pool-1-thread-6
Mon Oct 06 17:24:53 CST 2014::pool-1-thread-7
Mon Oct 06 17:24:53 CST 2014::pool-1-thread-8
Mon Oct 06 17:24:53 CST 2014::pool-1-thread-9
Mon Oct 06 17:24:53 CST 2014::pool-1-thread-10
3. 如何避免死
Java多线程中的死锁
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,死锁的发生必须满足以下四个条件:
-
互斥条件:一个资源每次只能被一个进程使用。
-
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
-
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
-
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。
3.1) Java中活锁和死锁有什么区别?
这是上题的扩展,活锁和死锁类似,不同之处在于处于活锁的线程或进程的状态是不断改变的,活锁可以认为是一种特殊的饥饿。一个现实的活锁例子是两个 人在狭小的走廊碰到,两个人都试着避让对方好让彼此通过,但是因为避让的方向都一样导致最后谁都不能通过走廊。简单的说就是,活锁和死锁的主要区别是前者 进程的状态可以改变但是却不能继续执行。
3.2)怎么检测一个线程是否拥有锁?
我一直不知道我们竟然可以检测一个线程是否拥有锁,直到我参加了面试。在java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。
二. 常见的线程问题
1、继承Thread类,重写run函数方法: class xx extends Thread{ public void run(){ Thread.sleep(1000); //线程休眠1000毫秒,sleep使线程进入Block状态,并释放资源 } } xx.start(); //启动线程,run函数运行 2、实现Runnable接口,重写run函数方法: Runnable run =new Runnable() { @Override public void run() {} } 3、实现Callable接口,重写call函数方法: Callable call =new Callable() { @Override public Object call() throws Exception { return null;} }
④运行Callable任务可拿到一个 Future对象,Future表示异步计算的结果。 通过Future对象可了解任务执行情况, 可取消任务的执行。
try {
1、多线程和线程同步
线程概念:指进程中的一个按顺序执行的流程,一个进程中可以运行多个线程。线程总是属于某个进程,进程中多个线程共享进程的内存
1、启动一个线程方式
1)使用Thread类,重写Thread的run
Thread thread = new Thread() {
@Override
public void run() { } };
thread.start(); }
2)使用Runnable来定义工作
原理:runnable传到Thread构造方法里,当Thread的run执行时,该方法内部执行runnable的run。作用:便于runnable的重用
static void runnable() {
Runnable runnable = new Runnable() {
@Override
public void run() { } };
Thread thread = new Thread(runnable);
thread.start(); }
3)通过线程池来定义
static void executor() {
Executor executor = Executors.newCachedThreadPool();
executor.execute(runnable); }
4)有返回值的Runnable(很少用)。
static void callable() {
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(5000);//等待5秒模,拟耗时操作
return "Done!";
} };
ExecutorService executorService = Executors.newCachedThreadPool();
Future<String> future = executorService.submit(callable);
while (true) { //该循环的逻辑可理解成加载网络转菊花情况
//xx1() 处理其他事情
//xx2()
if (future.isDone()) { //判断任务是否执行完
try {
//卡主线程,阻塞式的方法,需要等5秒后(sleep的时长)取到值。用来获取任务执行完的时机
String result = future.get();
System.out.println("result:" + result);
}
break;
}} }