【从入门到放弃-Java】并发编程-锁-synchronized

简介: 简介上篇【从入门到放弃-Java】并发编程-线程安全中,我们了解到,可以通过加锁机制来保护共享对象,来实现线程安全。synchronized是java提供的一种内置的锁机制。通过synchronized关键字同步代码块。

简介

上篇【从入门到放弃-Java】并发编程-线程安全中,我们了解到,可以通过加锁机制来保护共享对象,来实现线程安全。

synchronized是java提供的一种内置的锁机制。通过synchronized关键字同步代码块。线程在进入同步代码块之前会自动获得锁,并在退出同步代码块时自动释放锁。内置锁是一种互斥锁。

本文来深入学习下synchronized。

使用

同步方法

同步非静态方法

public class Synchronized {
    private static int count;

    private synchronized void add1() {
        count++;
        System.out.println(count);
    }


    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync.add1();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync.add1();

            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        System.out.println(count);
    }
}

结果符合预期:synchronized作用于非静态方法,锁定的是实例对象,如上所示锁的是sync对象,因此线程能够正确的运行,count的结果总会是20000。

public class Synchronized {
    private static int count;

    private synchronized void add1() {
        count++;
        System.out.println(count);
    }


    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        Synchronized sync1 = new Synchronized();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync.add1();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync1.add1();
            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        System.out.println(count);
    }
}

结果不符合预期:如上所示,作用于非静态方法,锁的是实例化对象,因此当sync和sync1同时运行时,还是会出现线程安全问题,因为锁的是两个不同的实例化对象。

同步静态方法

public class Synchronized {
    private static int count;

    private static synchronized void add1() {
        count++;
        System.out.println(count);
    }

    private static synchronized void add11() {
        count++;
        System.out.println(count);
    }

    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        Synchronized sync1 = new Synchronized();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                Synchronized.add1();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                Synchronized.add11();

            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        System.out.println(count);
    }
}

结果符合预期:锁静态方法时,锁的是类对象。因此在不同的线程中调用add1和add11依然会得到正确的结果。

同步代码块

锁当前实例对象

public class Synchronized {
    private static int count;

    private void add1() {
        synchronized (this) {
            count++;
            System.out.println(count);
        }
    }

    private static synchronized void add11() {
        count++;
        System.out.println(count);
    }

    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        Synchronized sync1 = new Synchronized();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync.add1();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync1.add1();
            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        System.out.println(count);
    }
}

结果不符合预期:当synchronized同步方法块时,锁的是实例对象时,如上示例在不同的实例中调用此方法还是会出现线程安全问题。

锁其它实例对象

public class Synchronized {
    private static int count;
    public String lock = new String();

    private void add1() {
        synchronized (lock) {
            count++;
            System.out.println(count);
        }
    }

    private static synchronized void add11() {
        count++;
        System.out.println(count);
    }

    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        Synchronized sync1 = new Synchronized();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync.add1();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync1.add1();
            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        System.out.println(count);

        System.out.println(sync.lock == sync1.lock);
    }
}

结果不符合预期:当synchronized同步方法块时,锁的是其它实例对象时,如上示例在不同的实例中调用此方法还是会出现线程安全问题。

public class Synchronized {
    private static int count;
    public String lock = "";

    private void add1() {
        synchronized (lock) {
            count++;
            System.out.println(count);
        }
    }

    private static synchronized void add11() {
        count++;
        System.out.println(count);
    }

    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        Synchronized sync1 = new Synchronized();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync.add1();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync1.add1();
            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        System.out.println(count);

        System.out.println(sync.lock == sync1.lock);
    }
}

结果符合预期:当synchronized同步方法块时,锁的虽然是其它实例对象时,但已上实例中,因为String = "" 是存放在常量池中的,实际上锁的还是相同的对象,因此是线程安全的

锁类对象

public class Synchronized {
    private static int count;

    private void add1() {
        synchronized (Synchronized.class) {
            count++;
            System.out.println(count);
        }
    }

    private static synchronized void add11() {
        count++;
        System.out.println(count);
    }

    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        Synchronized sync1 = new Synchronized();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync.add1();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync1.add1();
            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        System.out.println(count);
    }
}

结果符合预期:当synchronized同步方法块时,锁的是类对象时,如上示例在不同的实例中调用此方法是线程安全的。

锁机制

public class Synchronized {
    private static int count;

    public static void main(String[] args) throws InterruptedException {
        synchronized (Synchronized.class) {
            count++;
        }
    }
}

使用javap -v Synchronized.class反编译class文件。

可以看到synchronized实际上是通过monitorenter和monitorexit来实现锁机制的。同一时刻,只能有一个线程进入监视区。从而保证线程的同步。

正常情况下在指令4进入监视区,指令14退出监视区然后指令15直接跳到指令23 return

但是在异常情况下异常都会跳转到指令18,依次执行到指令20monitorexit释放锁,防止出现异常时未释放的情况。
这其实也是synchronized的优点:无论代码执行情况如何,都不会忘记主动释放锁。

想了解Monitors更多的原理可以点击查看

锁升级

因为monitor依赖操作系统的Mutex lock实现,是一个比较重的操作,需要切换系统至内核态,开销非常大。因此在jdk1.6引入了偏向锁和轻量级锁。
synchronized有四种状态:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。

无锁

没有对资源进行锁定,所有线程都能访问和修改。但同时只有一个线程能修改成功

偏向锁

在锁竞争不强烈的情况下,通常一个线程会多次获取同一个锁,为了减少获取锁的代价 引入了偏向锁,会在java对象头中记录获取锁的线程的threadID。

  • 当线程发现对象头的threadID存在时。判断与当前线程是否是同一线程。
  • 如果是则不需要再次加、解锁。
  • 如果不是,则判断threadID是否存活。不存活:设置为无锁状态,其他线程竞争设置偏向锁。存活:查找threadID堆栈信息判断是否需要继续持有锁。需要持有则升级threadID线程的锁为轻量级锁。不需要持有则撤销锁,设置为无锁状态等待其它线程竞争。

因为偏向锁的撤销操作还是比较重的,导致进入安全点,因此在竞争比较激烈时,会影响性能,可以使用-XX:-UseBiasedLocking=false禁用偏向锁。

轻量级锁

当偏向锁升级为轻量级锁时,其它线程尝试通过CAS方式设置对象头来获取锁。

  • 会先在当前线程的栈帧中设置Lock Record,用于存储当前对象头中的mark word的拷贝。
  • 复制mark word的内容到lock record,并尝试使用cas将mark word的指针指向lock record
  • 如果替换成功,则获取偏向锁
  • 替换不成功,则会自旋重试一定次数。
  • 自旋一定次数或有新的线程来竞争锁时,轻量级锁膨胀为重量级锁。

CAS

CAS即compare and swap(比较并替换)。是一种乐观锁机制。通常有三个值

  • V:内存中的实际值
  • A:旧的预期值
  • B:要修改的新值
    即V与A相等时,则替换V为B。即内存中的实际值与我们的预期值相等时,则替换为新值。

CAS可能遇到ABA问题,即内存中的值为A,变为B后,又变为了A,此时A为新值,不应该替换。
可以采取:A-1,B-2,A-3的方式来避免这个问题

重量级锁

自旋是消耗CPU的,因此在自旋一段时间,或者一个线程在自旋时,又有新的线程来竞争锁,则轻量级锁会膨胀为重量级锁。
重量级锁,通过monitor实现,monitor底层实际是依赖操作系统的mutex lock(互斥锁)实现。
需要从用户态,切换为内核态,成本比较高

总结

本文我们一起学习了

  • synchronized的几种用法:同步方法、同步代码块。实际上是同步类或同步实例对象。
  • 锁升级:无锁、偏向锁、轻量级锁、重量级锁以及其膨胀过程。

synchronized作为内置锁,虽然帮我们解决了线程安全问题,但是带来了性能的损失,因此一定不能滥用。使用时请注意同步块的作用范围。通常,作用范围越小,对性能的影响也就越小(注意权衡获取、释放锁的成本,不能为了缩小作用范围,而频繁的获取、释放)。

更多文章见:https://nc2era.com

目录
相关文章
|
1天前
|
Java
Java一分钟之-并发编程:线程间通信(Phaser, CyclicBarrier, Semaphore)
【5月更文挑战第19天】Java并发编程中,Phaser、CyclicBarrier和Semaphore是三种强大的同步工具。Phaser用于阶段性任务协调,支持动态注册;CyclicBarrier允许线程同步执行,适合循环任务;Semaphore控制资源访问线程数,常用于限流和资源池管理。了解其使用场景、常见问题及避免策略,结合代码示例,能有效提升并发程序效率。注意异常处理和资源管理,以防止并发问题。
22 2
|
1天前
|
安全 Java 容器
Java一分钟之-并发编程:线程安全的集合类
【5月更文挑战第19天】Java提供线程安全集合类以解决并发环境中的数据一致性问题。例如,Vector是线程安全但效率低;可以使用Collections.synchronizedXxx将ArrayList或HashMap同步;ConcurrentHashMap是高效线程安全的映射;CopyOnWriteArrayList和CopyOnWriteArraySet适合读多写少场景;LinkedBlockingQueue是生产者-消费者模型中的线程安全队列。注意,过度同步可能影响性能,应尽量减少共享状态并利用并发工具类。
15 2
|
2天前
|
Java
深入理解Java并发编程:线程池的应用与优化
【5月更文挑战第18天】本文将深入探讨Java并发编程中的重要概念——线程池。我们将了解线程池的基本概念,应用场景,以及如何优化线程池的性能。通过实例分析,我们将看到线程池如何提高系统性能,减少资源消耗,并提高系统的响应速度。
12 5
|
2天前
|
安全 Java 容器
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第18天】随着多核处理器的普及,并发编程变得越来越重要。Java提供了丰富的并发编程工具,如synchronized关键字、显式锁Lock、原子类、并发容器等。本文将深入探讨Java并发编程的核心概念,包括线程安全、死锁、资源竞争等,并分享一些性能优化的技巧。
|
2天前
|
安全 Java
Java一分钟之-并发编程:原子类(AtomicInteger, AtomicReference)
【5月更文挑战第18天】Java并发编程中的原子类如`AtomicInteger`和`AtomicReference`提供无锁原子操作,适用于高性能并发场景。`AtomicInteger`支持原子整数操作,而`AtomicReference`允许原子更新对象引用。常见问题包括误解原子性、过度依赖原子类以及忽略对象内部状态的并发控制。要避免这些问题,需明确原子操作边界,合理选择同步策略,并精确控制原子更新。示例代码展示了如何使用这两个类。正确理解和使用原子类是构建高效并发程序的关键。
12 1
|
2天前
|
安全 Java 容器
Java一分钟之-并发编程:并发容器(ConcurrentHashMap, CopyOnWriteArrayList)
【5月更文挑战第18天】本文探讨了Java并发编程中的`ConcurrentHashMap`和`CopyOnWriteArrayList`,两者为多线程数据共享提供高效、线程安全的解决方案。`ConcurrentHashMap`采用分段锁策略,而`CopyOnWriteArrayList`适合读多写少的场景。注意,`ConcurrentHashMap`的`forEach`需避免手动同步,且并发修改时可能导致`ConcurrentModificationException`。`CopyOnWriteArrayList`在写操作时会复制数组。理解和正确使用这些特性是优化并发性能的关键。
9 1
|
2天前
|
Java 编译器
Java并发编程中的锁优化策略
【5月更文挑战第18天】在Java并发编程中,锁是一种常用的同步机制,用于保护共享资源的访问。然而,不当的锁使用可能导致性能问题和死锁风险。本文将探讨Java中锁的优化策略,包括锁粗化、锁消除、锁分离和读写锁等技术,以提高并发程序的性能和可靠性。
|
3天前
|
Java 编译器
Java 并发编程中的锁优化策略
【5月更文挑战第17天】在 Java 并发编程中,锁是一种常见的同步机制,用于保护共享资源的访问。然而,不当使用锁可能导致性能问题和死锁风险。本文将探讨 Java 中的锁优化策略,包括锁粗化、锁消除、锁降级以及读写锁等技术,以提高并发程序的性能和可靠性。
|
3天前
|
Java 编译器
Java并发编程中的锁优化策略
【5月更文挑战第17天】在Java并发编程中,锁是一种常见的同步机制,用于保护共享资源。然而,使用不当的锁可能导致性能下降和死锁等问题。本文将探讨Java中锁的优化策略,包括锁粗化、锁消除、锁排序等方法,以提高程序的性能和可靠性。
|
3天前
|
算法 Java C++
刷题两个月,从入门到字节跳动offer丨GitHub标星16k+,美团Java面试题
刷题两个月,从入门到字节跳动offer丨GitHub标星16k+,美团Java面试题