Skip to content

Latest commit

 

History

History
558 lines (263 loc) · 25.2 KB

多线程.md

File metadata and controls

558 lines (263 loc) · 25.2 KB

1.Java中创建线程的方式

1.继承Thread类,重写run方法

2.实现Runnable接口,传递给Thread(runnable)构造函数

3.通过FutureTask 传递给Thread()构造函数

CallableTest callableTest = new CallableTest();

FutureTask futureTask = new FutureTask<>(callableTest);

new Thread(futureTask).start();

创建FutureTask对象,创建Callable子类对象,复写call(相当于run)方法

创建Thread类对象,将FutureTask对象传递给Thread对象

4.通过ExecutorService 线程池进行创建多线程

Callable和Runnable的区别

(1) Callable重写的是call()方法,Runnable重写的方法是run()方法

(2) call()方法执行后可以有返回值,run()方法没有返回值.运行Callable任务可以拿到一个Future对象,表示异步计算的结果 。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果

(3) call()方法可以抛出异常,run()方法不可以

实现Runnable/Callable接口相比继承Thread类的优势

由于Java“单继承,多实现”的特性,Runnable接口使用起来比Thread更灵活。

如果使用线程时不需要使用Thread类的诸多方法,显然使用Runnable接口更为轻量。

Callable如何使用

Callable一般是配合线程池工具ExecutorService来使用的,Callable一般是配合线程池工具ExecutorService来使用的 通过这个Future的get方法得到结果。

Future和FutureTask

https://zhuanlan.zhihu.com/p/38514871

Future接口只有几个比较简单的方法:

public abstract interface Future { public abstract boolean cancel(boolean paramBoolean); public abstract boolean isCancelled(); public abstract boolean isDone(); public abstract V get() throws InterruptedException, ExecutionException; public abstract V get(long paramLong, TimeUnit paramTimeUnit) throws InterruptedException, ExecutionException, TimeoutException; }

 也就是说Future提供了三种功能:

  1)判断任务是否完成;

  2)能够中断任务;

  3)能够获取任务执行结果。

 因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask。

Future只是一个接口,而它里面的cancel,get,isDone等方法要自己实现起来都是非常复杂的。所以JDK提供了一个FutureTask类来供我们使用。

可以看出RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。

2. 线程的几种状态

3. 谈谈线程死锁,如何有效的避免线程死锁?

https://www.cnblogs.com/xiaoxi/p/8311034.html

什么是死锁

死锁 :在两个或多个并发进程中,如果每个进程持有某种资源而又都等待别的进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁

例如,某计算机系统中只有一台打印机和一台输入 设备,进程P1正占用输入设备,同时又提出使用打印机的请求,但此时打印机正被进程P2 所占用,而P2在未释放打印机之前,又提出请求使用正被P1占用着的输入设备。这样两个进程相互无休止地等待下去,均无法继续执行,此时两个进程陷入死锁状态。

死锁产生的原因

  1. 系统资源的竞争

通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在 运行过程中,会因争夺资源而陷入僵局。

  1. 进程推进顺序非法

进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都 会因为所需资源被占用而阻塞。

3.信号量使用不当也会造成死锁。

进程间彼此相互等待对方发来的消息,结果也会使得这 些进程间无法继续向前推进。例如,进程A等待进程B发的消息,进程B又在等待进程A 发的消息,可以看出进程A和B不是因为竞争同一资源,而是在等待对方的资源导致死锁。

如何避免死锁

1加锁顺序

线程按照一定的顺序加锁当,多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生

2加锁时限

在尝试获取锁的时候加一个超时时间,超过时限则放弃对该锁的请求,并释放自己占有的锁

3死锁检测

每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。

当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁

那么当检测出死锁时,这些线程该做些什么呢?

一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。

4. 如何实现多线程中的同步

synchronized对代码块或方法加锁

reentrantLock加锁结合Condition条件设置

volatile关键字

cas使用原子变量实现线程同步

参照UI线程更新UI的思路,使用handler把多线程的数据更新都集中在一个线程上,避免多线程出现脏读

5. synchronized和Lock的使用、区别,原理;

https://juejin.cn/post/6844903542440869896#heading-17

使用

synchronized

修饰实例方法

修饰静态方法

修饰代码块

当synchronized作用在实例方法时,监视器锁(monitor)便是对象实例(this); 当synchronized作用在静态方法时,监视器锁(monitor)便是对象的Class实例,因为Class数据存在于永久代,因此静态方法锁相当于该类的一个全局锁; 当synchronized作用在某一个对象实例时,监视器锁(monitor)便是括号括起来的对象实例;(https://www.cnblogs.com/aspirant/p/11470858.html)

类锁:锁是加持在类上的,用synchronized static 或者synchronized(class)方法使用的锁都是类锁,因为class和静态方法在系统中只会产生一份,所以在单系统环境中使用类锁是线程安全的https://zhuanlan.zhihu.com/p/31537595

对象锁:synchronized 修饰非静态的方法和synchronized(this)都是使用的对象锁,一个系统可以有多个对象实例,所以使用对象锁不是线程安全的,除非保证一个系统该类型的对象只会创建一个(通常使用单例模式)才能保证线程安全;

lock

Lock和ReadWriteLock是两大锁的根接口,Lock代表实现类是ReentrantLock(可重入锁),ReadWriteLock(读写锁)的代表实现类是ReentrantReadWriteLock

Lock

lock()、tryLock()、tryLock(long time, TimeUnit unit) 和 lockInterruptibly()都是用来获取锁的

lock:用来获取锁

unlock:释放锁 如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。

tryLock:tryLock方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,

lockInterruptibly:通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。

ReadWriteLock 接口只有两个方法:

//返回用于读取操作的锁
Lock readLock()
//返回用于写入操作的锁
Lock writeLock()

ReetrantLock

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞 Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

原理

首先ReentrantLock和NonReentrantLock都继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0。

当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0 则把status置为1,当前线程开始执行。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁。而非可重入锁是 如果status != 0的话会导致其获取锁失败,当前线程阻塞。

ReadWriteLock

https://www.cnblogs.com/myseries/p/10784076.html(使用)

共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后, 获得共享锁的线程只能读数据,不能修改数据。

ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作

只要没有 writer,读取锁可以由多个 reader 线程同时保持,而写入锁是独占的

区别:

synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

Lock可以让等待锁的线程响应中断,而synchronized却不行

通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

原理

synchronized(https://juejin.cn/post/6844903670933356551#heading-6,https://juejin.cn/post/6844904181510176775#heading-6)

monitor描述为一种同步机制,它通常被描述为一个对象,当一个 monitor 被某个线程持有后,它便处于锁定状态

每个对象都存在着一个 monitor 与之关联

jvm基于进入和退出Monitor对象来实现方法同步和代码块同步。

对象头和Monitor对象

对象头

synchronized 用的锁是存在Java对象头里的

Hopspot 对象头主要包括两部分数据:Mark Word(标记字段) 和 Klass Pointer(类型指针)

Mark Word:

Java 6 及其以后,一个对象其实有四种锁状态,它们级别由低到高依次是 无锁状态 偏向锁状态 轻量级锁状态 重量级锁状态

当对象状态为偏向锁时,Mark Word存储的是偏向的线程ID; 当状态为轻量级锁时,Mark Word存储的是指向栈中锁记录的指针; 当状态为重量级锁时,Mark Word为指向堆中的monitor对象的指针

在HotSpot JVM实现中,锁有个专门的名字:对象监视器Object Monitor

同步代码块

从字节码中可知同步语句块的实现使用的是monitorenter和monitorexit指令

线程将试图获取对象锁对应的 monitor 的持有权, monitor的进入计数器为 0,那线程可以成功取得monitor,并将计数器值设置为1,取锁成功。

同步方法

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中

当方法调用时,调用指令将会 检查方法的 访问标志是否被设置,如果设置了如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词),然后再执行方法,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。

6.volatile,synchronized和volatile的区别?为何不用volatile替代synchronized?

  1. 线程通信模型(http://concurrent.redspider.group/article/02/6.html)

内存可见控制的是线程执行结果在内存中对其它线程的可见性。根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存

从抽象的角度来说,JMM定义了线程和主内存之间的抽象关系。

一般来说,JMM中的主内存属于共享数据区域,他是包含了堆和方法区;同样,JMM中的本地内存属于私有数据区域,包含了程序计数器、本地方法栈、虚拟机栈。

根据JMM的规定,线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主内存中读取。

所有的共享变量都存在主内存中。

每个线程都保存了一份该线程使用到的共享变量的副本。

如果线程A与线程B之间要通信的话,必须经历下面2个步骤: 线程A将本地内存A中更新过的共享变量刷新到主内存中去。 线程B到主内存中去读取线程A之前已经更新过的共享变量。

  1. volatile修饰的变量具有可见性

volatile关键字解决的是内存可见性的问题,会使得所有对volatile变量的读写都会直接刷到主存,即保证了变量的可见性。这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求。

  1. volatile禁止指令重排

使用volatile关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性 是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。

区别

https://blog.csdn.net/suifeng3051/article/details/52611233 https://juejin.cn/post/6844903598644543502#heading-1

volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的

volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性

volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞

7.锁的分类,锁的几种状态,CAS原理

https://juejin.cn/post/6844904181510176775#heading-8 https://tech.meituan.com/2018/11/15/java-lock.html

无锁/偏向锁/轻量级锁/重量级锁

Hotspot的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

四种状态的转换(http://concurrent.redspider.group/article/02/9.html)

1.每一个线程在准备获取共享资源时: 第一步,检查MarkWord里面是不是放的自己的ThreadId ,如果是,表示当前线程是处于 “偏向锁” 。

2.第二步,如果MarkWord不是自己的ThreadId,会尝试使用CAS来替换Mark Word里面的线程ID为新线程的ID,成功,仍然为偏向锁,失败,升级为轻量级锁,会按照轻量级锁的方式进行竞争锁。

3.CAS将锁的Mark Word替换为指向锁记录的指针,成功的获得资源,失败的则进入自旋 。

4.自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果自旋失败,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己。

乐观锁/悲观锁

对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改

而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)

悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。 synchronized关键字和Lock的实现类都是悲观锁。

乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。 java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。

为何乐观锁能够做到不锁定同步资源也可以正确的实现线程同步呢

CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。

CAS算法涉及到三个操作数:

进行比较的值 A。 要写入的新值 B。 需要读写的内存值 C。

AtomicInteger的自增函数incrementAndGet()的源码时

去比较寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。然后通过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功为止

可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞 Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

自旋锁

阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。

8.为什么会有线程安全?如何保证线程安全

Moosphan/Android-Daily-Interview#108

在多个线程访问共同资源时,在某一个线程对资源进行写操作中途(写入已经开始,还没结束),其他线程对这个写了一般资源进行了读操作,或者基于这个写了一半操作进行写操作,导致数据问题

原子性(java.util.concurrent.atomic 包)

即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行

有序性( Synchronized, Lock)

即程序执行的顺序按照代码的先后顺序执行

可见性(Volatile)

指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

9.sleep()与wait()区别,run和start的区别,notify和notifyall区别,锁池,等待池

https://blog.csdn.net/u012050154/article/details/50903326 Moosphan/Android-Daily-Interview#117 sleep()方法正在执行的线程主动让出CPU(然后CPU就可以去执行其他任务),而并不会释放同步资源锁!!!

wait()方法则是指当前线程让自己暂时退让出同步资源锁,只有调用了notify()方法,之前调用wait()的线程才会解除wait状态

sleep不需要唤醒,wait需要唤醒

1.run和start的区别

start() 可以启动一个新线程,run()不会

tart()中的run代码可以不执行完就继续执行下面的代码 直接调用run方法必须等待其代码全部执行完才能继续执行下面的代码。

2.sleep wait区别

sleep是Thread方法,不会释放锁,wait是obje方法,会释放锁

sleep不需要Synchronized ,wait需要Synchronized

sleep休眠自动继续执行,wait需要notify来唤醒,得到锁

3.notify和notifyall区别

notify是只唤醒一个等待的线程,noftifyALl是唤醒所有等待的线程

notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争

某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中

https://www.zhihu.com/question/37601861/answer/145545371

锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中

10.Java多线程通信

http://concurrent.redspider.group/article/01/2.html

11.Java多线程

https://blog.csdn.net/weixin_40271838/article/details/79998327

http://concurrent.redspider.group/article/03/12.html

Java中开辟出了一种管理线程的概念,这个概念叫做线程池,多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。

好处

创建/销毁线程需要消耗系统资源,线程池可以复用已创建的线程。

控制并发的数量。并发数量过多,可能会导致资源消耗过多,从而造成服务器崩溃。(主要原因)

可以对线程做统一管理。

参数

https://blog.csdn.net/weixin_40271838/article/details/79998327

线程池中的corePoolSize就是线程池中的核心线程数量,这几个核心线程,只是在没有用的时候,也不会被回收 maximumPoolSize就是线程池中可以容纳的最大线程的数量 keepAliveTime,就是线程池中除了核心线程之外的其他的最长可以保留的时间 TimeUnit unit:keepAliveTime的单位 BlockingQueue workQueue:阻塞队列,任务可以储存在任务队列中等待被执行。

两个非必须的参

ThreadFactory threadFactory

创建线程的工厂 ,用于批量创建线程,统一在创建线程时设置一些参数,如是否守护线程、线程的优先级

RejectedExecutionHandler handler

拒绝处理策略,线程数量大于最大线程数就会采用拒绝处理策略

Java中的线程池共有几种

https://www.jianshu.com/p/5936a2242322

CachedThreadPool: 该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。SynchronousQueue

FixedThreadPool:定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程LinkedBlockingQueue

SecudleThreadPool:创建一个定长线程池,支持定时及周期性任务执行。

SingleThreadPool:只有一条线程来执行任务,使用了LinkedBlockingQueue(容量很大),所以,不会创建非核心线程。所有任务按照先来先执行的顺序执行LinkedBlockingQueue

何为阻塞队列?

http://concurrent.redspider.group/article/03/13.html

生产者-消费者模式 生产者一直生产资源,消费者一直消费资源,资源存储在一个缓冲池中,生产者将生产的资源存进缓冲池中,消费者从缓冲池中拿到资源进行消费。

我们自己coding实现这个模式的时候,因为需要让多个线程操作共享变量(即资源),所以很容易引发线程安全问题,造成重复消费和死锁。另外,当缓冲池空了,我们需要阻塞消费者,唤醒生产者;当缓冲池满了,我们需要阻塞生产者,唤醒消费者,这些个等待-唤醒逻辑都需要自己实现。

阻塞队列(BlockingQueue),你只管往里面存、取就行,而不用担心多线程环境下存、取共享变量的线程安全问题。

阻塞队列的原理很简单,利用了Lock锁的多条件(Condition)阻塞控制

put和take操作都需要先获取锁,没有获取到锁的线程会被挡在第一道大门之外自旋拿锁,直到获取到锁。

就算拿到锁了之后,也不一定会顺利进行put/take操作,需要判断队列是否可用(是否满/空),如果不可用,则会被阻塞,并释放锁。

有哪几种工作队列

1、LinkedBlockingQueue

一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()和Executors.newSingleThreadExecutor使用了这个队列。

2、SynchronousQueue

一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。

线程池原理

从数据结构的角度来看,线程池主要使用了阻塞队列(BlockingQueue)

1、如果正在运行的线程数 < coreSize,马上创建核心线程执行该task,不排队等待; 2、如果正在运行的线程数 >= coreSize,把该task放入阻塞队列; 3、如果队列已满 && 正在运行的线程数 < maximumPoolSize,创建新的非核心线程执行该task; 4、如果队列已满 && 正在运行的线程数 >= maximumPoolSize,线程池调用handler的reject方法拒绝本次提交。

理解记忆:1-2-3-4对应(核心线程->阻塞队列->非核心线程->handler拒绝提交)。

jvm3