目录
一、加锁的目的
二、加锁和解锁
三、加锁后是否会出现线程安全问题
1、两个线程,针对不同对象加锁
2、一个线程加锁,一个线程不加锁
3、针对加锁操作的一些混淆理解
(1)多个线程调用同一个类的方法,对其方法里面的变量加锁
(2)Test类里的add方法里面,加锁的对象换成Test.class
四、联系其他的相关知识点
1、StringBuffer 和 StringBuilder
2、C++加锁、解锁和java的区别
一、加锁的目的
因为加锁具有互斥的特性,给一段代码加锁,当运行这段代码时,这里的代码在系统上的指令就会就会被打包在一起,等这些指令,执行完了,其他的指令操作才能进行。而这,也是加锁的目的:把几个操作打包成一个原子的操作。
二、加锁和解锁
我们想让一个变量自增10_0000次,用两个线程来实现这一操作,分工各一半,
没有加锁的操作,是有线程问题的,因为两个线程修改同一个变量的原因。代码如下:
public class ThreadDemo1 { private static int count = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } }
执行效果:
count并不是我们预期的10_0000。
当我们给count++加上锁操作后的代码:
public class ThreadDemo1 { private static int count = 0; public static void main(String[] args) throws InterruptedException { Object locker = new Object(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized(locker) { count++; } } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized(locker){ count++; } } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } }
执行效果:
是我们预期的效果。
加锁的最核心规则:对于一个线程,针对一个对象进行加锁,但也有其他线程,也尝试对这个对象进行加锁,就会产生阻塞,这时,我们就把这已现象称为锁竞争 / 锁冲突。
例如下图,都是针对locker对象进行加锁,这时就会产生锁竞争
加锁和解锁的执行过程,针对上述代码,简单的画图展示一下:
也就是把t1先上锁,把t1的这几个操作打包成一个原子,执行完它们才能执行t2。
注意:给加锁的对象是任意的引用类型都可以的,我们也可以随便起个对象,但是要记住加锁的核心,两个线程之间加锁的对象,是否是同一个对象,是同一个对象,就会产生竞争,反之则不会。
三、加锁后是否会出现线程安全问题
1、两个线程,针对不同对象加锁
如下下代码,加锁用的不是同一个对象,则还是会存在线程安全问题
public class ThreadDemo1 { private static int count = 0; public static void main(String[] args) throws InterruptedException { Object locker1 = new Object(); Object locker2 = new Object(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized(locker1) { count++; } } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized(locker2){ count++; } } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } }
执行结果:
我们可以看到,结果和我们预期的10_0000不同,所以,还是存在线程安全问题。
2、一个线程加锁,一个线程不加锁
如下代码,和上面代码差不多,做一些小小的改动
public class ThreadDemo1 { private static int count = 0; public static void main(String[] args) throws InterruptedException { Object locker1 = new Object(); // Object locker2 = new Object(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized(locker1) { count++; } } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } }
执行结果:
可以看到,和我们预期的结果不同,所以还是存在线程安全问题。
3、针对加锁操作的一些混淆理解
(1)多个线程调用同一个类的方法,对其方法里面的变量加锁
还是之前的代码模板,不过做了一些改动,把count放到Test t 对象中,在这里面count++,并且对其加锁,加锁对象是 this,其他线程再来调用Test中的方法。
class Test { public int count = 0; public void add() { synchronized(this) { count++; } } } public class ThreadDemo2 { public static void main(String[] args) throws InterruptedException { Test t = new Test(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { t.add(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { t.add(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(t.count); } }
执行效果:
和我们的预期效果一样,这说明,这种情况是线程安全的。
解释:要解决这种情况的线程安全问题,核心还是上面所说的,看加锁是不是同一个对象,这里的Test类,add方法里对其加锁引用的对象是this,也就是当前Test类的实例对象,所有两个线程调用者方法的时候会产生锁竞争,结果也就可以达到我们的预期效果了。
(2)Test类里的add方法里面,加锁的对象换成Test.class
模板和之前差不多,只有一点小改动。代码如下:
class Test { public int count = 0; public void add() { synchronized(Test.class) { count++; } } } public class ThreadDemo2 { public static void main(String[] args) throws InterruptedException { Test t = new Test(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { t.add(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { t.add(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(t.count); } }
执行结果:
结果和我们预期的原因,是线程安全的情况。
解释:这里的 Test.class 是类对象(反射那一块的内容),而 t1 和 t2 拿到的都是同一个对象,就会有锁竞争,还是能保障线程安全的。
(3)还可以把synchronized加到方法上
如图:
不过比较少用。
四、联系其他的相关知识点
1、StringBuffer 和 StringBuilder
StringBuffer是线程安全的,就是在一些关键方法上,加上了synchronized;StringBuilder不是线程安全的,就是在一些关键方法上,没加synchronized。
其实通俗的说是不是线程安全,并不严谨,例如上面的代码例子,一个线程加锁,一个线程不加锁,或者两个线程给不同的对象加锁,任然是线程不安全的。具体还是要看代码怎么写。
2、C++加锁、解锁和java的区别
C++:它的加锁和解锁和java是不同的,在C++里,加锁:locker.lock() 解锁:locker.unlock(),他们的加锁和解锁是分开执行的,C++这种写法可能导致程序猿忘记调用unlock,或者unlock没执行到,这时就会产生很严重的bug,没解锁,其他加锁用和它一样对象的线程,就会一直等待。
java:它的是使用synchronized方法进行加锁,解锁的,这些操作已经打包好了,当synchronized代码块执行完后,就会自动释放锁,就不会有忘记或者没的解锁这种情况。