一.引出问题:
在多线程程序中,会出现多个线程抢占一个资源的情况,这时间有可能会造成冲突,也就是一个线程可能还没来得及将更改的 资源保存,另一个线程的更改就开始了。可能造成数据不一致。因此引入多线程同步,也就是说多个线程只能一个对共享的资源进行更改,其他线程不能对数据进行修改。
同步锁:
为了保证每个线程都能正常执行原子操作,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的性能能维持常态;