Java基础知识之线程同步的几种方式

一.引出问题:
在多线程程序中,会出现多个线程抢占一个资源的情况,这时间有可能会造成冲突,也就是一个线程可能还没来得及将更改的 资源保存,另一个线程的更改就开始了。可能造成数据不一致。因此引入多线程同步,也就是说多个线程只能一个对共享的资源进行更改,其他线程不能对数据进行修改。

同步锁:
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制.
同步监听对象/同步锁/同步监听器/互斥锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
Java程序运行使用任何对象作为同步监听对象,但是一般的,我们把当前并发访问的共同资源作为同步监听对象.
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着.

二.解决方案:
①同步代码块

class Apple implements Runnable{
    private  int num=100;
    @Override
    public void run() {
        for (int i = 0;i<100;i++){
            synchronized (this) {
                if (num > 0) {
                    System.out.println(Thread.currentThread().getName() + "吃了第" + num + "个苹果");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    num--;
                }
            }
        }
    }
}
public class SynchronizedBlockDemo {
    public static void main(String[] args) {
        Apple apple = new Apple();
        new Thread(apple, "A").start();
        new Thread(apple, "B").start();
        new Thread(apple, "C").start();
    }
}

②同步方法
同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着.
synchronized public void doWork(){
///TODO
}
同步锁是谁:
对于非static方法,同步锁就是this.
对于static方法,我们使用当前方法所在类的字节码对象(Apple2.class).

class Apple1 implements Runnable {
    private int num = 100;
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            eat();
        }
    }
   synchronized private void eat(){
        if (num > 0) {
            System.out.println(Thread.currentThread().getName() + "吃了第" + num + "个苹果");
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            num--;
        }
    }
}

public class SynchronizedMethodDemo {
    public static void main(String[] args) {
        Apple1 apple = new Apple1();
        new Thread(apple, "A").start();
        new Thread(apple, "B").start();
        new Thread(apple, "C").start();
    }
}

注意:不要使用synchronized修饰run方法,修饰之后,某一个线程就执行完了所有的功能. 好比是多个线程出现串行.
解决方案:把需要同步操作的代码定义在一个新的方法中,并且该方法使用synchronized修饰,再在run方法中调用该新的方法即可.

synchronized的好与坏:
好处:保证了多线程并发访问时的同步操作,避免线程的安全性问题.
缺点:使用synchronized的方法/代码块的性能比不用要低一些.
建议:尽量减小synchronized的作用域.

③双重检查加锁(单例模式-懒汉式)
可以使用“双重检查加锁”的方式来实现,就可以既实现线程安全,又能够使性能不受很大的影响。那么什么是“双重检查加锁”机制呢?
  所谓“双重检查加锁”机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。
  “双重检查加锁”机制的实现会使用关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
注意:在java1.4及以前版本中,很多JVM对于volatile关键字的实现的问题,会导致“双重检查加锁”的失败,因此“双重检查加锁”机制只只能用在java5及以上的版本。
提示:由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用“双重检查加锁”机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。

public class ThreadUtils {
    private ThreadUtils() {
    }
    private static volatile ThreadUtils instance = null;

    public static ThreadUtils getInstance() {
        if (instance == null) {
            synchronized (ThreadUtils.class) {
                if (instance == null) {
                    instance = new ThreadUtils();
                }
            }
        }
        return instance;
    }
}

④同步锁(Lock):
Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象.

class Apple2 implements Runnable {
    private int num = 100;
    private final Lock lock = new ReentrantLock();//创建一个锁对象

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            eat();
        }
    }

    private void eat() {
        //进入方法进加锁
        lock.lock();
        try {
            if (num > 0) {
                System.out.println(Thread.currentThread().getName() + "吃了第" + num + "个苹果");
                Thread.sleep(100);
            }
            num--;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();//释放锁
        }
    }
}

public class LockDemo {
    public static void main(String[] args) {
        Apple2 apple = new Apple2();
        new Thread(apple, "A").start();
        new Thread(apple, "B").start();
        new Thread(apple, "C").start();
    }
}

面试题:
①volatile与synchronized的区别

答:1)volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.
2)volatile仅能使用在变量级别,synchronized则可以使用在变量,方法.
3)volatile仅能实现变量的修改可见性,而synchronized则可以保证变量的修改可见性和原子性.
4)volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.

②同步方法和同步代码块的区别是什么?
一个是一个代码块的同步,一个是整个方法的同步,一般情况下,同步的范围越大,性能也就越差.

③synchronized和lock的区别?
答:1.synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,
但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中
2.在
资源竞争不是很激烈
的情况下,Synchronized的性能要优于ReetrantLock,
但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;

发布了99 篇原创文章 · 获赞 2 · 访问量 2617

猜你喜欢

转载自blog.csdn.net/weixin_41588751/article/details/105211453