进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
任务管理器。。看到window操作系统正在运行的进程
1)根本区别:进程是资源分配最小单位,线程是程序执行的最小单位;(资源调度的最小单位)
2)内存分配:进程有自己独立的地址空间,每启动一个进程,系统都会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段,线程没有独立的地址空间,它使用相同的地址空间共享数据;
所以
3)资源开销:线程占用的资源要?进程少很多。
进程对资源保护要求高,开销大,效率相对较低,线程资源保护要求不高,但开销小,效率高,可频繁切换;
4)通信方式:线程之间通信更方便,同一个进程下,线程共享全局变量,静态变量等数据
而进程之间互不干扰,相互独立,进程的通信机制相对很复杂,譬如管道,信号,消息队列,共享内存,套接字等通信机制
5)包含关系:线程是进程的一部分,一个进程内可以有多个线程
6)影响关系:多进程程序更安全,生命力更强,一个进程死掉不会对另一个进程造成影响(源于有独立的地址空间),多线程程序更不易维护,一个线程死掉,整个进程就死掉了(因为共享地址空间)
总结: 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
程序计数器主要有下面两个作用:
1)从当代互联网发展趋势来说: 多线程并发编程(目的就是为了能提高程序的执行效率提高程序运行速度)正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
2)从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。
在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。如CPU既可以进行计算,又可以进行IO操作
在多核 CPU 时代多线程主要是为了提高 CPU 利用率,一个CPU有多个CPU核心,这也意味着多个线程可以同时运行,从而被CPU核心利用到,这减少了线程上下文切换的开销。
内存泄漏:也称作"存储渗漏",用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。(其实说白了就是该内存空间使用完毕之后未回收)即所谓内存泄漏。
死锁
线程不安全
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态
由上图可以看出:线程创建之后它将处于 NEW(新建) 状态,调用 start()
方法后开始运行,线程这时候处于 READY(就绪) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。
当线程执行 wait()
方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)
方法或 wait(long millis)
方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。
当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的run()
方法之后将会进入到 TERMINATED(终止) 状态。
在操作系统中:
操作系统隐藏 Java 虚拟机(JVM)中的 READY 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:HowToDoInJava:Java Thread Life Cycle and Thread States),所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。
面试题
start()
方法,会启动一个线程并使线程进入了READY(就绪) 状态,可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态,就可以开始运行了,也就是自动开始执行run()方法。 start()
会执行线程的相应准备工作,然后自动执行 run()
方法的内容,这是真正的多线程工作。run()
方法,会把 run()
方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。总结: 调用 start()
方法方可启动线程并使线程进入就绪状态,直接执行 run()
方法的话不会以多线程的方式执行。
sleep()
方法没有释放锁,而 wait()
方法释放了锁 。wait()
通常被用于线程间交互/通信,sleep()
通常被用于暂停执行。wait()
方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()
或者 notifyAll()
方法。sleep()
方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)
超时后线程会自动苏醒。线程死锁描述的是这样一种情况:线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
对产生死锁的四个必要条件进行分析,为了避免死锁,我们只要破坏产生死锁的四个条件中的其中一个就可以了。现在我们来挨个分析一下:
synchronized
关键字解决的是多个线程之间访问资源的同步性,synchronized
关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。synchronized
属于 重量级锁,效率低下。
Mutex Lock
来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。synchronized
较大优化,所以现在的 synchronized
锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。1)修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
synchronized void method() {
//业务代码
}
2)修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份)。所以,如果一个线程 A 调用一个实例对象的非静态 synchronized
方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized
方法,是允许的,不会发生互斥现象,因为访问静态 synchronized
方法占用的锁是当前类的锁,而访问非静态 synchronized
方法占用的锁是当前实例对象锁。
synchronized static void method() {
//业务代码
}
3)修饰代码块 :指定加锁对象,对给定对象/类加锁。synchronized(this|object)
表示进入同步代码库前要获得给定对象的锁。synchronized(类.class)
表示进入同步代码前要获得 当前 class 的锁
synchronized(this) {
//业务代码
}
总结:
synchronized
关键字加到 static
静态方法和 synchronized(class)
代码块上都是是给 Class 类上锁。synchronized
关键字加到实例方法上是给对象实例上锁。synchronized(String a)
因为 JVM 中,字符串常量池具有缓存功能!public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
uniqueInstance
采用 volatile
关键字修饰,volatile 关键字 是线程同步的 轻量级实现
使用 volatile
可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
uniqueInstance = new Singleton();
这段代码其实是分为三步执行:
1)为 uniqueInstance
分配内存空间(给新创建出来Singleton的实例在堆中分配内存空间)
2)初始化 uniqueInstance
(调用构造函数初始化这个新创建出来Singleton的实例)
构造方法不需要用synchronized 关键字修饰,构造方法本身就属于线程安全的。
3)将 uniqueInstance
指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 线程T2 调用 getUniqueInstance
() 后发现 uniqueInstance
不为空,因此返回 uniqueInstance
,但此时 uniqueInstance
还未被初始化。
synchronized 关键字底层原理属于 JVM 层面。
synchronized(this) {
//业务代码
}
synchronized
同步语句块的实现使用的是 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。
在执行monitorenter
时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。
在执行 monitorexit
指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
synchronized
修饰方法来同步的情况synchronized
修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的确实是 ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。ACC_SYNCHRONIZED
访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。以上两种的本质都是对对象监视器 monitor 的获取。
级别从低到高依次是:
锁可以升级, 但不能降级. 即: 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁是单向的.
偏向锁是针对于一个线程而言的, 线程获得锁之后就不会再有解锁等操作了, 这样可以省略很多开销.
假如有两个线程来竞争该锁话, 那么偏向锁就失效了, 进而升级成轻量级锁了
偏向锁的加锁
当一个线程访问同步块并获取锁时, 会在锁对象的对象头和在 线程栈帧中的锁记录里存储锁偏向的线程ID, 以后该线程进入和退出同步块时不需要进行CAS操作来加锁和解锁, 只需要简单的测试一下锁对象的对象头的MarkWord里是否存储着指向当前线程的偏向锁(线程ID是当前线程)
偏向锁的撤销
偏向锁使用了一种等到竞争出现才释放锁的机制, 所以当其他线程尝试竞争偏向锁时, 持有偏向锁的线程才会释放锁
轻量级锁加锁
线程在执行同步块之前, JVM会先在当前线程的栈帧中创建用户存储锁记录的空间, 并将对象头中的MarkWord复制到锁记录空间中. 然后线程尝试使用CAS将对象头中的MarkWord替换为指向锁记录空间的指针. 如果成功, 当前线程获得锁; 如果失败, 表示其它线程竞争锁, 当前线程便尝试使用自旋来获取锁, 之后再来的线程, 发现是轻量级锁, 就开始进行自旋.
轻量级锁解锁
轻量级锁解锁时, 会使用原子的CAS操作将当前线程的锁记录空间替换回到对象头, 如果成功, 表示没有竞争发生; 如果失败, 表示当前锁存在竞争, 锁就会膨胀成重量级锁.
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗, 和执行非同步代码方法的性能相差无几. | 如果线程间存在锁竞争, 会带来额外的锁撤销的消耗. | 适用于只有一个线程访问的同步场景 |
轻量级锁 | 竞争的线程不会阻塞, 提高了程序的响应速度 | 如果始终得不到锁竞争的线程, 使用自旋会消耗CPU | 追求响应时间, 同步快执行速度非常快 |
重量级锁 | 线程竞争不适用自旋, 不会消耗CPU | 线程堵塞, 响应时间缓慢 | 追求吞吐量, 同步快执行时间速度较长 |
如果是数组对象的话, 对象头还有一部分是存储数组的长度.
多线程下synchronized的加锁就是对同一个对象的对象头中的MarkWord中的变量进行CAS操作.
“可重入锁” 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。
synchronized
是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized
关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock
是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
相比synchronized
,ReentrantLock
增加了一些高级功能。主要来说主要有三点:
ReentrantLock
提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly()
来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。ReentrantLock
可以指定是公平锁还是非公平锁。而synchronized
只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock
默认情况是非公平的,可以通过 ReentrantLock
类的ReentrantLock(boolean fair)
构造方法来制定是否是公平的。synchronized
关键字与wait()
和notify()
/notifyAll()
方法相结合可以实现等待/通知机制。ReentrantLock
类当然也可以实现,但是需要借助于Condition
接口与newCondition()
方法。ReentrantLock 和synchronized一样可以实现等待-通知机制,不同的是,被synchronized修饰的代码块(指定加锁的对象),在wait()方法后,需要其他线程调用同一个个对象的notify()方法,通知这个对象所有等待的线程。而ReentrantLock的锁对象,可以创建多个Condition接口的实现类,将(需要的)线程对象注册在指定的Conditon中,然后通过
signalAll()
方法就只会唤醒注册在该Condition
实例中的所有等待线程。实现有选择性的线程通知
在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
synchronized
可以保证代码片段的原子性。volatile
关键字可以保证共享变量的可见性。volatile
关键字可以禁止指令进行重排序优化。(使用 volatile
可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。)synchronized
关键字和 volatile
关键字是两个互补的存在,而不是对立的存在!
volatile
关键字是线程同步的轻量级实现,所以volatile
性能肯定比synchronized
关键字要好。但是volatile
关键字只能用于变量而 synchronized
关键字可以修饰方法以及代码块。volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证。volatile
关键字主要用于解决变量在多个线程之间的可见性,而 synchronized
关键字解决的是多个线程之间访问资源的同步性。通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢?
ThreadLocal
类主要解决的就是让每个线程绑定自己的值,是为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量get()
和 set()
方法来获取该线程下的副本变量,和将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。每个Thread
中都具备一个ThreadLocalMap
,而ThreadLocalMap
可以存储以ThreadLocal
为 key ,Object 对象为 value 的键值对。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
......
}
比如我们在同一个线程中声明了两个 ThreadLocal
对象的话,会使用 Thread
内部的ThreadLocalMap
存放数据,ThreadLocalMap
的 key 就是 ThreadLocal
对象(不同的ThreadLocal对象会有不同的hashCode值,故key不同),value 就是 ThreadLocal
对象调用set
方法设置的值(副本变量)。
ThreadLocal
类的 set
或get
方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap
类对应的 get()
、set()
方法。//`ThreadLocal`类的`set()`方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
通过上面这些内容,我们足以通过猜测得出结论:
ThreadLocalMap
中,并不是存在 ThreadLocal
上,ThreadLocal
可以理解为只是ThreadLocalMap
的封装,传递了变量值。 ThrealLocal
类中可以通过Thread.currentThread()
获取到当前线程对象后,直接通过getMap(Thread t)
可以访问到该线程的ThreadLocalMap
对象。然后再从这个在线程(Thread)中维护的map中取出数据或者存入对应数据。ThreadLocalMap
中使用的 key 为 ThreadLocal
的弱引用,而 value 是强引用。所以,如果 ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap
中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。
解决:每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
引用类型:强引用,软引用,弱引用,虚引用(引用强度逐渐减弱)
强引用:垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。
虚引用:任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。
Runnable
接口不会返回结果或抛出检查异常,但是Callable
接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable
接口,这样代码看起来会更加简洁。
@FunctionalInterface
public interface Runnable {
/**
* 被线程执行,没有返回值也无法抛出异常
*/
public abstract void run();
}
@FunctionalInterface
public interface Callable<V> {
/**
* 计算结果,或在无法这样做时抛出异常。
* @return 计算得出的结果
* @throws 如果无法计算结果,则抛出异常
*/
V call() throws Exception;
}
execute()
方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;submit()
方法用于提交需要返回值的任务。线程池会返回一个 Future
类型的对象,通过这个 Future
对象可以判断任务是否执行成功,并且可以通过 Future
的 get()
方法来获取返回值,get()
方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)
方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。我们可以创建三种类型的线程池
推荐使用
ThreadPoolExecutor
类中提供的四个构造方法ThreadPoolExecutor
中重要的参数:
corePoolSize
: 核心线程数定义了线程池的基本大小,可以同时运行的线程数量;只有在工作队列满了的情况下才会创建超出这个数量的线程。maximumPoolSize
: 线程池中允许的最大线程数,线程池中的当前线程数目不会超过该值。如果队列中任务已满,并且当前线程个数小于maximumPoolSize,那么会创建新的线程来执行任务。workQueue
: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数corePoolSize
,如果达到的话,新任务就会被存放在队列中。如果未达到,就会新增加一个线程处理这个提交的新任务。ThreadPoolExecutor
其他常见参数:keepAliveTime
:当线程池中的**线程数量大于 corePoolSize
**的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime
才会被回收销毁;
unit
: keepAliveTime
参数的时间单位。
threadFactory
:executor 创建新线程的时候会用到。
handler
:饱和策略
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor
定义的一些策略:
ThreadPoolExecutor.AbortPolicy
:抛出 RejectedExecutionException
来拒绝新任务的处理。
ThreadPoolExecutor.CallerRunsPolicy
:调用执行自己的线程运行任务,也就是直接在调用execute
方法的线程中运行(run
)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
ThreadPoolExecutor.DiscardPolicy
: 不处理新任务,直接丢弃掉。
ThreadPoolExecutor.DiscardOldestPolicy
: 此策略将丢弃最早的未处理的任务请求。
eg:举个例子
ThreadPoolTaskExecutor
或者我们直接通过 ThreadPoolExecutor
的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler
饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy
。在默认情况下,ThreadPoolExecutor
将抛出 RejectedExecutionException
来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy
。当最大池被填满时,此策略为我们提供可伸缩队列。
所谓原子类说简单点就是具有原子/原子操作特征的类。
在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。(一个操作是不可中断的)
并发包的原子类都放在java.util.concurrent.atomic
下
原子类可以分为4种类型
基本类型
使用原子的方式更新基本类型的数据
AtomicInteger
:整形原子类AtomicLong
:长整型原子类AtomicBoolean
:布尔型原子类数组类型
使用原子的方式更新数组里的某个元素
AtomicIntegerArray
:整形数组原子类AtomicLongArray
:长整形数组原子类AtomicReferenceArray
:引用类型数组原子类引用类型
基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用 引用类型原子类
使用原子的方式更新该引用类中的数据
AtomicReference
:引用类型原子类AtomicStampedReference
:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。AtomicMarkableReference
:原子更新带有标记位的引用类型对象的属性修改类型
如果需要原子更新某个类里的某个字段时,需要用到对象的属性修改类型原子类。
AtomicIntegerFieldUpdater
:原子更新整形字段的更新器AtomicLongFieldUpdater
:原子更新长整形字段的更新器AtomicReferenceFieldUpdater
:原子更新引用类型字段的更新器在多线程下,使用原子类,来更新数据,可以不用对方法加锁,也能保证线程安全!!!
AtomicInteger
为例子①多线程环境不使用原子类保证线程安全(基本数据类型)
class Test {
private volatile int count = 0;
//若要线程安全执行执行count++,需要加锁
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
②多线程环境使用原子类保证线程安全(基本数据类型)
class Test2 {
private AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet();
}
//使用AtomicInteger之后,不需要加锁,也可以实现线程安全。
public int getCount() {
return count.get();
}
}
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样当第二个线程想拿这个数据的时候,第二个线程会一直堵塞,直到第一个释放锁,他拿到锁后才可以访问。传统的数据库里面就用到了这种锁机制,例如:行锁,表锁,读锁,写锁,都是在操作前先上锁。java中的synchronized的实现就是一种悲观锁。
缺点:
在多线程竞争下,加锁、释放锁会导致较多的上下文切换和调度延时,引起性能问题
如果一个线程持有锁,其他的线程就都会挂起,等待持有锁的线程释放锁。
如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能风险。
乐观锁:乐观锁概念为,每次拿数据的时候都认为别的线程不会修改这个数据,所以不会上锁,但是在更新的时候会判断一下在此期间别的线程有没有修改过数据,乐观锁适用于读操作多的场景,这样可以提高程序的吞吐量。在Java中java.util.concurrent.atomic包下面的原子类就是使用了乐观锁的一种实现方式CAS实现。
ReentrantLock
,Semaphore,ReentrantReadWriteLock等同步器AbstractQueuedSynchronizer
)抽象队列同步器java.util.concurrent.locks
包下面。AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制(不是线程休眠被唤醒的机制),这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程封装成一个结点Node加入到FIFO队列中,然后阻塞该线程(此处获取不到锁的线程虽阻塞,但会一直自旋等待锁的释放),当获取锁的线程释放锁之后,会从FIFO队列中唤醒一个阻塞的线程(结点)。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。
互斥锁(mutexlock):最常使用于线程同步的锁;标记用来保证在任一时刻,只能有一个线程访问该对象,多个线程并发竞争锁的时候,没有抢到锁的线程会被挂起,进入休眠状态即sleep-waiting,当锁被释放的时候,处于休眠状态的一个线程会被唤醒,然后获取到锁。同一线程多次加锁操作会造成死锁
自旋锁:自旋锁与互斥锁有点类似,只不过没有抢到锁的线程会一直自旋等待锁的释放,处于busy-waiting的状态,此时等待锁的线程并不是睡眠等待唤醒,而是循环检测保持者已经释放了锁,这样做的好处是节省了线程从睡眠状态到唤醒之间内核会产生的消耗,在加锁时间短暂的环境下这点会提高很大效率。
自旋锁的缺点:
1、自旋锁一直占用CPU,他在未获得锁的情况下,一直运行自旋,所以占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低。
2、在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数也可能造成死锁,如 copy_to_user()、copy_from_user()、kmalloc()等。
https://blog.csdn.net/qq_40728028/article/details/106444575?ops_request_misc
AQS 使用一个 int 成员变量state来表示这个共享资源的同步状态
通过内置的 FIFO 队列来完成获取资源线程的排队工作。(一般通过头结点的顺序依次往下让线程公平地获取锁,如果头结点,被处理掉,或为空,则会从队尾结点的线程开始获取锁)
AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改(从而实现锁的分配)。
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
状态信息通过 protected 类型的 getState,setState,compareAndSetState 进行操作
//返回同步状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子(CAS操作),如果当前同步状态的值等于expect(期望值),将同步状态值设置为给定值update
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
ReentrantLock
(同步器)------>又可分为公平锁和非公平锁:
CountDownLatch
、Semaphore
、CountDownLatch
、 CyclicBarrier
、ReadWriteLock
(同步器)
ReentrantReadWriteLock
可以看成是组合式,因为 ReentrantReadWriteLock
也就是读写锁允许多个线程同时对某一资源进行读。不同的自定义同步器争用共享资源的方式是不同的。
自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在底层实现好了。
AQS已经将同步器的底层实现好了:
java并发包下很多API都是基于AQS来实现的加锁和释放锁等功能的,AQS是java并发包的基础类。
举个例子,比如说ReentrantLock、ReentrantReadWriteLock底层都是基于AQS来实现的。说白了,ReentrantLock内部包含了一个AQS对象,也就是AbstractQueuedSynchronizer类型的对象。
这个AQS对象就是ReentrantLock可以实现加锁和释放锁的关键性的核心组件。
模板方法模式一个经典的应用
AbstractQueuedSynchronizer
并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放)AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
Semaphore
(信号量)-允许多个线程同时访问: synchronized
和 ReentrantLock
都是一次只允许一个线程访问某个资源,Semaphore
(信号量)可以指定多个线程同时访问某个资源。
CountDownLatch
(倒计时器): CountDownLatch
是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
CountDownLatch在底层如何实现对共享资源state的获取与释放的?
CountDownLatch
允许 count
个线程阻塞在一个地方,直至所有线程的任务都执行完毕。
CountDownLatch
是共享锁的一种实现,它默认构造 AQS 的 state
值为 count
。当线程使用 countDown()
方法时,其实使用了tryReleaseShared
方法以 CAS 的操作来减少 state
,直至 state
为 0 。当调用 await()
方法的时候,如果 state
不为 0,那就证明任务还没有执行完毕,await()
方法就会一直阻塞,也就是说 await()
方法之后的语句不会被执行。然后,CountDownLatch
会自旋 CAS 判断 state == 0
,如果state == 0
的话,就会释放所有等待的线程,await()
方法之后的语句得到执行。
CountDownLatch 的两种典型用法
new CountDownLatch(n)
,每当一个任务线程执行完毕,就将计数器减 1 countdownlatch.countDown()
,当计数器的值变为 0 时,在CountDownLatch上 await()
的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。CountDownLatch
对象,将其计数器初始化为 1 :new CountDownLatch(1)
,多个线程在开始执行任务前首先 coundownlatch.await()
,当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。CountDownLatch 的不足
CyclicBarrier
(循环栅栏): CyclicBarrier
和 CountDownLatch
非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch
更加复杂和强大。
CyclicBarrier
(循环栅栏)两者的区别:
CyclicBarrier
(循环栅栏)重点是:多个线程执行,在任意一个线程没有完成,所有的线程都必须等待。(是对需要执行的线程执行完毕的计数)原文:https://www.cnblogs.com/lyj-study/p/14670863.html