精通Java并发——张龙

365平台被黑 📅 2025-07-12 07:23:22 👤 admin 👁️ 5123 ❤️ 205
精通Java并发——张龙

P1 对于我们日常编写的controller,默认就是单实例的。其实一个controller对应的多个请求就相当于是多线程,每一个请求都会对应controller中的一个方法,接收请求后会在框架里进行路由的适配,适配到某一个controller对应的方法上,这时不管是参数上声明的变量还是方法体中声明的变量都是线程私有的,各线程之间也不会产生并发的问题(框架在底层已经将多线程的问题已经合理的规避掉了)。因为从框架的角度来看,它已经做好了线程与线程直接的隔离和划分。

并发和同步问题的根本原因在于有共享资源,对于不变对象就不会存在这种问题。

Thread类与Runnable接口

Thread类源码中可以看出,Thread类实现了Runnable接口;有两种方式去创建一个执行的线程。方式一:

class PrimeThread extends Thread {

* long minPrime;

* PrimeThread(long minPrime) {

* this.minPrime = minPrime;

* }

重写run方法

* public void run() {

* // compute primes larger than minPrime

*

* }

* }

The following code would then create a thread and start it running:

* PrimeThread p = new PrimeThread(143);

* p.start();

方式二:

创建一个类去实现Runnable将接口,并重写run方法,然后该类的实例作为Thread构造方法的参数创建线程。

class PrimeRun implements Runnable {

long minPrime;

PrimeRun(long minPrime) {

this.minPrime = minPrime;

}

public void run() {

// compute primes larger than minPrime

. . .

}

}

The following code would then create a thread and start it running:

PrimeRun p = new PrimeRun(143);

new Thread(p).start();

Runnable接口是一个函数式接口,被注解@FunctionalInterface修饰,函数式接口只有一个方法,run方法。如果只想重写线程的run方法不重写线程的其他方法,那么应该使用实现Runnable接口的方式来完成。这个非常重要,除非程序员是想修改或增强类的基础行为,否则类是不应该被子类化的。

start()调用后会执行一个线程,JVM会调用这个线程的run(),这时有两个线程同时执行,一个是调用start()方法的这个线程,另外一个是执行run()的这个线程。

关于wait与notify及线程同步总结

在调用wait方法时,线程必须持有被调用对象的锁,当调用wait方法后,线程就会释放掉该对象的锁(monitor)。在调用Thread类的sleep方法时,线程不会释放对象的锁。wait方法和notify方法都属于object类的方法。

当调用wait时,首先需要确保调用了wait方法的线程已经持有了对象的锁。

当调用wait后,该线程就会释放对象的锁,然后进入等待状态(wait set)

当线程调用wait后进入到等待状态时,它就可以等待其他线程调用相同对象的notify或notifyAll来进行唤醒。

一旦这个线程被其他线程唤醒后,该线程就会与其他线程一同开始竞争这个对象的锁(公平竞争);只有当线程获得了这个对象的锁后,才会继续往下执行。

调用wait方法的代码片段需要放在一个synchronized块或是synchronized方法中,这样才可以确保线程在调用wait方法前已经取得了对象的锁

当调用对象的notify方法时,它会随机唤醒这个对象的等待集合中的任意一个线程,当某个线程被唤醒后,他就会与其他线程一同竞争对象的锁

当调用对象notifyAll方法时,它会唤醒该对象等待集合中的所有线程,这些线程唤醒后,又会开始竞争对象的锁。

在某一时刻,只有一个对象可以拥有对象的锁。

wait方法

当调用wait方法后,线程会处于等待状态,并且释放对象的锁。

线程在四种情况下可以被唤醒,1:等待的线程被其他线程调用对象的notify方法或者notifyAll方法唤醒;2.其它线程中断了等待的线程;3.wait方法中设置的时间已过时,线程会唤醒,wait()无参数的这个方法,默认是wait(0),这种情况只能被其它线程唤醒。

除了这几种情况外,还有一种唤醒被称为虚假唤醒,虽然这种情况发生的概率很低,但是我们应该避免这种情况的发生。

In other words, waits should always occur in loops, like this one:

synchronized (obj) {

while ()

obj.wait(timeout);

... // Perform action appropriate to condition

}

wait与notify的案例分析

编写一个多线程程序,实现这样一个目标:

1.存在一个对象,该对象有一个int类型的成员变量counter,该成员变量的初始值是0

2.创建两个线程,其中一个线程对该对象的成员变量counter增1,另一个线程对该对象变量减1。

3.输出变量counter每次变化后的值

4.输出的结果应为10101010.........

public class MyObject {

private int counter;

public synchronized void increase(){

if (counter!=0){

try {

wait();

} catch (InterruptedException ex) {

ex.printStackTrace();

}

}

counter++;

System.out.println(counter);

notify();

}

public synchronized void decrease(){

if (counter==0){

try {

wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

counter--;

System.out.println(counter);

notify();

}

}

public class IncreaseThread extends Thread {

private MyObject myObject;

public IncreaseThread(MyObject myObject){

this.myObject=myObject;

}

@Override

public void run() {

for (int i = 0;i<30;++i){

try {

Thread.sleep((long) (Math.random()*1000));

} catch (InterruptedException ex) {

ex.printStackTrace();

}

myObject.increase();

}

}

}

public class DecreaseThread extends Thread{

private MyObject myObject;

public DecreaseThread(MyObject myObject){

this.myObject=myObject;

}

@Override

public void run() {

for (int i=0;i<30;++i){

try {

Thread.sleep((long) (Math.random()*1000));

} catch (InterruptedException e) {

e.printStackTrace();

}

myObject.decrease();

}

}

}

public class Client {

public static void main(String[] args) {

MyObject object = new MyObject();

Thread decreaseThread = new DecreaseThread(object);

Thread increaseThread = new IncreaseThread(object);

decreaseThread.start();

increaseThread.start();

}

}

如果将两个线程类各再增加一个实例,此时共有四个线程,这时启动程序输出的结果可能不会达到预期效果,原因在于,notify方法会随机唤醒等待集合中一个集合,这时就可能会出现错误。那么想要达到预期效果,则可以将if改为while循环就可以了。

synchronized关键字原理详解

synchronized一般用在两个地方,一个是修饰代码块synchronized(){ }另外一个地方是修饰方法,方法分为实例方法和静态方法。

偏向锁、轻量级锁、重量级锁 在偏向锁向轻量级锁以及轻量级锁向重量级锁升级的过程中也会牵扯到线程的自旋(自旋锁)。synchronized关键字,在JVM层上,伴随着monitorenter和monitorexit指令。

当我们使用synchronized关键字来修饰代码块时,字节码层面上通过monitorenter与monitorexit指令来实现锁的获取与释放动作。

当线程进入到monitorenter指令后,线程将会持有monitor对象,退出monitorenter指令后,线程将会释放monitor对象。

对于synchronized关键字修饰方法来说,并没有出现monitorenter和monitorexit指令,而是出现了一个ACC_SYNCHRONIZED标志。

JVM使用ACC_SYNCHRONIZED访问标志来区分一个方法是否为同步方法;当方法被调用时,调用指令检查该方法是否拥有ACC_SYNCHRONIZED标志,如果有,那么执行线程将会先持有方法所在对象的monitor对象,然后再执行方法体;在方法执行期间,其他线程均无法获得这个Monitor对象。(静态方法多了一个ACC_STATIC标志,要先获取静态方法所在类的对象)

synchronized底层是原理以及自旋

JVM中的同步是基于进入与退出监视器对象(monitor)来实现的,每个对象实例都会有一个monitor对象,monitor对象会和Java对象一同创建并销毁。Monitor对象是由C++来实现的。

当多线程访问一段同步代码时,这些线程会被放进一个EntryList集合中,处于阻塞状态的线程都会放到该列表当中。接下来,当线程获取到对象的monitor时,monitor是依赖于底层操作系统的mutex lock来实现互斥的,线程获取mutex成功,则会持有该mutex,这时其他线程就无法再获取该mutex。

如果线程调用了wait方法,那么该线程就会释放掉所持有的mutex,并且该线程会进入到waitset集合中等待,等待下一次被其他线程调用notify/notifyAll唤醒。如果当前线程顺利执行完毕方法,那么它也会释放掉所持有的mutex。

总结一下:同步锁在这种实现方式中,因为monitor是依赖于底层的操作系统实现的,这样就存在用户态与内核态之间的切换,所以会增加性能开销。通过对象互斥的概念来保证共享数据操作的完整性。每个对象都对应于一个可称为【互斥锁 】的标记,这个标记用于保证在任何时刻,只能有一个线程访问该对象。

当一个线程正在执行业务代码时,这种状态属于用户态;当线程执行同步代码时,若没有拿到对象的monitor,这时线程就会在waitset集合中等待,这种状态是内核态。JVM有一种优化就是,当一个线程正在执行同步代码时且该段同步代码并不会花费太多时间,若这时另外一个线程要执行这段代码,但获取不到对象的monitor,此时JVM不会让其转为内核态,而是通过自旋进行等待。因为线程自旋占用CPU资源,若自旋时间较长且还没拿到到monitor对象,则该线程只好进入阻塞状态(内核态)。

那些处于entryset和waitset中的线程均处于阻塞状态,阻塞操作是由操作系统来完成的,在Linux下是通过pthread_mutex_lock函数实现的。线程被阻塞后便会进入到内核调度状态,这会导致系统在用户态与之间来回切换,严重影响锁的性能。

解决上述问题的办法便是自旋。其原理是:当发生对monitor的争用时,若owner能够在很短的时间内释放锁,则那些正在争用的线程就可以稍微等待一下)(即所谓的自旋),在owner线程释放锁之后,争用线程可能会立刻获取到锁,从而避免了系统阻塞。不过,当owner运行的时间超过了临界值后,争用线程自旋一段时间后依然无法获取到锁,这时争用线程则会停止自旋而进入到阻塞状态。所以总结的思想是:先自旋,不成功再进行阻塞,尽量降低阻塞的可能性,这对那些执行时间很短代码块来说有极大的性能提升。显然,自旋在多处理器(多核心)上才有意义。

互斥锁属性与monitor对象特性

互斥锁的属性:

PTHREAD_MUTEX_TIMED_NP:这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将会形成一个等待队列,并且解锁后按照优先级获取到锁。这种策略可以确保资源分配的公平性。

PTHREAD_MUTEX_RECURSIVE_NP:嵌套锁,在Java语言中成为可重入锁。允许一个线程对同一锁成功获取多次,并通过unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。

PTHREAD_MUTEX_ ERRORCHECK_NP:检错锁。如果一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同,这样就保证了当不允许多次加锁时不会出现最简单情况下的死锁。

PTHREAD_MUTEX_ADAPTIVE_NP:适应锁,动作最简单的锁类型,仅仅等待解锁后重新竞争。(这次竞争不用考虑优先级)

轻量级锁与重量级锁的变化

在JDK1.5之前,我们若想实现线程同步,只能通过synchronized关键字这一种方式实现,底层,Java也是通过synchronized关键字来做到数据的原子性维护,synchronized关键字是JVM实现的一种内置锁,从底层角度来说,这种锁的获取与释放都是JVM帮助我们隐式实现的。

从JDK1.5开始,并发包引入了Lock锁,Lock同步锁是基于Java来实现的,因此锁的获取与释放都是Java代码实现与控制的;然而,synchronized是基于底层操作系统的Mutex Lock来实现的,每次对锁的获取与释放动作都会带来用户态与内核态之间的切换,这种切换极大的增加系统的负担;在并发量较高时,也就是说锁的竞争比较激烈时,synchronized锁在性能上表现的就很差。

从JDK1.6开始,synchronized锁的实现发生了很大的变化;JVM引入相应的优化手段来提升synchronized锁的性能,这种提升涉及到偏向锁、轻量级锁、重量级锁等,从而减少锁的竞争所带来的用户态与内核态之间的切换;这种锁的优化实际上是通过Java对象头中的一些标志位来实现的;对于锁的访问与改变,实际上都与Java对象头息息相关。

从JDK1.6开始,对象实例在堆当中会划分为三个组成部分:对象头、实例数据与对齐填充。

对象头主要有三块内容组成:1.Mark Word 2.指向类的指针 3.数组长度

其中Mark word(它记录了对象、锁及垃圾回收相关的信息,在64位的JVM中,其长度也是64bit)的位信息包括了如下组成部分:1.无锁标记2.偏向锁标记3.轻量级锁标记4.重量级锁标记5.GC标记

对于synchronized锁来说,锁的升级主要都是通过Mark Word中的锁标志位与是否是偏向锁标志位来达成;synchronized关键字所对应的锁都是先从偏向锁开始,随着锁竞争的不断升级,逐步演化至轻量级锁,最后变成重量级锁。

对于锁的演化来说,它会经历如下阶段:

偏向锁:针对于一个线程来说的,它的主要作用就是优化同一个线程多次获取一个锁的情况;如果一个synchronized方法被一个线程访问,那么这个方法所在的对象就会在其Mark Word中将偏向锁进行标记,同时还会有一个字段来储存该线程的ID;当这个线程再次访问同一个synchronized方法时,它会检查这个线程的Mark Word的偏向锁标记以及是否指向了其线程ID,如果是的话,那么该线程就无需再去进入管程(Monitor)了,而是直接进入到方法体中。(适用于两个线程的情况)

如果是另一个线程访问这个synchronized方法,那么实际情况会如何呢?

偏向锁会被取消掉。

轻量级锁:若第一个线程已经获取到了当前对象的锁,这时第二个线程又开始尝试抢夺该对象的锁,由于该对象的锁已经被第一个 线程获取到,因此它是偏向锁,而第二个线程在争抢时,会发现该对象头中的Mark Word已经是偏向锁,但里面存储的线程ID并不是自己(是第一个线程),那么它会进行CAS(compare and swap),从而获取到锁,这里面存在两种情况:

1.获取锁成功:那么它会直接将Mark Word中的线程ID由第一个线程变成自己(偏向锁标记位保持不变),这样该对象依然会保持偏向锁的状态。

2.获取锁失败:则表示这时可能会有多个线程同时在尝试争抢该对象的锁,那么这时偏向锁会进行升级,升级为轻量级锁。

重量级锁:线程最终从用户态进入到内核态。

死锁

死锁:线程1等待线程2互斥持有的资源,而线程2也在等待线程1互斥持有的资源,两个线程都无法继续执行。

Lock锁

lock锁中的方法,lock(),若拿不到锁对象就进入等待;trylock()尝试获取锁,返回的是Boolean值。

Lock lock = ...;

if (lock.tryLock()) {

try {

// manipulate protected state

} finally {

lock.unlock();

}

} else {

// perform alternative actions

}

trylock(long time,@NotNull TimeUnit unit),返回值也是Boolean值,该方法若得不到锁对象,则会等待指定的时间。

newCondition(),返回值是Condition,调用这个方法会返回一个condition实例,该实例被绑定在这个调用condition方法的lock锁实例上。在等待这个条件之前,这个锁必须得被当前线程所。在wating之前,调用Condition.await()时会自动释放锁,并且wait返回之前会重新获取这个锁。condition也是一个接口,接口中有7个方法,其中有5个是await的重载方法,还有signal方法和signalAll方法,signal是发信号的意思。condition接口,可以将object的monitor方法,由JVM提供的方法(wait notify notifyAll)放置到各种各样的Java对象当中,可以让一个对象有多个等待集合。同一个对象不同的等待线程,底层可放到该对象不同的等待集合,传统方式做不到这一点,他是怎么做到这一点的呢?它是通过组合那些对实现lock的那些东东,Lock代替了synchronized的使用,condition代替了那些monitor的方法。

Conditions为线程提供了一种方式可以让线程处于等待状态,直到被另外一个线程唤醒,另外的那一个线程在那个时刻的某些状态为true (如另外一个线程将一个条件变量变为true时) 。由于在不同的线程都可以访问这个共享信息,所以要被保护起来,这也就是为什么返回的condition一定要与某个lock绑定起来,等待着一个条件所提供的一个关键属性是可以自动的释放掉与condition所关联的锁,然后挂起这个锁,就像object.wait()一样。

Lock锁与synchronized关键字区别

锁的获取方式和具体实现方式,前者是由Java代码实现的,后者是通过JVM实现的;锁的释放方式,前者通过unlock()方法在finally块中手工释放,后者通过JVM来释放;锁的具体类型:前者提供了多种,reentrantlock是lock的实现,如公平锁和分公平锁,后者与前者均提供了可重入锁。

相关推荐

打印机怎么更换粉盒? 打印机换墨粉的图文教程
谁知道365bet网址

打印机怎么更换粉盒? 打印机换墨粉的图文教程

📅 07-11 👁️ 5092
绳缚中绳子的种类以及长度与粗度的选择
365平台被黑

绳缚中绳子的种类以及长度与粗度的选择

📅 06-27 👁️ 1564
知道手机号如何找到抖音号 详细攻略:通过联系人找人、隐私设置与替代方法