Synchronized 关键字
为什么需要锁
当多个线程去访问同一个资源的时候,需要对此资源上锁。
why:比如我们对一个数字做递增算法,如果由两个线程共同访问的时候,线程一读它为0,然后+1计算,在线程一内存里面还没有计算完成写回去的时候,线程二读它,还是为0,线程二+1再写回去。本来这是做了两次递增,结果应为2,但实际成了1.所以我们要对这个数字的访问上一把锁,就是说,一个线程再对这个数字访问的时候是独占此数字的,不允许别的线程来访问计算,只有线程一加完1并写回后,其他线程才可对数字继续进行调用。
以下代码,没有volatile或者synchronized的话,输出结果很小几率是我们想要的0:
package com.rongyu;
public class NoSync {
public static void main(String[] args) {
T t = new T();
for (int i = 0; i < 100; i++) {
new Thread(t,"Thread"+i).start();
}
}
static class T implements Runnable {
private /*volatile*/ int count = 100;
public /*synchronized*/ void run() {
count--;
System.out.println(Thread.currentThread().getName() + "count =" + count);
}
}
}
加锁的几种方式
锁是锁定的某个对象,加锁的几种方法example:
package com.rongyu;
public class Sync {
/**
* synchronized关键字 对某个对象加锁
*/
private int count = 0;
private Object object = new Object();
public void m() {
synchronized (object) {
count++;
System.out.println(Thread.currentThread().getName() + "count =" + count);
}
}
// 锁定当前对象方法一
public void n() {
//每次new一个对象太麻烦,可以使用this代替
synchronized (this) {
count++;
System.out.println(Thread.currentThread().getName() + "count =" + count);
}
}
// 锁定当前对象方法二
public synchronized void b() {//等同于 synchronized (this)
count++;
System.out.println(Thread.currentThread().getName() + "count =" + count);
}
}
注意,同步方法和非同步方式是可以同时调用的。因为非同步方法不需要拿锁。
不加锁导致的脏读
下面我们来看一个用所解决脏读问题的案例:
模拟银行账户,对业务的写方法加锁,对读方法不加锁,看会产生什么问题:
package com.rongyu;
import java.util.concurrent.TimeUnit;
public class Account {
String name;
double balance;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
// 写
public synchronized void set(String name, double balance) {
this.name = name;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.balance = balance;
}
// 读
public /*synchronized*/ double getBalance(String name){
return this.balance;
}
public static void main(String[] args) {
Account a = new Account();
new Thread(()->a.set("zhangsan",100.0)).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.getBalance("zhangsan"));
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.getBalance("zhangsan"));
}
}
"C:\Program Files\Eclipse Adoptium\jdk-11.0.14.101-hotspot\bin\java.exe" "-javaagent:D:\ideaFamily\IntelliJ IDEA 2021.1.3\lib\idea_rt.jar=57039:D:\ideaFamily\IntelliJ IDEA 2021.1.3\bin" -Dfile.encoding=UTF-8 -classpath D:\workspace\thread-demo\out\production\thread-demo com.rongyu.Account
0.0
100.0
Process finished with exit code 0
因为getBalance方法没有加锁,所以说我不需要等线程中的set方法执行完就可以执行,而set在放入“zhangsan”后,睡了2s才放100, 第一次执行的时候100还未放入,所以读到的为0.这就是脏读。当我们程序允许脏读的时候可以不加锁,因为锁会影响代码效率。
锁的可重入性
可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
synchronized 和 ReentrantLock 都是可重入锁。
个人理解:如果一个同步方法调用另外一个同步方法,如果两个方法加了同一把锁,那么这个时候申请仍然可以得到该对象的锁。比如:方法m1和m2都是是synchronized修饰的,m1能不能调用m2.m1开始的时候线程拿到了这把锁,然后m1调用m2,如果说这个时候不允许任何线程再来拿这把锁,就会发生死锁。但是如果这个时候发现m2是同一个线程,那么就被允许,这种叫做可重入锁。
example:
package com.rongyu;
import java.util.concurrent.TimeUnit;
public class KeChongRu {
public synchronized void m1() {
System.out.println("m1 start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
m2();
System.out.println("m1 end");
}
synchronized void m2() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m2 start");
}
public static void main(String[] args) {
new KeChongRu().m1();
}
}
输出结果:
m1 start
m2 start
m1 end
所谓的重入锁,就是拿到锁之后可以不停地对其加锁加锁,但锁定地还是同一个对象,去掉一个锁就减一个。减没有了,此对象就可被别的线程调用。
异常锁
程序在执行过程中,如果发生了异常,默认情况下锁会被释放掉。所以,在并发处理地过程中,有异常一定要注意,不然锁失效会导致结果不对的情况。
package com.rongyu;
import java.util.concurrent.TimeUnit;
public class AbnormalLock {
int count = 0;
synchronized void m() {
System.out.println(Thread.currentThread().getName() + "start");
while (true){
count++;
System.out.println(Thread.currentThread().getName() + "count =" + count);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(count==5){
int i =1/0;//此处会发生异常导致锁被释放,如果不想释放锁,可以及逆行catch让循环继续
System.out.println(i);
}
}
}
public static void main(String[] args) {
AbnormalLock t = new AbnormalLock();
Runnable r = new Runnable() {
@Override
public void run() {
t.m();
}
};
new Thread(r,"t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(r,"t2").start();
}
}
输出结果可以看到,t1执行到5报错后,锁被释放,t2拿到继续执行。
t1start
t1count =1
t1count =2
t1count =3
t1count =4
t1count =5
t2start
t2count =6
Exception in thread "t1" java.lang.ArithmeticException: / by zero
at com.rongyu.AbnormalLock.m(AbnormalLock.java:18)
at com.rongyu.AbnormalLock$1.run(AbnormalLock.java:29)
at java.base/java.lang.Thread.run(Thread.java:829)
t2count =7
t2count =8
t2count =9
t2count =10
...
锁升级
new - 偏向锁 - 轻量级锁 (无锁, 自旋锁,自适应自旋)- 重量级锁
jdk1.5之前,sync是重量级锁,如果要使用sync的话,他首先会去操作系统去申请一把大锁,但是在之后,sync进行了锁优化,第一个访问这把锁的线程,它会在对象的markword中做一个标记,这就是偏向锁,如果有多个线程去访问资源的时候,他会将偏向锁撤销,换成轻量级锁,如果竞争加剧,那么他就会向操作系统去申请一把重量级锁。
1,锁的升级过程就是锁的竞争程度的结果
1,偏向锁:同步只有一个线程使用,使用过程中不存在竞争
2,轻量锁:第一个线程进入同步块后,第二个线程来竞争锁,此时会增加在调用栈记录mark word的锁信息(对象中mark指向其),如果由于第二个线程的竞争导致栈中的mark word更新失败,并且目前对象中的mark不指向当前线程栈中的mark就会升级为重量锁(第二个线程会修改栈中的记录,尝试修改成功),第二个线程不会阻塞会不断的尝试修改(一定时间内),自旋成功获取锁,此时还是自旋锁,自选失败升级为重量锁,阻塞
3,重量锁
synchronized的执行过程:
检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁
如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1
如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁
如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
如果自旋成功则依然处于轻量级状态。
如果自旋失败,则升级为重量级锁。
上面几种锁都是JVM自己内部实现,当执行synchronized同步块的时候jvm会根据启用的锁和当前线程的争用情况,决定如何执行同步操作。
在所有的锁都启用的情况下线程进入临界区时会先去获取偏向锁,如果已经存在偏向锁了,则会尝试获取轻量级锁,启用自旋锁,如果自旋也没有获取到锁,则使用重量级锁,没有获取到锁的线程阻塞挂起,直到持有锁的线程执行完同步块唤醒他们
偏向锁是在无锁争用的情况下使用的,也就是同步开在当前线程没有执行完之前,没有其它线程会执行该同步块,一旦有了第二个线程的争用,偏向锁就会升级为轻量级锁,如果轻量级锁自旋到达阈值后,没有获取到锁,就会升级为重量级锁。
如果线程争用激烈,那么应该禁用偏向锁。
引用:https://blog.csdn.net/xitingcan/article/details/117516942