Java多线程之同步锁定--synchronized同步方法和同步块、脏读、锁重入、死琐
来源:互联网 发布:大型酒店网络解决方案 编辑:程序博客网 时间:2024/05/17 20:34
线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏。
一、同步注意事项
1.1、方法内的变量为线程安全
“非线程安全”问题存在于“实例变量”中,如果是方法内部的私有变量,则不存在“非线程安全”问题(这是方法内部的变量是私有的特性造成的,所得结果也就是“线程安全”的了,方法的变量存放在JVM里的虚拟机栈里)。
2、实例变量非线程安全
如果多个线程共同访问1个对象中的实例变量,则可能出现”非线程安全“问题。
如果对象仅有1个实例变量,则有可能出现覆盖的情况。
// 简单一个类public class HasSelfPrivateNum { //如果把HasSelfPrivateNum类里的synchronized关键字去除,则单例模式中的实例变量为非线程安全状 //可能两个值 为一样 public synchronized void add(String username) { try { int num; if (username.equals("a")) { num = 100; System.out.println("when username is a, then set num 100"); Thread.sleep(1000); } else { num = 200; System.out.println("when username is not a, then set num 200"); } System.out.println(username + ", num=" + num); } catch (InterruptedException e) { e.printStackTrace(); } }}// 两个线程public class MyThread1 extends Thread { private HasSelfPrivateNum hasSelfPrivateNum; public MyThread1(HasSelfPrivateNum hasSelfPrivateNum) { super(); this.hasSelfPrivateNum = hasSelfPrivateNum; } public void run() { hasSelfPrivateNum.add("a"); }}public class MyThread2 extends Thread { private HasSelfPrivateNum hasSelfPrivateNum; public MyThread2(HasSelfPrivateNum hasSelfPrivateNum) { super(); this.hasSelfPrivateNum = hasSelfPrivateNum; } public void run() { hasSelfPrivateNum.add("b"); }}//测试类public class Test1 { public static void main(String[] args) { // 如果多个线程共同访问1个对象中的实例变量,则可能出现”非线程安全“问题。 // 如果对象仅有1个实例变量,则有可能出现覆盖的情况。 HasSelfPrivateNum numRef = new HasSelfPrivateNum();//只创建一个对象 Thread myThread1 = new MyThread1(numRef); myThread1.start(); Thread myThread2 = new MyThread2(numRef); myThread2.start(); }}
when username is a, then set num 100
a, num=100
when username is not a, then set num 200
b, num=200
当然结果也有可能是先输出b,再输出a,线程有随机性 。
如果把HasSelfPrivateNum类里的synchronized关键字去除,则单例模式中的实例变量为非线程安全状态,结果为
when username is a, then set num 100
when username is not a, then set num 200
b, num=200
a, num=100
3、多个对象多个锁
上面的HasSelfPrivateNum类,MyThread1,MyThread2不变,只修改main方法代码如下
public class Test2 { public static void main(String[] args) { // 多个对象多个锁 // 本示例由于创建了2个业务对象,在系统中产生出2个锁,所以运行结果是异步的. HasSelfPrivateNum numRef1 = new HasSelfPrivateNum(); HasSelfPrivateNum numRef2 = new HasSelfPrivateNum(); Thread aThread1 = new MyThread1(numRef1); aThread1.start(); Thread bThread2 = new MyThread2(numRef2); bThread2.start(); }}
when username is a, then set num 100
when username is not a, then set num 200
b, num=200
a, num=100
上面示例是两个线程分别访问同一个类的两个不同实例的相同名称的同步方法,效果却是以异步的方式运行的。本示例由于创建了2个业务对象,在系统中产生出2个锁,所以运行结果是异步的.
从上面程序运行结果来看,虽然在HasSelfPrivateNum中使用了synchronized关键字,但打印的顺序却不是同步的,是交叉的。为什么是这样的结果呢?
关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁,所以在上面的示例中,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁Lock,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象。
但如果多个线程访间多个对象,则JVM会创建多个锁。上面的示例就是创建了2个HasSelfPrivateNumjava类的对象,所以就会产生出2个锁。
同步的单词为synchronized,异步的单词为asynchronized。
结论:调用用关键字synchronized声明的方法一定是排队运行的。另外需要牢牢记住“共享”这两个字,只有共享资源的读写访问才需要同步化,如果不是共享资源,那么根本就没有同步的必要。
4、脏读
虽然在赋值时进行了同步,但在取值时有可能出现一些意想不到的意外,这种情况就是脏读(dirtyRead)。发生脏读的情况是在读取实例变量时,此值已经被其他线程更改过了。
1).脏读: 对于两个事物 T1, T2, T1 读取了已经被 T2 更新但还没有被提交的字段. 之后, 若 T2 回滚, T1读取的内容就是临时且无效的。也就是读取了其他事务还没有提交的数据
2).不可重复读: 对于两个事物 T1, T2, T1 读取了一个字段, 然后 T2 更新了该字段. 之后, T1再次读取同一个字段, 值就不同了。当前事务已经读取的数据记录,被其他事务修改或删除。
3).幻读: 对于两个事物 T1, T2, T1 从一个表中读取了一个字段, 然后 T2 在该表中插入了一些新的行. 之后, 如果 T1 再次读取同一个表, 就会多出几行。其他事务插入了新的数据,当前事务以相同的查询条件,在那个事务插入数据之前和之后查询数据,得到的数据记录的条数不一样
// 简单的get,set方法实现public class PublicVar { public String username = "username"; public String password = "password"; public synchronized void setValue(String username, String password) { try { this.username = username; Thread.sleep(3000); this.password = password; System.out.println("setValue method thread name=" + Thread.currentThread().getName() + ", username=" +username + ", password="+password); } catch (InterruptedException e) { e.printStackTrace(); } } public void getValue() { //如果加synchronized,就不会出现脏读 System.out.println("getValue method thread name=" + Thread.currentThread().getName() + ", username=" +username + ", password="+password); }}// 创建一个线程public class MyThread3 extends Thread { private PublicVar publicVar; public MyThread1(PublicVar publicVar) { super(); this.publicVar = publicVar; } public void run() { publicVar.setValue("username2", "password2"); }}// 测试类public class Test3 { public static void main(String[] args) throws InterruptedException { // 这个测试类出现脏读是因为public void getValue()方法并不是同步的 PublicVar publicVar = new PublicVar(); Thread thread3 = new MyThread3(publicVar); thread3.start(); Thread.sleep(2000); publicVar.getValue(); }}
getValue method thread name=main, username=username2, password=password
setValue method thread name=Thread-0, username=username2, password=password2
上面的结果在取值时出现脏读,username变成了username2了
出现脏读是因为public void getValue()方法并不是同步的,所以可以在任意时候进行调用。解决办法当然就是加上同步synchronized关键字
可见,方法setValue()和getValue()被依次执行。通过这个案例不仅要知道脏读是通过synchronized关键字解决的,还要知道如下内容:
当A线程调用anyObject对象加入synchronized关键字的X方法时,A线程就获得了X方法锁,更准确地讲,是获得了对象的锁,所以其他线程必须等A线程执行完毕才可以调用X方法,但B线程可以随意调用其他的非synchronized同步方法。
当A线程调用anyObject对象加入synchronized关健字的X方法时,A线程就获得了X方法所在对象的锁,所以其他线程必须等A线程执行完毕才可以调用X方法,而B线程如果调用声明了synchronized关键字的非X方法时,必须等A线程将X方法执行完,也就是释放对象锁后才可以调用。这时A线程已经执行了一个完整的任务,也就是说usernarne和password这两个实例变量已经同时被赋值,不存在脏读的基本环境。
脏读一定会出现操作实例变量的情况下,这就是不同线程“争抢”实例变量的结果。
5、synchronized锁重入
关键字synchronized拥有锁重入的功能。这也证明在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。
“可重入锁”的概念是:自己可以再次获取自己的内部锁。比如有1个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。
可重入锁也支持在父子类继承的环境中。
public class Service { public synchronized void service1() { System.out.println("service1"); service2(); } public synchronized void service2() { System.out.println("service2"); service3(); } public synchronized void service3() { System.out.println("service3"); }}// 线程public class MyThread4 extends Thread { public void run() { Service service = new Service(); service.service1(); }}// 测试类public class Test4 { public static void main(String[] args) { Thread myThread4 = new MyThread4(); myThread4.start(); }}
service1
service2
service3
6、出现异常,锁自动释放
当一个线程执行的代码出现异常时,其所持有的锁会自动释放。使用这个可以用来停止线程
public class Service { public synchronized void testMethod() { if (Thread.currentThread().getName().equals("a")) { System.out.println("ThreadName=" + Thread.currentThread().getName() +", run beginTime=" + System.currentTimeMillis()); int i = 1; while (i == 1) { if (("" + Math.random()).substring(0, 8).equals("0.123456")) { System.out.println("ThreadName=" + Thread.currentThread().getName() +", run beginTime=" + System.currentTimeMillis()); Integer.parseInt("a"); } else { System.out.println("Thread B run Time=" + System.currentTimeMillis()); } } } }}public class MyThread1 extends Thread { private Service service; public MyThread1(Service service) { super(); this.service = service; } public void run() { service.testMethod(); }}public class MyThread2 extends Thread{ private Service service; public MyThread2(Service service) { super(); this.service = service; } public void run() { service.testMethod(); }}//测试类public class Test { public static void main(String[] args) { try { Service service = new Service(); MyThread1 aThread = new MyThread1(service); aThread.setName("a"); aThread.start(); Thread.sleep(1000); MyThread2 bThread2 = new MyThread2(service); bThread2.setName("b"); bThread2.start(); } catch (InterruptedException e) { e.printStackTrace(); } }}
Thread B run Time=1462333804804
Thread B run Time=1462333804804
Thread B run Time=1462333804804
ThreadName=a, run beginTime=1462333804804
Exception in thread “a” java.lang.NumberFormatException: For input string: “a”
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
线程a出现异常并释放锁,线程b进入方法正常打印,实验的结论就 出现异常的锁被自动释放。
7、同步不具有继承性
二、死锁
Java线程死锁是一个经典的多线程问题,因为不同的线程都在等待根本不可能被释放的锁,从而导致所有的任务都无法继续完成。在多线程技术中,“死锁”是必须避免的,因为这会造成线程的“假死”。
产生死锁的四个必要条件:
1).互斥条件:一个资源每次只能被一个进程使用。
2).请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3).不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
4).循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
// 死锁线程public class DeathThread1 implements Runnable { public String username; public Object lock1 = new Object(); public Object lock2 = new Object(); public void setFlag(String username) { this.username = username; } public void run() { if (username.equals("a")) { synchronized (lock1) { try { System.out.println("username = " + username); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2) { System.out.println("按lock1->lock2代码顺序执行了"); } } } if (username.equals("b")) { synchronized (lock2) { try { System.out.println("username = " + username); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock1) { System.out.println("按lock1->lock2代码顺序执行了"); } } } }}// 测试类public class DeathThreadTest_03 { public static void main(String[] args) throws InterruptedException { // ---------使用嵌套的synchronized代码结出现死锁-------------- DeathThread1 deathThread1 = new DeathThread1(); deathThread1.setFlag("a"); Thread thread1 = new Thread(deathThread1); thread1.start(); Thread.sleep(1000); //这个必须要有 deathThread1.setFlag("b"); Thread thread2 = new Thread(deathThread1); thread2.start(); }}
username = a
username = b
之后就不输出了,出现死锁
死锁是程序设计的Bug.在设计程序时就要避免双方互相持有对方的锁的情况。需要说明的是,本实验使用synchronized嵌套的代码结构来实现死锁,其实不使用嵌套的synchronized代码结构也会出现死锁,与嵌套不嵌套无任何的关系,不要被代码结构所误导。
三、同步块
用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个长时间的任务,那么B线程则必须等待比较长时间。在这样的情况下可以使用synchronized同步语句块来解决。
下面这个实验要说明:不在synchronized块中就是异步执行,在synchronized块中就是同步执行。
// 业务类public class Task { // 不在synchronized块中就是异步执行,在synchronized块中就是同步执行。 public void doLongTimeTask() { for (int i=0; i<100; i++) { System.out.println("asynchronized threadName=" + Thread.currentThread().getName() + ", i=" + (i+1)); } synchronized (this) { for (int i=0; i<100; i++) { System.out.println("synchronized threadName=" + Thread.currentThread().getName() + ", i=" + (i+1)); } } }}// 线程类public class MyThread5 extends Thread { private Task task; public MyThread5(Task task) { super(); this.task = task; } public void run() { task.doLongTimeTask(); }}// 测试类public class Test5 { public static void main(String[] args) { Task task = new Task(); Thread thread1 = new MyThread5(task); thread1.start(); Thread thread2 = new MyThread5(task); thread2.start(); }}
……
asynchronized threadName=Thread-0, i=78
asynchronized threadName=Thread-1, i=100
asynchronized threadName=Thread-0, i=79
synchronized threadName=Thread-1, i=1
asynchronized threadName=Thread-0, i=80
synchronized threadName=Thread-1, i=2
asynchronized threadName=Thread-0, i=81
synchronized threadName=Thread-1, i=3
……
synchronized threadName=Thread-1, i=98
synchronized threadName=Thread-1, i=99
synchronized threadName=Thread-1, i=100
synchronized threadName=Thread-0, i=1
synchronized threadName=Thread-0, i=2
synchronized threadName=Thread-0, i=3
……
可以看到结果里的非同步时交叉打印,同步是排除执行
多个线程调用同一个对象中的不同名称的synchronized同步方法或synchronized(this)同步代码块时,调用的效果就是按顺序执行,也就是同步的,阻塞的。
这说明synchronized同步方法或synchronized(this)同步代码块分别有两种作用。
1)对其他synchronized同步方法或synchronized(this)同步代码块调用呈阻塞状态。
2)同一时间只有一个线程可以执行synchronized同步方法中的代码。
2、将任意对象作为对象监视器
在前面的学习中,使用synchronized(this)格式来同步代码块,其实Java还支持对“任意对象”作为“对象监视器”来实现同步的功能。这个“任意对象”大多数是实例变量及方法的参数,使用格式为synchronized(非this对象)。
根据前面对:synchronized(this)同步代码块的作用总结可知,synchronized(非this对象)格式的作用只有1种:synchronized(非this对象x)同步代码块。
1)在多个线程持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行:synchronized(非this对象x)同步代码块中的代码。
2)当持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this对象x)同步代码块中的代码。
示例:测试synchronized (非this对象x)是同一对象
public class Service2 { private String username; private String password; //注释1,这里测试synchronized (非this对象x)是同一对象 private String anyString = new String(); // 使用“synchronized(非this对象x)同步代码块”格式进行同步操作时,对象监视器必须是同一个对象。 // 如果不是同一个对象监视器,运行的结果就是异步调用了(如果两个方法的监视器不同也一样),就会交叉运行。 public void setUsernamePwd(String username, String password) { //注释2,这里测试synchronized (非this对象x)不是同一对象,结果会交替执行// String anyString = new String(); try { synchronized (anyString) { System.out.println("Thread name=" + Thread.currentThread().getName() +" 在 "+System.currentTimeMillis() + " 进入同步块"); this.username = username; Thread.sleep(2000); this.password = password; System.out.println("Thread name=" + Thread.currentThread().getName() +" 在 "+System.currentTimeMillis() + " 离开同步块"); } } catch (InterruptedException e) { e.printStackTrace(); } }}// 两个线程类public class MyThread6 extends Thread{ private Service2 service2; public MyThread6(Service2 service2) { super(); this.service2 = service2; } public void run() { service2.setUsernamePwd("b", "bb"); }}public class MyThread7 extends Thread { private Service2 service2; public MyThread7 (Service2 service2) { super(); this.service2 = service2; } public void run() { service2.setUsernamePwd("a", "aa"); }}public class Test6 { public static void main(String[] args) { // 如果不是同一个对象监视器,运行的结果就是异步调用了(如果两个方法的监视器不同也一样),就会交叉运行。 Service2 service2 = new Service2(); Thread a = new MyThread6(service2); a.setName("A"); a.start(); Thread b = new MyThread7(service2); b.setName("B"); b.start(); }}
Thread name=A 在 1503808454604 进入同步块
Thread name=A 在 1503808456605 离开同步块
Thread name=B 在 1503808456605 进入同步块
Thread name=B 在 1503808458609 离开同步块
锁非this对象具有一定的优点:如果在一个类中有很多个synchronized方法,这时虽然能实现同步,但会受到阻塞,所以影响运行效率;但如果使用同步代码块锁非this对象,则(synchronized非this)代码块中的程序与同步方法是异步的,不与其他锁this同步方法争抢this锁,则可大大提高运行效率。
如果将Service类里注释1下的代码注释,注释2下的代码打开,则结果如下
Thread name=B 在 1503808749563 进入同步块
Thread name=A 在 1503808749563 进入同步块
Thread name=B 在 1503808751567 离开同步块
Thread name=A 在 1503808751567 离开同步块
可见,使用“synchronized(非this对象x)同步代码块”格式进行同步操作时,对象监视器必须是同一个对象。如果不是同一个对象监视器,运行的结果就是异步调用了(如果两个方法的监视器不同也一样),就会交叉运行。
结论:
“synchronized(非this对象x)”格式的写法是将x对象本身作为“对象监视器”可以得出以下3个结论:
1)当多个线程同时执行synchronized(x){}同步代码块时呈同步效果。
2)当其他线程执行x对象中synchronized同步方法时呈同步效果。
3)当其他线程执行x对象方法里面的synchronized(this)代码块时也呈现同步效果。
但需要注意:如果其他线程调用不加synchronized关键字的方法时,还是异步调用。
3.静态同步synchronized方法与synchronized(class)代码块
synchronized关键字加到static静态方法上是给Class类上锁,而synchronized关键字加到非static静态方法上是给对象上锁。再者并不是同一个锁,证明如下
// 一个是静态方法,一个是普通方法public class Service3 { public synchronized static void printA() { try { System.out.println("Thread name=" + Thread.currentThread().getName() +" 在 "+System.currentTimeMillis() + " 进入同步块"); Thread.sleep(2000); System.out.println("Thread name=" + Thread.currentThread().getName() +" 在 "+System.currentTimeMillis() + " 离开同步块"); } catch (InterruptedException e) { e.printStackTrace(); } } public synchronized void printB() { //如果这里也是static,则运行为是同步 try { System.out.println("Thread name=" + Thread.currentThread().getName() +" 在 "+System.currentTimeMillis() + " 进入同步块"); Thread.sleep(2000); System.out.println("Thread name=" + Thread.currentThread().getName() +" 在 "+System.currentTimeMillis() + " 离开同步块"); } catch (InterruptedException e) { e.printStackTrace(); } }}// 两个线程public class MyThread8 extends Thread { private Service3 service3; public MyThread8(Service3 service3) { super(); this.service3 = service3; } public void run() { service3.printA(); }}public class MyThread9 extends Thread{ private Service3 service3; public MyThread9 (Service3 service3) { super(); this.service3 = service3; } public void run() { service3.printB(); }}// 测评类public class Test8 { public static void main(String[] args) { /* - synchronized关键字加到static静态方法上是给Class类上锁, 而synchronized关键字加到非static静态方法上是给对象上锁。 - 两者并不是同一个锁 - 异步的原因是持有不同的锁,一个是对象锁,另外一个是Class锁,而Class锁可以对类的所有对象实例起作用。 */ // 两个方法都加static就能同步,或者多个线程启动多个实例也能同步 Service3 service3 = new Service3(); Thread thread8 = new MyThread8(service3); thread8.setName("A"); thread8.start(); Thread thread9 = new MyThread9(service3); thread9.setName("B"); thread9.start(); }}
Thread name=A 在 1503811254382 进入同步块
Thread name=B 在 1503811254382 进入同步块
Thread name=A 在 1503811256386 离开同步块
Thread name=B 在 1503811256386 离开同步块
异步的原因是持有不同的锁,一个是对象锁,另外一个是Class锁,而Class锁可以对类的所有对象实例起作用。
如果在Service类里的printB方法前加上synchronized,并将测试类修改为:
//如果在Service类里的printB方法前加上synchronizedpublic class Run { public static void main(String[] args) { Service3 service1 = new Service3(); Service3 service2 = new Service3(); MyThread1 a = new MyThread8(service1); a.setName("A"); a.start(); MyThread2 b = new MyThread9(service2); b.setName("B"); b.start(); }}
Thread name=B 在 1462348587331 进入同步块
Thread name=B 在 1462348589332 离开同步块
Thread name=A 在 1462348589332 进入同步块
Thread name=A 在 1462348591332 离开同步块
虽然是不同对象,但静态的同步方法还是同步运行的。
同步synchronized(class)代码块的作用其实和synchronized static方法的作用一样。将上面的Service类修改为如下结果也是上面一样
public static void printA() { synchronized(Service.class) { //对应的代码 } } public static void printB() { synchronized(Service.class) { //对应的代码 } }
4.数据类型String的常量池特性
在JVM中具有String常量池缓存的功能,如下
@Test public void test() { String a = "a"; String b = "a"; System.out.println(a==b); //true }
将synchronized(string)同步块与String联合使用时,要注意常量池带来的一些例外
public class Service5 { public static void print(String str) { try { synchronized (str) { while (true) { System.out.println("currentThread:" +Thread.currentThread().getName()); Thread.sleep(1000); } } } catch (InterruptedException e) { e.printStackTrace(); } }}// 两个线程public class MyThread13 extends Thread { private Service5 service5; public MyThread13(Service5 service5) { super(); this.service5 = service5; } public void run() { service5.print("AA"); }}public class MyThread14 extends Thread { private Service5 service5; public MyThread14(Service5 service5) { super(); this.service5 = service5; } public void run() { service5.print("AA"); // //一定要跟上面相同 }}// 测试类public class Test10 { public static void main(String[] args) { Service5 service5 = new Service5(); Thread a = new MyThread13(service5); a.setName("A"); a.start(); Thread b = new MyThread14(service5); b.setName("B"); b.start(); }}
输出结果:全为A,或者全为B
出现这样的原因是因为String的两个值都是AA,两个线程持有相同的锁,所以造成线程B不能执行。这就是String常量池所带来的问题。因此在大多数情况下,同步synchronized代码块都不能使用String作为锁对象,而改用其他,比如new Object()实例化一个Object对象,但它并不放入缓存中。
6、同步synchronized方法无限等待与解决
同步方法容易造成死循环,而同步块可以解决死循环
//同步方法public class Service { public synchronized void methodA() { System.out.println("methodA begin"); boolean flag = true; while (flag) {//故意死循环 } System.out.println("methodB end"); } public synchronized void methodB() { System.out.println("methodB begin"); System.out.println("methodB end"); }}
自定义线程代码,就是将上面的MyThread14和MyThread15里的run方法分别修改为service.methodA();和service.methodB(); 测试类Run不变,结果为:
methodA begin
线程B永远不会运行,死锁。
这里就可以使用同步块来解决这样的问题了。更改Service.java文件后如下:
//同步块public class Service { Object object1 = new Object(); public void methodA() { synchronized (object1) { System.out.println("methodA begin"); boolean flag = true; while (flag) {//故意死循环 } System.out.println("methodB end"); } } Object object2 = new Object(); public void methodB() { synchronized (object2) { System.out.println("methodB begin"); System.out.println("methodB end"); } }}
methodA begin
methodB begin
methodB end
需要注意的是:虽然线程B执行了,但线程A还是死循环。
- Java多线程之同步锁定--synchronized同步方法和同步块、脏读、锁重入、死琐
- Java多线程编程2--同步锁定--synchronized同步块
- Java多线程编程2--同步锁定--synchronized同步方法、脏读、锁重入
- 【转载】Java多线程编程2--同步锁定--synchronized同步方法、脏读、锁重入
- Synchronized 同步方法和同步代码块
- 【多线程】synchronized同步块
- 【JAVA】多线程之synchronized 同步数据 方法
- java多线程之-----静态同步synchronized方法与synchronized(class) 代码块
- 初学Java多线程:使用Synchronized块同步方法
- 十、初学Java多线程:使用Synchronized块同步方法
- java多线程——同步方法和同步代码块
- 多线程---java同步方法和同步代码块的区别
- java多线程编程之使用Synchronized块同步变量
- Java---13---多线程---synchronized 同步代码块
- 《多线程编程》学习之五:synchronized同步语句块,静态同步synchronized方法与synchronized(类名.class)代码块
- Thinking in Java学习笔记 synchronized同步方法和synchronized同步块
- 【多线程】synchronized同步方法
- java同步方法和同步代码块
- Linux ubuntu中13个常用命令
- Syn 三次与FiN四次相关讨论
- 稀疏编码系列3:理解稀疏编码sparse coding
- iOS弹幕库OCBarrage-如何hold住每秒5000条巨量弹幕
- Android studid—每日笔记1
- Java多线程之同步锁定--synchronized同步方法和同步块、脏读、锁重入、死琐
- TP5的命令新增模块
- Python学习之路-C++程序员的血泪史
- ocamlrun未安装的问题
- iOS修改transform形变也能够实现动画
- 四分树
- Linux SYN Cookie的原理以及代码实现
- Redux笔记
- 计算机网络--http协议的长链接与短连接