多线程——线程同步

一、线程安全概述

【示例】通过代码模拟取钱的过程

// 银行账户类

class Account {

    // 账户余额

    private int balance = 5000;

    // 获取账户余额方法

    public int getBalance() {

        return balance;

    }

    // 取钱方法

    public void drawMoney(int money) {

        balance -= money;

    }

}

// Runnable类,模拟取钱的操作

public class AccountRunnable implements Runnable {

    // 银行账户对象

    private Account account;

    public AccountRunnable(Account account) {

        this.account = account;

    }

    @Override

    public void run() {

        // 先判断余额够不够,如果足够则取款

        if(account.getBalance() >= 3000) {

            try {

                Thread.sleep(1); // 取钱线程中断,起切换线程的作用

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

            // 取款3000元

            account.drawMoney(3000);

            // 输出余额

            System.out.println(Thread.currentThread().getName() + "取款成功"

            + ",余额为:" + account.getBalance());

        }

        // 余额不够,则不允许取款

        else {

            System.out.println(Thread.currentThread().getName()

                    +"取款失败,余额为:" + account.getBalance());

        }

    }

}

// 测试类,模拟你和你老婆取钱

public class Test {

    public static void main(String[] args) {

        // 实例化银行账户对象

        Account account = new Account();

        // 创建并开启线程

        AccountRunnable ar = new AccountRunnable(account);

        new Thread(ar, "你").start();

        new Thread(ar, "你老婆").start();

    }

}

执行以上代码,输出结果为:

eb3d32c827564f28985f6884cf1c29eb.png

通过运行结果观察,我们可以发现共享数据明明只有5000(账户余额)的完整性被破坏了,两个线程同时操作一个银行账户,银行账户元,结果两人却轻松取出了3000元。

为了避免这样的事情发生,我们要保证线程同步互斥(同步互斥就是:并发执行的多个线程在某个时间内只允许一个线程在执行并访问共享数据)。

为了解决这个问题,java提供了线程同步机制,它能够解决上述的线程安全问题,线程同步的方式有两种:同步代码块和同步方法。

二、同步代码块

“非线程安全”其实就是在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是“脏读”,也就是取到的数据其实是被更改过的的结果。在Java中,关键字synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作)。

在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,此时我们可以使用同步代码块。同步代码块,就是在代码块声明上加上synchronized关键字。

synchronized (同步监视器) {

    // 可能会产生线程安全问题的代码

}

同步监视器可以理解为就是一把锁,锁住了别的线程就进不去了,直到该线程释放掉这个锁(释放锁是指持锁线程退出了synchronized同步代码块。

同步监视器注意事项:

        1)所谓的同步监视器就是一个“对象”,我们可以理解为是一个“锁”。

        2)多个线程访问同一个同步代码块时,要求必须使用同一个“对象”来作为“锁”。

        3)尽量不要用String和包装类型作为同步监视器,容易造成指向堆中地址的变化。

        4)我们一般使用this作为同步监视器,也可以专门创建一个对象来作为同步监视器。

对银行取钱案例中 AccountRunnable 类进行如下代码修改:

【示例】使用同步代码块模拟取钱

// Runnable类,模拟取钱的操作

public class AccountRunnable implements Runnable {

    // 银行账户对象

    private Account account;

    public AccountRunnable(Account account) {

        this.account = account;

    }

    @Override

    public void run() {

        // 同步代码块

        synchronized (this) {

            // 先判断余额够不够,如果足够则取款

            if(account.getBalance() >= 3000) {

                try {

                    Thread.sleep(1); // 取钱线程中断,起切换线程的作用

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

                // 取款3000元

                account.drawMoney(3000);

                // 输出余额

                System.out.println(Thread.currentThread().getName() + "取款成功"

                + ",余额为:" + account.getBalance());

            }

            // 余额不够,则不允许取款

            else {

                System.out.println(Thread.currentThread().getName()

                        +"取款失败,余额为:" + account.getBalance());

            }

        }

    }

}

当使用了同步代码块后,上述的线程的安全问题,解决了。

synchronized (this)”意味着线程需要获得this对象的“锁”才有资格运行同步块中的代码。 this对象的“锁”也成为“互斥锁”,只能同时被一个线程使用。例如,A线程拥有锁,则可以调用“同步块”中的代码;B线程没有锁,则进入this对象的“锁池队列”等待,直到A线程使用完毕释放了this对象的锁,B线程才可以开始调用“同步块”中的代码。

补充:使用同步代码块时,同步监视器一般为this,即当前类对象,这是使用的最多的一种方式。

三、同步方法

当某一个方法中的所有代码都需要同步的时候,这时候我们可以使用同步方法,同步方法就是在方法声明上加添加 synchronized关键字。

3.1 非静态同步方法

在非静态方法声明上添加synchronized,我们称之为非静态同步方法。

public synchronized void method() {

    // 可能会产生线程安全问题的代码

}

非静态同步方法和同步代码块的原理比较类似,不过非静态同步方法中的同步监视器是隐式的,该同步监视器默认为this(感兴趣的同学可以自己通过代码验证一下)。

接下来我们使用非静态同步方法,对银行取钱案例中 AccountRunnable 类进行如下代码修改:

【示例】使用非静态同步方法模拟取钱

// Runnable类,模拟取钱的操作

public class AccountRunnable implements Runnable {

    // 银行账户对象

    private Account account;

    public AccountRunnable(Account account) {

        this.account = account;

    }

    @Override

    public void run() {

        // 此处省略500句(与安全无关)

        drawMoney();

        // 此处省略500句(与安全无关)

    }

    /**

     * 同步方法

     */

    public synchronized void drawMoney() {

        // 先判断余额够不够,如果足够则取款

        if (account.getBalance() >= 3000) {

            try {

                Thread.sleep(1); // 起切换线程的作用

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

            // 取款3000元

            account.drawMoney(3000);

            // 输出余额

            System.out.println(Thread.currentThread().getName() + "取款成功" + ",余额为:" + account.getBalance());

        }

        // 余额不够,则不允许取款

        else {

            System.out.println(Thread.currentThread().getName() + "取款失败,余额为:" + account.getBalance());

        }

    }

}

建议,不要在重写的run方法添加synchronized,否则run方法中的任务都变为同步了。

目前我们已接触到的类中,StringBuffer、Hashtable和Vector都属于线程安全类,这些线程安全类中的方法都为非静态同步方法,例如StringBuffer类中的append方法就是synchronized修饰的。

e5ec0de550b4492baddfbfb29e7f6230.png

3.2 静态同步方法 

synchronized关键字不但能修饰成员方法,还能修饰静态方法。在静态方法的声明上添加synchronized,我们称之为静态同步方法。

public static synchronized void method() {

    // 可能会产生线程安全问题的代码

}

静态同步方法中同步监视器是隐式的,静态同步方法的同步监视器默认为“类名.class”

synchronized修饰成员方法,实际上是对调用该方法的对象(也就是this)加锁,俗称“对象锁”;synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”。

另外,同步方法直接在方法声明上加synchronized实现加锁,同步代码块则在方法内部加锁,很明显,同步方法锁的范围比较大,而同步代码块范围要小点。并且,同步是一种高开销的操作,因此应该尽量减少同步的内容。 通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。 

四、Synchronized

总结1synchronized关键字使用注意点

        1)只能同步方法和代码块,而不能同步变量和类。

        2)只有共享资源的读写访问才需要同步,如果不是共享资源,根本没有同步的必要。

        3)编写线程安全的代码会使系统的总体效率会降低,并且容易发生死锁的情况。

        4)同步代码块和同步方法,都必须获得对象锁才能够进入同步代码块或同步方法进行操作。

        5)成员同步方法,它的对象锁为“this”对象;静态同步方法,它的对象锁为“类名.class”。

总结2:一个线程取得了同步锁,那么在什么时候才会释放掉呢?

        1)同步方法或同步代码块正常执行完毕。

        2)使用return或 break终止了执行,或者抛出了未处理的异常。

        3)当线程执行同步方法或代码块时,程序执行了同步锁对象的wait()方法。

总结3synchronized的可重入性

在使用synchronized时,当一个线程得到一个对象锁后(只要该线程还没有释放这个对象锁),再次请求此对象锁时是可以再次得到该对象的锁的。

【示例】在同步方法中,可以调用当前类的另一个同步方法

class TestRunnable implements Runnable {

    @Override

    public void run() {

        method01();

    }

    public synchronized void method01() {

        System.out.println("执行method01方法啦");

        // 在同步方法中调用同一个对象的另外一个同步方法

        method02();

    }

    public synchronized void method02() {

        System.out.println("执行method02方法啦");

    }

}

// 测试类

public class Test {

    public static void main(String[] args) {

        new Thread(new TestRunnable()).start();

    }

}

可重入锁也支持在父子类继承的环境中。当存在父子类继承关系时,子类是完全可以通过“可重入锁”调用父类的同步方法。

【示例】在子类同步方法中,可以调用父类的同步方法

// 父类

class Parent {

    public synchronized void parentMethod() {

        System.out.println("父类parentMethod方法被执行啦");

    }

}

// 子类

class ChildRunnable extends Parent implements Runnable {

    @Override

    public void run() {

        childMethod();

    }

    public synchronized void childMethod() {

        System.out.println("子类childMethod方法被执行啦");

        // 调用父类方法

        super.parentMethod();

    }

}

// 测试类

public class Test {

    public static void main(String[] args) {

        new Thread(new ChildRunnable()).start();

    }

}

总结4:同步方法和方法重写的要求

【示例】子类重写父类的同步方法,子类重写的方法可以为非同步方法

//父类

class Parent {

    // 父类的method方法为同步方法

    public synchronized void method() {

        System.out.println("父类method方法");

    }

}

//子类

class Child {

    // 子类重写父类的method方法,但是子类方法可以不是同步方法

    public void method() {

        System.out.println("子类method方法");

    }

}

五、死锁

简单的说就是:当A线程等待B线程释放资源,而同时B又在等待A线程释放资源,这就形成了死锁。这里举一个通俗的例子,例如在人行道上两个人迎面相遇,为了给对方让道,两人同时向一侧迈出一步,双方无法通过,又同时向另一侧迈出一步,这样还是无法通过。假设这种情况一直持续下去,这样就会发生死锁现象。

导致死锁的根源在于不适当地运用“synchronized”关键词来管理线程对特定对象的访问。比如有两个对象A 和 B 。第一个线程锁住了A,然后休眠1秒,轮到第二个线程执行,第二个线程锁住了B,然后也休眠1秒,然后有轮到第一个线程执行。第一个线程又企图锁住B,可是B已经被第二个线程锁定了,所以第一个线程进入阻塞状态,又切换到第二个线程执行。第二个线程又企图锁住A,可是A已经被第一个线程锁定了,所以第二个线程也进入阻塞状态。就这样,死锁造成了。

 

如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

c30c1d4f22c8489abbb882196abb9ca9.png 

 

【示例】死锁情况的代码演示

package com.powernode.thread;



public class DeadLockDemo {

    private static Object lock1 = new Object();//锁1,资源1
    private static Object lock2 = new Object();//锁2,资源2


    public static void main(String[] args) {

        //启动一个线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized(lock1){
                    System.out.println(Thread.currentThread().getName()+"拿到了锁1,资源1");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"等待锁2,资源2");
                    synchronized (lock2){
                        System.out.println(Thread.currentThread().getName()+"拿到了锁2,资源2");
                    }
                }
            }
        },"线程1").start();



        //产生死锁的线程
//        new Thread(new Runnable() {
//            @Override
//            public void run() {
//                synchronized(lock2){
//                    System.out.println(Thread.currentThread().getName()+"拿到了锁2,资源2");
//                    try {
//                        Thread.sleep(1000);
//                    } catch (InterruptedException e) {
//                        e.printStackTrace();
//                    }
//                    System.out.println(Thread.currentThread().getName()+"等待锁1,资源1");
//                    synchronized (lock1){
//                        System.out.println(Thread.currentThread().getName()+"拿到了锁1,资源1");
//                    }
//                }
//            }
//        },"线程2").start();
    }
}

 

线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过Thread.sleep(1000);让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。

0eff5e84309a483dac86df261ba607fb.png 破坏死锁

 

//破坏死锁
new Thread(new Runnable() {
    @Override
    public void run() {
        synchronized(lock1){
            System.out.println(Thread.currentThread().getName()+"拿到了锁1,资源1");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"等待锁2,资源2");
            synchronized (lock2){
                System.out.println(Thread.currentThread().getName()+"拿到了锁2,资源2");
            }
        }
    }
},"线程2").start();

 

线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。

猜你喜欢

转载自blog.csdn.net/shengshanlaolin_/article/details/127476938