Dans la programmation multithread, la sécurité des threads est l'un des problèmes les plus critiques. Le concept de base réside dans l'exactitude, c'est-à-dire lorsque plusieurs threads accèdent à un , les données variables n'entraîneront jamais de corruption de données ou d'autres résultats inattendus. Lorsque tous les modes de concurrence résolvent ce problème, ils utilisent un accès sérialisé aux ressources critiques. En Java, deux méthodes sont proposées pour implémenter un accès synchronisé mutuellement exclusif : synchronisé et verrouillé. Cet article traite en détail de l'application des verrous intégrés synchronisés dans la concurrence Java, y compris ses scénarios d'utilisation spécifiques (méthodes synchronisées, blocs de code synchronisés, verrous d'objet d'instance et verrous d'objet de classe), la réentrance et les précautions.
Les problèmes de sécurité des threads ne se produiront pas dans les threads uniques, mais dans la programmation multithread, il est possible d'accéder aux mêmes ressources partagées et variables en même temps. , cette ressource peut être : une variable, un objet, un fichier, etc. Portez une attention particulière à deux points,
Partagé : signifie que la ressource est accessible par plusieurs threads en même temps
Variable : signifie que la ressource peut être modifiée au cours de sa durée de vie. Par conséquent, lorsque plusieurs threads accèdent à ce type de ressource en même temps, il y aura un problème : puisque le processus d'exécution de chaque thread est incontrôlable, un mécanisme de synchronisation doit être utilisé pour coordonner l'accès à l'état variable de l'objet.
Donnez un exemple de lecture de données sales :
//资源类 class PublicVar { public String username = "A"; public String password = "AA"; //同步实例方法 public synchronized void setValue(String username, String password) { try { this.username = username; Thread.sleep(5000); this.password = password; System.out.println("method=setValue " +"\t" + "threadName=" + Thread.currentThread().getName() + "\t" + "username=" + username + ", password=" + password); } catch (InterruptedException e) { e.printStackTrace(); } } //非同步实例方法 public void getValue() { System.out.println("method=getValue " + "\t" + "threadName=" + Thread.currentThread().getName()+ "\t" + " username=" + username + ", password=" + password); } } //线程类 class ThreadA extends Thread { private PublicVar publicVar; public ThreadA(PublicVar publicVar) { super(); this.publicVar = publicVar; } @Override public void run() { super.run(); publicVar.setValue("B", "BB"); } } //测试类 public class Test { public static void main(String[] args) { try { //临界资源 PublicVar publicVarRef = new PublicVar(); //创建并启动线程 ThreadA thread = new ThreadA(publicVarRef); thread.start(); Thread.sleep(200);// 打印结果受此值大小影响 //在主线程中调用 publicVarRef.getValue(); } catch (InterruptedException e) { e.printStackTrace(); } } }/* Output ( 数据交叉 ): method=getValue threadName=main username=B, password=AA method=setValue threadName=Thread-0 username=B, password=BB *///:~
Comme le montre la sortie du programme, bien que l'opération d'écriture soit synchronisée, elle peut toujours se produire dans l'opération de lecture. Certaines situations inattendues, telles que la lecture sale illustrée ci-dessus. Une lecture sale se produit lorsque les données correspondantes ont été partiellement modifiées par d'autres threads lors de l'exécution d'une opération de lecture, ce qui entraîne un croisement de données.
Il s'agit en fait d'un problème de sécurité des threads, c'est-à-dire que lorsque plusieurs threads accèdent à une ressource en même temps, le résultat de l'exécution du programme ne sera pas le résultat que vous souhaitez voir. Ici, cette ressource est appelée : ressource critique. C'est-à-dire que lorsque plusieurs threads accèdent à des ressources critiques (un objet, un attribut dans un objet, un fichier, une base de données, etc.) en même temps, des problèmes de sécurité des threads peuvent survenir.
Cependant, lorsque plusieurs threads exécutent une méthode, les variables locales à l'intérieur de la méthode ne sont pas des ressources critiques, car ces variables locales sont dans la pile privée de chaque thread, elles ne sont donc pas partagées et ne mèneront pas au thread. problèmes de sécurité.
En fait, tous les modes de concurrence utilisent un accès sérialisé aux ressources critiques pour résoudre les problèmes de sécurité des threads. Autrement dit, en même temps, un seul thread peut accéder aux ressources critiques, également appelé accès synchrone mutuellement exclusif. En d'autres termes, ajoutez un verrou devant le code qui accède à la ressource critique, libérez le verrou pour permettre aux autres threads de continuer à y accéder.
En Java, deux manières sont proposées pour implémenter un accès synchrone mutuellement exclusif : synchronisé et verrouillé. Cet article parle principalement de l'utilisation de synchronisé. L'utilisation de Lock est expliquée dans mon autre article de blog "Java Concurrency: Lock Framework Explication détaillée".
Avant de comprendre comment utiliser le mot-clé synchronisé, examinons d'abord un concept : le verrouillage mutex, qui peut atteindre l'objectif de verrouillage d'accès par exclusion mutuelle. Pour donner un exemple simple, si un verrou mutex est ajouté à une ressource critique, lorsqu'un thread accède à la ressource critique, les autres threads ne peuvent qu'attendre.
En Java, vous pouvez utiliser le mot-clé synchronisé pour marquer une méthode ou un bloc de code Lorsqu'un thread appelle la méthode synchronisée de l'objet ou accède au bloc de code synchronisé, le thread obtient le verrou de l'objet. les threads ne peuvent pas accéder temporairement à cette méthode. Ce n'est que lorsque cette méthode ou ce bloc de code est terminé que ce thread libère le verrou de l'objet et que d'autres threads peuvent exécuter cette méthode ou ce bloc de code.
Dans le code suivant, les deux threads appellent respectivement l'objet insertData pour insérer des données :
1) méthode synchronisée
public class Test { public static void main(String[] args) { final InsertData insertData = new InsertData(); // 启动线程 1 new Thread() { public void run() { insertData.insert(Thread.currentThread()); }; }.start(); // 启动线程 2 new Thread() { public void run() { insertData.insert(Thread.currentThread()); }; }.start(); } } class InsertData { // 共享、可变资源 private ArrayList<Integer> arrayList = new ArrayList<Integer>(); //对共享可变资源的访问 public void insert(Thread thread){ for(int i=0;i<5;i++){ System.out.println(thread.getName()+"在插入数据"+i); arrayList.add(i); } } }/* Output: Thread-0在插入数据0 Thread-1在插入数据0 Thread-0在插入数据1 Thread-0在插入数据2 Thread-1在插入数据1 Thread-1在插入数据2 *///:~
D'après les résultats d'exécution, on peut voir que ces deux threads exécutent la méthode insert() en même temps. Et si le mot-clé synchronisé est ajouté devant la méthode insert(), le résultat en cours d'exécution est :
class InsertData { private ArrayList<Integer> arrayList = new ArrayList<Integer>(); public synchronized void insert(Thread thread){ for(int i=0;i<5;i++){ System.out.println(thread.getName()+"在插入数据"+i); arrayList.add(i); } } }/* Output: Thread-0在插入数据0 Thread-0在插入数据1 Thread-0在插入数据2 Thread-1在插入数据0 Thread-1在插入数据1 Thread-1在插入数据2 *///:~
Comme le montrent les résultats de sortie ci-dessus, Thread-1 insère les données après que Thread-0 ait inséré les données. Je viens d'effectuer. Notez que Thread-0 et Thread-1 exécutent la méthode insert() séquentiellement. C'est ce que fait le mot-clé synchronisé pour les méthodes.
Cependant, vous devez faire attention aux trois points suivants :
1)当一个线程正在访问一个对象的 synchronized 方法,那么其他线程不能访问该对象的其他 synchronized 方法。这个原因很简单,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized方法。
2)当一个线程正在访问一个对象的 synchronized 方法,那么其他线程能访问该对象的非 synchronized 方法。这个原因很简单,访问非 synchronized 方法不需要获得该对象的锁,假如一个方法没用 synchronized 关键字修饰,说明它不会使用到临界资源,那么其他线程是可以访问这个方法的,
3)如果一个线程 A 需要访问对象 object1 的 synchronized 方法 fun1,另外一个线程 B 需要访问对象 object2 的 synchronized 方法 fun1,即使 object1 和 object2 是同一类型),也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。
2) synchronized 同步块
synchronized 代码块类似于以下这种形式:
synchronized (lock){ //访问共享可变资源 ... }
当在某个线程中执行这段代码块,该线程会获取对象lock的锁,从而使得其他线程无法同时访问该代码块。其中,lock 可以是 this,代表获取当前对象的锁,也可以是类中的一个属性,代表获取该属性的锁。特别地, 实例同步方法 与 synchronized(this)同步块 是互斥的,因为它们锁的是同一个对象。但与 synchronized(非this)同步块 是异步的,因为它们锁的是不同对象。
比如上面的insert()方法可以改成以下两种形式:
// this 监视器 class InsertData { private ArrayList<Integer> arrayList = new ArrayList<Integer>(); public void insert(Thread thread){ synchronized (this) { for(int i=0;i<100;i++){ System.out.println(thread.getName()+"在插入数据"+i); arrayList.add(i); } } } } // 对象监视器 class InsertData { private ArrayList<Integer> arrayList = new ArrayList<Integer>(); private Object object = new Object(); public void insert(Thread thread){ synchronized (object) { for(int i=0;i<100;i++){ System.out.println(thread.getName()+"在插入数据"+i); arrayList.add(i); } } } }
从上面代码可以看出,synchronized代码块 比 synchronized方法 的粒度更细一些,使用起来也灵活得多。因为也许一个方法中只有一部分代码只需要同步,如果此时对整个方法用synchronized进行同步,会影响程序执行效率。而使用synchronized代码块就可以避免这个问题,synchronized代码块可以实现只对需要同步的地方进行同步。
3) class 对象锁
特别地,每个类也会有一个锁,静态的 synchronized方法 就是以Class对象作为锁。另外,它可以用来控制对 static 数据成员 (static 数据成员不专属于任何一个对象,是类成员) 的并发访问。并且,如果一个线程执行一个对象的非static synchronized 方法,另外一个线程需要执行这个对象所属类的 static synchronized 方法,也不会发生互斥现象。因为访问 static synchronized 方法占用的是类锁,而访问非 static synchronized 方法占用的是对象锁,所以不存在互斥现象。例如,
public class Test { public static void main(String[] args) { final InsertData insertData = new InsertData(); new Thread(){ @Override public void run() { insertData.insert(); } }.start(); new Thread(){ @Override public void run() { insertData.insert1(); } }.start(); } } class InsertData { // 非 static synchronized 方法 public synchronized void insert(){ System.out.println("执行insert"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("执行insert完毕"); } // static synchronized 方法 public synchronized static void insert1() { System.out.println("执行insert1"); System.out.println("执行insert1完毕"); } }/* Output: 执行insert 执行insert1 执行insert1完毕 执行insert完毕 *///:~
根据执行结果,我们可以看到第一个线程里面执行的是insert方法,不会导致第二个线程执行insert1方法发生阻塞现象。下面,我们看一下 synchronized 关键字到底做了什么事情,我们来反编译它的字节码看一下,下面这段代码反编译后的字节码为:
public class InsertData { private Object object = new Object(); public void insert(Thread thread){ synchronized (object) {} } public synchronized void insert1(Thread thread){} public void insert2(Thread thread){} }
从反编译获得的字节码可以看出,synchronized 代码块实际上多了 monitorenter 和 monitorexit 两条指令。 monitorenter指令执行时会让对象的锁计数加1,而monitorexit指令执行时会让对象的锁计数减1,其实这个与操作系统里面的PV操作很像,操作系统里面的PV操作就是用来控制多个进程对临界资源的访问。对于synchronized方法,执行中的线程识别该方法的 method_info 结构是否有 ACC_SYNCHRONIZED 标记设置,然后它自动获取对象的锁,调用方法,最后释放锁。如果有异常发生,线程自动释放锁。
有一点要注意:对于 synchronized方法 或者 synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。
一般地,当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于 Java 的内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁时,那么这个请求就会成功。可重入锁最大的作用是避免死锁。例如:
public class Test implements Runnable { // 可重入锁测试 public synchronized void get() { System.out.println(Thread.currentThread().getName()); set(); } public synchronized void set() { System.out.println(Thread.currentThread().getName()); } @Override public void run() { get(); } public static void main(String[] args) { Test test = new Test(); new Thread(test,"Thread-0").start(); new Thread(test,"Thread-1").start(); new Thread(test,"Thread-2").start(); } }/* Output: Thread-1 Thread-1 Thread-2 Thread-2 Thread-0 Thread-0 *///:~
由于字符串常量池的原因,在大多数情况下,同步synchronized代码块 都不使用 String 作为锁对象,而改用其他,比如 new Object() 实例化一个 Object 对象,因为它并不会被放入缓存中。看下面的例子:
//资源类 class Service { public void print(String stringParam) { try { synchronized (stringParam) { while (true) { System.out.println(Thread.currentThread().getName()); Thread.sleep(1000); } } } catch (InterruptedException e) { e.printStackTrace(); } } } //线程A class ThreadA extends Thread { private Service service; public ThreadA(Service service) { super(); this.service = service; } @Override public void run() { service.print("AA"); } } //线程B class ThreadB extends Thread { private Service service; public ThreadB(Service service) { super(); this.service = service; } @Override public void run() { service.print("AA"); } } //测试 public class Run { public static void main(String[] args) { //临界资源 Service service = new Service(); //创建并启动线程A ThreadA a = new ThreadA(service); a.setName("A"); a.start(); //创建并启动线程B ThreadB b = new ThreadB(service); b.setName("B"); b.start(); } }/* Output (死锁): A A A A ... *///:~
出现上述结果就是因为 String 类型的参数都是 “AA”,两个线程持有相同的锁,所以 线程B 始终得不到执行,造成死锁。进一步地,所谓死锁是指:
不同的线程都在等待根本不可能被释放的锁,从而导致所有的任务都无法继续完成。
b). 锁的是对象而非引用
在将任何数据类型作为同步锁时,需要注意的是,是否有多个线程将同时去竞争该锁对象:
1).若它们将同时竞争同一把锁,则这些线程之间就是同步的;
2).否则,这些线程之间就是异步的。
看下面的例子:
//资源类 class MyService { private String lock = "123"; public void testMethod() { try { synchronized (lock) { System.out.println(Thread.currentThread().getName() + " begin " + System.currentTimeMillis()); lock = "456"; Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + " end " + System.currentTimeMillis()); } } catch (InterruptedException e) { e.printStackTrace(); } } } //线程B class ThreadB extends Thread { private MyService service; public ThreadB(MyService service) { super(); this.service = service; } @Override public void run() { service.testMethod(); } } //线程A class ThreadA extends Thread { private MyService service; public ThreadA(MyService service) { super(); this.service = service; } @Override public void run() { service.testMethod(); } } //测试 public class Run1 { public static void main(String[] args) throws InterruptedException { //临界资源 MyService service = new MyService(); //线程A ThreadA a = new ThreadA(service); a.setName("A"); //线程B ThreadB b = new ThreadB(service); b.setName("B"); a.start(); Thread.sleep(50);// 存在50毫秒 b.start(); } }/* Output(循环): A begin 1484319778766 B begin 1484319778815 A end 1484319780766 B end 1484319780815 *///:~
由上述结果可知,线程 A、B 是异步的。因为50毫秒过后, 线程B 取得的锁对象是 “456”,而 线程A 依然持有的锁对象是 “123”。所以,这两个线程是异步的。若将上述语句 “Thread.sleep(50);” 注释,则有:
//测试 public class Run1 { public static void main(String[] args) throws InterruptedException { //临界资源 MyService service = new MyService(); //线程A ThreadA a = new ThreadA(service); a.setName("A"); //线程B ThreadB b = new ThreadB(service); b.setName("B"); a.start(); // Thread.sleep(50);// 存在50毫秒 b.start(); } }/* Output(循环): B begin 1484319952017 B end 1484319954018 A begin 1484319954018 A end 1484319956019 *///:~
由上述结果可知,线程 A、B 是同步的。因为线程 A、B 竞争的是同一个锁“123”,虽然先获得运行的线程将 lock 指向了 对象“456”,但结果还是同步的。因为线程 A 和 B 共同争抢的锁对象是“123”,也就是说,锁的是对象而非引用。
用一句话来说,synchronized 内置锁 是一种 对象锁 (锁的是对象而非引用), 作用粒度是对象 ,可以用来实现对 临界资源的同步互斥访问 ,是 可重入 的。特别地,对于 临界资源 有:
若该资源是静态的,即被 static 关键字修饰,那么访问它的方法必须是同步且是静态的,synchronized 块必须是 class锁;
若该资源是非静态的,即没有被 static 关键字修饰,那么访问它的方法必须是同步的,synchronized 块是实例对象锁;
实质上,关键字synchronized 主要包含两个特征:
互斥性:保证在同一时刻,只有一个线程可以执行某一个方法或某一个代码块;
可见性:保证线程工作内存中的变量与公共内存中的变量同步,使多线程读取共享变量时可以获得最新值的使用。
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!