多线程的同步

2019-05-12  本文已影响0人  ClawHub的技术分享

线程同步方式:

①synchronized关键字
②同步锁Lock
③volatile
④atomic

以下所有例子我都使用了同一个题目:实现两个用户同时存款向同一个账户存款。

synchronized关键字

①同步代码块
synchronized(同步监视器){
//需要被同步的代码

}
说明:
① 需要被同步的代码即为操作共享数据的代码。
② 共享数据:多个线程共同操作的数据
③ 同步监视器,俗称:锁。
任何一个类的对象,都可以充当同步监视器。但是,要求多个线程必须共用同一个同步监视器。

实现Runnable接口

public class CommonAccount implements Runnable {
    private int money = 0;
    /**
     * @see Thread#run()
     */
    @Override
    public void run() {
            for(int i = 1; i < 3; i++) {
//实现接口时,由于多个分线程都使用同一个CommonAccount 对象,所以同步监视器不管是CommonAccount .class,Object.class,this都是可以的
                synchronized (this) {
                money += 1000;
                System.out.println(Thread.currentThread().getName() + "第" + i + "次存款后金额变为" +  money);
            }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    }
}
public class Test {
    /**
     * The entry point of application.
     *
     * @param args the input arguments
     */
    public static void main(String[] args) {
        CommonAccount account = new CommonAccount();
        Thread t1 = new Thread(account, "存款人1号");
        Thread t2 = new Thread(account, "存款人2号");
        Thread t3 = new Thread(account, "存款人3号");
            t1.start();
            t2.start();
            t3.start();
      }
}

结果:(为什么大家都是第1次存款后才执行的第2次,是因为我sleep()的原因)

存款人1号第1次存款后金额变为1000
存款人3号第1次存款后金额变为2000
存款人2号第1次存款后金额变为3000
存款人3号第2次存款后金额变为4000
存款人2号第2次存款后金额变为5000
存款人1号第2次存款后金额变为6000

继承Thread类

public class CommonAccount1 extends Thread {
    //继承Thread类,共享资源必须为静态
    private static int money = 0;

    //定义一个静态的对象作为监视器
    static Object object = new Object();

    /**
     * @see Thread#run()
     */
    @Override
    public void run() {
            for(int i = 1; i < 3; i++) {
                //CommonAccount1.class也可以
                synchronized (object) {
                money += 1000;
                System.out.println(Thread.currentThread().getName() + "第" + i + "次存款后金额变为" +  money);
            }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    }
}

public class Test {
    /**
     * The entry point of application.
     *
     * @param args the input arguments
     */
    public static void main(String[] args) {
        CommonAccount1 account1 = new CommonAccount1();
        CommonAccount1 account2 = new CommonAccount1();
        account1.setName("存款人1号");
        account1.setName("存款人2号");
        account1.start();
        account2.start();
      }
}

结果:

存款人2号第1次存款后金额变为1000
存款人1号第1次存款后金额变为2000
存款人2号第2次存款后金额变为3000
存款人1号第2次存款后金额变为4000

②同步方法
如果操作共享数据的代码恰好声明在一个或多个方法中,则可以将此方法或此多个方法声明为同步方法。

非静态的同步方法,默认的同步监视器是:this
静态的同步方法,默认的同步监视器是:当前类本身(类名.class)因为静态方法是随着类的加载而加载,不是随着对象创建才创建。
所以如果继承Thread类写同步方法,同步方法必须要是静态的,因为此时多个分线程用的不是同一个对象

实现接口

public class CommonAccount2 implements Runnable {
    /**
     * The constant money.
     */
    private int money = 0;
    @Override
    public void run() {
        for (int i = 1; i < 10; i++) {
            save(i);
        }
    }

    /**
     * Save.
     */
//实现Runnable接口时,由于用的是同一个对象,所以监视器默认为this
    public  synchronized void save(int i) {
        money += 1000;
        System.out.println(Thread.currentThread().getName() + "第" + i + "次存款后金额变为" +  money);
    }
}

继承Thread类

public class CommonAccount2 extends Thread {
    /**
     * The constant money.
     */
    private static int money = 0;
    @Override
    public void run() {
        for (int i = 1; i < 10; i++) {
            save(i);
        }
    }

    /**
     * Save.
     */
    public static synchronized void save(int i) {
        money += 1000;
        System.out.println(Thread.currentThread().getName() + "第" + i + "次存款后金额变为" +  money);
    }
}

Lock

必须要手动关锁释放资源,不然很容易造成死锁

public class CommonAccount3 implements Runnable {
    /**
     * The Money.
     */
    private int money = 0;

    /**
     * The Lock.
     */
    Lock lock = new ReentrantLock();
    /**
     * @see Thread#run()
     */
    @Override
    public void run() {
        for(int i = 1; i < 1000; i++) {
               try {
                   lock.lock();
                   money += 1000;
                   System.out.println(Thread.currentThread().getName() + "第" + i + "次存款后金额变为" +  money);
               } finally {
                   lock.unlock();
               }

            }
    }
}

死锁

什么是死锁?

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。是操作系统层面的一个错误,是进程死锁的简称,最早在 1965 年由 Dijkstra 在研究银行家算法时提出的,它是计算机操作系统乃至整个并发程序设计领域最难处理的问题之一。

怎么防止死锁?

死锁的四个必要条件:

互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不满足,就不会发生死锁。

理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和 解除死锁。

所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确 定资源的合理分配算法,避免进程永久占据系统资源。

此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。

面试:

说一下 synchronized 底层实现原理?

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

普通同步方法,锁是当前实例对象
静态同步方法,锁是当前类的class对象
同步方法块,锁是括号里面的对象

synchronized 和 volatile 的区别是什么?

volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

synchronized 和 Lock 有什么区别?

首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);
Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

synchronized 和 ReentrantLock 区别是什么?

synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:

ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁
ReentrantLock可以获取各种锁的信息
ReentrantLock可以灵活地实现多路通知

另外,二者的锁机制其实也是不一样的:ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word。

说一下 atomic 的原理

Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。

Atomic系列的类中的核心方法都会调用unsafe类中的几个本地方法。我们需要先知道一个东西就是Unsafe类,全名为:sun.misc.Unsafe,这个类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过unsafe分配内存的时候,如果自己指定某些区域可能会导致一些类似C++一样的指针越界到其他进程的问题。

上一篇 下一篇

猜你喜欢

热点阅读