• 线程同步


    当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。这个时候,有个单线程模型下不存在的问题就来了:如果多个线程同时读写共享变量,会出现数据不一致的问题。

    Java程序使用synchronized关键字对一个对象进行加锁,synchronized保证了代码块在任意时刻最多只有一个线程能执行。

    public class Main {
        public static void main(String[] args) throws Exception {
            var add = new AddThread();
            var dec = new DecThread();
            add.start();
            dec.start();
            add.join();
            dec.join();
            System.out.println(Counter.count);
        }
    }
    
    class Counter {
        public static final Object lock = new Object();
        public static int count = 0;
    }
    
    class AddThread extends Thread {
        public void run() {
            for (int i=0; i<10000; i++) {
                synchronized(Counter.lock) {
                    Counter.count += 1;
                }
            }
        }
    }
    
    class DecThread extends Thread {
        public void run() {
            for (int i=0; i<10000; i++) {
                synchronized(Counter.lock) {
                    Counter.count -= 1;
                }
            }
        }
    }
    View Code

    注意到代码:

    synchronized(Counter.lock) { // 获取锁
        ...
    } // 释放锁

    它表示用Counter.lock实例作为锁,两个线程在执行各自的synchronized(Counter.lock) { ... }代码块时,必须先获得锁,才能进入代码块进行。执行结束后,在synchronized语句块结束会自动释放锁。这样一来,对Counter.count变量进行读写就不可能同时进行。上述代码无论运行多少次,最终结果都是0。

    使用synchronized解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为synchronized代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,synchronized会降低程序的执行效率。

    Java程序依靠synchronized对线程进行同步,使用synchronized的时候,锁住的是哪个对象非常重要。

    让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把synchronized逻辑封装起来。例如,我们编写一个计数器如下:

    public class Counter {
        private int count = 0;
    
        public void add(int n) {
            synchronized(this) {
                count += n;
            }
        }
    
        public void dec(int n) {
            synchronized(this) {
                count -= n;
            }
        }
    
        public int get() {
            return count;
        }
    }
    View Code

    这样一来,线程调用add()dec()方法时,它不必关心同步逻辑,因为synchronized代码块在add()dec()方法内部。并且,我们注意到,synchronized锁住的对象是this,即当前实例,这又使得创建多个Counter实例的时候,它们之间互不影响,可以并发执行。

    如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe),上面的Counter类就是线程安全的。Java标准库的java.lang.StringBuffer也是线程安全的。

    还有一些不变类,例如StringIntegerLocalDate,它们的所有成员变量都是final,多线程同时访问时只能读不能写,这些不变类也是线程安全的。

    最后,类似Math这些只提供静态方法,没有成员变量的类,也是线程安全的。

    除了上述几种少数情况,大部分类,例如ArrayList,都是非线程安全的类,我们不能在多线程中修改它们。但是,如果所有线程都只读取,不写入,那么ArrayList是可以安全地在线程间共享的。

    当我们锁住的是this实例时,实际上可以用synchronized修饰这个方法。下面两种写法是等价的:

    public void add(int n) {
        synchronized(this) { // 锁住this
            count += n;
        } // 解锁
    }
    View Code
    public synchronized void add(int n) { // 锁住this
        count += n;
    } // 解锁
    View Code

    因此,用synchronized修饰的方法就是同步方法,它表示整个方法都必须用this实例加锁。

    参考资料

    廖雪峰Java

    廖雪峰Java

  • 相关阅读:
    java基础——DecimalFormat
    剑指——重建二叉树
    error error: illegal character: 'u3000'
    Android: Unhandled exception java.net.malformedurlexception 异常笔记
    Android获取系统时间
    java基础——hashCode笔记
    golang 红黑树
    golang 实现跳表skiplist
    快排
    堆排序
  • 原文地址:https://www.cnblogs.com/xumaomao/p/12837817.html
Copyright © 2020-2023  润新知