Java基础之-多线程
线程和进程的区别?
进程:具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位。
线程:是进程的一个实体,是 cpu 调度和分派的基本单位,是比进程更小的可以独立运行的基本单位。
特点:
- 线程的划分尺度小于进程,这使多线程程序拥有高并发性,
- 进程在运行时各自内存单元相互独立,线程之间内存共享,
- 这使多线程编程可以拥有更好的性能和用户体验
注意:多线程编程对于其它程序是不友好的,占据大量 cpu 资源。
一个Java应用程序至少有几个线程?
两个:
- 主线程:负责main方法代码的执行,
- 垃圾回收器线程:负责了回收垃圾。
如何停止一个线程?
- Thread.stop(),不建议使用
- 通过一个变量去控制,当符合这个条件时,自动结束。
- interrupt()
sleep() 和 wait() 有什么区别?
sleep()方法
- Thread类中的静态方法,
- 当一个线程调用sleep()方法以后,不会释放同步资源锁,其他线程仍然会等待资源锁的释放。
wait()方法
- Object类提供的一个普通方法,
- 而且必须同同步资源锁对象在同步代码块或者同步方法中调用。
当调用wait()方法后,当前线程会立刻释放掉同步锁资源。其他线程就有机会获得同步资源锁从而继续往下执行。
多线程的创建方式?
方式一:继承 Thread 类
Thread 本质上也是实现了 Runnable 接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。这种方式实现多线程很简单,通过自己的类直接 extend Thread,并重写 run()方法,就可以启动新线程并执行自己定义的 run()方法。例如:继承 Thread 类实现多线程,并在合适的地方启动线程。
public class MyThread extends Thread {public void run() {System.out.println("MyThread.run()");}MyThread myThread1 = new MyThread();MyThread myThread2 = new MyThread();myThread1.start();myThread2.start();
}
方式二:实现 Runnable 接口的方式实现多线程,并且实例化 Thread,传入自己的 Thread 实例,调用 run( )方法
public class MyThread implements Runnable {public void run() {System.out.println("MyThread.run()");}
}
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
方式三:通过Callable和Future创建线程
class T implements Callable<String> {@Overridepublic String call() throws Exception {return null;}
}
谈谈你对线程池的理解?jdk提供了哪几种线程池?他们有什么区别?
线程池可以提高线程的创建和销毁的开销
jdk提供了以下几种线程池:
- new SingleThreadExecutor(单线程的线程池)
只有一个线程在执行,相对于单线程执行任务 - new FixedThreadPool(固定线程数的线程池)
固定线程数处理任务;当任务过多,则固定的线程数谁先执行完任务,就执行剩余任务 - new ScheduledThreadPool(控制线程池定时周期任务执行)
- new CachedThreadPool(可缓存的线程池)
一般工作中使用的是new ThreadPoolExecutor
说一下ThreadPoolExecutor各个参数的含义?
ThreadPoolExecutor(int corePoolSize, //核心线程池大小int maximumPoolSize, //最大线程池大小long keepAliveTime, //线程最大空闲时间TimeUnit unit, //时间单位BlockingQueue<Runnable> workQueue, //线程等待队列ThreadFactory threadFactory, //线程创建工厂RejectedExecutionHandler handler //拒绝策略
) {
说一下线程的生命周期?
- 新建状态(New):
当线程对象对创建后,即进入了新建状态,如:Thread thread= new MyThread(); - 就绪状态(Runnable):
当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行; - 运行状态(Running):
当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。 - 阻塞状态(Blocked):
处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:①等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;②同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;③其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。 - 死亡状态(Dead):
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
注意:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
多线程的创建方式?
方式一:继承Thread类创建线程类
class T extends Thread{@Overridepublic void run() {}
}
方式二:通过Runnable接口创建线程类
class T implements Runnable{@Overridepublic void run() {}
}
方式三:通过Callable和Future创建线程
class T implements Callable<String> {@Overridepublic String call() throws Exception {return null;}
}
启动一个线程是调用 run() 方法还是 start() 方法?
启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由 JVM 调度并执行,这并不意味着线程就会立即运行。
run()方法是线程启动后要进行回调(callback)的方法。
什么情况下导致线程死锁,遇到线程死锁该怎么解决?
死锁的定义
死锁是指多个线程因竞争资源而造成的一种互相等待状态,若无外力作用,这些进程都将无法向前推进。
死锁的条件
互斥条件:线程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个线程
所占有。此时若有其他线程请求该资源,则请求线程只能等待。
不剥夺条件:线程所获得的资源在未使用完毕之前,不能被其他线程强行夺走,即只能由获得该资源的线程自己来释放(只能是主动释放)。
请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
循环等待条件:存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个线程所请求。即存在一个处于等待状态的线程集合{Pl, P2, …, pn},其中 Pi 等待的资源被 P(i+1)占有(i=0, 1, …, n-1),Pn 等待的资源被 P0 占有.
避免死锁:
①加锁顺序(线程按照一定的顺序加锁)
②加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
什么是乐观锁和悲观锁?
悲观锁
Java在JDK1.5之前都是靠synchronized关键字保证同步的,这种通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有共享变量的锁,都采用独占的方式来访问这些变量。独占锁其实就是一种悲观锁,所以可以说synchronized是悲观锁。
乐观锁
乐观锁(Optimistic Locking)其实是一种思想。相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。
乐观锁一定就是好的吗?
乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,但它也有缺点:
1.乐观锁只能保证一个共享变量的原子操作。如果多一个或几个变量,乐观锁将变得力不从心,
但互斥锁能轻易解决,不管对象数量多少及对象颗粒度大小。
2.长时间自旋可能导致开销大。假如CAS长时间不成功而一直自旋,会给CPU带来很大的开销。
3.ABA问题。CAS的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后来被一条线程改为B,最后又被改成了A,则CAS认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。
解决的思路是引入版本号,每次变量更新都把版本号加一。
说一下线程池的启动策略?
线程池的执行过程描述:
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
1、线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
2、当调用 execute() 方法添加一个任务时,线程池会做如下判断:
- ①如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
- ②如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。
- ③如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这
个任务; - ④如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告
诉调用者“我不能再接受任务了”。
3、当一个线程完成任务时,它会从队列中取下一个任务来执行。
4、当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
请说出同步线程及线程调度相关的方法?
wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
注意:java 5 通过 Lock 接口提供了显示的锁机制,Lock 接口中定义了加锁(lock()方法)和解锁(unLock()方法),增强了多线程编程的灵活性及对线程的协调
线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好的吗?
不是。线程池默认初始化后不启动 Worker,等待有请求时才启动。
当调用 execute方法添加一个任务时,线程池会做如下判断:
- 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
- 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
- 如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
- 如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会抛出异常RejectExecutionException
当一个线程完成任务时,它会从队列中取下一个任务来执行。
当一个线程无事可做,超过一定的时间(keepAlive)时,线程池会判断。
如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize的大小。
volatile关键字的作用?
对于可见性,Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。主要的原理是使用了内存指令。
- LoadLoad重排序:一个处理器先执行一个L1读操作,再执行一个L2读操作;但是另外一个处理器看到的是先L2再L1
- StoreStore重排序:一个处理器先执行一个W1写操作,再执行一个W2写操作;但是另外一个处理器看到的是先W2再W1
- LoadStore重排序:一个处理器先执行一个L1读操作,再执行一个W2写操作;但是另外一个处理器看到的是先W2再L1
- StoreLoad重排序:一个处理器先执行一个W1写操作,再执行一个L2读操作;但是另外一个处理器看到的是先L2再W1
说一下volatile关键字对原子性、可见性以及有序性的保证?
在volatile变量写操作的前面会加入一个Release屏障,然后在之后会加入一个Store屏障,这样就可以保证volatile写跟Release屏障之前的任何读写操作都不会指令重排,然后Store屏障保证了,写完数据之后,立马会执行flush处理器缓存的操作。
在volatile变量读操作的前面会加入一个Load屏障,这样就可以保证对这个变量的读取时,如果被别的处理器修改过了,必须得从其他 处理器的高速缓存(或者主内存)中加载到自己本地高速缓存里,保证读到的是最新数据;在之后会加入一个Acquire屏障,禁止volatile读操作之后的任何读写操作会跟volatile读指令重排序。
与volatie读写内存屏障对比一下,是类似的意思。Acquire屏障其实就是LoadLoad屏障 + LoadStore屏障,Release屏障其实就是StoreLoad屏障 + StoreStore屏障
什么是CAS?
CAS(compare and swap)的缩写。Java利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,实现原子操作。其它原子操作都是利用类似的特性完成的。
CAS有3个操作数:内存值V,旧的预期值A,要修改的新值B。
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS的缺点:
- CPU开销过大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。 - 不能保证代码块的原子性
CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。 - ABA问题
这是CAS机制最大的问题所在。
什么是AQS?
AQS,即AbstractQueuedSynchronizer,队列同步器,它是Java并发用来构建锁和其他同步组件的基础框架。
同步组件对AQS的使用:
AQS是一个抽象类,主是是以继承的方式使用。
AQS本身是没有实现任何同步接口的,它仅仅只是定义了同步状态的获取和释放的方法来供自定义的同步组件的使用。查看源码可知,在java的同步组件中,AQS的子类(Sync等)一般是同步组件的静态内部类,即通过组合的方式使用。
抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch
它维护了一个volatile int state(代表共享资源)和一个FIFO(双向队列)线程等待队列(多线程争用资源被阻塞时会进入此队列)
public class CountDownLatch {/*** Synchronization control For CountDownLatch.* Uses AQS state to represent count.*/private static final class Sync extends AbstractQueuedSynchronizer {private static final long serialVersionUID = 4982264981922014374L;Sync(int count) {setState(count);}int getCount() {return getState();}protected int tryAcquireShared(int acquires) {return (getState() == 0) ? 1 : -1;}protected boolean tryReleaseShared(int releases) {// Decrement count; signal when transition to zerofor (;;) {int c = getState();if (c == 0)return false;int nextc = c-1;if (compareAndSetState(c, nextc))return nextc == 0;}}}
}
Semaphore是什么?
Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。
semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。
由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。
public static void main(String[] args) { int N = 8; //工人数 Semaphore semaphore = new Semaphore(5); //机器数目 for(int i=0;i<N;i++) new Worker(i,semaphore).start();
}
static class Worker extends Thread{ private int num; private Semaphore semaphore; public Worker(int num,Semaphore semaphore){ this.num = num; this.semaphore = semaphore; } @Override public void run() { try { semaphore.acquire(); System.out.println("工人"+this.num+"占用一个机器在生产..."); Thread.sleep(2000); System.out.println("工人"+this.num+"释放出机器"); semaphore.release(); } catch (InterruptedException e) { e.printStackTrace(); } }
}
Synchronized的原理是什么?
Synchronized是由JVM实现的一种实现互斥同步的方式,查看被Synchronized修饰过的程序块编译后的字节码,会发现,被Synchronized修饰过的程序块,在编译前后被编译器生成了monitorenter和monitorexit两个字节码指令。
在虚拟机执行到monitorenter指令时,首先要尝试获取对象的锁:如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器+1;
当执行monitorexit指令时,将锁计数器-1;当计数器为0时,锁就被释放了。如果获取对象失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。
Java中Synchronize通过在对象头设置标志,达到了获取锁和释放锁的目的。
为什么说Synchronized是非公平锁?
非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等待线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争到锁,这样做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象。
JVM对java的原生锁做了哪些优化?
在Java6之前, Monitor的实现完全依赖底层操作系统的互斥锁来实现.
由于Java层面的线程与操作系统的原生线程有映射关系,如果要将一个线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换到内核态来执行,这种切换代价十分昂贵,很耗处理器时间,现代JDK中做了大量的优化。
一种优化是使用自旋锁,即在把线程进行阻塞操作之前先让线程自旋等待一段时间,可能在等待期间其他线程已经解锁,这时就无需再让线程执行阻塞操作,避免了用户态到内核态的切换。
现代JDK中还提供了三种不同的 Monitor实现,也就是三种不同的锁:
- 偏向锁(Biased Locking)
- 轻量级锁
- 重量级锁
这三种锁使得JDK得以优化 Synchronized的运行,当JVM检测到不同的竞争状况时,会自动切换到适合的锁实现,这就是锁的升级、降级。当没有竞争出现时,默认会使用偏向锁。
JVM会利用CAS操作,在对象头上的 Mark Word部分设置线程ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁,因为在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏向锁可以降低无竞争开销。
如果有另一线程试图锁定某个被偏向过的对象,JVM就撤销偏向锁,切换到轻量级锁实现。
轻量级锁依赖CAS操作 Mark Word来试图获取锁,如果重试成功,就使用普通的轻量级锁否则,进一步升级为重量级锁。
Synchronized和 ReentrantLock的异同?
synchronized
是java内置的关键字,它提供了一种独占的加锁方式。synchronized的获取和释放锁由JVM实现,用户不需要显示的释放锁,非常方便。然而synchronized也有一些问题:
当线程尝试获取锁的时候,如果获取不到锁会一直阻塞。
如果获取锁的线程进入休眠或者阻塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待。
ReentrantLock
ReentrantLock是Lock的实现类,是一个互斥的同步锁。ReentrantLock是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。
等待可中断避免,出现死锁的情况(如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false)
公平锁与非公平锁多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
从功能角度:
ReentrantLock比 Synchronized的同步操作更精细(因为可以像普通对象一样使用),甚至实现 Synchronized没有的高级功能,如:
- 等待可中断当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,对处理执行时间非常长的同步块很有用。
- 带超时的获取锁尝试在指定的时间范围内获取锁,如果时间到了仍然无法获取则返回。
- 可以判断是否有线程在排队等待获取锁。
- 可以响应中断请求与Synchronized不同,当获取到锁的线程被中断时,能够响应中断,中断异常将会被抛出,同时锁会被释放。
- 可以实现公平锁。
从锁释放角度:
Synchronized在JVM层面上实现的,不但可以通过一些监控工具监控 Synchronized的锁定,而且在代码执行出现异常时,JVM会自动释放锁定,但是使用Lock则不行,Lock是通过代码实现的,要保证锁定一定会被释放,就必须将 unLock()放到 finally{}中。
从性能角度:
Synchronized早期实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大。
但是在Java6中对其进行了非常多的改进,
在竞争不激烈时:Synchronized的性能要优于 ReetrantLock;
在高竞争情况下:Synchronized的性能会下降几十倍,但是 ReetrantLock的性能能维持常态。