目录
synchronized的特性
1)互斥
多个线程如果同时针对同一个对象进行加锁时(进入synchronized修饰的代码块相当于加锁,退出synchronized修饰的代码块相当于解锁),会发生“锁竞争”,但只有一个线程(先进行加锁操作的)能够加锁成功,其他线程就会阻塞等待
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁
就比如说我们在追女神的时候,有好几个哥么一起追,但是不会说我先追的女神,女神就必须当我的女朋友,这是要去竞争的(这就相当于锁竞争),当有一个兄弟追到了女神,我们只能眼睁睁看着(阻塞等待),等到女神和这个兄弟分手后再次竞争(锁竞争),并不会遵循先来后到的规则
但是如果多个线程针对不同的对象进行加锁时,不会发生锁竞争:线程1针对A对象加锁,线程2针对B对象加锁,此时不会发生“锁竞争”,也就不会产生阻塞等待。好比几个兄弟各自使用不同的坑位
加锁需要考虑好锁那段代码,锁的代码范围不一样,对代码执行的效果会有很大的影响,锁的代码越多,就叫做"锁定粒度越大/越粗" ; 锁的代码越少,就叫做"锁的粒度越小/越细"
注意:
在这里我们提出一共疑问:当我们需要用到两个线程时,一个线程加锁,一个线程不加锁,这个时候会咋样?线程安全吗?
注意:线程安全,不是加了锁就一定安全.而是通过加锁,让并发修改同一个变量=>串行修改同一个变量,才安全的 ,不正确的加锁姿势,不一定能解决线程安全问题!!!!
class Count{
public int count = 0;
public synchronized void increase(){
for (int i = 0; i < 50000; i++) {
count++;
}
}
public void increase2(){
for (int i = 0; i < 50000; i++) {
count++;
}
}
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
Count c = new Count();
Thread t1 = new Thread(() -> {
c.increase();
});
Thread t2 = new Thread(() -> {
c.increase2();
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + c.count);
}
}
如果只给一个线程加锁,这个是没啥用的.一个线程加锁,不涉及到"锁竞争",也就不会阻塞等待也就不会并发修改=>串行修改
好比A追到了妹子~~B也想追妹子.虽然A官宣了(加锁了),但是B不讲武德,搞偷袭,搞挖墙脚~~(只要锄头挥得好,没有墙角挖不倒)
2)直接修饰普通方法
synchronized修饰普通成员方法时,被加锁的对象(即锁对象)为当前对象本身,相当于this
public class Test {
public static int count;
//synchronized修饰普通成员方法
public synchronized void increase(){
count++;
}
public static void main(String[] args) throws InterruptedException {
Test test = new Test();
//让两个线程同时进行50000次count++操作
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
test.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
test.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + Test.count);
}
}
一个线程在调用increase方法进行count++操作时,另一个线程只能阻塞等待,这样就相当于把count++这个不是原子的操作,打包成一个原子操作,从而使count得到正确的结果:
具体操作就如同这张图中展示的,当一个线程在进行操作时先加锁了,另一个线程阻塞等待,直到上一个线程释放锁时再执行
3)修饰静态方法
4)修饰代码块
锁当前对象:
锁类对象
关于锁对象:
明确指定锁哪个对象:在Java当中,任意的对象,都可以作为锁对象
关于synchronized(),的()的里面咱可以有:
等等...
这里的synchronized(),()里面要填的就是->你要针对哪个对象加锁!!(被用来加锁的对象,就简称为"锁对象")
使用锁的时候,一定要明确,当前是针对哪个对象加锁!!,very关键!!
咱们写多线程的时候,不关心这个锁对象究竟是谁,是那种形态,咱们只关心,多个线程是否锁的是同一个对象,是锁同一个对象,是锁同一个对象就有竞争,不同对象就无竞争
在大部分情况下,咱们可以直接在()中写this,但是在多线程代码中,切忌"无脑写",在大部分情况下是可以直接写this 的,但是具体还是看你的实际需求,希望在那些场景下产生竞争,哪些场景下不产生竞争,锁的对象是不同的!!!
5)可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
但是一个线程针对同一把锁加锁两次,就可能造成死锁!
public synchronized void func(){
synchronized (this){
count++;
}
}
第一次加锁,加锁成功,第二次加锁时第一次加的锁还没有释放,阻塞等待
就相当于:第一把锁需要完成锁内的任务才能释放锁,而他的任务包含了第二把锁,而第二把锁需要等待到第一把锁释放锁才能继续完成任务,这就形成了"死锁",这样的锁就称为:"不可重入锁"
Java 中的 synchronized 是 可重入锁, 因此没有上面的问题
在可重入锁的内部,包含“线程持有者”和“计数器”两个信息:
线程持有者:记录第一次加锁的线程,如果该线程在释放第一次加的锁之前再次进行加锁,那么仍然可以加锁成功,并让计数器自增一次。
计数器:加锁时自增一次,解锁时自减一次,只有当计数器递减为0的时候,线程进行解锁操作才会真正释放锁(其他线程才能获取到这个锁)。