1.线程和进程有什么区别?
线程是资源调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。进程是资源分配的最小单位,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。
2.如何在Java中实现线程?
1)继承 java.lang.Thread 类,重写run()方法
public class TestThread extends Thread {//自定义类继承Thread类
//run()方法里是线程体
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(this.getName() + ":" + i);//getName()方法是返回线程名称
}
}
public static void main(String[] args) {
TestThread thread1 = new TestThread();//创建线程对象
thread1.start();//启动线程
TestThread thread2 = new TestThread();
thread2.start();
}
}
2)直接调用Runnable接口来重写run()方法
public class TestThread2 implements Runnable {//自定义类实现Runnable接口;
//run()方法里是线程体;
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
public static void main(String[] args) {
//创建线程对象,把实现了Runnable接口的对象作为参数传入;
Thread thread1 = new Thread(new TestThread2());
thread1.start();//启动线程;
Thread thread2 = new Thread(new TestThread2());
thread2.start();
}
}
3)实现Callable<>接口并重写call方法
public class testCallable implements Callable {
@Override
public Object call() throws Exception {
String [] str= {"apple","pear","banana","orange","grape"};
int i=(int)(Math.random()*5);
return str[i];
}
public static void main(String[] args) throws Exception {
//创建任务
testCallable tc=new testCallable();
/**FutureTask同时实现了Runnable,Future接口。
* 它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
*/
//交付任务管理
FutureTask<String> task=new FutureTask<>(tc);//可以看成FutureTask实现了runnable接口
Thread t=new Thread(task);
t.start();
System.out.println("获取结果:"+task.get());
System.out.println("任务是否完成:"+task.isDone());
}
}
区别:
- callable的核心是call方法,允许返回值,runnable的核心是run方法,没有返回值
- call方法可以抛出异常,但是run方法不行
- 因为runnable是java1.1就有了,所以他不存在返回值,后期在java1.5进行了优化,就出现了callable,就有了返回值和抛异常
- callable和runnable都可以应用于executors,而thread类只支持runnable
3线程状态
新建(NEW):新创建了一个线程对象。
可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
(二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
4.wait()和sleep()
最大的不同是在等待时wait()会释放锁,而sleep()一直持有锁。wait()通常被用于线程间交互,sleep()通常被用于暂停执行。
5.thread类的方法有哪些
getName() 返回该线程的名称。
setName(String name) 改变线程名称,使之与参数 name 相同。
getPriority() 返回线程的优先级
setPriority(int newPriority) 更改线程的优先级。
isDaemon() 测试该线程是否为守护线程。
setDaemon(boolean on) 将该线程标记为守护线程或用户线程。
interrupt() 中断线程。
yield() 可以让正在运行的线程直接进入就绪状态,让出CPU的使用权。
join() 等待该线程终止。
6.start()和 run()方法有什么区别
start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。
7.synchronized和volatile关键字的作用
synchronized的使用
- 修饰实例方法,对当前实例对象加锁
- 修饰静态方法,多当前类的Class对象加锁
- 修饰代码块,对synchronized括号内的对象加锁
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步
代码块的同步是利用monitor_enter和monitor_exit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的;禁止进行指令重排序。
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性。
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
volatile实现原理:有volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令,该指令在多核处理器下会引发两件事情。
- 将当前处理器缓存行数据刷写到系统主内存。
- 这个刷写回主内存的操作会使其他CPU缓存的该共享变量内存地址的数据无效。
8.synchronized和Reentrantlock
相同点:两个都是可重入锁,都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)。
ReentrantLock takeLock = new ReentrantLock();
// 获取锁
takeLock.lock();
try {
// 业务逻辑
} finally {
// 释放锁
takeLock.unlock();
}
1.ReentrantLock功能性方面更全面,比如时间锁等候,可中断锁等候,锁投票等,因此更有扩展性。在多个条件变量和高度竞争锁的地方,用ReentrantLock更合适,ReentrantLock还提供了Condition,对线程的等待和唤醒等操作更加灵活,一个ReentrantLock可以有多个Condition实例,所以更有扩展性。
2.ReentrantLock必须在finally中释放锁,否则后果很严重,编码角度来说使用synchronized更加简单,不容易遗漏或者出错。
3.ReentrantLock 的性能比synchronized会好点。
4.ReentrantLock提供了可轮询的锁请求,他可以尝试的去取得锁,如果取得成功则继续处理,取得不成功,可以等下次运行的时候处理,所以不容易产生死锁,而synchronized则一旦进入锁请求要么成功,要么一直阻塞,所以更容易产生死锁。
9.线程池
线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用new线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高的代码执行效率。在JDK的java.util.concurrent.Executors中提供了生成多种线程池的静态方法。
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(4);
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(4);
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
线程池的构造函数参数
public ThreadPoolExecutor(intcorePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize:线程池中核心线程数的最大值
maximumPoolSize:线程池中能拥有最多线程数
workQueue:用于缓存任务的阻塞队列
keepAliveTime:表示空闲线程的存活时间。
TimeUnitunit:表示keepAliveTime的单位。
handler:表示当workQueue已满,且池中的线程数达到maximumPoolSize时,线程池拒绝添加新任务时采取的策略。
submit()和execute()的区别:
- 接收的参数不一样;
- submit()有返回值,而execute()没有;
- submit()可以进行Exception处理;
10.常用线程池
newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。用于多个任务顺序执行的场景
newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。适用于任务量比较固定但耗时长的任务
newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。比较适合任务量大但耗时少的任务
newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。适用于执行定时任务和具体固定周期的重复任务
11.notify()和 notifyAll()有什么区别
notify()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。而notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。
12. 什么是FutureTask?
在Java并发程序中FutureTask表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完 成的时候结果才能取回,如果运算尚未完成get方法将会阻塞。一个FutureTask对象可以对调用了Callable和Runnable的对象进行包 装,由于FutureTask也是调用了Runnable接口所以它可以提交给Executor来执行。
public class MyTask implements Callable<Object>{
private String args1;
private String args2;
//构造函数,用来向task中传递任务的参数
public MyTask(String args1,String args2) {
this.args1=args1;
this.args2=args2;
}
//任务执行的动作
@Override
public Object call() throws Exception {
for(int i=0;i<100;i++){
System.out.println(args1+args2+i);
}
return true;
}
public static void main(String[] args) {
MyTask myTask = new MyTask("11", "22");//实例化任务,传递参数
FutureTask<Object> futureTask = new FutureTask<>(myTask);//将任务放进FutureTask里
//采用thread来开启多线程,futuretask继承了Runnable,可以放在线程池中来启动执行
Thread thread = new Thread(futureTask);
thread.start();
try {
//get():获取任务执行结果,如果任务还没完成则会阻塞等待直到任务执行完成。如果任务被取消则会抛出CancellationException异常,
//如果任务执行过程发生异常则会抛出ExecutionException异常,如果阻塞等待过程中被中断则会抛出InterruptedException异常。
boolean result = (boolean) futureTask.get();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
13.如何避免死锁
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,死锁的发生必须满足以下四个条件:
互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。
14.为什么wait(),notify(),notifyAll()在对象中,而不在Thread类中
Java提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。简单的说,由于wait,notify,notifyAll都是锁级别的操作,所以把他们定义在object类中因为锁属于对象。
15.java.util.concurrent(JUC并发)包
ConcorrenctHashMap由Segment数组结构和HashEntry数组结构组成,Segment是一种可重入锁ReentrantLock
ConcurrentSkipListMap提供了一种线程安全的并发访问的排序映射表
ReentrantLock(可重入锁)效果和synchronized一样,都可以同步执行,lock方法获得锁,unlock方法释放锁
ReentrantLock是一个排他锁,同一时间只允许一个线程访问,而ReentrantReadWriteLock允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问
CountDownLatch利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了
Semaphore 信号量 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可对资源的保护。
Future用来得到返回值。Callable接口可以看作是Runnable接口的补充,call方法带有返回值,并且可以抛出异常runnable接口实现的没有返回值的并发编程。callable实现的存在返回值的并发编程。
16.Java线程锁机制是怎么样的,偏向锁、轻量级锁、重量级锁有什么区别?锁机制是怎样的?
Java的锁是在对象的MarkWord中记录一个状态,无锁、偏向锁、轻量级锁、重量级锁对应不同的状态。
Java的锁机制是根据资源竞争的激烈程度不断升级的过程。
偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。
重量级锁:有实际竞争,且锁竞争时间长。
17.对AbstractQueuedSynchronizer(AQS)的理解。AQS怎么实现可重入锁?
AQS是一个JAVA线程同步框架,是jdk部分锁的核心实现框架。AQS中维护一个信号量state和一个线程组成的双向链表队列,用于线程排队或放行,对于不同的场景有不同的意义。
- AQS 是一个锁框架,它定义了锁的实现机制,并开放出扩展的地方,让子类去实现,比如我们在 lock 的时候,AQS 开放出 state 字段,让子类可以根据 state 字段来决定是否能够获得锁,对于获取不到锁的线程 AQS 会自动进行管理,无需子类锁关心,这就是 lock 时锁的内部机制,封装的很好,又暴露出子类锁需要扩展的地方;
- AQS 底层是由同步队列 + 条件队列联手组成,同步队列管理着获取不到锁的线程的排队和释放,条件队列是在一定场景下,对同步队列的补充,比如获得锁的线程从空队列中拿数据,肯定是拿不到数据的,这时候条件队列就会管理该线程,使该线程阻塞;
- AQS 围绕两个队列,提供了四大场景,分别是:获得锁、释放锁、条件队列的阻塞,条件队列的唤醒。
18.CAS原理
CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术。简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。现在CPU内部已经执行原子的CAS操作。Java5以来,可以使用java.util.concurrent.atomic包中的一些原子类来使用CPU中的这些功能。
19.共享锁和排它锁的区别
- 排它锁的意思是同一时刻,只能有一个线程可以获得锁,也只能有一个线程可以释放锁。
- 共享锁可以允许多个线程获得同一个锁,并且可以设置获取锁的线程数量,共享锁之所以能够做到这些,是因为线程一旦获得共享锁,把自己设置成同步队列的头节点后,会自动的去释放头节点后等待获取共享锁的节点,让这些等待节点也一起来获得共享锁,而排它锁就不会这么干。