-
Notifications
You must be signed in to change notification settings - Fork 116
多线程与并发基础
- interrupt()的作用是用来中断线程,但只是形式上的中断,并不真正意义上的打断一个正在运行的线程。而是修改这个线程中的中断状态(interrupt)。同时,对于sleep()、wait()和join()方法阻塞下的线程会抛出一个中断异常。
- isInterrupt()方法会返回线程的中断标记位。默认返回false,在调用了interrupt()方法后就会返回true。
- interrupted()是Thread类的一个静态方法,它的作用与isInterrupt()作用类似,都会返回线程的标记位。只不过interrupted()方法会将标记位重置为false。
在线程的run方法中可以根据interrupt标记位来决定是否停止线程,即可以通过isInterrupt()方法或者interrupted()方法来判断,如果为true,则不执行业务逻辑。所以说Java的线程是协作式的,而非抢占式的。
-
start() 它的作用是启动一个新线程。 通过start()方法来启动的新线程,处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行相应线程的run()方法,这里方法run()称为线程体,它包含了要执行的这个线程的内容,run方法运行结束,此线程随即终止。start()不能被重复调用。用start方法来启动线程,真正实现了多线程运行,即无需等待某个线程的run方法体代码执行完毕就直接继续执行下面的代码。这里无需等待run方法执行完毕,即可继续执行下面的代码,即进行了线程切换。
-
run()就和普通的成员方法一样,可以被重复调用。如果直接调用run方法,并不会启动新线程!程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到多线程的目的。
总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。
- sleep()方法是Thread中的一个方法,调用sleep()方法后线程会释放CPU,进入阻塞状态,但线程不会释放资源锁。
- yield()方法是Thread中的一个方法,调用yield()方法后线程会让出CPU执行权,进入就绪状态。调用yield()方法后线程不会释放资源锁。
- Join()方法是Thread中的一个方法,调用join可以使一个线程在另一个线程结束后再执行。暂停的线程会进入阻塞状态并释放CPU。例如,在线程A中执行线程B的join方法,会使A让出CPU进入阻塞状态,让B先执行,等到B执行完后再执行A。使用join方法可以保证两个线程顺序执行。
- wait()方法是Object中的方法,调用wait()方法会使当前线程暂停执行并释放CPU,进入阻塞状态,且会释放资源锁。通过notify()方法或者notifyAll()使线程进入就绪状态。
可以通过interrupt()方法来安全的停止线程。interrupt()方法会修改线程内部的interrupt标记位,在线程的run方法中根据标记位决定是否执行线程逻辑:
public static void main(String[] args) {
Thread thread = new Thread(()->{
int counter =0;
while(!Thread.currentThread().isInterrupted()){
try {
Thread.sleep(1000);
System.out.println("cuonter:"+counter++);
} catch (Exception e) {
e.printStackTrace();
//注意这里,需要再次调用intercept
Thread.currentThread().interrupt();
}
}
});
thread.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
在线程调用interrupt()方法后,如果线程正在sleep则会重置标记位为false,因此需要在catch中再次调用intercept()方法。
java中线程分为两种类型:用户线程和守护线程。通过Thread.setDaemon(false)设置为用户线程;通过Thread.setDaemon(true)设置为守护线程。如果不设置次属性,默认为用户线程。
用户线程和守护线程的区别:
-
主线程结束后用户线程还会继续运行,JVM存活;主线程结束后守护线程和JVM的状态有下面第2条确定。
-
如果没有用户线程,都是守护线程,那么JVM结束(随之而来的是所有的一切烟消云散,包括所有的守护线程)
stop()方法已经被标记位废弃。stop方法会暴力停止线程,使逻辑处理不完整,是一个比较危险的操作,推荐使用intercept方法终止线程。
suspend()方法也被标记为废弃,suspend会挂起线程但是并不会释放锁,使得其他线程无法访问公共同步对象,可能产生死锁的问题。
(1)可以使用join方法来实现,比如在T2线程中调用了T1的join方法,那么T2就会等待T1执行结束之后再执行
public class ThreadLoopOne {
public static void main(String[] args) {
Thread t1 = new Thread(new Work(null));
Thread t2 = new Thread(new Work(t1));
Thread t3 = new Thread(new Work(t2));
t1.start();
t2.start();
t3.start();
}
static class Work implements Runnable {
private Thread beforeThread;
public Work(Thread beforeThread){
this.beforeThread = beforeThread;
}
@Override
public void run() {
// 如果有线程,就 join 进来,没有的话就直接输出
if (beforeThread != null ){
try {
beforeThread.join();
System.out.println("thread start : " + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
System.out.println("thread start : " + Thread.currentThread().getName());
}
}
}
}
(2)使用单个线程池实现
public class ThreadLoopThree {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread start : " + Thread.currentThread().getName() + " run one");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread start : " + Thread.currentThread().getName() + " run two");
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread start : " + Thread.currentThread().getName() + " run three");
}
});
ExecutorService executor = Executors.newSingleThreadExecutor();
// 将线程依次加入到线程池中
executor.submit(t1);
executor.submit(t2);
executor.submit(t3);
// 及时将线程池关闭
executor.shutdown();
}
}
(3)可以使用CountDownLatch
public class ThreadLoopTwo {
public static void main(String[] args) {
// 设置线程 1 的信号量为 0
CountDownLatch cOne = new CountDownLatch(0);
// 设置线程 2 的信号量为 1
CountDownLatch cTwo = new CountDownLatch(1);
// 设置线程 3 的信号量为 1
CountDownLatch cThree = new CountDownLatch(1);
// 因为 cOne 为 0 ,故 t1 可以直接执行
Thread t1 = new Thread(new Work(cOne,cTwo));
// 线程 t1 执行完毕之后,此时的 cTwo 为 0 , t2 开始执行
Thread t2 = new Thread(new Work(cTwo,cThree));
// 线程 t2 执行完毕,此时 cThree 为 0 , t3 开始执行
Thread t3 = new Thread(new Work(cThree,cThree));
t1.start();
t2.start();
t3.start();
}
static class Work implements Runnable{
CountDownLatch cOne;
CountDownLatch cTwo;
public Work(CountDownLatch cOne, CountDownLatch cTwo){
super();
this.cOne = cOne;
this.cTwo = cTwo;
}
@Override
public void run() {
try {
// 当前一个线程信号量为 0 时,才执行
cOne.await();
System.out.println("thread start : " + Thread.currentThread().getName());
// 后一个线程信号量减 1
cTwo.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1)首先是定义
进程:是执行中一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。进程是表示资源分配的的基本概念,又是调度运行的基本单位,是系统中的并发执行的单位。
线程:线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。
2)一个线程只能属于一个进程,但是一个进程可以拥有多个线程。多线程处理就是允许一个进程中在同一时刻执行多个任务。
3)线程是一种轻量级的进程,与进程相比,线程给操作系统带来侧创建、维护、和管理的负担要轻,意味着线程的代价或开销比较小。
4)线程没有地址空间,线程包含在进程的地址空间中。线程上下文只包含一个堆栈、一个寄存器、一个优先权,线程文本包含在他的进程 的文本片段中,进程拥有的所有资源都属于线程。所有的线程共享进程的内存和资源。 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段, 寄存器的内容,栈段又叫运行时段,用来存放所有局部变量和临时变量。
5)父和子进程使用进程间通信机制,同一进程的线程通过读取和写入数据到进程变量来通信。
6)进程内的任何线程都被看做是同位体,且处于相同的级别。不管是哪个线程创建了哪一个线程,进程内的任何线程都可以销毁、挂起、恢复和更改其它线程的优先权。线程也要对进程施加控制,进程中任何线程都可以通过销毁主线程来销毁进程,销毁主线程将导致该进程的销毁,对主线程的修改可能影响所有的线程。
7)子进程不对任何其他子进程施加控制,进程的线程可以对同一进程的其它线程施加控制。子进程不能对父进程施加控制,进程中所有线程都可以对主线程施加控制。
相同点:
进程和线程都有ID/寄存器组、状态和优先权、信息块,创建后都可更改自己的属性,都可与父进程共享资源、都不鞥直接访问其他无关进程或线程的资源。
所以sleep()和wait()方法的最大区别是:
sleep()睡眠时,保持对象锁,仍然占有该锁;
而wait()睡眠时,释放对象锁。
但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。
在语言层面有两种方式。java.lang.Thread 类的实例就是一个线程但是它需要调用java.lang.Runnable接口来执行,由于线程类本身就是调用的Runnable接口所以你可以继承java.lang.Thread 类或者直接调用Runnable接口来重写run()方法实现线程。
可以通过继承Thread类或者调用Runnable接口来实现线程,问题是,那个方法更好呢?什么情况下使用它?这个问题很容易回,Java不支持类的多重继承,但允许实现多个接口。所以如果你要继承其他类,当然是调用Runnable接口好了。
这个问题经常被问到,但还是能从此区分出面试者对Java线程模型的理解程度。start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。
这又是一个刁钻的问题,因为多线程可以等待单监控锁,Java API 的设计人员提供了一些方法当等待条件改变的时候通知它们,但是这些方法没有完全实现。notify()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。而notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。
这是个设计相关的问题,它考察的是面试者对现有系统和一些普遍存在但看起来不合理的事物的看法。回答这些问题的时候,你要说明为什么把这些方法放在Object类里是有意义的,还有不把它放在Thread类里的原因。一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。
ThreadLocal是一个线程内部的数据存储类,通过它可以在制定的线程中存储数据,数据存储以后,只有在指定线程中才可以获取到存储的数据,对于其它线程来说无法获取到数据。
Java多线程中的死锁 死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,死锁的发生必须满足以下四个条件:
互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。这篇教程有代码示例和避免死锁的讨论细节。
不能被中断,Lock 可以被中断
1)synchronized和lock的用法区别
synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
lock:需要显示指定起始位置和终止位置。一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类做为对象才能保证锁的生效。且在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
用法区别比较简单,这里不赘述了,如果不懂的可以看看Java基本语法。
2)synchronized和lock性能区别
synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。
说到这里,还是想提一下这2中机制的具体区别。据我所知,synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。
现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
3)synchronized和lock用途区别
先说第一种情况,ReentrantLock的lock机制有2种,忽略中断锁和响应中断锁,这给我们带来了很大的灵活性。比如:如果A、B2个线程去竞争锁,A线程得到了锁,B线程等待,但是A线程这个时候实在有太多事情要处理,就是一直不返回,B线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候ReentrantLock就提供了2种机制,第一,B线程中断自己(或者别的线程中断它),但是ReentrantLock不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此);第二,B线程中断自己(或者别的线程中断它),ReentrantLock处理了这个中断,并且不再等待这个锁的到来,完全放弃。
wait():使一个线程处于等待状态,并且释放所持有的对象的lock。
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
allnotity():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。
同步和异步有何异同,在什么情况下分别使用他们?举例说明。 如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。 当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。
有两种实现方法,分别是继承Thread类与实现Runnable接口用synchronized关键字修饰同步方法。
反对使用stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状 态下检查和修改它们。结果很难检查出真正的问题所在。suspend()方法容易发生死锁。
调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被"挂起"的线程恢复运行。对任何线程来说, 如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend(),而应在自己的Thread 类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用 wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。
- JMM与volatile关键字
- synchronized的实现原理
- synchronized等待与唤醒机制
- AQS的实现原理
- ReentrantLock的实现原理
- ReentrantLock等待与唤醒机制
- CAS、Unsafe类以及Automic并发包
- ThreadLocal的实现原理
- 线程池的实现原理
- Java线程中断机制
- 多线程与并发常见面试题
- Android基础知识汇总
- MVC、MVP与MVVM
- SparseArray实现原理
- ArrayMap的实现原理
- SharedPreferences
- Bitmap
- Activity的启动模式
- Fragment核心原理
- 组件化项目架构搭建
- 组件化WebView架构搭建
- 为什么 Activity.finish() 之后 10s 才 onDestroy ?
- Binder与AIDL
- Binder实现原理
- Android系统启动流程
- InputManagerService
- WindowManagerService
- Choreographer详解
- SurfaceFlinger
- ViewRootImpl
- ActivityManagerService
- APP启动流程
- PMS安装与签名校验
- Dalvik与ART
- 内存优化策略
- UI界面及卡顿优化
- App启动优化
- ANR问题
- 包体积优化
- APK打包流程
- 电池电量优化
- Android屏幕适配
- 线上性能监控1--线上监控切入点
- 线上性能监控2--Matrix实现原理
- Glide实现原理
- OkHttp实现原理
- Retrofit实现原理
- RxJava实现原理
- RxJava中的线程切换与线程池
- LeakCanary实现原理
- ButterKnife实现原理
- ARouter实现原理
- Tinker实现原理
- 2. 两数相加
- 19.删除链表的倒数第 N 个结点
- 21. 合并两个有序链表
- 24. 两两交换链表中的节点
- 61. 旋转链表
- 86. 分隔链表
- 92. 反转链表 II
- 141. 环形链表
- 160. 相交链表
- 206. 反转链表
- 206 反转链表 扩展
- 234. 回文链表
- 237. 删除链表中的节点
- 445. 两数相加 II
- 面试题 02.02. 返回倒数第 k 个节点
- 面试题 02.08. 环路检测
- 剑指 Offer 06. 从尾到头打印链表
- 剑指 Offer 18. 删除链表的节点
- 剑指 Offer 22. 链表中倒数第k个节点
- 剑指 Offer 35. 复杂链表的复制
- 1. 两数之和
- 11. 盛最多水的容器
- 53. 最大子序和
- 75. 颜色分类
- 124.验证回文串
- 167. 两数之和 II - 输入有序数组 -169. 多数元素
- 189.旋转数组
- 209. 长度最小的子数组
- 283.移动0
- 303.区域和检索 - 数组不可变
- 338. 比特位计数
- 448. 找到所有数组中消失的数字
- 643.有序数组的平方
- 977. 有序数组的平方