Lorsque nous écrivons des programmes concurrents, une exigence très courante est de s'assurer qu'un seul thread exécute un certain morceau de code à un certain moment. Ce type de code est appelé section critique, et il s'agit généralement de sections critiques. garanti qu'un seul thread exécute un certain morceau de code à la fois. La méthode permettant aux threads d'exécuter du code dans les sections critiques est le verrouillage. Dans cet article, nous analyserons attentivement et en apprendrons davantage sur les verrous de rotation. Ce que l'on appelle le verrou de rotation est implémenté via une boucle while, qui permet au thread qui a obtenu le verrou d'entrer dans la section critique pour exécuter le code, et autorise le thread. qui n'a pas obtenu le verrou pour continuer à mourir pendant la boucle, il s'agit en fait du thread qui "tourne" lui-même dans la boucle while, donc ce type de verrou est appelé un verrou tournant.
Avant de parler de spin locks, nous devons parler d'atomicité. Ce qu'on appelle l'atomicité signifie simplement que chaque opération n'est pas effectuée ou est effectuée. Tout faire signifie qu'elle ne peut pas être interrompue pendant l'opération. Par exemple, en ajouter une aux données variables comporte les trois étapes suivantes :
Charger les données. de la mémoire pour s'inscrire.
Ajoutez-en un à la valeur des données.
Écrivez le résultat en mémoire.
L'atomicité signifie que lorsqu'un thread effectue une opération d'addition, il ne peut pas être interrompu par d'autres threads. Ce n'est que lorsque ce thread termine ces trois processus que d'autres threads peuvent exploiter les données.
Faisons l'expérience maintenant avec le code. En Java, nous pouvons utiliser AtomicInteger pour effectuer des opérations atomiques sur des données entières :
import java.util.concurrent.atomic.AtomicInteger; public class AtomicDemo { public static void main(String[] args) throws InterruptedException { AtomicInteger data = new AtomicInteger(); data.set(0); // 将数据初始化位0 Thread t1 = new Thread(() -> { for (int i = 0; i < 100000; i++) { data.addAndGet(1); // 对数据 data 进行原子加1操作 } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 100000; i++) { data.addAndGet(1);// 对数据 data 进行原子加1操作 } }); // 启动两个线程 t1.start(); t2.start(); // 等待两个线程执行完成 t1.join(); t2.join(); // 打印最终的结果 System.out.println(data); // 200000 } }
D'après l'analyse de code ci-dessus, nous pouvons savoir que s'il s'agit d'une variable entière générale, si deux threads opèrent à dans le même temps, le résultat final sera inférieur à 200 000.
Simulons maintenant le processus de problèmes avec des variables entières générales :
La valeur initiale des données de la mémoire principale est égale à 0 et la valeur initiale des données obtenues par les deux threads est égale à 0.
Maintenant, le thread un en ajoute un aux données, puis le thread un synchronise la valeur des données avec la mémoire principale. Les modifications des données dans toute la mémoire sont les suivantes :
Maintenant, le thread deux en ajoute un. data, puis synchronise la valeur de data Retour à la mémoire principale (écrasement de la valeur originale de la mémoire principale) :
Nous espérions à l'origine que la valeur de data deviendrait 2 après les modifications ci-dessus, mais le thread deux a écrasé notre valeur , donc dans une situation multithread Down, réduira notre résultat final.
Mais dans le programme ci-dessus, notre résultat final est égal à 20000. En effet, l'opération de +1 sur les données est atomique et indivisible, et les autres threads ne peuvent pas opérer sur les données pendant l'opération. C'est l'avantage de l'atomicité.
Classe AtomicInteger
Maintenant que nous avons compris le rôle de l'atomicité, apprenons maintenant une autre opération atomique de la classe AtomicInteger - compareAndSet, cette opération est appelée comparaison et échange (CAS), c'est atomique.
public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(); atomicInteger.set(0); atomicInteger.compareAndSet(0, 1); }
La signification de la fonction compareAndSet : Tout d'abord, elle comparera le premier paramètre (correspondant au code ci-dessus est 0) et la valeur de atomicInteger s'ils sont égaux, ils seront échangés, c'est-à-dire la valeur de atomicInteger. sera défini sur le deuxième paramètre (correspondant au code ci-dessus). Le code est 1). Si ces opérations réussissent, la fonction compareAndSet renvoie true. Si l'opération échoue, elle renvoie false. le premier paramètre (valeur attendue) n'est pas égal à atomicInteger. S'ils sont égaux, cela peut également être dû au changement. La valeur de atomicInteger échoue (car plusieurs threads peuvent fonctionner et à cause de l'existence d'une atomicité). un seul thread peut fonctionner avec succès).
Principe d'implémentation du spin lock
Nous pouvons utiliser la classe AtomicInteger pour implémenter le spin lock. Nous pouvons utiliser la valeur 0 pour indiquer qu'il n'est pas verrouillé, et la valeur 1 pour indiquer qu'il est verrouillé.
La valeur initiale de la classe AtomicInteger est 0.
Lors du verrouillage, nous pouvons utiliser le code atomicInteger.compareAndSet(0, 1) pour l'implémenter. Nous avons déjà mentionné qu'un seul thread peut effectuer cette opération, ce qui signifie qu'un seul thread peut appeler cette ligne de code puis retourner. true et les autres threads renverront false.Ces threads qui renvoient false ne peuvent pas entrer dans la section critique, nous avons donc besoin que ces threads s'arrêtent à atomicInteger.compareAndSet(0, 1). Cette ligne de code ne peut pas être exécutée davantage. boucle pour arrêter ces threads. Le thread continue de s'arrêter ici pendant (!value.compareAndSet(0, 1));, seul le thread qui renvoie true peut sortir de la boucle, et les autres threads continueront à boucler ici. ce comportement tourne, et ce verrou est donc également appelé verrou tournant.
线程在出临界区的时候需要重新将锁的状态调整为未上锁的上状态,我们使用代码value.compareAndSet(1, 0);就可以实现,将锁的状态还原为未上锁的状态,这样其他的自旋的线程就可以拿到锁,然后进入临界区了。
自旋锁代码实现
import java.util.concurrent.atomic.AtomicInteger; public class SpinLock { // 0 表示未上锁状态 // 1 表示上锁状态 protected AtomicInteger value; public SpinLock() { this.value = new AtomicInteger(); // 设置 value 的初始值为0 表示未上锁的状态 this.value.set(0); } public void lock() { // 进行自旋操作 while (!value.compareAndSet(0, 1)); } public void unlock() { // 将锁的状态设置为未上锁状态 value.compareAndSet(1, 0); } }
上面就是我们自己实现的自旋锁的代码,这看起来实在太简单了,但是它确实帮助我们实现了一个锁,而且能够在真实场景进行使用的,我们现在用代码对上面我们写的锁进行测试。
测试程序:
public class SpinLockTest { public static int data; public static SpinLock lock = new SpinLock(); public static void add() { for (int i = 0; i < 100000; i++) { // 上锁 只能有一个线程执行 data++ 操作 其余线程都只能进行while循环 lock.lock(); data++; lock.unlock(); } } public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[100]; // 设置100个线程 for (int i = 0; i < 100; i ++) { threads[i] = new Thread(SpinLockTest::add); } // 启动一百个线程 for (int i = 0; i < 100; i++) { threads[i].start(); } // 等待这100个线程执行完成 for (int i = 0; i < 100; i++) { threads[i].join(); } System.out.println(data); // 10000000 } }
在上面的代码单中,我们使用100个线程,然后每个线程循环执行100000data++操作,上面的代码最后输出的结果是10000000,和我们期待的结果是相等的,这就说明我们实现的自旋锁是正确的。
可重入自旋锁
在上面实现的自旋锁当中已经可以满足一些我们的基本需求了,就是一个时刻只能够有一个线程执行临界区的代码。但是上面的的代码并不能够满足重入的需求,也就是说上面写的自旋锁并不是一个可重入的自旋锁,事实上在上面实现的自旋锁当中重入的话就会产生死锁。
我们通过一份代码来模拟上面重入产生死锁的情况:
public static void add(int state) throws InterruptedException { TimeUnit.SECONDS.sleep(1); if (state <= 3) { lock.lock(); System.out.println(Thread.currentThread().getName() + "\t进入临界区 state = " + state); for (int i = 0; i < 10; i++) data++; add(state + 1); // 进行递归重入 重入之前锁状态已经是1了 因为这个线程进入了临界区 lock.unlock(); } }
在上面的代码当中加入我们传入的参数state的值为1,那么在线程执行for循环之后再次递归调用add函数的话,那么state的值就变成了2。
if条件仍然满足,这个线程也需要重新获得锁,但是此时锁的状态是1,这个线程已经获得过一次锁了,但是自旋锁期待的锁的状态是0,因为只有这样他才能够再次获得锁,进入临界区,但是现在锁的状态是1,也就是说虽然这个线程获得过一次锁,但是它也会一直进行while循环而且永远都出不来了,这样就形成了死锁了。
可重入自旋锁思想
针对上面这种情况我们需要实现一个可重入的自旋锁,我们的思想大致如下:
在我们实现的自旋锁当中,我们可以增加两个变量,owner一个用于存当前拥有锁的线程,count一个记录当前线程进入锁的次数。
如果线程获得锁,owner = Thread.currentThread()并且count = 1。
当线程下次再想获取锁的时候,首先先看owner是不是指向自己,则一直进行循环操作,如果是则直接进行count++操作,然后就可以进入临界区了。
我们在出临界区的时候,如果count大于一的话,说明这个线程重入了这把锁,因此不能够直接将锁设置为0也就是未上锁的状态,这种情况直接进行count--操作,如果count等于1的话,说明线程当前的状态不是重入状态(可能是重入之后递归返回了),因此在出临界区之前需要将锁的状态设置为0,也就是没上锁的状态,好让其他线程能够获取锁。
可重入锁代码实现
实现的可重入锁代码如下:
public class ReentrantSpinLock extends SpinLock { private Thread owner; private int count; @Override public void lock() { if (owner == null || owner != Thread.currentThread()) { while (!value.compareAndSet(0, 1)); owner = Thread.currentThread(); count = 1; }else { count++; } } @Override public void unlock() { if (count == 1) { count = 0; value.compareAndSet(1, 0); }else count--; } }
下面我们通过一个递归程序去验证我们写的可重入的自旋锁是否能够成功工作。
测试程序:
import java.util.concurrent.TimeUnit; public class ReentrantSpinLockTest { public static int data; public static ReentrantSpinLock lock = new ReentrantSpinLock(); public static void add(int state) throws InterruptedException { TimeUnit.SECONDS.sleep(1); if (state <= 3) { lock.lock(); System.out.println(Thread.currentThread().getName() + "\t进入临界区 state = " + state); for (int i = 0; i < 10; i++) data++; add(state + 1); lock.unlock(); } } public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[10]; for (int i = 0; i < 10; i++) { threads[i] = new Thread(new Thread(() -> { try { ReentrantSpinLockTest.add(1); } catch (InterruptedException e) { e.printStackTrace(); } }, String.valueOf(i))); } for (int i = 0; i < 10; i++) { threads[i].start(); } for (int i = 0; i < 10; i++) { threads[i].join(); } System.out.println(data); } }
上面程序的输出:
Thread-3 Entrer dans l'état de section critique = 1
Thread-3 Entrer dans l'état de section critique = 2
Thread-3 Entrer dans l'état de section critique = 3
Thread-0 Entrer dans l'état de section critique = 1
Thread-0 Entrer l'état de section critique = 2
Thread-0 Entrer dans l'état de section critique = 3
Thread-9 Entrer dans l'état de section critique = 1
Thread-9 Entrer dans l'état de section critique = 2
Thread-9 Entrer dans l'état de section critique = 3
Thread-4 Entrer dans l'état de section critique = 1
Thread-4 Entrer dans l'état de section critique = 2
Thread-4 Entrer dans l'état de section critique = 3
Thread-7 Entrer dans l'état de section critique = 1
Thread-7 Entrer dans le état de section critique = 2
Thread-7 Entrée dans l'état de section critique = 3
Thread-8 Entrée dans l'état de section critique = 1
Thread-8 Entrée dans l'état de section critique = 2
Thread-8 Entrée dans l'état de section critique = 3
Thread-5 Entrer dans l'état de section critique = 1
Thread-5 Entrer dans l'état de section critique = 2
Thread-5 Entrer dans l'état de section critique = 3
Thread-2 Entrer dans l'état de section critique = 1
Thread-2 Entrer dans l'état critique état de la section = 2
Thread-2 Entrer dans l'état de section critique = 3
Thread-6 Entrer dans l'état de section critique = 1
Thread-6 Entrer dans l'état de section critique = 2
Thread-6 Entrer dans l'état de section critique = 3
Thread -1 Entrer dans l'état de section critique = 1
Thread-1 Entrer dans l'état de section critique = 2
Thread-1 Entrer dans l'état de section critique = 3
300
À partir des résultats de sortie ci-dessus, nous pouvons savoir que lorsqu'un thread peut acquérir le verrou, il peut être réentrant, et le résultat final de sortie est également correct, il est donc vérifié que nous avons écrit un spin réentrant Le verrou fonctionne !
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!