Java高并发编程之synchronized关键字(一)

首先看一段简单的代码:

public class T001 {
    private int count = 0;
    private Object o = new Object();
    public void m() {
        //任何线程要执行下面这段代码,必须先拿到o的锁
        synchronized (o) {
            count++;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }
}

这段代码非常简单,o指向了堆内存中的一个对象,synchronized(o)起到的作用是,当有线程要执行其作用域中的代码的时候,需要先获取到o所指向的对象的锁,注意o是对象的引用,对象存在于堆内存中,锁的信息也是记录在堆内存中。只有拿到了锁的线程,才能执行,否则只有等待其他线程释放锁,所以这把锁叫做互斥锁。注意,synchronized锁定的不是一个代码块,而是一个对象。
将上面代码稍加修改,如下:

public class T002 {
    private int count = 0;
    public void m() {
        //任何线程要执行下面这段代码,必须先拿到this的锁
        synchronized (this) {
            count++;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }
}

synchronized (this)锁定的是自身这个对象,如果一段程序,开始的时候就要锁定自身,结束时才释放,那么有一种简单的写法:

public class T003 {
    private int count = 0;
    //任何线程要执行下面这段代码,必须先拿到this的锁
    public synchronized void m() {
        count++;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
}

synchronized关键字直接写在方法的声明中,它跟synchronized (this)是等价的,锁定的都是this对象(不是锁定一段代码哦)。
那么如果synchronized关键字写在静态方法的声明中,情况又会怎样呢?看如下代码:

public class T004 {
    private static int count = 0;
    //这里等同于 synchronized(T004.class)
    public synchronized static void m() {
        count++;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
    public static void mm() {
        synchronized (T004.class) {
            count++;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }
}

实际上ynchronized关键字写在静态方法的声明中,等同于锁定的是T004.class这个对象,T004.class是Class类的对象。静态的方法,不需要创建对象来访问,所以这时候是不需要this的,所以m方法等同于mm方法。
synchronized声明的方法代码块,相当于一个原子操作:

public class T005 implements Runnable {
    private static int count = 10;
    @Override
    public synchronized void run() {
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
    public static void main(String[] args) {
        T005 t005 = new T005();
        for(int i = 0; i < 5; i++) {
            new Thread(t005, "thread_" + i).start();
        }
    }
}

上面代码中,因为run方法由synchronized关键字,所以该方法的代码块,就相当于一个原子操作,尽管在main方法中启动了5个线程都对t005对象的count进行了修改,但打印的结果顺序完好。
假如同时存在同步方法(方法声明中有synchronized关键字)和非同步方法,那么在执行同步方法的过程中,非同步方法能否被其他线程执行呢?看下面这段代码:

public class T006 {
    private static int count = 10;

    public synchronized void m1() {
        System.out.println(Thread.currentThread().getName() + " m1 start...");
        try {
            Thread.sleep(10000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " m1 end.");
    }

    public void m2() {
        try {
            Thread.sleep(5000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " m2");
    }

    public static void main(String[] args) {
        T006 t006 = new T006();

        new Thread(()->t006.m1()).start();
        new Thread(()->t006.m2()).start();
    }
}

上面代码启动了两个线程,线程1执行m1方法,线程2执行m2方法,在m1执行的过程中睡眠了10秒,在此期间,m2被执行了,执行结果如下:

Thread-0 m1 start...
Thread-1 m2
Thread-0 m1 end.

这说明,在同步方法在被执行的过程中,非同步方法是可以被其他线程执行的。有一个经典的问题,那就是写方法同步,读方法非同步的时候,容易产生脏读,下面是一个例子:

public class T007 {
    private String name;
    private double balance;
    public synchronized void set(String name, double balance) {
        this.name = name;
        try {
            Thread.sleep(2000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        this.balance = balance;
    }
    
    public double getBalance() {
        return this.balance;
    }

    public static void main(String[] args) {
        T007 t007 = new T007();
        new Thread(()->t007.set("zhangsan", 100.0)).start();
	try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("balance:" + t007.getBalance());
    }
}

显然打印出来的并不会是set进去的100.0,而是0,这就是脏读问题。业务代码只对写加锁,而读不加锁,写方法在被执行的时候,读方法可以被其他线程执行。因此,在做业务的时候要考虑清楚,是否允许脏读。
synchronized获得的锁,是可重入的。即一个同步方法,可以调用另一个同步方法,一个线程已经拥有某个对象的锁,再次申请时,仍然会得到该对象的锁。下面是一个小例子:

public class T008 {
    public synchronized void m1() {
        System.out.println("m1 start...");
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        m2();
        System.out.println("m1 end.");
    }
    public synchronized void m2() {
        System.out.println("m2 start...");
        try {
            Thread.sleep(2000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("m2 end.");
    }
    public static void main(String[] args) {
        T008 t008 = new T008();
        new Thread(t008::m1).start();
    }
}

打印结果如下:

m1 start...
m2 start...
m2 end.
m1 end.

用一句话归纳,就是,同一个线程,同一把锁,则可重入。另一种情形与此相似,子类调用父类的方法:

public class T009 {
    public synchronized void m() {
        System.out.println("m start...");
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("m end.");
    }

    public static void main(String[] args) {
        TT009 tt009 = new TT009();
        new Thread(tt009::m).start();
    }
}

class TT009 extends T009 {
    @Override
    public synchronized void m() {
        System.out.println("child m start...");
        super.m();
        System.out.println("child m start...");
    }
}

打印结果如下:

child m start...
m start...
m end.
child m start...

在程序执行过程中,如果出现异常,默认情况锁会被释放,所以,在并发处理过程中,有异常要多加小心,不然可能发生不一致的情况。比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这是如果异常处理不合适,在第一个线程抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。因此要非常小心地处理同步业务逻辑中的异常。

public class T010 {
    private int count = 0;
    public synchronized void m() {
        System.out.println(Thread.currentThread().getName() + " start...");
        while (true) {
            count++;
            System.out.println("m start...");
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            if(5 == count) {
                //此处必定会抛出异常,要想锁不被释放,需要进行catch,让循环继续
                int i = 1/0;
            }
        }
    }

    public static void main(String[] args) {
        T010 t010 = new T010();
        Runnable r = new Runnable() {
            @Override
            public void run() {
                t010.m();
            }
        };
        new Thread(r, "thread_1").start();
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        new Thread(r, "thread_2").start();
    }
}

在上面这段代码中,thread_1抛出异常时,thread_2马上开始执行,这说明,thread_1抛出异常的时候,也释放了锁。

猜你喜欢

转载自blog.csdn.net/weixin_42486373/article/details/83960450