synchronized原理
synchronized 是重量级锁,通过调用底层操作系统来实现
早期的synchronized运行很慢,后来jdk对它进行了优化,引入很多新的概念
当一个线程A过来请求一个锁资源时:
- 偏向锁:A首先判断一下这个线程是否持有这个锁。在每个对象头里都有一个markword区域,里面记录了目前获取锁的线程id,如果这个线程再次请求了这个锁,就不会再执行lock和unlock操作从而来提高性能
- 自旋锁:A如果判断不是,就会锁膨胀为自旋锁,默认10次。自旋锁,就是持有cpu继续请求这个锁,并不会直接加入到等待队列当中,自旋锁适合锁定的代码操作不是很多,就是经常会释放和获取的场景。对于比较耗时的操作,不经常释放的锁,如果使用自旋锁会导致浪费cpu。
- 如果自旋锁没有获取到锁,这个时候,才用将当前线程A放到等待队列wait中
- wait队列中的线程还没有具备竞选锁的权力。因为再高并发的场景下,会有大量线程对我们的wait队列进行cas操作,如果从wati队列取一个线程获取锁容易出错。
- 假如现在获取锁的线程是B,当B比执行unlock的时候,就会从wait队列中挑选一部分线程放到一个新的等待队列static中,并从static中选取一个(默认是第一个)给它举行一次竞争的资格,注意是举行竞争,并不是说它就是下一个获取锁的。加入我们的A线程进入了static中
- 第一个线程进行一次竞争,加入A线程竞争上了,这个时候A算是正式获取了锁,然后就是正常的流程sleep呀或者是wait或者是unlock。
加锁之所以慢,是因为需要操作系统实现,真正意义的加锁,都是需要底层的操作系统(windows、linux)实现的,像轻量级锁 偏向锁,乐观锁等等都是后来人们经过研究,发现的在一些特定的条件下不需要加锁,从而取消了一部分加锁的操作,从而提高效率
为什么需要操作系统就慢
因为需要从用户态到内核态操作,用户态和内核态之间的切换是很消耗时间的,操作系统加锁的方式是在指定的代码前后分别加上标识(英文怎么说我不记得了)
jvm并没有规定如何实现synchronized,在每个对象头上都有一个markword 里面有两位用来标记是否被锁定。
脏读
当只是对写进行加锁的时候,容易出现脏读,如果我们项目不允许出现脏读,则需要注意避免
/**
* @program: solution
* @description: 脏读
* @author: Wang Hai Xin
* @create: 2022-10-27 10:48
**/
public class Synchronization {
int count = 100 ;
String name ="张三";
public static void main(String[] args) {
Synchronization t = new Synchronization();
new Thread(t::set,"t1").start();
new Thread(t::read,"t2").start();
}
private synchronized void set() {
try{
System.out.println("开始修改");
Thread.sleep(1000);
}catch (InterruptedException e) {
throw new RuntimeException(e);
}
this.count = 1000;
System.out.println("修改完毕");
read();
}
public /*synchronized*/ void read(){
System.out.println(name + ":" + count);
}
}
运行结果:
开始修改
张三:100
修改完毕
张三:1000
解决办法: 对读操作也加锁处理
可重入锁
一个同步方法可以调用另一个同步方法,一个线程获取到某个对象的锁之后,再次请求的时候仍然可以获得该对象的锁。也就是说 synchronization是可重入锁
synchronized 是可重入锁,如下代码,我们启动一个线程调用一个用 synchronized修饰的方法, 在方法内调用父类用synchronized的方法依然可以访问到,这是因为一个synchronized是可重入锁。否则的话就形成了死锁
package 线程同步synchronization;
/**
* @program: solution
* @description: synchronized 是可重入锁
* @author: Wang Hai Xin
* @create: 2022-11-02 16:08
**/
public class synchronizedT {
public static void main(String[] args) {
mm mm = new mm();
new Thread(mm::read,"t1").start();
}
}
class m{
public synchronized void read(){
System.out.print("hello ");
}
}
class mm extends m{
public synchronized void read(){
super.read();
System.out.println("world");
}
}
运行结果
hello world
synchronized遇到异常会释放
默认情况下 锁在遇到异常时,会释放
因此在多线程的情况下,要格外注意异常的处理,不然可能会导致一个线程意外释放锁资源
导致数据不一致
如在一个程序中多个线程请求同一个资源,如果第一个线程遇到异常意外释放锁资源,
其它线程就会进入修改数据。
如下代码,正常情况下不应该有第二个线程进入。
package 线程同步synchronization;
/**
* @program: solution
* @description: 锁在遇到错误时默认会自动释放
* @author: Wang Hai Xin
* @create: 2022-11-02 16:23
**/
public class synchronizedT1 {
int count = 0;
public static void main(String[] args) {
synchronizedT1 synchronizedT1 = new synchronizedT1();
new Thread(synchronizedT1:: m,"t1").start();
new Thread(synchronizedT1::m,"t2").start();
}
public void m(){
System.out.println(Thread.currentThread().getName() + "start");
while (true){
count++;
System.out.println("count:" + count);
if (count == 5) {
int i = 10/0;//制造异常
}
if (10 == count) {
break;
}
}
}
}
运行结果
t1start
count:1
count:2
count:3
count:4
count:5
t2start
count:6
count:7
count:8
count:9
count:10
Exception in thread "t1" java.lang.ArithmeticException: / by zero
at 线程同步synchronization.synchronizedT1.m(synchronizedT1.java:23)
at java.lang.Thread.run(Thread.java:748)
进程已结束,退出代码0
那些情况一下不用synchronized
synchronized不要用在 Integer 、Lone等基本数据类型 和string常量。上
Integer 、Lone等基本数据类型和String常量 在我们常量池里只有一份,假设我们将其锁定,其它调用我们这个jar包的很容易出现死锁,另外基本数据类型,变化的时候很容易产生新的对象,导致失效。所以最好不要用来锁。