目录
多个线程如果同时针对一个对象进行加锁时(进入synchronized修饰的代码块相当于加锁,退出synchronized修饰的代码块相当于解锁),会发生“锁竞争”,但只有一个线程(先进行加锁操作的)能够加锁成功,其他线程就会阻塞等待,这就是synchronized的互斥。
举个例子,就好比几个兄弟同时去抢占厕所的一个坑位,即“所(锁)竞争”,先抢到的先上厕所,没抢到的兄弟只能在外面等,当第一个兄弟上完厕所出来后,其他几个兄弟会再次进行“所(锁)竞争”,并不会遵循先来后到的规则。
如果多个线程针对不同的对象进行加锁时,不会发生锁竞争:线程1针对A对象加锁,线程2针对B对象加锁,此时不会发生“锁竞争”,也就不会产生阻塞等待。好比几个兄弟各自使用不同的坑位:
synchronized可以通过修饰普通成员方法、代码块、静态成员方法进行加锁操作,针对同一个对象进行加锁就会产生互斥,针对不同对象加锁就不会互斥。
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);
- }
- }
上述代码中的两个线程,都是通过test对象来调用被synchronized修饰的increase方法,那么两个线程的锁对象都是test对象,因此两个线程之间就会产生锁竞争,一个线程在调用increase方法进行count++操作时,另一个线程只能阻塞等待,这样就相当于把count++这个不是原子的操作,打包成一个原子操作,从而使count得到正确的结果:
synchronized修饰代码块时,括号中填入的对象就是被加锁的对象,这个对象可以是任意类型的对象。
- public class Test {
- public static int count;
- public void increase(){
- //synchronized修饰代码块
- synchronized(this){
- 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);
- }
- }
上述代码中,synchronized的锁对象为this,两个线程也都是通过test对象调用的increase方法,所以this指向的都是test对象,即两个线程还是在针对同一个对象进行加锁,因此也会发生锁竞争,说明当前线程也是安全的:
synchronized修饰静态成员方法时,锁对象相当于是一个类对象,类对象只有一份,所以在多线程环境下是安全的。
- public class Test {
- public static int count;
- //synchronized修饰静态方法
- public static synchronized void increase(){
- count++;
- }
- public static void main(String[] args) throws InterruptedException {
- //让两个线程同时进行50000次count++操作
- Thread t1 = new Thread(() -> {
- for (int i = 0; i < 50000; i++) {
- increase();
- }
- });
- Thread t2 = new Thread(() -> {
- for (int i = 0; i < 50000; i++) {
- increase();
- }
- });
- t1.start();
- t2.start();
-
- t1.join();
- t2.join();
-
- System.out.println("count = " + Test.count);
- }
- }
(1)使用synchronized加锁的目的是保证线程安全;
(2)多个线程的锁对象为同一个对象时,会产生锁竞争,把不是原子的操作打包成一个原子操作;多个线程的锁对象为不同对象时,不会产生锁竞争。
一个线程如果加锁两次是否会把自己锁死呢?,比如下面这段代码:
- public synchronized void func(){
- synchronized (this){
- count++;
- }
- }
进入synchronized修饰的func方法时,第一次加锁可以成功,但是进入synchronized修饰的代码块时,也需要进行加锁,但是第一次加的锁还没有释放,可以进行第二次加锁吗?而想要释放第一次加的锁,又必须进行第二次加锁、解锁操作,此时就产生了矛盾,这样的锁被称为不可重入锁。
Java中的synchronized是可重入锁,不会出现一个线程把自己锁死的问题,所以上面的代码完全是OK的。
在可重入锁的内部,包含“线程持有者”和“计数器”两个信息:
线程持有者:记录第一次加锁的线程,如果该线程在释放第一次加的锁之前再次进行加锁,那么仍然可以加锁成功,并让计数器自增一次。
计数器:加锁时自增一次,解锁时自减一次,只有当计数器递减为0的时候,线程进行解锁操作才会真正释放锁(其他线程才能获取到这个锁)。